APUE笔记:Files and Directories(四)
本文记录《UNIX环境高级编程》第3版 第4章 Files and Directories 的一些知识点。
本章将描述文件的性质。将从 stat 函数开始,逐个说明 stat 结构的每一个成员以了解文件的所有属性。
函数stat、fstat、fstatat和lstat
1 |
|
Linux中stat结构的基本形式是:
1 |
|
timespec结构类型按照秒和纳秒定义了时间,至少包括下面两个字段:
1 | time_t tv_sec; |
文件类型
文件类型包括如下几种:
(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)位。
当一个程序文件设置了 set-user-ID(通过
chmod u+s filename 配置)时,其执行逻辑与
saved set-user-ID 密切相关:
- 程序启动时:
- 进程的
EUID会被设置为程序文件的所有者ID(例如,/bin/passwd是 root 所有且设置了 set-user-ID 位,普通用户执行它时,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表示其他。
新文件和目录的所有权
新文件的用户ID设置为进程的有效用户ID。关于组ID,POSIX.1允许实现选择下列之一作为新文件的组ID。
(1)新文件的组ID可以是进程的有效组ID。
(2)新文件的组ID可以是它所在目录的组ID。
函数access和faccessat
access和faccessat函数是按实际用户ID和实际组ID进行访问权限测试。
1 |
|
mode 要么是用于测试文件是否存在的值
F_OK,要么是下图中所示任意标志的按位或运算结果。
| mode | Description |
|---|---|
R_OK |
test for read permission |
W_OK |
test for write permission |
X_OK |
test for execute permission |
flag 参数可用于更改 faccessat
的行为。如果设置了 AT_EACCESS
标志,访问检查将使用调用进程的有效用户ID和组ID,而不是实际用户ID和组ID。
函数umask
umask函数为进程设置文件模式创建屏蔽字,并返回之前的值。
1 |
|
其中,参数cmask是由9个常量(S_IRUSR、S_IWUSR等)中的若干个按位或构成。
在进程创建一个新文件或新目录时,就一定会使用文件模式创建屏蔽字。
用户可以设置umask值以控制他们所创建文件的默认权限。该值表示成八进制数。设置了相应位后,它所对应的权限就会被拒绝。常用的几种umask值是002、022和027。002阻止其他用户写入你的文件,022阻止同组成员和其他用户写入你的文件,027阻止同组成员写你的文件以及其他用户读、写或执行你的文件。
函数chmod、fchmod和fchmodat
这3个函数可以更改现有文件的访问权限。
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不变。
1 |
|
File Size
stat 结构体的 st_size
成员包含文件的大小(以字节为单位)。此字段仅对普通文件、目录和符号链接有意义。
对于普通文件,允许文件大小为0。在第一次读取该文件时会得到文件结束的指示。对于目录,文件大小通常是某个数字的倍数,比如16或512。对于符号链接,文件大小是文件名所占的字节数。例如,在下面这种情况下,大小为7的文件对应的是路径名
usr/lib 的长度:
1 | lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> usr/lib |
文件截断
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函数。
1 |
|
这两个函数创建一个新目录项 newpath,它引用现有文件
existingpath。如果 newpath
已经存在,则返回出错。只创建 newpath
中的最后一个分量,路径中的其他部分应当已经存在。
为了删除一个现有的目录项,可以调用 unlink 函数。
1 |
|
也可以用remove函数解除对一个文件或目录的链接。对于文件,remove
的功能与 unlink 相同。对于目录,remove
的功能与 rmdir 相同。
1 |
|
函数rename和renameat
文件或目录可以用rename函数或者renameat函数进行重命名。
1 |
|
Symbolic Links
符号链接是指向文件的间接指针,这与硬链接不同,硬链接直接指向文件的索引节点。引入符号链接是为了规避硬链接的局限性:
- 硬链接通常要求链接和文件位于同一个文件系统中。
- 只有超级用户可以创建指向目录的硬链接(当底层文件系统支持时)。
下图总结了不同的函数是否会跟随符号链接。

创建和读取符号链接
可以用symlink或symlinkat函数创建一个符号链接。
1 |
|
因为open函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字。readlink和readlinkat函数提供了这种功能。
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 结构。
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扩展选项中。
1 |
|
times参数是指向包含两个时间戳(访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微秒表示的。
1 | struct timeval { |
注意,不能对状态更改时间st_ctim(i节点最近被修改的时间)指定一个值,因为调用utimes函数时,此字段会被自动更新。
函数mkdir、mkdirat和rmdir
用mkdir和mkdirat函数创建目录。
1 |
|
用rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录。
1 |
|
读目录
1 |
|
由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。它们通常并不按字母顺序排列。
函数chdir、fchdir和getcwd
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。当用户登录到
UNIX
系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登录项的第6个字段—用户的起始目录(home
directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
进程调用chdir或fchdir函数可以更改当前工作目录。
1 |
|
在这两个函数中,分别用pathname或打开文件描述符来指定新的当前工作目录。
用getcwd获取当前工作目录。
1 |
|
必须向此函数传递两个参数,一个是缓冲区地址
buf,另一个是缓冲区的长度
size(以字节为单位)。该缓冲区必须有足够的长度以容纳绝对路径名再加上一个终止
null 字节,否则返回出错。
设备特殊文件
字段st_dev和st_rdev一些使用规则如下:
- 每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型
dev_t。一个磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但是次设备号却不同。 - 通常可以使用两个宏:
major和minor来访问主、次设备号,大多数实现都定义这两个宏。这意味着无需关心这两个数是如何存放在dev_t对象中的。 - 系统中与每个文件名关联的
st_dev值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的 i 节点。 - 只有字符特殊文件和块特殊文件才有
st_rdev值。此值包含实际设备的设备号。
文件访问权限位小结
下图列出了所有这些文件访问权限位,以及它们对普通文件和目录文件的作用。
最后9个常量可以分成以下三组:
1 | S_IRWXU = S_IRUSR | S_IWUSR | S_IXUSR |