jklincn


Linux open 系统调用源码分析(Linux-6.6.46)


未完待续,暂作保存!


从用户态到内核态

当用户态的应用程序调用 open() 函数时,实际上调用的是 C 标准库(例如 glibc)中提供的封装函数。

以 glibc-2.35 为例,open() 函数的定义位于 fcntl.h(系统路径一般为 /usr/include/fcntl.h)

个人(坏)习惯:本文不区分传统 C 语言中函数声明(Declaration)和函数定义(Definition)的概念,有时会把声明叫做定义,有时会把定义又叫做实现。

1
extern int open (const char *__file, int __oflag, ...) __nonnull ((1));

由于一般用户态程序在编译时是直接链接 glibc 的动态库(/lib32/libc.so.6),因此 open() 的代码实现无法直接查看,需要手动下载 glibc 源代码。

在 glibc-2.35/sysdeps/unix/sysv/linux/open64.c 中有

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int
__libc_open64 (const char *file, int oflag, ...)
{
  int mode = 0;

  if (__OPEN_NEEDS_MODE (oflag))
    {
      va_list arg;
      va_start (arg, oflag);
      mode = va_arg (arg, int);
      va_end (arg);
    }

  return SYSCALL_CANCEL (openat, AT_FDCWD, file, oflag | O_LARGEFILE,
			 mode);
}

strong_alias (__libc_open64, __open64)
libc_hidden_weak (__open64)
weak_alias (__libc_open64, open64)

#ifdef __OFF_T_MATCHES_OFF64_T
strong_alias (__libc_open64, __libc_open)
strong_alias (__libc_open64, __open)
libc_hidden_weak (__open)
weak_alias (__libc_open64, open)
#endif

可以看到当 __OFF_T_MATCHES_OFF64_T 被定义(即 off_t 类型与 off64_t 类型相同,也就是说文件偏移量是 64 位)时,会将 __libc_open64 作为 open 的实现。

openat 系统调用是 open 系统调用的一个更通用的版本。它允许在文件路径相对于某个目录文件描述符的基础上执行文件打开操作。对于普通的 open 操作,openat 的目录描述符参数设置为 AT_FDCWD,表示相对于当前工作目录。此处的 openat 实际上是一个系统调用号,在编译时由预处理器和编译器替换成 __NR_openat,在 x86_64 下其定义位于 glibc-2.35/sysdeps/unix/sysv/linux/x86_64/64/arch-syscall.h 中,可以看到其值是 257。

1
2
3
···
#define __NR_openat 257
···

Linux 内核 open 系统调用的的版本从旧到新排序为 open、openat 和 openat2。

  1. open:最早的 open 系统调用是 UNIX 系统的一部分,后来被引入到 Linux 内核中,系统调用号是 2。open 用于打开一个文件并返回文件描述符,允许进程对该文件进行后续操作(如读、写、关闭等)。open 系统调用签名如下,其参数分别为:filename(要打开的文件路径)、flags(文件打开的标志)、mode(如果创建文件时使用的权限模式)。open64 是为了支持大文件(大于 2GB),因此在 open 的基础上额外设置了 O_LARGEFILE 标志,由于 64 位系统的普及,在较新的 glibc 中,open64 的功能已经整合进 open 中。

    1
    2
    3
    4
    5
    6
    
    SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
    {
    	if (force_o_largefile())
    		flags |= O_LARGEFILE;
    	return do_sys_open(AT_FDCWD, filename, flags, mode);
    }
    
  2. openat:在 Linux 2.6.16 内核中引入的,目的是为了解决目录遍历的竞态条件问题,并简化文件系统的命名空间操作,尤其是在文件描述符相对于指定目录的情况下。在传统的 open 调用中,如果程序在打开文件时先改变了当前工作目录,可能会引发竞态条件。openat 允许相对于特定的目录文件描述符打开文件,避免了这种问题。openat 的系统调用号为 257,glibc 中的 open 实际上是使用 AT_FDCWD(当前工作目录)填充了 dfd 参数,最后调用 openat 系统调用。

    1
    2
    3
    4
    5
    6
    7
    
    SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
    		umode_t, mode)
    {
    	if (force_o_largefile())
    		flags |= O_LARGEFILE;
    	return do_sys_open(dfd, filename, flags, mode);
    }
    
  3. openat2:在 Linux 5.6 内核中引入的,作为对 openat 的增强版本,目的是进一步扩展文件打开操作的灵活性和安全性。openat2 引入了 open_how 结构体,支持更多的控制选项,特别是在路径解析、符号链接处理、挂载点处理等方面,为开发者提供了更精细的控制,其系统调用号为 437。参考资料:openat2(2) - Linux manual page

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
    SYSCALL_DEFINE4(openat2, int, dfd, const char __user *, filename,
    		struct open_how __user *, how, size_t, usize)
    {
    	int err;
    	struct open_how tmp;
    
    	BUILD_BUG_ON(sizeof(struct open_how) < OPEN_HOW_SIZE_VER0);
    	BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_LATEST);
    
    	if (unlikely(usize < OPEN_HOW_SIZE_VER0))
    		return -EINVAL;
    
    	err = copy_struct_from_user(&tmp, sizeof(tmp), how, usize);
    	if (err)
    		return err;
    
    	audit_openat2_how(&tmp);
    
    	/* O_LARGEFILE is only allowed for non-O_PATH. */
    	if (!(tmp.flags & O_PATH) && force_o_largefile())
    		tmp.flags |= O_LARGEFILE;
    
    	return do_sys_openat2(dfd, filename, &tmp);
    }
    
