笔记和练习博客总目录见:开始读TLPI。
在本章中,我们研究文件的各种属性(文件元数据)。我们首先介绍 stat() 系统调用,它返回一个包含许多这些属性的结构体,包括文件时间戳、文件所有权和文件权限。然后我们继续研究用于更改这些属性的各种系统调用。(文件权限的讨论将在第17章继续,届时我们将研究访问控制列表。)本章最后讨论 i 节点标志(也称为 ext2 扩展文件属性),它们控制内核对文件处理的各个方面。
15.1 Retrieving File Information: stat()
stat()、lstat() 和 fstat() 系统调用用于获取文件信息,这些信息大多来自文件 i 节点。
#include <sys/stat.h>
int stat(const char *pathname, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
这三个系统调用仅在文件指定的方式上有所不同:
- stat() 返回有关指定文件的信息;
- lstat() 类似于 stat(),不同之处在于,如果指定的文件是符号链接,则返回该链接本身的信息,而不是链接所指向的文件的信息;
- fstat() 返回由打开的文件描述符引用的文件的信息。
stat() 和 lstat() 系统调用不需要对文件本身的权限。然而,pathname 指定的所有父目录都需要具有执行(搜索)权限。fstat() 系统调用如果提供了有效的文件描述符,则总是会成功。
所有这些系统调用都会在 statbuf 指向的缓冲区中返回一个 stat 结构。该结构具有以下形式:
struct stat {
dev_t st_dev; /* IDs of device on which file resides */
ino_t st_ino; /* I-node number of file */
mode_t st_mode; /* File type and permissions */
nlink_t st_nlink; /* Number of (hard) links to file */
uid_t st_uid; /* User ID of file owner */
gid_t st_gid; /* Group ID of file owner */
dev_t st_rdev; /* IDs for device special files */
off_t st_size; /* Total file size (bytes) */
blksize_t st_blksize; /* Optimal block size for I/O (bytes) */
blkcnt_t st_blocks; /* Number of (512B) blocks allocated */
time_t st_atime; /* Time of last file access */
time_t st_mtime; /* Time of last file modification */
time_t st_ctime; /* Time of last status change */
};
💡 实际的stat结构可能与上面不同,详见Manual page stat(3type)
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 nanosecond
precision 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_mtine st_mtim.tv_sec
#define st_ctime st_ctim.tv_sec
};
从以上输出可知,最后3个时间字段已经过时,因其精度较低。
用于对 stat 结构中的字段进行类型定义的各种数据类型都在 SUSv3 中进行了指定。有关这些类型的更多信息,请参见第 3.6.2 节。
根据 SUSv3,当 lstat() 应用于符号链接时,只需要在 st_size 字段和 st_mode 字段中的文件类型组件(稍后描述)中返回有效信息。其他字段(例如时间字段)不需要包含有效信息。这为实现提供了不维护这些字段的自由,这可能出于效率原因而进行。特别是,早期 UNIX 标准的意图是允许符号链接以 i 节点或目录条目的形式实现。在后一种实现下,无法实现 stat 结构所要求的所有字段。(在所有主要的现代 UNIX 实现中,符号链接都实现为 i 节点。)在 Linux 上,当 lstat() 应用于符号链接时,会在所有 stat 字段中返回信息。
在接下来的页面中,我们将更详细地查看一些 stat 结构字段,并以一个显示整个 stat 结构的示例程序作为结束。
Device IDs and i-node number
st_dev 字段标识文件所在的设备。st_ino 字段包含文件的 i-node 编号。st_dev 和 st_ino 的组合可以唯一标识所有文件系统中的文件。dev_t 类型记录一个设备的主设备号和次设备号(参见第 14.1 节)。
如果这是一个设备的 i-node,那么 st_rdev 字段包含该设备的主设备号和次设备号。
dev_t 值的主设备号和次设备号可以使用两个宏提取:major() 和 minor()。获取这两个宏声明所需的头文件在不同的 UNIX 实现中有所不同。在 Linux 上,如果定义了 _BSD_SOURCE 宏,则它们由 <sys/types.h> 提供。
major() 和 minor() 返回的整数值大小在不同的 UNIX 实现中可能不同。为了可移植性,在打印时我们总是将返回值转换为 long 类型(参见第 3.6.2 节)。
File ownership
st_uid 和 st_gid 字段分别标识文件所属的所有者(用户 ID)和所属组(组 ID)。
Link count
st_nlink 字段表示指向该文件的(硬)链接数量。我们将在第18章中详细描述链接。
File type and permissions
st_mode 字段是一个位掩码,具有双重作用:既标识文件类型,又指定文件权限。该字段的各个位如图15-1所示。

