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 读取。

linkunlink(不会解除符号链接引用)

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() 系统调用对其两个参数中的符号链接均不进行解引用。

mkdirrmdir(不会解除符号链接引用)

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

读目录 opendirreaddir

追踪目录类型用的是 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 中还有 dirnamebasename 两个函数,可以获取路径的分量。dirname 返回的路径不一定是绝对路径,如果需要的话还需要用 realpath 将其转换成绝对路径。