1
2
3
4
5
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
	struct open_how how = build_open_how(flags, mode);
	return do_sys_openat2(dfd, filename, &how);
}

可以看到 do_sys_open() 实际上也是先把传统的打开标志和权限模式这两个参数转换成 open_how 结构体,然后再调用 do_sys_openat2() 函数。因此在 6.6 的内核中,对各类 open 系统调用的处理已经交由 do_sys_openat2() 这个函数统一处理。

SYSCALL_CANCEL 是 glibc 用于封装系统调用的宏,定义在 glibc-2.35/sysdeps/unix/sysdep.h 中。CANCEL 是指它特别处理了系统调用中涉及的线程取消的问题,最后使用 INLINE_SYSCALL_CALL 宏执行系统调用。在这一过程中会涉及到非常多宏的拼接,用于系统调用的分发和参数数量的判定等等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#define SYSCALL_CANCEL(...) \
  ({									     \
    long int sc_ret;							     \
    if (NO_SYSCALL_CANCEL_CHECKING)					     \
      sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__); 			     \
    else								     \
      {									     \
	int sc_cancel_oldtype = LIBC_CANCEL_ASYNC ();			     \
	sc_ret = INLINE_SYSCALL_CALL (__VA_ARGS__);			     \
        LIBC_CANCEL_RESET (sc_cancel_oldtype);				     \
      }									     \
    sc_ret;								     \
  })

以 x86_64 为例,最后实际触发系统调用的代码位于 glibc-2.35/sysdeps/unix/sysv/linux/x86_64/sysdep.h ,其中包含了不同参数数量的系统调用入口,最后使用 syscall 指令触发系统调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#undef internal_syscall0
#define internal_syscall0(number, dummy...)				\
({									\
    unsigned long int resultvar;					\
    asm volatile (							\
    "syscall\n\t"							\
    : "=a" (resultvar)							\
    : "0" (number)							\
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);			\
    (long int) resultvar;						\
})

#undef internal_syscall1
#define internal_syscall1(number, arg1)					\
({									\
    unsigned long int resultvar;					\
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);			 	\
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;			\
    asm volatile (							\
    "syscall\n\t"							\
    : "=a" (resultvar)							\
    : "0" (number), "r" (_a1)						\
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);			\
    (long int) resultvar;						\
})

#undef internal_syscall2
#define internal_syscall2(number, arg1, arg2)				\
({									\
    unsigned long int resultvar;					\
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);			 	\
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);			 	\
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;			\
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;			\
    asm volatile (							\
    "syscall\n\t"							\
    : "=a" (resultvar)							\
    : "0" (number), "r" (_a1), "r" (_a2)				\
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);			\
    (long int) resultvar;						\
})

...

内核接收到一个系统调用后,会根据系统调用号配合系统调用表进行函数的分发。

x86 的系统调用号我们已经从 glibc 传递而来,即 257,接下来就是在系统调用表中进行查询。系统调用表位于 arch/x86/entry/syscalls/syscall_64.tbl。

···
257	common	openat			sys_openat
···

可以看到 257 号系统调用正是 openat,其对应函数是 sys_openat(),其定义在文件 fs/open.c 中。

1
2
3
4
5
6
7
SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
		umode_t, mode)
{
	if (force_o_largefile())
		flags |= O_LARGEFILE;
	return do_sys_open(dfd, filename, flags, mode);
}

宏 SYSCALLDEFINE4 会生成一个带有 “sys” 前缀的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#define __SYSCALL_DEFINEx(x, name, ...)					\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));	\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));\
	__X64_SYS_STUBx(x, name, __VA_ARGS__)				\
	__IA32_SYS_STUBx(x, name, __VA_ARGS__)				\
	static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)

如前文所述,由于 open64 已经是 open 的默认版本,因此使用 force_o_largefile() 来检查当前的系统配置或运行环境是否需要强制启用 O_LARGEFILE 标志,从而在 32 位系统上提供向后兼容性。

do_sys_openat2

如前文所述,do_sys_open() 是一层包装,它先调用 build_open_how() 函数将传入的 flags 和 mode 转换为一个 open_how 结构体,然后调用 do_sys_openat2() 来执行实际的文件打开操作。

1
2
3
4
5
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
	struct open_how how = build_open_how(flags, mode);
	return do_sys_openat2(dfd, filename, &how);
}

open_how 结构体是一个用于统一表示文件打开操作参数的结构体。它在 Linux 内核中被用来抽象和传递各种文件打开选项。具体的结构体和函数定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct open_how {
	__u64 flags; // 文件打开的标志,例如:O_RDONLY
	__u64 mode; // 创建新文件时的文件权限模式,例如:0644
	__u64 resolve; // 文件路径解析的选项,例如:是否跟随符号链接
};

