本文记录《UNIX环境高级编程》第3版中第4章文件与目录的一些知识点。
本章将描述文件系统的其他特征和文件的性质。将从stat函数开始,逐个说明stat结构的每一个成员以了解文件的所有属性。
函数stat、fstat、fstatat和lstat
#include <sys/stat.h>
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
// 所有4个函数的返回值:若成功;返回0;若出错,返回-1
Linux中stat结构的基本形式是:
#include <sys/stat.h>
struct stat {dev_t st_dev; /* ID of device containing file */ino_t st_ino; /* Inode number */mode_t st_mode; /* File type and mode */nlink_t st_nlink; /* Number of hard links */uid_t st_uid; /* User ID of owner */gid_t st_gid; /* Group ID of owner */dev_t st_rdev; /* Device ID (if special file) */off_t st_size; /* Total size, in bytes */blksize_t st_blksize; /* Block size for filesystem I/O */blkcnt_t st_blocks; /* Number of 512 B blocks allocated/* Since POSIX.1-2008, this structure supports nanosecondprecision for the following timestamp fields.For the details before POSIX.1-2008, see VERSIONS. */struct timespec st_atim; /* Time of last access */struct timespec st_mtim; /* Time of last modification */struct timespec st_ctim; /* Time of last status change */
#define st_atime st_atim.tv_sec /* Backward compatibility */
#define st_mtime st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
timespec结构类型按照秒和纳秒定义了时间,至少包括下面两个字段:
time_t tv_sec;
long tv_nsec;
文件类型
文件类型包括如下几种:
(1)普通文件(regular file)。
(2)目录文件(directory file)。这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。进程必须使用本章介绍的函数才能更改目录。
(3)块特殊文件(block special file)。这种类型的文件提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。
(4)字符特殊文件(character special file)。这种类型的文件提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
(5)FIFO。这种类型的文件用于进程间通信,有时也称为命名管道(named pipe)。
(6)套接字(socket)。这种类型的文件用于进程间的网络通信。套接字也可用于在一台宿主机上进程之间的非网络通信。
(7)符号链接(symbolic link)。这种类型的文件指向另一个文件。
文件类型信息包含在stat结构的st_mode成员中。可以用下图中的宏确定文件类型。这些宏的参数都是stat结构中的st_mode成员。
| 宏 | 文件类型 |
|---|---|
S_ISREG() |
普通文件 |
S_ISDIR() |
目录文件 |
S_ISCHR() |
字符特殊文件 |
S_ISBLK() |
块特殊文件 |
S_ISFIFO |
管道或FIFO |
S_ISLINK |
符号链接 |
S_ISSOCK() |
套接字 |
设置用户ID和设置组ID
在 Unix/Linux 中,每个进程有三个与用户ID相关的标识符:
- real user ID(真实用户ID,RUID):标识进程的实际拥有者(登录用户)。
- effective user ID(有效用户ID,EUID):决定进程当前的权限(如能否访问文件、执行特权操作)。
- saved set-user-ID(保存的设置用户ID,SUID):保存了程序启动时的有效用户ID(设置set-user-id位时是程序文件的所有者ID),作为权限切换的“备份”或“中转站”。
通常,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
每个文件有一个所有者和组所有者,所有者由stat结构中的st_uid指定,组所有者则由st_gid指定。
当执行一个程序文件时,进程的有效用户ID通常就是实际用户ID,有效组ID通常是实际组ID。但是可以在文件模式字(st_mode)中设置一个特殊标志,其含义是“当执行此文件时,将进程的有效用户ID设置为文件所有者的用户ID(st_uid)”。与此相类似,在文件模式字中可以设置另一位,它将执行此文件的进程的有效组ID设置为文件的组所有者ID(st_gid)。在文件模式字中的这两位被称为设置用户ID(set-user-ID)位和设置组ID(set-group-ID)位。
当一个程序文件设置了 SUID 位(通过 chmod u+s filename 配置)时,其执行逻辑与 saved set-user-ID 密切相关:
-
程序启动时:
- 进程的
EUID会被设置为程序文件的所有者ID(例如,/bin/passwd是 root 所有且设置了 SUID 位,普通用户执行它时,EUID会变为 root)。 - 同时,
saved set-user-ID会被初始化为这个EUID(即 root 的 ID),作为后续权限切换的“基准”。
- 进程的
-
程序执行中:
- 进程可以通过
setuid()等系统调用临时降低权限(例如,passwd程序大部分操作不需要 root 权限,会将EUID切换为普通用户的 RUID)。 - 当需要执行特权操作时(如写入
/etc/passwd),进程可以通过saved set-user-ID恢复之前保存的高权限EUID(即 root),完成操作后再切换回低权限。
- 进程可以通过
-
作用:
- 防止权限被滥用:如果没有
saved set-user-ID,进程一旦降低EUID就无法再恢复到原来的高权限(除非直接使用 root 身份,风险更高)。 - 实现“最小权限原则”:仅在必要时临时提权,其他时间保持低权限,提升系统安全性。
- 防止权限被滥用:如果没有
再回到stat函数,设置用户ID位及设置组ID位都包含在文件的st_mode值中。这两位可分别用常量S_ISUID和S_ISGID测试。
saved set-user-ID 的核心作用是保存程序启动时的有效用户ID,允许进程在执行过程中安全地切换权限(从高权限临时降为低权限,必要时再恢复),是 SUID 程序实现安全权限管理的关键机制。常见的 SUID 程序(如 passwd、sudo)都依赖它来平衡功能需求与系统安全。
文件访问权限
st_mode值也包含了对文件的访问权限位。所有文件类型都有访问权限。
每个文件有9个访问权限位,可将它们分成3类,见下图。
前3行中,术语用户指的是文件所有者(owner)。chmod命令用于修改这9个权限位。该命令允许用u表示用户(所有者),用g表示组,用o表示其他。
图中的3类访问权限(即读、写及执行)以各种方式由不同的函数使用。我们将这些不同的使用方式汇总在下面。
-
第一个规则是,用名字打开任一类型的文件时,对包含该名字的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。例如,为了打开文件
/usr/include/stdio.h,需要对目录/、/usr和/usr/include具有执行权限。然后,需要具有对文件本身的适当权限,这取决于以何种模式打开它(只读、读-写等)。注意,对于目录的读权限和执行权限的意义是不相同的。读权限允许读目录,获得在该目录中所有文件名的列表。当一个目录是我们要访问文件的路径名的一个组成部分时,对该目录的执行权限使我们可通过该目录(也就是搜索该目录,寻找一个特定的文件名)。
-
对于一个文件的读权限决定了是否能够打开现有文件进行读操作。这与
open函数的O_RDONLY和O_RDWR标志相关。 -
对于一个文件的写权限决定了是否能够打开现有文件进行写操作。这与
open函数的O_WRONLY和O_RDWR标志相关。 -
为了在
open函数中对一个文件指定O_TRUNC标志,必须对该文件具有写权限。 -
为了在一个目录中创建一个新文件,必须对该目录具有写权限和执行权限。
-
为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限。对该文件本身则不需要有读、写权限。
-
如果用7个exec函数中的任何一个执行某个文件,都必须对该文件具有执行权限。该文件还必须是一个普通文件。
进程每次打开、创建或删除一个文件时,内核就进行文件访问权限测试,而这种测试可能涉及文件的所有者(st_uid和st_gid)、进程的有效ID(有效用户ID和有效组ID)以及进程的附属组ID(若支持的话)。两个所有者ID是文件的性质,而两个有效ID和附属组ID则是进程的性质。内核进行的测试具体如下。
(1)若进程的有效用户ID是0(超级用户),则允许访问。这给予了超级用户对整个文件系统进行处理的最充分的自由。
(2)若进程的有效用户ID等于文件的所有者ID(也就是进程拥有此文件),那么如果所有者适当的访问权限位被设置,则允许访问;否则拒绝访问。适当的访问权限位指的是,若进程为读而打开该文件,则用户读位应为1;若进程为写而打开该文件,则用户写位应为1;若进程将执行该文件,则用户执行位应为1。
(3)若进程的有效组ID或进程的附属组ID之一等于文件的组ID,那么如果组适当的访问权限位被设置,则允许访问;否则拒绝访问。
(4)若其他用户适当的访问权限位被设置,则允许访问;否则拒绝访问。
新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。
(1)新文件的组ID可以是进程的有效组ID。
(2)新文件的组ID可以是它所在目录的组ID。
函数access和faccessat
正如前面所说,当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试。有时,进程也希望按其实际用户ID和实际组ID来测试其访问能力。access和faccessat函数是按实际用户ID和实际组ID进行访问权限测试的。(该测试也分成4步,这与上节中所述的一样,但将有效改为实际。)
#include <unistd.h>
int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
函数umask
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值。
#include <sys/stat.h>
mode_t umask(mode_t cmask);
// 返回值:之前的文件模式创建屏蔽字
其中,参数cmask是由9个常量(S_IRUSR、S_IWUSR等)中的若干个按位或构成。
在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字。
用户可以设置umask值以控制他们所创建文件的默认权限。该值表示成八进制数。设置了相应位后,它所对应的权限就会被拒绝。常用的几种umask值是002、022和027。002阻止其他用户写入你的文件,022阻止同组成员和其他用户写入你的文件,027阻止同组成员写你的文件以及其他用户读、写或执行你的文件。
函数chmod、fchmod和fchmodat
这3个函数可以更改现有文件的访问权限。
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
// 3个函数返回值:若成功,返回0;若出错,返回−1
为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。
参数mode是下图中所示常量的按位或。
注意,在图中,另外加了6个权限位,它们是两个设置ID常量(S_ISUID和S_ISGID)、保存正文常量(S_ISVTX)以及3个组合常量(S_IRWXU、S_IRWXG和S_IRWXO)。
粘性位
“sticky”这个名称的由来,是因为可执行文件的机器代码正文段会一直“滞留”在交换区(swap area)中,直到系统重启才会消失。UNIX系统的后续版本将其称为保存正文位(saved-text bit);因此才有了常量S_ISVTX。
在如今较新的UNIX系统中,大多数都配备了虚拟内存系统和速度更快的文件系统,因此这种技术的需求已经不复存在了。
现今的系统扩展了粘着位的使用范围, Single
UNIX Specification允许针对目录设置粘着位。如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
- 拥有此文件;
- 拥有此目录;
- 是超级用户。
目录/tmp 和 /var/tmp 是设置粘着位的典型候选者,任何用户都可在这两个目录中创建文件。任一用户(用户、组和其他)对这两个目录的权限通常都是读、写和执行。但是用户不应能删除或重命名属于其他人的文件,为此在这两个目录的文件模式中都设置了粘性位。
在 Linux 系统中,粘性位(sticky bit) 的符号表示为小写字母 t 或大写字母 T,出现在文件或目录权限的最后一位(执行位的位置)。具体规则如下:
- 当目录或文件同时具备执行权限(x) 且设置了粘性位时,符号为 t。
- 当未设置执行权限(x)但设置了粘性位时,符号为 T。
函数chown、fchown、fchownat和lchown
下面几个chown函数可用于更改文件的用户ID和组ID。如果两个参数owner或group中的任意一个是-1,则对应的ID不变。
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char *pathname, uid_t owner, gid_t group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group);
// 4个函数的返回值:若成功,返回0;若出错,返回-1
文件截断
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数将一个现有文件长度截断为length。如果该文件以前的长度大于length,则超过length以外的数据就不再能访问。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。
文件系统
磁盘可以划分成几个部分,每个部分可以包含一个文件系统,如下图所示。

