18.1-2 i-node 和链接
i-node
i 节点一般写作 i-node,中间有个短横线。i-node 表的编号开始于 1,因为 0 用来表示未使用的条目,然后 i-node 1 用来记录文件系统的坏块,i-node 2 用来记录根目录,即 /。由于可能有多个路径不同、名称不同的文件(通过硬链接)指向同一个 i-node,因此 i-node 中并不记录本文件的名称。
符号链接
符号链接的所有权和权限在大多数情况下会被忽略。仅在符号链接所在目录具有粘着位,又要删除或者重命名符号链接时,才会考虑符号链接本身的所有权。
符号链接解引用的次数是有限的,SUSv3 规定对路径中的每个符号链接部件至少 _POSIX_SYMLOOP_MAX
即 8 次。内核 2.6.18 之前,Linux 最多支持 5 次,2.6.18 时支持了 8 次。Linux 还规定一个完整路径中符号链接引用解除次数最多 40 次。
符号链接使用 symlink
创建,使用 readlink
读取。
link
和 unlink
(不会解除符号链接引用)
link
创建(硬)链接,unlink
解除链接(也就是删除文件)。SUSv3 规定 link
要解符号链接引用,但是 Linux 并没有这样实现。现在 SUSv4 规定 link
调用是否解除链接由实现定义。
unlink
不会对符号链接解引用(SUSv3 在 Linux 上也没有歧义?)。
rename
(不会解除符号链接引用)
这个系统调用可以重命名文件或者目录,其实际效果是移动文件,但是又和 mv
命令有些不同。第一,rename 两个参数要么都是目录名,要么都不是目录名,不像 mv
默认行为那样当第二个参数为已存在的目录名时,会将第一个参数移动到第二个目录中去。mv
其实也有 -T
参数,这样就和 rename
的语义一样:如果 oldpath 是文件夹,newpath 也是文件夹,假设 oldpath 存在,只有当 newpath 存在、为空、进程有权限将其删除,或者 newpath 不存在当进程有权限创建时才能重命名文件夹。第二,oldpath 和 newpath 必须存在于同一个文件系统内,mv
命令则做了包装,在遇到跨文件系统拷贝时实际上是先复制再删除旧文件。
rename()
系统调用对其两个参数中的符号链接均不进行解引用。
mkdir
和 rmdir
(不会解除符号链接引用)
mkdir
需要路径名 pathname 和文件创建模式 mode。对于 mode 参数,在 Linux 中:
- set-user-ID 始终关闭,因为对目录没用。
- set-group-ID 被忽略,转而按照父目录中的 set-group-ID 位来传播。(所以第一次使用 set-group-ID 需要在创建目录后显式设置,要分两步。)
- 粘滞位被尊重。
- 新创建目录的权限(9 位)受到父目录默认型 ACL 或者进程当前 umask 的影响。
SUSv3 明文规定,
mkdir()
对 set-user-ID、set-group-ID 以及粘滞位的处理方式由实现定义。
rmdir
要删除的目录必须是空的,而且 rmdir
不会对符号链接解引用。
remove
(不会解除符号链接引用)
此函数由 stdlib.h 提供,在文件为文件夹时调用 rmdir
,否则调用 unlink
。
读目录 opendir
和 readdir
追踪目录类型用的是 DIR *
,这个和 C 语言标准库中的 FILE *
非常相似。用 opendir
按名字打开目录、用 fopendir
按 fd 打开目录。不过使用 fopendir
之后,目录就应该由 DIR *
结构管理,在用 closedir
关闭目录的时候,内层的 fd 也会被关闭,不能被再次使用,我们也不需要手动对 fd 关闭。opendir
还会默认为和目录关联的文件描述符设置 FD_CLOEXEC
标志,以保证在执行 exec()
时自动关闭该文件描述符。
因为目录流和 fd 是有关联的,所以可以通过系统调用 dirfd
来获取目录流对应的 fd,就像 fileno
可以获取文件流对应的 fd 一样。比如,可以将 dirfd
返回的 fd 传给 fchdir
。
每次调用对 DIR *
调用 readdir 都会返回一个 struct dirent *
,其中至少有 i-node 号和文件名信息,其他信息在不同的 UNIX 系统中各有补充。这个结构是静态分配的,不需要我们 free
。
readdir
是不可重入的,但是对同一个目录打开多个目录流则相互不会影响,可以保证线程安全(就像文件流一样)。有一个可重入的版本是 readdir_r
,需要我们提前分配好内存(需要使用到 offsetof
这个宏来保证兼容性,因为 d_name
这个字段的值是可变长的、放在结构体最后的,但是前面有多少个属性在不同平台上会有不一样),但是因为其实现的原因(比如不支持非常长的文件名等),已经不推荐使用。
readdir
返回的文件名没有经过排序。scandir
返回的文件名经过了排序。
文件树遍历:nftw
#include <ftw.h>
int nftw(const char *dirpath,
int (*fn)(const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf),
int nopenfd, int flags);
ftw
是 file tree walk,nftw
加了 n 可能表示 new。默认情况下,nftw
执行的是前序遍历,也就是先遍历目录本身,再去遍历目录中的文件和文件夹。
nftw 的参数
第 1 个参数是要遍历的目录。第 2 个参数是回调。
第 3 个参数是允许 nftw 打开的文件描述符的数量,如果 nftw 打开的文件描述符快要超过给定的数量限制,就会记录信息然后关闭一部分文件描述符。书中认为,现在的操作系统上这个值可以给的高一些,比如 10 或者更多。
第 4 个参数 flags
可以用以下常量的按位或组成:
FTW_CHDIR
是每次执行回调之前都改变当前文件夹。FTW_DEPTH
是后序遍历。FTW_MOUNT
是指不会进入另外一个文件系统,也就是如果一个目录是挂载点,就不会深入遍历。FTW_PHYS
会告诉 nftw 遍历的时候不要对符号引用解引用。
nftw 回调函数的参数
nftw
回调函数的第 3 个参数 typeflag
给出了当前文件的信息,可能是以下值之一:
FTW_D
目录。FTW_DNR
不能遍历的目录。FTW_DP
正在对一个目录进行后序遍历,当前项是一个目录,其所包含的文件和子目录已经处理完毕。FTW_F
该文件的类型是除目录和符号链接以外的任何类型。FTW_NS
对该文件调用stat()
失败,可能是因为权限限制。const struct stat *sb
中的值未定义。FTW_SL
符号链接,仅当标志 FTW_PHYS 存在的时候才可能出现这个类型。FTW_SLN
悬空的符号链接。
第 4 个参数 ftwbuf
所指向的类型定义如下:
struct FTW {
int base; // basename
int level; // 文件遍历深度
};
回调函数返回 0 表示继续遍历,返回非 0 表示停止遍历,结束整个遍历操作。不过,始于 2.3.3 版本,glibc 允许在 ntfw()
的 flags 参数中指定一个额外的非标准标志 FTW_ ACTIONRETVAL
,有了这个标志则能够实现更加精细的控制(就像 Java 的 Files.walkFileTree
一样),详见手册。
当前工作目录
使用 getcwd
可以获取当前的工作目录。getcwd
有一些限制,内核中 getcwd
的实现是用了一个页面来存放信息,所以在一般页面上(4KB)只能存储 4096 个字节的长度,这导致工作目录路径超过这个限制时会被截断。
还有一种方式是使用 readlink
去读取 /proc/PID/cwd
的值,也能获取当前的工作目录。这需要是进程本身,或者有 CAP_SYS_PTRACE
能力才能读取。
可以用 chdir
或者 fchdir
来改变当前的工作目录。
进程的根目录
特权级进程(CAP_SYS_CHROOT
)可以使用 chroot
可以修改进程的根目录,所有绝对路径将从给定的位置开始查找。正因为进程的根目录可能有差异,所以 /proc/PID/root 中还包含了进程的根目录信息。
使用 chroot
改变进程的根目录可以创建出一个监禁区,这样进程对文件的访问就将局限于这个文件夹中(根目录的父目录就是根目录本身,所以用简单方法无法逃出这个文件夹)。不过 chroot 并不是绝对安全的。比如 chroot
没有改变进程的工作目录、如果进程持有监禁区外的文件的 fd 就能配合 fchdir
越狱成功、可以通过 UNIX 域套接字从其他进程接受来自监禁区外的文件描述符。
Tip
在一些 BSD 衍生系统上,有 jail
系统调用能够创建出一个对特权级进程也安全的监禁区。
解析路径
stdlib.h 中的 realpath
可以解析路径字符串中的 .
和 ..
。
libgen.h 中还有 dirname
和 basename
两个函数,可以获取路径的分量。dirname
返回的路径不一定是绝对路径,如果需要的话还需要用 realpath
将其转换成绝对路径。