inline struct open_how build_open_how(int flags, umode_t mode)
{
    // VALID_OPEN_FLAGS 和 S_IALLUGO 都用来过滤和验证传入的参数,防止无效或不支持的标志被传递下去。
	struct open_how how = {
		.flags = flags & VALID_OPEN_FLAGS,
		.mode = mode & S_IALLUGO,
	};

	// 如果 flags 包含 O_PATH 标志,则只保留 O_PATH_FLAGS 掩码中的标志。
	if (how.flags & O_PATH)
		how.flags &= O_PATH_FLAGS;

    // 用于检查 flags 中是否包含创建相关的标志(如 O_CREAT、O_TMPFILE 等)。
    // 如果没有创建相关的标志,how.mode 被设置为 0
	if (!WILL_CREATE(how.flags))
		how.mode = 0;
	return how;
}

然后我们来看 do_sys_openat2() 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
static long do_sys_openat2(int dfd, const char __user *filename,
			   struct open_how *how)
{
	struct open_flags op;
	int fd = build_open_flags(how, &op);
	struct filename *tmp;

    // 如果 fd 为零,说明标志构建成功,继续执行后续操作
	if (fd)
		return fd;

    // 从用户空间获取并解析文件名,返回一个 filename 结构体的指针,表示内核空间中的文件名
	tmp = getname(filename);
    // 检查 tmp 是否为错误指针。getname 可能返回一个错误指针来表示路径解析或内存分配失败
	if (IS_ERR(tmp))
		return PTR_ERR(tmp);

    // 获取一个未使用的文件描述符,并将标志传递给它
	fd = get_unused_fd_flags(how->flags);
    // 检查 fd 是否有效。如果 fd 为负值,意味着没有可用的文件描述符,函数将跳过打开文件的步骤。
	if (fd >= 0) {
        // 实际的文件打开操作,返回一个 file 结构体的指针
		struct file *f = do_filp_open(dfd, tmp, &op);
        // 如果 do_filp_open 失败,则释放之前分配的文件描述符并返回错误码
		if (IS_ERR(f)) {
			put_unused_fd(fd);
			fd = PTR_ERR(f);
		} else {
            // 如果 do_filp_open 成功,将文件对象 f 安装到文件描述符表中,关联文件描述符 fd 和文件对象 f。
			fd_install(fd, f);
		}
	}
    // 释放 getname 分配的 tmp 结构体,避免内存泄漏。
	putname(tmp);
    // 如果一切正常,返回新分配的文件描述符 fd。否则,fd 是一个负的错误码。
	return fd;
}

这里使用 build_open_flags() 函数进一步将 how 中的打开标志和模式信息转换并填充到 open_flags 结构体中,同时返回一个整数 fd 作为错误码。如果 fd 为零,则说明打开标志构建成功,继续执行后续操作;如果 fd 非零,则返回该错误码。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
struct open_flags {
	int open_flag;  // 文件打开的标志(如 O_RDONLY、O_WRONLY、O_CREAT 等)
	umode_t mode;   // 创建新文件时的文件权限模式,例如:0644
	int acc_mode;   // 文件的访问模式,通常用于权限检查(如读、写、追加等)
	int intent;     // 表示文件查找的意图,用于在文件系统中执行具体的操作时优化行为
	int lookup_flags; // 控制文件路径解析的标志(如是否跟随符号链接、是否限制跨设备等)
};