更详细地柱面组(cylinder group)中索引节点(i-node)与数据块(data block)的结构排布如图所示。

在mkdir testdir创建目录后柱面组的分布如图所示。其中i-node = 2549指向testdir目录,i-node = 1267指向testdir的父目录。

函数link、linkat、unlink、unlinkat和remove
创建一个指向现有文件的链接的方法是使用link函数或linkat函数。
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
int linkat(int efd, const char *existingpath, int nfd, const char *newpath, int flag);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
这两个函数创建一个新目录项newpath,它引用现有文件existingpath。如果newpath已经存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
为了删除一个现有的目录项,可以调用unlink函数。
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
也可以用remove函数解除对一个文件或目录的链接。对于文件,remove的功能与unlink相同。对于目录,remove的功能与rmdir相同。
#include <stdio.h>
int remove(const char *pathname);
// 返回值:若成功,返回0;若出错,返回-1
函数rename和renameat
文件或目录可以用rename函数或者renameat函数进行重命名。
#include <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
创建和读取符号链接
可以用symlink或symlinkat函数创建一个符号链接。
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字。readlink和readlinkat函数提供了这种功能。
#include <unistd.h>
ssize_t readlink(const char *restrict pathname, char *restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char* restrict pathname, char *restrict buf, size_t bufsize);
// 两个函数的返回值:若成功,返回读取的字节数;若出错,返回-1
两个函数组合了 open、read和close的所有操作。如果函数成功执行,则返回读入buf的字节数。在buf中返回的符号链接的内容不以null字节终止。
文件的时间
对每个文件都维护3个时间字段,它们的含义如下表。
| 字段 | 描述 | 示例 | ls 选项 |
|---|---|---|---|
st_atim |
最后访问文件数据的时间 | read |
-u |
st_mtim |
最后修改文件数据的时间 | write |
默认 |
st_ctim |
i节点状态的最后更改时间 | chmod, chown |
-c |
不同函数对文件和父目录的访问、修改和状态改变的时间的影响如下图所示。