Figure 15-1: Layout of st_mode bit mask
文件类型可以通过将此字段与常量 S_IFMT 进行按位与 (&) 来提取。(在 Linux 上,st_mode 字段的文件类型部分使用 4 位。然而,由于 SUSv3 对文件类型的表示方式没有规定,这一细节在不同实现中可能会有所不同。)然后可以将得到的值与一系列常量进行比较,以确定文件类型,如下所示:
if ((statbuf.st_mode & S_IFMT) == S_IFREG)
printf("regular file\n");
由于这是一个常见操作,因此提供了标准宏来简化上述操作,如下:
if (S_ISREG(statbuf.st_mode))
printf("regular file\n");
文件类型宏的完整集合(在 <sys/stat.h> 中定义)如表 15-1 所示。表 15-1 中的所有文件类型宏都在 SUSv3 中指定,并且在 Linux 上出现。一些其他的 UNIX 实现定义了额外的文件类型(例如,Solaris 上的门文件 S_IFDOOR)。类型 S_IFLNK 仅由 lstat() 调用返回,因为 stat() 调用总是跟随符号链接。
最初的 POSIX.1 标准没有指定表 15-1 第一列中显示的常量,尽管它们中的大多数出现在大多数 UNIX 实现中。SUSv3 要求这些常量。
为了从 <sys/stat.h> 获取 S_IFSOCK 和 S_ISSOCK() 的定义,我们必须定义 _BSD_SOURCE 特性测试宏,或者定义 _XOPEN_SOURCE 且其值大于或等于 500。(在不同的 glibc 版本中规则有所不同:在某些情况下,_XOPEN_SOURCE 必须定义为 600 或更大值。)
Table 15-1: Macros for checking file types in the st_mode field of the stat structure
| Constant | Test macro | File type |
|---|---|---|
| S_IFREG | S_ISREG() | Regular file |
| S_IFDIR | S_ISDIR() | Directory |
| S_IFCHR | S_ISCHR() | Character device |
| S_IFBLK | S_ISBLK() | Block device |
| S_IFIFO | S_ISFIFO() | FIFO or pipe |
| S_IFSOCK | S_ISSOCK() | Socket |
| S_IFLNK | S_ISLNK() | Symbolic link |
这些标准宏也可以在inode(7)中找到:
Because tests of the above form are common, additional macros are defined by POSIX to allow the test
of the file type in st_mode to be written more concisely:
S_ISREG(m) is it a regular file?
S_ISDIR(m) directory?
st_mode 字段的最低 12 位定义了文件的权限。我们在第 15.4 节中描述了文件权限位。现在,我们只需注意权限位中最低的 9 位是所有者、组和其他类别的读、写和执行权限。
File size, blocks allocated, and optimal I/O block size
对于常规文件,st_size 字段是文件的总大小,以字节为单位。对于符号链接,此字段包含链接所指向路径名的长度(以字节为单位)。
对于共享内存对象(第54章),此字段包含对象的大小。st_blocks 字段表示分配给该文件的总块数,以 512 字节为单位。这一总数包括为指针块分配的空间(见第258页图14-2)。选择 512 字节作为计量单位是历史原因——这是在 UNIX 下实现的任何文件系统中最小的块大小。更现代的文件系统使用更大的逻辑块大小。例如,在 ext2 下,st_blocks 的值总是 2、4 或 8 的倍数,具体取决于 ext2 逻辑块大小是 1024、2048 还是 4096 字节。
SUSv3 并未定义 st_blocks 的计量单位,这允许实现使用除 512 字节之外的单位。大多数 UNIX 实现确实使用 512 字节单位,但 HP-UX 11 在某些情况下使用文件系统特定的单位(例如 1024 字节)。
st_blocks 字段记录实际分配的磁盘块数量。如果文件包含空洞(第4.7节),该值将小于根据文件的相应字节数(st_size)可能预期的值。(磁盘使用命令 du -k file 显示文件的实际分配空间,以千字节为单位;也就是从文件的 st_blocks 值计算出的数字,而不是 st_size 值。)
st_blksize 字段的命名有些误导。它并不是底层文件系统的块大小,而是该文件系统上文件进行 I/O 的最佳块大小(以字节为单位)。以小于此大小的块进行 I/O 效率较低(参见第13.1节)。st_blksize 返回的典型值是 4096。
File timestamps
st_atime、st_mtime 和 st_ctime 字段分别包含上次文件访问时间、上次文件修改时间和上次状态更改时间。这些字段的类型为 time_t,即自纪元以来的秒数的标准 UNIX 时间格式。我们将在第 15.2 节中更多地讨论这些字段。
Example program
清单15-1中的程序使用 stat() 来检索命令行中指定文件的信息。如果指定了 -l 命令行选项,那么程序将改用 lstat(),以便我们可以检索符号链接的信息,而不是它所引用的文件。程序打印返回的 stat 结构的所有字段。(关于我们为什么将 st_size 和 st_blocks 字段强制转换为 long long,请参见第5.10节。)该程序使用的 filePermStr() 函数显示在第296页的清单15-4中。
下面是该程序使用的一个示例:
$ echo ABC > apue
$ chmod g+s apue # 打开设置组ID位;影响最后状态更改时间
$ cat apue > /dev/null # 影响最后文件访问时间
$ ./t_stat apue
File type: regular file
Device containing i-node: major=252 minor=0
I-node number: 17398022
Mode: 102644 (rw-r--r--)
special bits set: set-GID
Number of (hard) links: 1
Ownership: UID=1000 GID=1000
File size: 4 bytes
Optimal I/O block size: 4096 bytes
512B blocks allocated: 8
Last file access: Fri Mar 6 02:18:59 2026
Last file modification: Fri Mar 6 02:18:12 2026
Last status change: Fri Mar 6 02:18:50 2026
$ stat apue
File: apue
Size: 4 Blocks: 8 IO Block: 4096 regular file
Device: fc00h/64512d Inode: 17398022 Links: 1
Access: (2644/-rw-r-Sr--) Uid: ( 1000/ vagrant) Gid: ( 1000/ vagrant)
Context: unconfined_u:object_r:user_home_t:s0
Access: 2026-03-06 02:18:59.940780712 +0000
Modify: 2026-03-06 02:18:12.025488898 +0000
Change: 2026-03-06 02:18:50.838726549 +0000
Birth: 2026-03-06 02:18:12.025488898 +0000
Listing 15-1: Retrieving and interpreting file stat information
// files/t_stat.c
// 代码略
15.2 File Timestamps
stat 结构的 st_atime、st_mtime 和 st_ctime 字段包含文件时间戳。这些字段分别记录最后访问文件的时间、最后修改文件的时间,以及最后更改文件状态(即文件 i 节点信息的最后更改)的时间。时间戳以自 Epoch(1970 年 1 月 1 日;参见第 10.1 节)以来的秒数记录。
大多数原生 Linux 和 UNIX 文件系统支持所有时间戳字段,但一些非 UNIX 文件系统可能不支持。
表 15-2 总结了本书描述的各种系统调用和库函数会更改哪些时间戳字段(在某些情况下,还包括父目录中的类似字段)。在该表的标题中,a、m 和 c 分别表示 st_atime、st_mtime 和 st_ctime 字段。在大多数情况下,相关时间戳由系统调用设置为当前时间。例外情况是 utime() 和类似调用(在第 15.2.1 节和 15.2.2 节中讨论),它们可以用于显式将最后的文件访问和修改时间设置为任意值。
💡 st_ctime中的c不是create,而是status change。
Table 15-2: Effect of various functions on file timestamps
| Function | a | m | c | a | m | c | Notes |
|---|---|---|---|---|---|---|---|
| File or directory | … | … | Parent directory | … | … | ||
| chmod() | • | Same for fchmod() | |||||
| chown() | • | Same for lchown() and fchown() | |||||
| exec() | • | ||||||
| link() | • | • | • | Affects parent directory of second argument | |||
| mkdir() | • | • | • | • | • | ||
| mkfifo() | • | • | • | • | • | ||
| mknod() | • | • | • | • | • | ||
| mmap() | • | • | • | st_mtime and st_ctime are changed only on updates to MAP_SHARED mapping | |||
| msync() | • | • | Changed only if file is modified | ||||
| open(), creat() | • | • | • | • | • | When creating new file | |
| open(), creat() | • | • | When truncating existing file | ||||
| pipe() | • | • | • | ||||
| read() | • | Same for readv(), pread(), and preadv() | |||||
| readdir() | • | readdir() may buffer directory entries; timestamps updated only if directory is read | |||||
| removexattr() | • | Same for fremovexattr() and lremovexattr() | |||||
| rename() | • | • | • | Affects timestamps in both parent directories; SUSv3 doesn’t specify file st_ctime change, but notes that some implementations do this | |||
| rmdir() | • | • | Same for remove(directory) | ||||
| sendfile() | • | Timestamp changed for input file | |||||
| setxattr() | • | Same for fsetxattr() and lsetxattr() | |||||
| symlink() | • | • | • | • | • | Sets timestamps of link (not target file) | |
| truncate() | • | • | Same for ftruncate(); timestamps change only if file size changes | ||||
| unlink() | • | • | • | Same for remove(file); file st_ctime changes if previous link count was > 1 | |||
| utime() | • | • | • | Same for utimes(), futimes(), futimens(), lutimes(), and utimensat() | |||
| write() | • | • | Same for writev(), pwrite(), and pwritev() |
在第 14.8.1 节和第 15.5 节中,我们描述了 mount(2) 选项以及防止更新文件最后访问时间的每文件标志。第 4.3.1 节描述的 open() O_NOATIME 标志也具有类似的作用。在某些应用中,这可能在性能方面非常有用,因为它减少了访问文件时所需的磁盘操作次数。
尽管大多数 UNIX 系统不记录文件的创建时间,但在最近的 BSD 系统中,这一时间会记录在一个名为 st_birthtime 的 stat 字段中。
Linux的xfs和ext4都是支持创建时间的,参见输出中的Birth:
$ ls >1
$ stat 1
File: 1
Size: 19 Blocks: 2 IO Block: 1024 regular file
Device: 811h/2065d Inode: 13 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ vagrant) Gid: ( 1000/ vagrant)
Context: unconfined_u:object_r:unlabeled_t:s0
Access: 2026-04-23 08:59:36.861174499 +0000
Modify: 2026-04-23 08:59:36.862174504 +0000
Change: 2026-04-23 08:59:36.862174504 +0000
Birth: 2026-04-23 08:59:36.861174499 +0000
Nanosecond timestamps
在2.6版本中,Linux 对 stat 结构的三个时间戳字段支持纳秒级分辨率。纳秒级分辨率提高了需要根据文件时间戳相对顺序做出决策的程序的准确性(例如,make(1))。
SUSv3 并未为 stat 结构指定纳秒时间戳,但 SUSv4 增加了这一规范。
并非所有文件系统都支持纳秒时间戳。JFS、XFS、ext4 和 Btrfs 支持,而 ext2、ext3 和 Reiserfs 不支持。
在 glibc API(自 2.3 版本起)下,时间戳字段各自被定义为 timespec 结构(我们在本节稍后讨论 utimensat() 时会描述此结构),该结构以秒和纳秒表示时间。合适的宏定义使得这些结构的秒数部分可以通过传统字段名(st_atime、st_mtime 和 st_ctime)访问。纳秒部分可以使用字段名访问,例如 st_atim.tv_nsec,表示上次文件访问时间的纳秒部分。
15.2.1 Changing File Timestamps with utime() and utimes()
存储在文件 i-node 中的最后文件访问和修改时间戳可以使用 utime() 或一组相关的系统调用显式更改。像 tar(1) 和 unzip(1) 这样的程序在解压归档时使用这些系统调用来重置文件时间戳。
#include <utime.h>
int utime(const char *pathname, const struct utimbuf *buf);
pathname 参数用于标识我们希望修改其时间的文件。如果 pathname 是符号链接,它将被解引用。buf 参数可以为 NULL 或指向 utimbuf 结构的指针:
struct utimbuf {
time_t actime; /* Access time */
time_t modtime; /* Modification time */
};
该结构中的字段以自纪元以来的秒数来衡量时间(第10.1节)。
有两种不同的情况决定了 utime() 的工作方式:
- 如果 buf 指定为 NULL,则最后访问时间和最后修改时间都设置为当前时间。在这种情况下,进程的有效用户 ID 必须与文件的用户 ID(所有者)匹配,或者进程必须对文件具有写权限(逻辑上,因为具有文件写权限的进程可以使用其他系统调用,这些调用会有更改这些文件时间戳的副作用),或者进程必须具有特权(CAP_FOWNER 或 CAP_DAC_OVERRIDE)。
- (准确地说,在 Linux 上,检查文件的用户 ID 时,是进程的文件系统用户 ID,而不是其有效用户 ID,如第 9.5 节所述。)
- 如果 buf 被指定为指向一个 utimbuf 结构的指针,则将使用该结构的相应字段更新文件的最后访问和修改时间。在这种情况下,进程的有效用户 ID 必须与文件的用户 ID 匹配(仅对文件具有写权限是不够的),或者调用者必须具有特权(CAP_FOWNER)。
要仅更改一个文件的时间戳,我们首先使用 stat() 来检索两个时间,然后使用其中一个时间来初始化 utimbuf 结构,再将另一个时间设置为所需时间。以下代码演示了这一点,它将文件的最后修改时间设置为与最后访问时间相同:
struct stat sb;
struct utimbuf utb;
if (stat(pathname, &sb) == -1)
errExit("stat");
utb.actime = sb.st_atime; /* Leave access time unchanged */
utb.modtime = sb.st_atime;
if (utime(pathname, &utb) == -1)
errExit("utime");
对 utime() 的成功调用总是将最后状态更改时间设置为当前时间。
Linux 还提供了源自 BSD 的 utimes() 系统调用,它执行与 utime() 类似的任务。
#include <sys/time.h>
int utimes(const char *pathname, const struct timeval tv[2]);
utime() 和 utimes() 之间最显著的区别在于 utimes() 允许以微秒精度指定时间值(timeval 结构在第 10.1 节中描述)。这提供了对 Linux 2.6 中提供的文件时间戳的纳秒精度的(部分)访问。新的文件访问时间在 tv[0] 中指定,新的修改时间在 tv[1] 中指定。
utimes() 使用示例在本书的源代码分发中的 files/t_utimes.c 文件中提供。
futimes() 和 lutimes() 库函数执行与 utimes() 类似的任务。它们与 utimes() 的区别在于用于指定要更改时间戳的文件的参数不同。
#include <sys/time.h>
int futimes(int fd, const struct timeval tv[2]);
int lutimes(const char *pathname, const struct timeval tv[2]);
使用 futimes() 时,文件是通过一个打开的文件描述符 fd 来指定的。
使用 lutimes() 时,文件是通过路径名来指定的,其区别在于与 utimes() 不同,如果路径名指向一个符号链接,则不会取消引用该链接;相反,会更改链接本身的时间戳。
futimes() 函数自 glibc 2.3 起支持。lutimes() 函数自 glibc 2.6 起支持。
15.2.2 Changing File Timestamps with utimensat() and futimens()
utimensat() 系统调用(自内核 2.6.22 起支持)和 futimens() 库函数(自 glibc 2.6 起支持)提供了用于设置文件最后访问时间和最后修改时间的扩展功能。这些接口的优点包括:
- 我们可以以纳秒(即函数名中的ns)为单位设置时间戳。这比 utimes() 提供的微秒精度更高。
- 可以独立设置时间戳(即一次设置一个)。如前所示,要使用旧接口仅更改一个时间戳,我们必须先调用 stat() 获取另一个时间戳的值,然后将检索到的值与要更改的时间戳一起指定。(如果在这两个步骤之间另一个进程执行了更新时间戳的操作,这可能导致竞态条件。)
- 我们可以独立将任一时间戳设置为当前时间。要使用旧接口仅将一个时间戳更改为当前时间,我们需要调用 stat() 获取希望保持不变的时间戳的设置,并调用 gettimeofday() 获取当前时间。
这些接口在 SUSv3 中没有指定,但包含在 SUSv4 中。
utimensat() 系统调用将由 pathname 指定的文件的时间戳更新为数组 times 中指定的值。
#define _XOPEN_SOURCE 700 /* Or define _POSIX_C_SOURCE >= 200809 */
#include <sys/stat.h>
int utimensat(int dirfd, const char *pathname,
const struct timespec times[2], int flags);
如果 times 被指定为 NULL,则两个文件时间戳都会更新为当前时间。如果 times 不为 NULL,则新的最后访问时间戳由 times[0] 指定,新的最后修改时间戳由 times[1] 指定。数组 times 的每个元素都是以下形式的结构:
struct timespec {
time_t tv_sec; /* Seconds ('time_t' is an integer type) */
long tv_nsec; /* Nanoseconds */
};
该结构中的字段指定自纪元(第10.1节)以来以秒和纳秒为单位的时间。
要将其中一个时间戳设置为当前时间,我们在相应的 tv_nsec 字段中指定特殊值 UTIME_NOW。要保持其中一个时间戳不变,我们在相应的 tv_nsec 字段中指定特殊值 UTIME_OMIT。在这两种情况下,相应 tv_sec 字段中的值将被忽略。
dirfd 参数可以指定 AT_FDCWD,这种情况下 pathname 参数的解释与 utimes() 相同;也可以指定指向目录的文件描述符。后者的用途在第18.11节中描述。
flags 参数可以为 0,或者 AT_SYMLINK_NOFOLLOW,意思是如果 pathname 是符号链接,则不应对其进行解引用(即应更改符号链接本身的时间戳)。相比之下,utimes() 总是会解引用符号链接。
下面的代码片段将最后访问时间设置为当前时间,而将最后修改时间保持不变:
struct timespec times[2];
times[0].tv_sec = 0;
times[0].tv_nsec = UTIME_NOW;
times[1].tv_sec = 0;
times[1].tv_nsec = UTIME_OMIT;
if (utimensat(AT_FDCWD, "myfile", times, 0) == -1)
errExit("utimensat");
使用 utimensat()(以及 futimens())更改时间戳的权限规则与较旧的 API 类似,详细信息可参见 utimensat(2) 手册页。
futimens() 库函数会更新由打开的文件描述符 fd 所引用的文件的时间戳。
#include _GNU_SOURCE
#include <sys/stat.h>
int futimens(int fd, const struct timespec times[2]);
futimens() 的 times 参数的使用方式与 utimensat() 相同。
15.3 File Ownership
每个文件都有一个关联的用户 ID(UID)和组 ID(GID)。这些 ID 决定了文件属于哪个用户和组。我们现在来看决定新文件所有权的规则,并描述用于更改文件所有权的系统调用。
15.3.1 Ownership of New Files
当创建新文件时,其用户ID取自进程的有效用户ID。新文件的组ID可以取自进程的有效组ID(相当于System V的默认行为),也可以取自父目录的组ID(BSD行为)。后一种情况对于创建所有文件属于特定组且该组成员可以访问的项目目录非常有用。新文件使用哪一个值作为组ID取决于各种因素,包括新文件所在的文件系统类型。我们首先描述ext2和其他一些文件系统所遵循的规则。
准确地说,在Linux中,本节中所有对有效用户ID或组ID的使用实际上应该是文件系统用户ID或组ID(见第9.5节)。
当挂载 ext2 文件系统时,可以在 mount 命令中指定 –o grpid(或同义的 –o bsdgroups)选项,或者 –o nogrpid(或同义的 –o sysvgroups)选项。(如果未指定任何选项,默认是 –o nogrpid。)如果指定 –o grpid,则新文件总是继承其父目录的组 ID。如果指定 –o nogrpid,则默认情况下,新文件会从进程的有效组 ID 获取其组 ID。然而,如果目录启用了 set-group-ID 位(通过 chmod g s),那么文件的组 ID 会从父目录继承。这些规则在表 15-3 中进行了总结。
在第 18.6 节中,我们将看到,当目录上设置了 set-group-ID 位时,新创建的子目录也会继承该设置。通过这种方式,正文中描述的 set-group-ID 行为会沿整个目录树向下传播。
Table 15-3: Rules determining the group ownership of a newly created file
| File system mount option | Set-group-ID bit enabled on parent directory? | Group ownership of new file taken from |
|---|---|---|
| -o grpid, -o bsdgroups (ignored) | (ignored) | parent directory group ID |
| -o nogrpid, -o sysvgroups (default) | no | process effective group ID |
| -o nogrpid, -o sysvgroups (default) | yes | parent directory group ID |
在撰写本文时,唯一支持 grpid 和 nogrpid 挂载选项的文件系统是 ext2、ext3、ext4,以及(自 Linux 2.6.14 起)XFS。其他文件系统遵循 nogrpid 规则。
15.3.2 Changing File Ownership: chown(), fchown(), and Ichown()
chown()、lchown() 和 fchown() 系统调用用于更改文件的所有者(用户 ID)和组(组 ID)。
#include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
#define _XOPEN_SOURCE 500 /* Or: #define _BSD_SOURCE */
#include <unistd.h>
int lchown(const char *pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
这三个系统调用之间的区别类似于 stat() 系列系统调用:
- chown() 更改 pathname 参数中指定文件的所有权;
- lchown() 也执行相同的操作,但如果 pathname 是符号链接,则更改的是链接文件的所有权,而不是它所指向的文件;
- fchown() 更改通过打开的文件描述符 fd 所引用的文件的所有权。
owner 参数指定文件的新用户 ID,而 group 参数指定文件的新组 ID。要仅更改其中一个 ID,可以为另一个参数指定 –1 来保持该 ID 不变。
在 Linux 2.2 之前,chown() 不会取消符号链接的引用。Linux 2.2 改变了 chown() 的语义,并新增了 lchown() 系统调用以提供旧 chown() 系统调用的行为。只有拥有特权的 (CAP_CHOWN) 进程才能使用 chown() 来更改文件的用户 ID。
非特权进程可以使用 chown() 来更改它拥有的文件的组 ID(即进程的有效用户 ID 与文件的用户 ID 匹配),可更改为其所属的任何组。拥有特权的进程则可以将文件的组 ID 更改为任何值。
如果文件的所有者或所属组发生变化,则 set-user-ID 和 set-group-ID 权限位都会被关闭。这是一种安全预防措施,以确保普通用户无法在可执行文件上启用 set-user-ID(或 set-group-ID)位,然后以某种方式将其所有权变为某个特权用户(或组),从而在执行该文件时获得该特权身份。
示例:
$ cat a.sh
#!/bin/bash
id
$ chmod a+x a.sh
$ chmod u+s,g+s a.sh
$ ls -l a.sh
-rwsr-sr-x. 1 vagrant vagrant 15 Mar 6 07:00 a.sh
$ ./a.sh
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
$ sudo chown root a.sh
$ ls -l a.sh
-rwxr-xr-x. 1 root vagrant 15 Mar 6 07:00 a.sh
$ ./a.sh
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
SUSv3 并未指定当超级用户更改可执行文件的所有者或组时,是否应关闭 set-user-ID 和 set-group-ID 位。Linux 2.0 在这种情况下确实关闭了这些权限位,而一些早期的 2.2 内核(直到 2.2.12)则没有关闭。后来 2.2 内核恢复了 2.0 的行为,即超级用户的修改与其他用户相同,并且这种行为在后续内核版本中得以维持。(然而,如果我们在 root 登录下使用 chown(1) 命令更改文件所有权,那么在调用 chown(2) 之后,chown 命令会使用 chmod() 系统调用重新启用 set-user-ID 和 set-group-ID 权限位。)
在更改文件的所有者或组时,如果组执行权限位已经关闭,或者我们正在更改目录的所有权,则不会关闭 set-group-ID 权限位。在这两种情况下,set-group-ID 位被用于创建 set-group-ID 程序以外的目的,因此不希望关闭该位。set-group-ID 位的其他用途如下:
- 如果组执行权限位关闭,则 set-group-ID 权限位用于启用强制文件锁定(参见第 55.4 节)。
- 对于目录,set-group-ID 位用于控制在该目录中创建的新文件的所有权(参见第 15.3.1 节)。
第15-2清单演示了chown()的使用,这是一个程序,允许用户更改任意数量文件的所有者和组,这些文件作为命令行参数指定。(该程序使用第159页的清单8-1中的userIdFromName()和groupIdFromName()函数,将用户和组名称转换为相应的数字ID。)
Listing 15-2: Changing the owner and group of a file
// files/t_chown.c
// 代码略
15.4 File Permissions
在本节中,我们描述了应用于文件和目录的权限方案。虽然我们在这里主要讨论权限如何应用于常规文件和目录,但我们描述的规则适用于所有类型的文件,包括设备、FIFO 和 UNIX 域套接字。此外,System V 和 POSIX 进程间通信对象(共享内存、信号量和消息队列)也有权限掩码,这些对象适用的规则与文件的规则类似。
15.4.1 Permissions on Regular Files
如第15.1节所述,stat结构的st_mode字段的最低12位定义了文件的权限。这12位中的前三位是特殊位,称为设置用户ID(set-user-ID)、设置组ID(set-group-ID)和粘滞位(sticky bit)(在图15-1中分别标记为U、G和T)。我们将在第15.4.5节中详细讨论这些位。其余9位构成了定义授予访问文件的不同类别用户权限的掩码。文件权限掩码将世界分为三类:
- 所有者(也称为用户):授予文件所有者的权限。命令如 chmod(1) 使用术语用户,其中缩写 u 用于指代此权限类别。
- 用户组:授予属于文件所在组的用户的权限。
- 其他:授予所有其他用户的权限。
每个用户类别可能被授予三种权限: - 读取:可以读取文件内容。
- 写入:可以修改文件内容。
- 执行:可以执行文件(即它是程序或脚本)。为了执行脚本文件(例如bash脚本),需要同时具备读取和执行权限。
可以使用命令 ls –l 查看文件的权限和所有权,如以下示例所示:
$ ls -l t_chown
-rwxr-x--- 1 vagrant vagrant 27648 Mar 6 02:12 t_chown
在上面的例子中,文件权限显示为 rwxr-x—(此字符串前面的初始连字符表示此文件的类型:普通文件)。要解释这个字符串,我们将这 9 个字符分成每组三个字符,分别表示读取、写入和执行权限是否启用。第一组表示所有者的权限,拥有读取、写入和执行权限。下一组表示用户组的权限,拥有读取和执行权限,但不拥有写入权限。最后一组是其他用户的权限,未启用任何权限。
<sys/stat.h> 定义了可以与 stat 结构的 st_mode 进行按位与 (&) 操作的常量,以检查特定权限位是否被设置。(这些常量也通过包含 <fcntl.h> 定义,该文件声明了 open() 系统调用。)这些常量在表 15-4 中显示。
Table 15-4: Constants for file permission bits
| Constant | Octal value | Permission bit |
|---|---|---|
| S_ISUID | 04000 | Set-user-ID |
| S_ISGID | 02000 | Set-group-ID |
| S_ISVTX | 01000 | Sticky |
| S_IRUSR | 0400 | User-read |
| S_IWUSR | 0200 | User-write |
| S_IXUSR | 0100 | User-execute |
| S_IRGRP | 040 | Group-read |
| S_IWGRP | 020 | Group-write |
| S_IXGRP | 010 | Group-execute |
| S_IROTH | 04 | Other-read |
| S_IWOTH | 02 | Other-write |
| S_IXOTH | 01 | Other-execute |
除了表 15-4 中显示的常量外,还定义了三个常量,用于表示每个类别(所有者、组和其他)的三种权限的掩码:S_IRWXU (0700)、S_IRWXG (070) 和 S_IRWXO (07)。
清单 15-3 中的头文件声明了一个函数 filePermStr(),该函数给定一个文件权限掩码时,会返回该掩码的静态分配字符串表示,格式与 ls(1) 使用的风格相同。
Listing 15-3: Header file for file_perms.c
// files/file_perms.h
// 代码略
如果在 filePermStr() 的 flags 参数中设置了 FP_SPECIAL 标志,那么返回的字符串将包括 set-user-ID、set-group-ID 和 sticky 位的设置,同样采用 ls(1) 的风格。
filePermStr() 函数的实现如清单 15-4 所示。我们在清单 15-1 的程序中使用了此函数。
Listing 15-4: Convert file permissions mask to string
// files/file_perms.c
// 代码略
15.4.2 Permissions on Directories
目录具有与文件相同的权限方案。然而,这三种权限的解释有所不同:
- 读取:目录的内容(即文件名列表)可以被列出(例如,通过 ls)。
如果实验以验证目录读取权限位的操作,请注意某些 Linux 发行版将 ls 命令别名为包含需要访问目录中文件的 i 节点信息的标志(例如 -F),这需要目录的执行权限。为了确保我们使用的是原始的 ls 命令,可以指定命令的完整路径名(/bin/ls)。 - 写入:可以在目录中创建和删除文件。注意,要删除文件,本身并不需要对文件拥有任何权限。
- 执行:可以访问目录中的文件。目录的执行权限有时也称为搜索权限。
在访问文件时,路径名中列出的所有目录都需要执行权限。例如,读取文件 /home/mtk/x 需要对 /、/home 和 /home/mtk 拥有执行权限(以及对文件 x 本身的读取权限)。
如果当前工作目录是 /home/mtk/sub1,并且我们访问相对路径 …/sub2/x,那么我们需要对 /home/mtk 和 /home/mtk/sub2 拥有执行权限(但不需要对 / 或 /home 拥有权限)。
对目录的读取权限只允许我们查看目录中的文件名列表。我们必须对目录具有执行权限才能访问目录中文件的内容或 i 节点信息。
相反,如果我们对目录有执行权限,但没有读取权限,那么如果我们知道文件名,就可以访问目录中的文件,但无法列出目录的内容(即目录中的其他文件名)。这是一种简单且经常使用的技术,用于控制对公共目录内容的访问。
$ sudo mkdir test
$ sudo sh -c 'echo one > ./test/1'
$ sudo chmod o+x-r test
$ sudo ls -ld test
drwxr-x--x. 2 root root 15 Mar 6 07:29 test
$ ls test
ls: cannot open directory 'test': Permission denied
$ cat ./test/1
one
要在目录中添加或删除文件,我们需要对目录同时具有执行和写入权限。
15.4.3 Permission-Checking Algorithm
每当我们在访问文件或目录的系统调用中指定路径名时,内核都会检查文件权限。当系统调用提供的路径名包含目录前缀时,除了检查文件本身所需的权限之外,内核还会检查该前缀中每个目录的执行权限。权限检查使用进程的有效用户ID、有效组ID和补充组ID。(严格来说,在Linux上进行文件权限检查时,使用的是文件系统的用户和组ID,而不是相应的有效ID,如第9.5节所述。)
一旦文件已经使用 open() 打开,后续使用返回的文件描述符的系统调用(例如 read()、write()、fstat()、fcntl() 和 mmap())将不再执行权限检查。
内核在检查权限时应用的规则如下:
- 如果进程具有特权,则授予所有访问权限。
- 如果进程的有效用户ID与文件的用户ID(所有者)相同,则根据文件上所有者的权限授予访问。例如,如果文件权限掩码中启用了所有者读取权限位,则授予读取访问权限;否则,拒绝读取访问权限。
- 如果进程的有效组ID或任何辅助组ID与文件的组ID(组所有者)匹配,则根据文件上的组权限授予访问权限。
- 否则,根据文件上的其他权限授予访问权限。
在内核代码中,上述测试实际上是这样构造的:只有当一个进程未通过其他测试之一获得所需权限时,才会执行检查该进程是否具有特权的测试。这么做是为了避免不必要地设置 ASU 进程计费标志,该标志表明进程使用了超级用户权限(第28.1节)。
对所有者、组和其他权限的检查是按顺序进行的,一旦找到适用的规则,检查就会停止。这可能会产生意想不到的后果:例如,如果组的权限超过所有者的权限,那么实际上所有者对文件的权限会少于文件组的成员,如下例所示:
代码略
如果其他用户授予的权限比所有者或组更多,也适用类似的说法。
由于文件权限和所有权信息都保存在文件的 i 节点中,所有指向同一个 i 节点的文件名(链接)共享这些信息。
Linux 2.6 提供了访问控制列表(ACLs),使得可以基于每个用户和每个组定义文件权限。如果一个文件具有 ACL,则使用上述算法的修改版本。我们在第 17 章中描述 ACL。
Permission checking for privileged processes
如上所述,我们提到如果一个进程具有特权,则在检查权限时会授予所有访问权限。我们需要在此声明中添加一个附带条件。对于不是目录的文件,Linux 仅在至少一个文件权限类别授予执行权限时,才会授予特权进程执行权限。在其他一些 UNIX 实现中,即使没有任何权限类别授予执行权限,特权进程也可以执行文件。访问目录时,特权进程总是被授予执行(搜索)权限。
💡 特权进程访问目录时总能获得执行(搜索)权限,但执行普通文件时,必须至少有一个权限类别(属主 / 属组 / 其他)开启执行位,否则即使是 root 也无法执行。
示例:
$ sudo -s
$ touch a.sh
$ ls -l a.sh
-rwxr-xr-x. 1 root vagrant 15 Mar 6 07:51 a.sh
$ chmod a-x a.sh
$ ls -l a.sh
-rw-r--r--. 1 root vagrant 15 Mar 6 07:51 a.sh
$ ./a.sh
bash: ./a.sh: Permission denied
我们可以用两个 Linux 进程权限来重新描述特权进程:CAP_DAC_READ_SEARCH 和 CAP_DAC_OVERRIDE(第 39.2 节)。拥有 CAP_DAC_READ_SEARCH 权限的进程总是对任何类型的文件具有读取权限,并且总是对目录具有读取和执行权限(即,总是可以访问目录中的文件并读取目录中的文件列表)。拥有 CAP_DAC_OVERRIDE 权限的进程总是对任何类型的文件具有读取和写入权限,如果文件是目录或者文件的至少一个权限类别被授予了执行权限,也拥有执行权限。
15.4.4 Checking File Accessibility: access()
如第15.4.3节所述,有效用户ID和组ID,以及附加组ID,用于确定进程在访问文件时所具有的权限。程序(例如,set-user-ID或set-group-ID程序)也可以根据进程的真实用户和组ID检查文件的可访问性。
access()系统调用根据进程的真实用户ID和组ID(以及附加组ID)检查pathname指定的文件的可访问性。
#include <unistd.h>
int access(const char *pathname, int mode);
如果 pathname 是一个符号链接,access() 会对其进行解引用。
mode 参数是一个位掩码,由表 15-5 中显示的一个或多个常量通过按位或 (|) 组合而成。如果 pathname 上授予了 mode 中指定的所有权限,则 access() 返回 0;如果至少有一个请求的权限不可用(或发生错误),则 access() 返回 –1。
Table 15-5: mode constants for access()
| Constant | Description |
|---|---|
| F_OK | Does the file exist? |
| R_OK | Can the file be read? |
| W_OK | Can the file be written? |
| X_OK | Can the file be executed? |
从调用 access() 到随后的对文件操作之间的时间差意味着无法保证 access() 返回的信息在后续操作时仍然成立(无论间隔多么短)。这种情况可能导致某些应用程序设计中的安全漏洞。
例如,假设我们有一个 set-user-ID-root 程序,它使用 access() 来检查文件对于程序的真实用户 ID 是否可访问,如果可访问,则对文件执行某个操作(例如 open() 或 exec())。
问题在于,如果传递给 access() 的路径名是符号链接,并且恶意用户在第二步之前成功更改了该链接,使其指向另一个文件,那么以 set-user-ID-root 运行的程序可能最终会操作一个实际用户 ID 没有权限的文件。(这是第 38.6 节中描述的检查时-使用时竞态条件类型的一个例子。)因此,推荐的做法是完全避免使用 access()(例如参见 [Borisov, 2005])。在刚才给出的例子中,我们可以通过临时更改 set-user-ID 进程的有效(或文件系统)用户 ID,尝试所需的操作(例如 open() 或 exec()),然后检查返回值和 errno,以确定操作是否因权限问题而失败来实现这一点。
GNU C 库提供了一个类似的、非标准函数 euidaccess()(或同义词 eaccess()),它使用进程的有效用户 ID 来检查文件访问权限。
15.4.5 Set-User-lD, Set-Group-lD, and Sticky Bits
除了用于所有者、组和其他权限的9个位之外,文件权限掩码还包含3个附加位,称为设置用户ID(位04000)、设置组ID(位02000)和粘滞位(位01000)。我们已经在第9.3节中讨论了设置用户ID和设置组ID权限位用于创建特权程序的用途。设置组ID位还有另外两个用途,我们在其他地方描述过:控制在使用nogrpid选项挂载的目录中创建的新文件的组所有权(第15.3.1节),以及在文件上启用强制锁定(第55.4节)。在本节的其余部分,我们将讨论范围限制在粘滞位的使用上。
在较早的UNIX实现中,粘性位的存在是为了让常用程序运行更快。如果粘性位被设置在程序文件上,那么程序第一次执行时,程序文本的副本会保存在交换区域——因此它会停留在交换中,并在后续执行中加载得更快。
现代UNIX实现拥有更复杂的内存管理系统,使得这种粘性权限位的使用变得过时。
表15-4中,S_ISVTX中粘性许可位常数的名称源自粘性位的另一个名称:保存文本位。
在现代 UNIX 实现(包括 Linux)中,粘性权限位还有另一个完全不同的用途。对于目录,粘性位充当受限删除标志。在目录上设置此位意味着非特权进程只有在拥有该目录写入权限且拥有文件或目录时,才能解除链接(unlink()、rmdir())和重命名(rename())文件。(具有CAP_FOWNER能力的进程可以绕过后者的所有权检查。)这使得创建一个由多个用户共享的目录成为可能,每个用户可以在目录中创建和删除自己的文件,但不能删除其他用户拥有的文件。因此,/tmp目录中通常设置了粘性权限位。
$ ls -ld /tmp
drwxrwxrwt. 9 root root 4096 Mar 6 07:48 /tmp
$ touch tfile
$ ls -l tfile
-rw-r--r--. 1 vagrant vagrant 0 Mar 6 08:06 tfile
$ chmod +t tfile
$ ls -l tfile
-rw-r--r-T. 1 vagrant vagrant 0 Mar 6 08:06 tfile
$ chmod o+x tfile
$ ls -l tfile
-rw-r--r-t. 1 vagrant vagrant 0 Mar 6 08:06 tfile
$ rm tfile
15.4.6 The Process File Mode Creation Mask umask()
我们现在更详细地考虑新创建的文件或目录所赋予的权限。对于新文件,内核会根据模式参数指定的权限打开()或创建。对于新目录,权限根据 mod 参数 mkdir( 的参数)设置。然而,这些设置会被文件模式创建掩码(也称为umask)修改。umask 是一个进程属性,指定在进程创建新文件或目录时应始终关闭哪些权限位。
通常,进程仅使用其继承自父壳的 umask,通常理想的结果是用户可以通过 shell 内置命令 umask 控制从该 shell 执行的程序 umask,从而改变 shell 进程的 umask。
大多数shell的初始化文件将默认umask设置为八进制值022(----w–w-)。该值指定写权限应始终关闭组和其他用户。因此,假设调用open()时的模式参数为0666(即所有用户均允许读写,这很常见),那么新文件会创建,拥有所有者的读写权限,仅对其他人有读取权限(用 ls –l 表示为 rw-r-r–)。相应地,假设 mkdir() 的 mode 参数指定为 0777(即所有用户均获授权),则创建新目录,所有者获得所有权限,仅读取并执行组及其他权限(如 rwxr-xr-x)。
umask() 系统调用将进程的 umask 变为 mask 指定值。
#include <sys/stat.h>
mode_t umask(mode_t mask);
掩码参数可以指定为八进制数,也可以通过按位或(|)组合表 15-4 中列出的常量来指定。
对 umask() 的调用总是成功的,并返回之前的 umask。
清单 15-5 展示了 umask() 与 open() 和 mkdir() 结合使用的情况。当我们运行这个程序时,会看到以下内容:
$ ./t_umask
Requested file perms: rw-rw---- # 要求的;普通文件的最大权限(660)
Process umask: ----wx-wx # 被拒绝的
Actual file perms: rw-r----- # 最终的
Requested dir. perms: rwxrwxrwx
Process umask: ----wx-wx
Actual dir. perms: rwxr--r--
Listing 15-5: Using umask()
// files/t_umask.c
// 代码略
15.4.7 Changing File Permissions: chmod() and fchmod()
chmod() 和 fchmod() 系统调用用于更改文件的权限。
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
#define _XOPEN_SOURCE 500 /* Or: #define _BSD_SOURCE */
#include <sys/stat.h>
int fchmod(int fd, mode_t mode);
chmod() 系统调用更改 pathname 中指定文件的权限。如果该参数是符号链接,chmod() 会更改它所指向文件的权限,而不是链接本身的权限。(符号链接总是为所有用户启用读、写和执行权限创建的,并且这些权限不能更改。取消引用该链接时,这些权限会被忽略。)
fchmod() 系统调用用于更改由打开的文件描述符 fd 指向的文件的权限。
mode 参数指定文件的新权限,可以用数字形式(八进制)表示,也可以通过按位或(|)形成的掩码表示,如表 15-4 所列的权限位。为了更改文件权限,进程必须具有特权(CAP_FOWNER),或者其有效用户 ID 必须与文件的所有者(用户 ID)匹配。(严格来说,在 Linux 上,对于非特权进程,应使用进程的文件系统用户 ID,而不是其有效用户 ID,与文件的用户 ID 匹配,如第 9.5 节所述。)
为了设置文件权限,使所有用户仅具有读取权限,我们可以使用以下调用:
if (chmod("myfile", S_IRUSR | S_IRGRP | S_IROTH) == -1)
errExit("chmod");
/* Or equivalently: chmod("myfile", 0444); */
为了修改文件权限的选定位,我们首先使用 stat() 获取现有权限,调整我们想要更改的位,然后使用 chmod() 更新权限:
struct stat sb;
mode_t mode;
if (stat("myfile", &sb) == -1)
errExit("stat");
mode = (sb.st_mode | S_IWUSR) & ~S_IROTH;
/* owner-write on, other-read off, remaining bits unchanged */
if (chmod("myfile", mode) == -1)
errExit("chmod");
上述等价于以下 shell 命令:
$ chmod u w,o-r myfile
在第15.3.1节中,我们注意到,如果一个目录位于使用 -o bsdgroups 选项挂载的 ext2 系统上,或者位于使用 -o sysvgroups 选项挂载且目录的 set-group-ID 权限位被打开的系统上,那么在该目录中新创建的文件会继承父目录的所有权,而不是创建该文件的进程的有效组ID。这样的文件的组ID可能与创建进程的任何组ID都不匹配。因此,当一个非特权进程(没有 CAP_FSETID 能力的进程)对一个其组ID不等于该进程的有效组ID或任何补充组ID的文件调用 chmod()(或 fchmod())时,内核总是会清除 set-group-ID 权限位。这是一种安全措施,旨在防止用户为其不属于的组创建 set-group-ID 程序。下面的 shell 命令展示了该措施预防的尝试性漏洞利用情况:
$ sudo mount -t ext4 -o bsdgroups /dev/sdb1 /testfs
$ mount |grep testfs
/dev/sdb1 on /testfs type ext4 (rw,relatime,seclabel,grpid)
$ ls -ld /testfs
drwxr-xr-x. 3 root root 1024 Mar 5 09:09 /testfs
$ sudo chmod a+w /testfs
$ ls -ld /testfs
drwxrwxrwx. 3 root root 1024 Mar 5 09:09 /testfs
$ id
uid=1000(vagrant) gid=1000(vagrant) groups=1000(vagrant),10(wheel) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
$ cd /testfs
$ cp ~/myprog .
$ ls -l myprog
-rw-r--r--. 1 vagrant root 0 Mar 6 08:39 myprog
$ chmod g+s myprog
# 虽未报错,但实际并没修改成功
$ echo $?
0
$ ls -l myprog
-rw-r--r--. 1 vagrant root 0 Mar 6 08:39 myprog
15.5 i-node Flags (ext2 Extended File Attributes)
一些 Linux 文件系统允许在文件和目录上设置各种 i-node 标志。这一特性是非标准的 Linux 扩展。
现代 BSD 系统提供了类似 i-node 标志的功能,通过使用 chflags(1) 和 chflags(2) 设置文件标志。
第一个支持 i-node 标志的 Linux 文件系统是 ext2,这些标志有时被称为 ext2 扩展文件属性。随后,其他文件系统也添加了对 i-node 标志的支持,包括 Btrfs、ext3、ext4、Reiserfs(自 Linux 2.4.19 起)、XFS(自 Linux 2.4.25 和 2.6 起)以及 JFS(自 Linux 2.6.17 起)。
不同文件系统支持的 i-node 标志范围略有不同。为了在 Reiserfs 文件系统上使用 i-node 标志,我们在挂载文件系统时必须使用 mount –o attrs 选项。
在 shell 中,可以使用 chattr 和 lsattr 命令设置和查看 i-node 标志,如以下示例所示:
$ lsattr myprog
--------------e------- myprog
$ chattr +ai myprog
chattr: Operation not permitted while setting flags on myprog
$ sudo chattr +ai myprog
$ lsattr myprog
----ia--------e------- myprog
在程序中,可以使用 ioctl() 系统调用来获取和修改 i-node 标志,如下文所述。
i-node 标志可以设置在普通文件和目录上。大多数 i-node 标志是用于普通文件的,尽管其中一些也适用于(或仅适用于)目录。表 15-6 总结了可用的 i-node 标志范围,显示了在程序中通过 ioctl() 调用使用的对应标志名称(在 <linux/fs.h> 中定义),以及在 chattr 命令中使用的选项字母。
在 Linux 2.6.19 之前,表 15-6 中显示的 FS_* 常量在 <linux/fs.h> 中未定义。相反,有一组文件系统特定的头文件,它们定义了文件系统特定的常量名称,且值都相同。因此,ext2 有 EXT2_APPEND_FL,在 <linux/ext2_fs.h> 中定义;Reiserfs 有 REISERFS_APPEND_FL,在 <linux/reiser_fs.h> 中以相同的值定义;依此类推。由于每个头文件都用相同的值定义了相应的常量,因此在不提供 <linux/fs.h> 定义的旧系统上,可以包含任何一个头文件并使用文件系统特定的名称。
Table 15-6: I-node flags
| Constant | chattr option | Purpose |
|---|---|---|
| FS_APPEND_FL | a | Append only (privilege required) |
| FS_COMPR_FL | c | Enable file compression (not implemented) |
| FS_DIRSYNC_FL | D | Synchronous directory updates (since Linux 2.6) |
| FS_IMMUTABLE_FL | i | Immutable (privilege required) |
| FS_JOURNAL_DATA_FL | j | Enable data journaling (privilege required) |
| FS_NOATIME_FL | A | Don’t update file last access time |
| FS_NODUMP_FL | d | No dump |
| FS_NOTAIL_FL | t | No tail packing |
| FS_SECRM_FL | s | Secure deletion (not implemented) |
| FS_SYNC_FL | S | Synchronous file (and directory) updates |
| FS_TOPDIR_FL | T | Treat as top-level directory for Orlov (since Linux 2.6) |
| FS_UNRM_FL | u | File can be undeleted (not implemented) |
各种 FL_* 标志及其含义如下:
- FS_APPEND_FL
仅当指定 O_APPEND 标志时,文件才能以写入方式打开(从而强制所有文件更新追加到文件末尾)。例如,这个标志可以用于日志文件。只有具有特权的进程(CAP_LINUX_IMMUTABLE)才能设置此标志。 - FS_COMPR_FL
以压缩格式将文件存储在磁盘上。该功能并未作为任何主要本地 Linux 文件系统的标准部分实现。(有一些软件包为 ext2 和 ext3 实现了此功能。)考虑到磁盘存储成本低、压缩和解压缩涉及的 CPU 开销,以及压缩文件意味着通过 lseek() 随机访问文件内容不再那样简单,因此对于许多应用程序来说,文件压缩是不理想的。 - FS_DIRSYNC_FL(自 Linux 2.6 起)
使目录更新(例如 open(pathname, O_CREAT)、link()、unlink() 和 mkdir())同步。这类似于第13.3节中描述的同步文件更新机制。与同步文件更新一样,同步目录更新也会影响性能。此设置只能应用于目录。(第14.8.1节中描述的 MS_DIRSYNC 挂载标志提供了类似功能,但它是按挂载点应用的。) - FS_IMMUTABLE_FL
使文件不可变。文件数据无法更新(write() 和 truncate()),元数据更改也被阻止(例如 chmod()、chown()、unlink()、link()、rename()、rmdir()、utime()、setxattr() 和 removexattr())。只能由特权(CAP_LINUX_IMMUTABLE)进程为文件设置此标志。当设置该标志时,即使是特权进程也无法更改文件内容或元数据。 - FS_JOURNAL_DATA_FL
启用数据日志记录。此标志仅支持 ext3 和 ext4 文件系统。这些文件系统提供三种日志记录级别:journal、ordered 和 writeback。所有模式都会记录文件元数据的更新,但 journal 模式还会记录文件数据的更新。在以 ordered 或 writeback 模式进行日志记录的文件系统上,具有特权(CAP_SYS_RESOURCE)的进程可以通过设置此标志按文件启用数据更新的日志记录。(mount(8) 手册页描述了 ordered 和 writeback 模式之间的区别。) - FS_NOATIME_FL
在访问文件时,不更新时间文件的最后访问时间。这消除了每次访问文件时更新文件 i 节点的需求,从而提高 I/O 性能(参见第 14.8.1 节中 MS_NOATIME 标志的描述)。 - FS_NODUMP_FL
不要将此文件包含在使用 dump(8) 制作的备份中。此标志的效果取决于 dump(8) 手册页中描述的 –h 选项。 - FS_NOTAIL_FL
禁用尾部打包。此标志仅在 Reiserfs 文件系统上受支持。它禁用 Reiserfs 的尾部打包功能,该功能试图将小文件(以及较大文件的最后片段)打包到与文件元数据相同的磁盘块中。也可以通过使用 mount –notail 选项挂载整个 Reiserfs 文件系统来禁用尾部打包。 - FS_SECRM_FL
安全删除文件。此未实现功能的预期用途是在删除文件时安全删除,即先覆盖文件以防止磁盘扫描程序读取或重新创建它。(真正安全删除的问题相当复杂:在磁性介质上可能需要多次写入才能安全擦除之前记录的数据;参见 [Gutmann, 1996]。) - FS_SYNC_FL
使文件更新同步。当应用于文件时,此标志会导致对文件的写入操作是同步的(就好像在所有打开此文件的操作中指定了 O_SYNC 标志一样)。当应用于目录时,此标志具有与上述同步目录更新标志相同的效果。 - FS_TOPDIR_FL(自 Linux 2.6 起)
这会标记一个目录,在 Orlov 块分配策略下进行特殊处理。Orlov 策略是受 BSD 启发的 ext2 块分配策略的修改,试图提高相关文件(例如单个目录中的文件)被放置在磁盘上彼此相近的几率,从而可以改善磁盘寻道时间。详情请参见 [Corbet, 2002] 和 [Kumar 等, 2008]。FS_TOPDIR_FL 仅对 ext2 及其衍生版本 ext3 和 ext4 起作用。 - FS_UNRM_FL
允许在文件被删除后恢复该文件(撤销删除)。此功能尚未实现,因为可以在内核之外实现文件恢复机制。
通常,当在目录上设置 i-node 标志时,它们会被该目录中创建的新文件和子目录自动继承。但此规则有例外:
- FS_DIRSYNC_FL(chattr D)标志只能应用于目录,仅会被该目录中创建的子目录继承。
- 当 FS_IMMUTABLE_FL(chattr i)标志应用于目录时,它不会被该目录中创建的文件和子目录继承,因为该标志会阻止向目录中添加新条目。
在程序中,可以使用 ioctl() 的 FS_IOC_GETFLAGS 和 FS_IOC_SETFLAGS 操作来获取和修改 i-node 标志。(这些常量在 <linux/fs.h> 中定义。)以下代码演示了如何在由打开的文件描述符 fd 引用的文件上启用 FS_NOATIME_FL 标志:
int attr;
if (ioctl(fd, FS_IOC_GETFLAGS, &attr) == -1) /* Fetch current flags */
errExit("ioctl");
attr |= FS_NOATIME_FL;
if (ioctl(fd, FS_IOC_SETFLAGS, &attr) == -1) /* Update flags */
errExit("ioctl");
为了更改文件的 i-node 标志,进程的有效用户 ID 必须与文件的用户 ID(所有者)匹配,或者进程必须具有特权(CAP_FOWNER)。准确地说,在 Linux 上,对于非特权进程,必须匹配文件的用户 ID 的不是进程的有效用户 ID,而是进程的文件系统用户 ID,如第 9.5 节所述。
15.6 Summary
stat() 系统调用用于检索文件的信息(元数据),大部分信息来自文件的 i 节点。这些信息包括文件的所有权、文件权限和文件时间戳。
程序可以使用 utime()、utimes() 以及各种类似接口来更新文件的最后访问时间和最后修改时间。
每个文件都有一个关联的用户 ID(所有者)和组 ID,以及一组权限位。出于权限的目的,文件用户被分为三类:所有者(也称为用户)、组和其他人。每类用户可以被授予三种权限:读取、写入和执行。目录也使用相同的方案,尽管权限位的含义略有不同。chown() 和 chmod() 系统调用用于更改文件的所有权和权限。umask() 系统调用设置一个权限位掩码,当调用进程创建文件时,该掩码中的权限位始终被关闭。
文件和目录使用三个附加的权限位。设置用户ID(set-user-ID)和设置组ID(set-group-ID)权限位可以应用于程序文件,用于创建使执行进程通过采用不同的有效用户或组身份(即程序文件的身份)来获得特权的程序。对于使用 nogrpid(sysvgroups)选项挂载的文件系统上的目录,设置组ID权限位可以用来控制目录中新建的文件是继承进程的有效组ID,还是继承父目录的组ID。当应用于目录时,粘滞位(sticky bit)权限作为受限删除标志。
I 节点标志控制文件和目录的各种行为。虽然最初为 ext2 定义,这些标志现在也被多个其他文件系统支持。
6953

被折叠的 条评论
为什么被折叠?