inline int build_open_flags(const struct open_how *how, struct open_flags *op)
{
	u64 flags = how->flags;
	u64 strip = __FMODE_NONOTIFY | O_CLOEXEC;
	int lookup_flags = 0;
	int acc_mode = ACC_MODE(flags);

    // 编译时检查,确保 VALID_OPEN_FLAGS 中没有超过 32 位的标志,否则会抛出错误信息
	BUILD_BUG_ON_MSG(upper_32_bits(VALID_OPEN_FLAGS),
			 "struct open_flags doesn't yet handle flags > 32 bits");

    // 删除不应由用户空间设置的标志(FMODE_NONOTIFY)和与构建 open_flags 无关的标志(O_CLOEXEC)
	flags &= ~strip;

	// 较旧的系统调用在调用 build_open_flags() 之前会隐式清除所有无效标志或参数值(正如之前的 build_open_how() 函数),但 openat2(2) 会检查其所有参数(即在较旧的系统调用上是重复工作)。

    // 检查 flags 标志是否合法
	if (flags & ~VALID_OPEN_FLAGS)
		return -EINVAL;
    // 检查 resolve 标志是否合法
	if (how->resolve & ~VALID_RESOLVE_FLAGS)
		return -EINVAL;

	// 检查 resolve 标志中是否存在互斥的标志(RESOLVE_BENEATH 和 RESOLVE_IN_ROOT 不能同时存在)
	if ((how->resolve & RESOLVE_BENEATH) && (how->resolve & RESOLVE_IN_ROOT))
		return -EINVAL;

	// 处理创建新文件时的权限模式
	if (WILL_CREATE(flags)) {
        // 检查 mode 标志是否合法
		if (how->mode & ~S_IALLUGO)
			return -EINVAL;
        // 默认为常规文件(S_IFREG)
		op->mode = how->mode | S_IFREG;
	} else {
		if (how->mode != 0)
			return -EINVAL;
        // 如果不涉及文件创建,则 mode 必须为 0。
		op->mode = 0;
	}

	// 阻止同时使用 O_DIRECTORY 和 O_CREAT 标志创建文件的行为,这还可以保护下面的 O_TMPFILE
	if ((flags & (O_DIRECTORY | O_CREAT)) == (O_DIRECTORY | O_CREAT))
		return -EINVAL;

	// 处理 O_TMPFILE 标志(创建临时文件,必须和 O_DIRECTORY 一起使用)
	if (flags & __O_TMPFILE) {
		// 为了确保当程序在旧内核上使用 O_TMPFILE 时能够获得明确的错误,内核强制要求在使用 O_TMPFILE 时同时设置 O_DIRECTORY 标志
		if (!(flags & O_DIRECTORY))
			return -EINVAL;
        // 检查是否有写权限
		if (!(acc_mode & MAY_WRITE))
			return -EINVAL;
	}

    // 检查 O_PATH 标志的相关错误
	if (flags & O_PATH) {
		// O_PATH 只允许设置某些其他标志。
		if (flags & ~O_PATH_FLAGS)
			return -EINVAL;
        // O_PATH 不进行 I/O 操作
		acc_mode = 0;
	}

    // O_SYNC 实现为 __O_SYNC 和 O_DSYNC。由于许多地方仅在需要同步时才检查 O_DSYNC,因此我们强制始终设置它,而不必处理恶意应用程序仅设置 __O_SYNC 时可能出现的奇怪行为。
	if (flags & __O_SYNC)
		flags |= O_DSYNC;

    // 完成对 open_flag 字段的设置
	op->open_flag = flags;

	// O_TRUNC 意味着我们需要对写入权限进行访问检查
	if (flags & O_TRUNC)
		acc_mode |= MAY_WRITE;

	// 允许 Linux 安全模块(LSM,Linux Security Modules)的权限检查钩子(permission hook)能够区分追加访问(append access)和一般的写访问(general write access)
	if (flags & O_APPEND)
		acc_mode |= MAY_APPEND;

    // 完成对 acc_mode 字段的设置
	op->acc_mode = acc_mode;

    // 设置文件查找的意图。如果是只打开路径,则没有额外的查找意图;否则,设置 LOOKUP_OPEN 来让文件系统准备好执行文件打开的操作
	op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;

    // 如果设置了 O_CREAT,则设置相应的查找意图,并处理 O_EXCL 标志(这意味着不能跟随符号链接)
	if (flags & O_CREAT) {
		op->intent |= LOOKUP_CREATE;
		if (flags & O_EXCL) {
			op->intent |= LOOKUP_EXCL;
			flags |= O_NOFOLLOW;
		}
	}

    // 判断路径解析是否为目录
	if (flags & O_DIRECTORY)
		lookup_flags |= LOOKUP_DIRECTORY;
    // 判断路径解析是否允许跟随符号链接
	if (!(flags & O_NOFOLLOW))
		lookup_flags |= LOOKUP_FOLLOW;

    // 根据 resolve 设置路径解析标志,这些标志控制着如何处理符号链接、设备、挂载点等
	if (how->resolve & RESOLVE_NO_XDEV)
		lookup_flags |= LOOKUP_NO_XDEV;
	if (how->resolve & RESOLVE_NO_MAGICLINKS)
		lookup_flags |= LOOKUP_NO_MAGICLINKS;
	if (how->resolve & RESOLVE_NO_SYMLINKS)
		lookup_flags |= LOOKUP_NO_SYMLINKS;
	if (how->resolve & RESOLVE_BENEATH)
		lookup_flags |= LOOKUP_BENEATH;
	if (how->resolve & RESOLVE_IN_ROOT)
		lookup_flags |= LOOKUP_IN_ROOT;
	if (how->resolve & RESOLVE_CACHED) {
		// 使用路径解析缓存时与文件创建、截断、或者临时文件创建冲突(这些操作需要保证路径解析是最新和准确的)
		if (flags & (O_TRUNC | O_CREAT | __O_TMPFILE))
			return -EAGAIN;
		lookup_flags |= LOOKUP_CACHED;
	}

	// 完成对 lookup_flags 字段的设置
	op->lookup_flags = lookup_flags;
	return 0;
}

到这里终于完成了对文件打开标志的处理,do_sys_openat2() 还涉及到从用户空间获取并解析文件名、获取文件描述符等,这里不详细展开。我们再看一下 fd_install() 函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void fd_install(unsigned int fd, struct file *file)
{
	struct files_struct *files = current->files; // 获取当前进程的文件表
	struct fdtable *fdt;

    // 获取一个 RCU(Read-Copy Update)读锁
	rcu_read_lock_sched();

    // 如果文件描述符表正在扩展(例如,为了容纳更多的文件描述符),函数会采取不同的同步方式
	if (unlikely(files->resize_in_progress)) {
		rcu_read_unlock_sched();               // 释放 RCU 读锁
		spin_lock(&files->file_lock);          // 获取一个自旋锁来保护并发访问
		fdt = files_fdtable(files);            // 获取当前进程的文件描述符表
		BUG_ON(fdt->fd[fd] != NULL);           // 检查文件描述符槽位是否为空
		rcu_assign_pointer(fdt->fd[fd], file); // 将文件指针安全地分配给文件描述符表
		spin_unlock(&files->file_lock);        // 释放自旋锁
		return;
	}

	smp_rmb(); // 读取内存屏障,确保随后的读取操作不会在该点之前进行重排序
	fdt = rcu_dereference_sched(files->fdt); // 安全地获取当前文件描述符表的指针
	BUG_ON(fdt->fd[fd] != NULL);             // 检查文件描述符槽位是否为空
	rcu_assign_pointer(fdt->fd[fd], file);   // 将文件指针安全地分配给文件描述符表
	rcu_read_unlock_sched();                 // 释放 RCU 读锁
}