函数futimens、utimensat和utimes
一个文件的访问和修改时间可以用以下几个函数更改。futimens和utimensat函数可以指定纳秒级精度的时间戳。用到的数据结构是与stat函数族相同的timespec结构。
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
// 两个函数返回值:若成功,返回0;若出错,返回-1
这两个函数的times数组参数的第一个元素包含访问时间,第二元素包含修改时间。这两个时间值是日历时间,这是自特定时间(1970年1月1日00:00:00)以来所经过的秒数。不足秒的部分用纳秒表示。
时间戳可以按下列4种方式之一进行指定。
(1)如果times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。
(2)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段。
(3)如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段。
(4)如果times参数指向两个timespec结构的数组,且tv_nsec字段的值为既不是UTIME_NOW也不是UTIME_OMIT,在这种情况下,相应的时间戳设置为相应的 tv_sec 和tv_nsec字段的值。
futimens和utimensat函数都包含在POSIX.1 中,第3 个函数utimes包含在Single UNIX Specification的XSI扩展选项中。
#include <sys/time.h>
int utimes(const char *pathname, const struct timeval times[2]);
// 函数返回值:若成功,返回0;若出错,返回-1
times参数是指向包含两个时间戳(访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微妙表示的。
struct timeval {time_t tv_sec; /* seconds */long tv_usec; /* microseconds */
};
注意,不能对状态更改时间st_ctim(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新。
函数mkdir、mkdirat和rmdir
用mkdir和mkdirat函数创建目录。
#include <sys/stat.h>
int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
// 两个函数返回值:若成功,返回0;若出错,返回-1
用rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录。
#include <unistd.h>
int rmdir(const char *pathname);
// 返回值:若成功,返回0;若出错,返回-1
读目录
#include <dirent.h>
DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
//两个函数返回值:若成功,返回指针;若出错,返回NULLstruct dirent *readdir(DIR *dp);
//返回值:若成功,返回指针;若在目录尾或出错,返回NULLvoid rewinddir(DIR *dp);int closedir(DIR *dp);
// 返回值:若成功,返回0;若出错,返回-1long telldir(DIR *dp);
// 返回值:与dp关联的目录中的当前位置void seekdir(DIR *dp, long loc);
由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。它们通常并不按字母顺序排列。
函数chdir、fchdir和getcwd
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到 UNIX 系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登录项的第6个字段—用户的起始目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
进程调用chdir或fchdir函数可以更改当前工作目录。
#include <unistd.h>
int chdir(const char *pathname);
int fchdir(int fd);
// 两个函数的返回值:若成功,返回0;若出错,返回-1
在这两个函数中,分别用pathname或打开文件描述符来指定新的当前工作目录。
用getcwd获取当前工作目录。
#include <unistd.h>
char *getcwd(char *buf, size_t size);
// 返回值:若成功,返回buf;若出错,返回NULL
必须向此函数传递两个参数,一个是缓冲区地址buf,另一个是缓冲区的长度size(以字节为单位)。该缓冲区必须有足够的长度以容纳绝对路径名再加上一个终止 null 字节,否则返回出错。
设备特殊文件
字段st_dev和st_rdev一些使用规则如下:
- 每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型
dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。一个磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但是次设备号却不同。 - 通常可以使用两个宏:
major和minor来访问主、次设备号,大多数实现都定义这两个宏。这意味着无需关心这两个数是如何存放在dev_t对象中的。 - 系统中与每个文件名关联的
st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的 i 节点。 - 只有字符特殊文件和块特殊文件才有
st_rdev值。此值包含实际设备的设备号。
文件访问权限位小结
下图列出了所有这些文件访问权限位,以及它们对普通文件和目录文件的作用。

最后9个常量可以分成以下三组:
S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR
S_IRWXG = S_IRGRP | S_IWGRP | S_IXGRP
S_IRWXO = S_IROTH | S_IWOTH | S_IXOTH