从功能上简单来说就是把指定的文件描述符(fd)插入到当前进程的文件表中(files_struct 结构体),大部分代码都是为了并发的安全性,涉及到 RCU 和自旋锁等操作。

do_filp_open

接下来就进入 do_filp_open() 函数,其定义在 fs/namei.c 中。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
struct file *do_filp_open(int dfd, struct filename *pathname,
		const struct open_flags *op)
{
	struct nameidata nd;
	int flags = op->lookup_flags;
	struct file *filp;

    // 初始化 nameidata 结构体,把 pathname 存储到 nameidata 中
	set_nameidata(&nd, dfd, pathname, NULL);

	filp = path_openat(&nd, op, flags | LOOKUP_RCU);
	if (unlikely(filp == ERR_PTR(-ECHILD)))
		filp = path_openat(&nd, op, flags);
	if (unlikely(filp == ERR_PTR(-ESTALE)))
		filp = path_openat(&nd, op, flags | LOOKUP_REVAL);

    // 恢复 nameidata 结构的状态。由于路径查找和文件打开过程中可能修改了 nameidata,因此在操作完成后需要恢复原来的状态。
	restore_nameidata();
	return filp;
}

这里有三次查找:

  1. RCU 模式:第一次调用时,传入了 LOOKUP_RCU 标志,表示希望使用 RCU(Read-Copy-Update)模式进行快速路径查找。如果在这种模式下找到文件,性能会更好。
  2. 常规模式:如果第一次 path_openat 调用返回了 -ECHILD 错误,表示路径查找过程中遇到了某些复杂情况,导致 RCU 模式无法处理。例如,路径中涉及的某些目录或文件可能在查找过程中发生了变化,无法安全地继续使用 RCU 模式。在这种情况下,重新调用 path_openat,但不使用 LOOKUP_RCU,改用常规路径查找机制。
  3. 强制刷新模式:如果第二次调用返回 -ESTALE 错误,表示路径中的某些部分可能已经过期或被修改,特别是在分布式文件系统中可能发生这种情况。这种情况下,使用 LOOKUP_REVAL 标志再次调用 path_openat,强制重新验证路径,确保获取最新的文件状态。这种重试可以帮助解决由于路径缓存或文件系统状态不一致导致的错误。

可以再看看 nameidata 结构体的内容,它在 Linux 内核中用来处理处理路径名解析和相关操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#define EMBEDDED_LEVELS 2
struct nameidata {
	struct path	path; // 当前正在解析的路径
	struct qstr	last; // 保存需要解析的文件路径分量
	struct path	root; // 路径解析过程中的根路径,这通常是一个起始点,比如文件系统的根目录或者当前进程的根目录
	struct inode	*inode; // 指向路径中最后一个组件的 inode
	unsigned int	flags, state; // 标志位和状态位,用来跟踪路径解析的状态和控制路径解析行为
	unsigned	seq, next_seq, m_seq, r_seq; // 序列号,用于路径解析中的一致性验证
	int		last_type; // 路径中最后一个组件的类型
	unsigned	depth; // 路径解析过程中目录层次的深度
	int		total_link_count; // 在解析路径时遇到的符号链接的总数,防止符号链接的无限循环解析
	struct saved {
		struct path link;
		struct delayed_call done;
		const char *name;
		unsigned seq;
	} *stack, internal[EMBEDDED_LEVELS]; // stack 保存解析路径时的中间状态;internal 用作临时缓冲区以提高性能
	struct filename	*name; // 保存正在解析的路径的原始字符串
	struct nameidata *saved; // 指向另一个 nameidata 结构体的指针,用于保存嵌套解析操作的状态
	unsigned	root_seq; // 根路径的序列号,用来确保路径解析的一致性
	int		dfd; // 目录文件描述符,表示路径解析的起始目录
	vfsuid_t	dir_vfsuid; // 目录的虚拟文件系统用户 ID (VFS UID),用于权限检查
	umode_t		dir_mode; // 目录的权限模式 (mode),用来控制对目录的访问
} __randomize_layout; // 随机化结构体布局,增加内核的安全性

path 保存已经成功解析到的信息,last 用来存放当前需要解析的信息,如果 last 解析成功,那么就会更新 path。

函数 set_nameidata() 使用 dfd 和 pathname 初始化了 nd 变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void __set_nameidata(struct nameidata *p, int dfd, struct filename *name)
{
	struct nameidata *old = current->nameidata; // 保存了当前线程的 nameidata 上下文
	p->stack = p->internal; // 初始化路径查找栈的指针
	p->depth = 0;           // 设置路径查找的深度为 0,表示新的路径查找过程开始
	p->dfd = dfd;           // 设置文件描述符,作为相对路径查找的基点
	p->name = name;         // 设置要查找或操作的文件名
	p->path.mnt = NULL;     // 初始化路径的挂载点为 NULL
	p->path.dentry = NULL;  // // 初始化路径的挂载点为 NULL
	p->total_link_count = old ? old->total_link_count : 0; // 如果有旧的 nameidata 上下文,继承其符号链接解析次数,否则从零开始。这是为了防止符号链接解析过程中的循环
	p->saved = old; // 将旧的 nameidata 上下文保存,以便恢复时使用。
	current->nameidata = p; // 将当前线程的 nameidata 指针更新为新的结构体指针
}

static inline void set_nameidata(struct nameidata *p, int dfd, struct filename *name,
			  const struct path *root)
{
	__set_nameidata(p, dfd, name);
	p->state = 0; // 初始化路径解析的状态
	if (unlikely(root)) {
		p->state = ND_ROOT_PRESET; // 表示已经预设了一个根路径,这里我们的 root 是 NULL,因此不会进入分支
		p->root = *root;
	}
}

path_openat

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
static struct file *path_openat(struct nameidata *nd,
			const struct open_flags *op, unsigned flags)
{
	struct file *file;
	int error;

    // 基于给定的打开标志和当前的安全凭据分配一个空的文件结构体。如果分配失败,则直接返回错误。
	file = alloc_empty_file(op->open_flag, current_cred());
	if (IS_ERR(file))
		return file;

    // 处理特殊文件标志:创建临时文件和只获取文件路径,这里不深入展开
	if (unlikely(file->f_flags & __O_TMPFILE)) {
		error = do_tmpfile(nd, flags, op, file);
	} else if (unlikely(file->f_flags & O_PATH)) {
		error = do_o_path(nd, flags, file);
	} else {
        // 路径初始化,确定查找的起始目录,初始化结构体 nameidata 的成员 path。
		const char *s = path_init(nd, flags);
        // 使用 link_path_walk 解析文件路径的每个组成部分,最后一个组成部分除外。
        // 使用 open_last_lookups 解析文件路径的最后一个组成部分
		while (!(error = link_path_walk(s, nd)) &&
		       (s = open_last_lookups(nd, file, op)) != NULL)
			;
        // 如果没有错误则打开文件
		if (!error)
			error = do_open(nd, file, op);
        // 结束查找,清理相关的路径查找数据
		terminate_walk(nd);
	}
	if (likely(!error)) {
        // 如果在文件打开过程中没有错误,检查 file->f_mode & FMODE_OPENED 确保文件被正确打开
		if (likely(file->f_mode & FMODE_OPENED))
            // 返回 file 结构体
			return file;
		WARN_ON(1);
        // 如果有错误,则设置错误码
		error = -EINVAL;
	}
    // 以下都是打开错误的处理:释放文件结构体和设置不同的错误码
	fput(file);
	if (error == -EOPENSTALE) {
		if (flags & LOOKUP_RCU)
			error = -ECHILD;
		else
			error = -ESTALE;
	}
	return ERR_PTR(error);
}

path_openat() 里主要调用了 3 个函数 path_init()、link_path_walk() 和 open_last_lookups()。

path_init() 函数的主要任务是为路径查找准备好一些必要的上下文信息,包括设置路径的起点、锁定一些资源、处理不同查找标志的行为等,它最终返回路径字符串 s,即查找的起点。

这里重点查看 link_path_walk() 函数,它的作用是逐步解析路径的每个组成部分(component),处理符号链接、相对路径(如 . 和 ..),将给定的路径名解析为最终的目录项(dentry)。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
static int link_path_walk(const char *name, struct nameidata *nd)
{
	int depth = 0; // 表示符号链接解析的深度
	int err;

    // 初始化,将路径解析状态设为根路径,并启用 LOOKUP_PARENT 标志
	nd->last_type = LAST_ROOT;
	nd->flags |= LOOKUP_PARENT;

    // 如果 name 是错误指针,返回错误码
	if (IS_ERR(name))
		return PTR_ERR(name);

    // 跳过路径名开头的所有 '/'
	while (*name=='/')
		name++;

    // 如果现在的路径名为空(即搜索路径只包含 '/'),则搜索完成,返回
	if (!*name) {
		nd->dir_mode = 0; // 简化处理
		return 0;
	}

	// 开始逐步解析路径的每个部分
	for(;;) {
		struct mnt_idmap *idmap;
		const char *link;
		u64 hash_len;
		int type;

        // 获取挂载点的 ID 映射
		idmap = mnt_idmap(nd->path.mnt);

        // 检查查找权限
		err = may_lookup(idmap, nd);
		if (err)
			return err;

        // 计算当前路径部分的哈希值,返回值 hash_len 的高位是哈希值部分,低位是路径名长度部分
		hash_len = hash_name(nd->path.dentry, name);

        // 判断路径部分类型
		type = LAST_NORM;
		if (name[0] == '.') switch (hashlen_len(hash_len)) {
			case 2:
				if (name[1] == '.') {
					type = LAST_DOTDOT;
					nd->state |= ND_JUMPED; // 标记为跳转到父目录
				}
				break;
			case 1:
				type = LAST_DOT; // 当前目录 `.`
		}

        // 处理普通路径部分
		if (likely(type == LAST_NORM)) {
			struct dentry *parent = nd->path.dentry; // 获取当前路径解析的父目录
			nd->state &= ~ND_JUMPED; // 清除跳转标志,表明当前路径部分是普通路径

            // 检查 DCACHE_OP_HASH 标志,是否支持自定义哈希函数(d_hash())。
            // 这种情况通常出现在特定文件系统中,某些文件系统可能会使用特殊的哈希算法来处理路径名。
			if (unlikely(parent->d_flags & DCACHE_OP_HASH)) {
				struct qstr this = { { .hash_len = hash_len }, .name = name };
				err = parent->d_op->d_hash(parent, &this);
				if (err < 0)
					return err;
				hash_len = this.hash_len;
				name = this.name;
			}
		}

        // 更新 last 字段,供 walk_component() 使用
		nd->last.hash_len = hash_len;
		nd->last.name = name;
		nd->last_type = type;

        // 待搜索路径向前推进
		name += hashlen_len(hash_len);

        // 如果路径名解析完毕,进入结束处理
		if (!*name)
			goto OK;

        // 如果不为空,那就是 '/',因此跳过
		do {
			name++;
		} while (unlikely(*name == '/'));

        // 如果路径解析到末尾
		if (unlikely(!*name)) {
OK:
			// 常规文件类型在这里返回
			if (!depth) {
				nd->dir_vfsuid = i_uid_into_vfsuid(idmap, nd->inode);
				nd->dir_mode = nd->inode->i_mode;
				nd->flags &= ~LOOKUP_PARENT;
				return 0;
			}
			// 处理嵌套符号链接的最后部分
			name = nd->stack[--depth].name;
			link = walk_component(nd, 0);
		} else {
			// 继续解析下一个路径部分
			link = walk_component(nd, WALK_MORE);
		}

        // 如果遇到符号链接,需要递归处理
		if (unlikely(link)) {
			if (IS_ERR(link))
				return PTR_ERR(link);
			// 保存当前路径,继续解析符号链接
			nd->stack[depth++].name = name;
			name = link;
			continue;
		}
        // 如果当前路径部分不是目录,返回 -ENOTDIR 错误
		if (unlikely(!d_can_lookup(nd->path.dentry))) {
			if (nd->flags & LOOKUP_RCU) {
				if (!try_to_unlazy(nd))
					return -ECHILD;
			}
			return -ENOTDIR;
		}
	}
}

在路径查找过程中,文件系统会多次遇到路径名的组成部分(例如 /home/user/file.txt 的 /home 和 /user 等)。对于每个部分,内核需要查找它是否存在于当前目录下。通过哈希计算和哈希表,可以快速找到对应的目录项,而不用逐字符比较。这提高了路径解析的效率,尤其是在包含大量文件和目录的复杂文件系统中。hash_name() 函数用于计算路径中每个组件(目录名或文件名)的哈希值,返回 hash_len;hashlen_len() 函数用于从 hash_len 中提取路径名的长度信息。

link_path_walk() 函数会循环解析路径的每一个组成部分,使用 walk_component() 函数查找并在 step_into() 函数中更新 nd 变量。

例如:当查找路径为 /home/user/docs/file.txt 时,link_path_walk() 先解析出 /home ,然后使用 walk_component() 在 / 目录下查找 home,并在其调用的 step_into 中更新 nd->path 和 nd->inode 为 /home。同样地, link_path_walk() 的第二次循环就解析 user,调用 walk_component() 在 /home 中查找 user 目录,更新 nd->path 和 nd->inode 为 /home/user,以此类推。到最后阶段时,由于 name 加上 file.txt 的长度后已经是 ‘\0’,因此不会调用 walk_component() 函数,而是退出循环,交给上层 path_openat() 中的 open_last_lookups() 来完成。

walk_component

walk_component() 是路径解析过程中用于处理单个路径组成部分的核心函数。它根据路径的不同类型(如 ‘.’ 和 ‘..’,或者普通的文件名)采取相应的动作,最终将解析的结果返回。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
static const char *walk_component(struct nameidata *nd, int flags)
{
	struct dentry *dentry;
	// 处理特殊路径组成部分
	if (unlikely(nd->last_type != LAST_NORM)) {
		if (!(flags & WALK_MORE) && nd->depth)
			put_link(nd);
		return handle_dots(nd, nd->last_type);
	}
    // 使用快速查找机制来解析路径组件,如果成功,返回对应的目录项
	dentry = lookup_fast(nd);
    // 如果发生错误,则返回错误码
	if (IS_ERR(dentry))
		return ERR_CAST(dentry);
    // 如果没有找到路径组成部分并且也没有发生错误,则进行更慢的路径查找操作
	if (unlikely(!dentry)) {
		dentry = lookup_slow(&nd->last, nd->path.dentry, nd->flags);
		if (IS_ERR(dentry))
			return ERR_CAST(dentry);
	}
    // 如果没有更多的路径组成部分需要处理,并且之前有符号链接被解析,则进行清理
	if (!(flags & WALK_MORE) && nd->depth)
		put_link(nd);
    // 找到对应的目录项之后,更新路径状态,将当前目录项作为已解析的部分。
	return step_into(nd, flags, dentry);
}

lookup_fast() 是使用缓存和锁的机制快速找到路径的某个组成部分,避免较慢的磁盘访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static struct dentry *lookup_fast(struct nameidata *nd)
{
	struct dentry *dentry, *parent = nd->path.dentry;
	int status = 1;

	// 处理 LOOKUP_RCU 模式,这可以提高并发效率
	if (nd->flags & LOOKUP_RCU) {
		dentry = __d_lookup_rcu(parent, &nd->last, &nd->next_seq);
        // 如果查找失败,切换到非 RCU 模式进行查找
		if (unlikely(!dentry)) {
			if (!try_to_unlazy(nd))
				return ERR_PTR(-ECHILD);
			return NULL;
		}

        // 检查父目录在查找过程中是否被修改。如果父目录的序列号发生了变化,可能是由于重命名或其他文件系统操作导致的并发修改,路径查找必须重新进行。
		if (read_seqcount_retry(&parent->d_seq, nd->seq))
			return ERR_PTR(-ECHILD);

        // 检查找到的 dentry 是否仍然有效
		status = d_revalidate(dentry, nd->flags);
		if (likely(status > 0))
			return dentry;
		if (!try_to_unlazy_next(nd, dentry))
			return ERR_PTR(-ECHILD);
		if (status == -ECHILD)
			/* we'd been told to redo it in non-rcu mode */
			status = d_revalidate(dentry, nd->flags);
	} else {
        // 处理非 RCU 模式查找
		dentry = __d_lookup(parent, &nd->last);
		if (unlikely(!dentry))
			return NULL;
		status = d_revalidate(dentry, nd->flags);
	}
    // 处理查找失败和无效的 dentry
	if (unlikely(status <= 0)) {
		if (!status)
			d_invalidate(dentry);
		dput(dentry);
		return ERR_PTR(status);
	}
	return dentry;
}

其中 __d_lookup_rcu() 和 __d_lookup() 函数都是用了哈希链表(hlist_bl_head)来保存其缓存的目录项,都使用 hlist_bl_for_each_entry_rcu 宏来遍历哈希链表,具体这里不展开。

lookup_slow() 是一个较为简单的包装函数,它负责在查找开始时对当前目录的 inode 加锁,在查找结束后解锁。这个锁是共享锁,允许多个读操作并发进行,但会阻塞写操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static struct dentry *lookup_slow(const struct qstr *name,
				  struct dentry *dir,
				  unsigned int flags)
{
	struct inode *inode = dir->d_inode;
	struct dentry *res;
	inode_lock_shared(inode);
	res = __lookup_slow(name, dir, flags);
	inode_unlock_shared(inode);
	return res;
}

__lookup_slow

__lookup_slow() 是慢速查找的执行函数,主要通过文件系统的操作函数来查找路径的具体部分。它的查找过程通常涉及磁盘操作或文件系统元数据的访问,速度比缓存查找慢。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
static struct dentry *__lookup_slow(const struct qstr *name,
				    struct dentry *dir,
				    unsigned int flags)
{
	struct dentry *dentry, *old;
	struct inode *inode = dir->d_inode;
	DECLARE_WAIT_QUEUE_HEAD_ONSTACK(wq); // 声明内核等待队列

	// 检查父目录是否已经失效
	if (unlikely(IS_DEADDIR(inode)))
		return ERR_PTR(-ENOENT);
again:
    // 为当前路径分配一个 dentry(这里也会在缓存中先寻找)
	dentry = d_alloc_parallel(dir, name, &wq);
	if (IS_ERR(dentry))
		return dentry;

    // 如果 dentry 已经存在并且在缓存中被标记为正在查找(d_in_lookup),则跳过这个步骤,进入文件系统查找逻辑。
	if (unlikely(!d_in_lookup(dentry))) {
        // 验证 dentry 是否有效
		int error = d_revalidate(dentry, flags);
		if (unlikely(error <= 0)) {
			if (!error) {
				d_invalidate(dentry);
				dput(dentry);
				goto again; // dentry 无效,重新查找
			}
			dput(dentry);
			dentry = ERR_PTR(error);
		}
	} else {
        // 调用文件系统的 lookup 回调函数
		old = inode->i_op->lookup(inode, dentry, flags);
		d_lookup_done(dentry); // 查找完成
		if (unlikely(old)) {
			dput(dentry);
			dentry = old;
		}
	}
	return dentry;
}

在并发情况下,多个查找进程可能会等待同一个 dentry 的结果,因此使用内核等待队列来处理这种并发情况。

这里会根据具体的文件系统来完成相应的查找操作,假设当前的文件系统是 ext4,那么可以在 fs/ext4/namei.c 下找到 ext4_dir_inode_operations 结构体,其对应的 lookup 函数为 ext4_lookup。

ext4_lookup


本站不记录浏览量,但如果您觉得本内容有帮助,请点个小红心,让我知道您的喜欢。