[toc]

fs/open.c 文件打开与创建(File Opening and Creation) open/creat系统调用的VFS核心

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术是VFS(虚拟文件系统)最核心的入口之一,它为了解决操作系统中一个最基本、最普遍的需求而诞生:如何定位、验证并实例化一个对文件系统对象的访问会话

fs/open.c通过实现open(2), creat(2)及其变体系统调用,解决了以下至关重要的问题:

  • 路径解析 (Path Traversal):当用户提供一个路径字符串(如/home/user/file.txt)时,内核需要一个标准的、安全的机制来逐级遍历目录,查找每一级路径组件,并最终定位到目标文件。这个过程必须能正确处理符号链接、挂载点和各种权限问题。
  • 权限与访问控制:在允许访问之前,内核必须根据用户请求的访问模式(读、写、执行)和文件的权限位(mode)、所有者(uid/gid)以及可能的访问控制列表(ACL)和安全模块(LSM)策略,来严格地进行权限检查。
  • 文件实例的创建:成功打开一个文件,意味着内核需要创建一个**“打开文件实例”**的内存表示。这个实例就是struct file对象。fs/open.c负责分配这个结构,并将其与进程的文件描述符表关联起来。struct file记录了这次访问会话的所有状态,如访问模式、当前文件偏移量等。
  • 统一的创建接口:当文件不存在时,open()(配合O_CREAT标志)或creat()需要提供一个统一的接口来调用底层文件系统,以在磁盘上创建新的文件元数据(inode)。
  • 统一的打开接口:为所有类型的文件系统对象(普通文件、目录、设备文件、FIFO等)提供一个单一的open入口点,并将具体的打开逻辑分派给各自的驱动或文件系统实现。

它的发展经历了哪些重要的里程碑或版本迭代?

open()是Unix的基石,自Linux诞生之初就已存在。fs/open.c的发展主要体现在对路径解析和文件打开过程的不断增强和安全加固上。

  1. opencreat:最初的POSIX接口。creat(path, mode)在功能上等同于open(path, O_CREAT|O_WRONLY|O_TRUNC, mode)
  2. 路径查找的重构:内核的路径查找逻辑(path_lookup)经历了多次重构,以提高性能(例如通过dcache)和修复安全漏洞(如符号链接攻击)。
  3. *at()系列系统调用的引入:这是一个重大的安全里程碑。openat(dirfd, path, flags)被引入,以解决传统open()存在的**TOCTOU (Time-of-check to time-of-use)**竞争条件漏洞。它允许相对于一个已打开的目录文件描述符dirfd来打开一个文件,这使得操作更加原子化和安全。
  4. O_TMPFILE标志:为了更安全、高效地创建临时文件,引入了O_TMPFILE标志。它允许创建一个匿名的、不可见的inode,只有当通过linkat(2)赋予其一个目录链接后,它才会成为文件系统中的一个持久文件。
  5. open_howopenat2:为了进一步扩展open的功能,引入了一个新的struct open_how结构和一个新的openat2系统调用。这提供了一个可扩展的、面向未来的接口,可以传递比简单flags更多的参数,例如RESOLVE_*标志,用于更精细地控制路径解析的行为(如禁止跟随符号链接、禁止穿越挂载点等)。

目前该技术的社区活跃度和主流应用情况如何?

fs/open.c是内核VFS层最核心、最稳定、最关键的部分之一。

  • 绝对核心:任何需要访问文件系统对象的程序,其生命周期的第一步几乎总是调用open或其变体。
  • 社区状态:其核心逻辑非常稳定。社区的活跃度主要体现在:
    • 持续的安全加固,特别是对路径解析逻辑的审查。
    • 通过openat2等新接口,提供更强大、更灵活的文件打开控制。
    • 为支持新的VFS特性而进行的适配性修改。

核心原理与设计

它的核心工作原理是什么?

fs/open.c的核心是一个多阶段的、涉及VFS多个组件(dcache, inode, file)的协作流程

一个典型的openat()系统调用的内核路径如下:

  1. 系统调用入口:用户空间调用openat(),进入内核。fs/open.c中的do_sys_openat2()或类似的函数是其入口。
  2. 路径查找与权限检查:这是最复杂的部分。内核会调用filename_lookup()等VFS路径解析函数。
    a. 这个函数会从一个起始点(由dirfd决定,如果是AT_FDCWD则为当前工作目录)开始。
    b. 它会逐级地在**dcache(目录项缓存)**中查找路径组件。如果命中缓存,则快速获取对应的dentryinode
    c. 如果未命中,则需要调用底层文件系统的.lookup操作,从磁盘读取目录内容来查找。
    d. 在遍历的每一步,都会进行权限检查(如目录的执行权限)。
    e. 它会根据flags处理符号链接(默认跟随)和挂载点。
  3. 定位或创建文件:路径查找的结果可能是:
    • 文件存在:返回了目标的dentryinode
    • 文件不存在:如果flags中有O_CREAT,则会在最后一个存在的目录的inode->i_op->create.mknod被调用,以在磁盘上创建新的inode和dentry。
  4. 分配struct file:一旦目标inode确定,内核会调用get_empty_filp()来分配一个struct file对象。这个对象是这次打开会话的“句柄”。
  5. 调用具体实现 (f_op->open)struct file被填充,包括将其f_op指针设置为inode->i_fop。然后,关键的一步是调用f_op->open()
    • 这是具体文件系统或设备驱动介入的机会。例如,一个字符设备驱动的.open函数可能会在这里初始化硬件。
    • 对于大多数常规文件系统,这个.open函数通常很简单,或者直接为空。
  6. 分配文件描述符:最后,内核在当前进程的文件描述符表中找到一个空闲的位置,将struct file对象的指针存入,并将这个位置的索引(即文件描述符fd)返回给用户空间。

它的主要优势体現在哪些方面?

  • 强大的抽象:将极其复杂的路径解析、权限检查和多态分派逻辑封装在一个统一的接口背后。
  • 高性能:通过dcache和icache,对已访问路径的重复查找可以完全在内存中快速完成。
  • 安全:VFS的集中式权限检查和*at()系列调用,为整个系统提供了一道坚固的安全屏障。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 性能瓶颈:对于需要以极高频率创建和打开大量小文件的应用(元数据密集型工作负载),open的路径查找和inode操作可能会成为性能瓶颈。
  • 路径长度限制:文件系统的路径名有最大长度限制(PATH_MAX)。
  • TOCTOU风险:传统的open(path, ...)在处理需要预先检查(如access())的场景时,存在固有的竞争条件风险,尽管这已被openat()在很大程度上解决。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?

它是所有需要按名访问文件系统对象的场景的唯一且标准的解决方案。

  • 几乎所有应用程序:任何程序在读写文件、加载配置文件、加载动态库之前,都必须先open()它。
  • Shell操作:当你执行cat file.txt./my_program时,shell会fork()一个子进程,子进程在execve()之前会open()相应的文件来设置标准输入输出,或者execve()本身也会在内核中执行类似open的路径查找逻辑。
  • 动态链接器 (ld.so):在程序启动时,动态链接器会open()mmap()程序所需的所有共享库文件。

是否有不推荐使用该技术的场景?为什么?

更准确的说法是,在什么场景下,程序会避免频繁调用open()

  • 处理大量文件:一个需要处理目录树中成千上万个文件的程序,如果对每个文件都open(), read(), close(),效率会很低。更好的做法是持续打开所需文件,或者使用更高级的I/O模型。
  • 作为IPC(进程间通信):虽然可以通过文件进行进程间通信,但这通常是最低效的方式。管道(pipe())、UNIX域套接字或共享内存是更优越的IPC机制,它们通常只需要一次性的设置调用,而不需要反复open()

对比分析

请将其 与 其他相似技术 进行详细对比。

open(path, ...) vs. openat(dirfd, path, ...)

特性 open(path, ...) (传统) openat(dirfd, path, ...) (现代)
路径解析基点 总是相对于当前工作目录(如果是相对路径)。 可以相对于任意一个已打开的目录文件描述符dirfd
安全性 易受TOCTOU攻击。在检查和打开之间,路径的某个上级目录可能被替换。 更安全。通过使用dirfd,可以锁定操作的根目录,避免了大部分TOCTOU风险。
功能 基本功能。 更强大。当path为空字符串时,可以用来重新打开dirfd本身以修改其O_CLOEXEC等标志。
适用场景 简单的、非安全敏感的脚本和应用。 所有新代码的首选。尤其是在需要操作不受信任的路径,或在多线程环境中避免chdir()时。

open() vs. socket(), pipe()

特性 open() socket(), pipe()
创建对象 打开一个已存在的、按名访问的文件系统对象,或创建一个新的。 创建一个匿名的、无名的I/O端点。
返回 文件描述符,指向一个struct file 文件描述符(或一对),指向一个struct file,但其后端是socketpipefs_inode
核心用途 文件系统持久化存储访问。 进程间通信 (IPC)
查找方式 通过文件系统路径。 无需查找,直接在内核中创建。FD可以通过fork()继承或通过UNIX域套接字传递。

VFS文件打开接口:内核空间的文件打开实现

本代码片段是Linux VFS(虚拟文件系统)层为内核其他部分提供的、用于打开文件的核心接口。其主要功能是提供一个内核空间的等价物 filp_open,其行为类似于用户空间的open(2)系统调用。这段代码的核心在于其健壮性和安全性,它负责将用户(这里指内核中的调用者)提供的简单flagsmode参数,经过一系列严格的验证和转换,最终构建出一个VFS路径查找和文件创建操作所需的全功能内部数据结构struct open_flags

实现原理分析

该功能的实现是一个层次分明、层层递进的调用链,将复杂的标志位逻辑封装在内,为上层提供简洁的API。

  1. 顶层API (filp_open): 这是暴露给内核大部分代码使用的标准接口。
    • 它接收一个普通的C字符串filename作为路径。
    • 它的首要职责是安全地处理这个路径字符串。它调用getname_kernel将内核空间的字符串拷贝到一个struct filename结构中。这是一个关键的安全步骤,可以防止在路径查找过程中,原始的filename指针指向的内存被意外修改或释放。
    • 完成拷贝后,它调用下一层的file_open_name来执行真正的打开操作。
    • 最后,无论成功与否,它都调用putname来释getname_kernel分配的资源。
  2. 参数构建与验证 (build_open_how, build_open_flags): 这是整个流程的核心逻辑所在。
    • build_open_how: 这是一个简单的辅助函数,它将flagsmode打包进一个较新的struct open_how结构体中。这个结构体是为了支持更复杂的openat2(2)系统调用而引入的,它除了flagsmode之外,还能包含resolve等扩展标志。
    • build_open_flags: 这个函数是验证器翻译器。它接收open_how结构,并填充open_flags结构。其工作包括:
      • 严格验证: 它执行大量的检查以拒绝无效或危险的标志组合。例如:检查是否有未知的标志位、O_CREATO_DIRECTORY不能同时使用(这是一个历史bug的修复)、使用__O_TMPFILE时必须附带O_DIRECTORYO_PATH与其他标志的互斥关系等。
      • 翻译与扩展: 它将一个单一的flags整型,翻译成VFS内部使用的、更具描述性的多个字段:
        • op.acc_mode: 访问模式。它不仅从O_RDWR, O_WRONLY, O_RDONLY推导,还会根据O_TRUNC(截断)或O_APPEND(追加)等标志推断出写权限(MAY_WRITE)是必需的。
        • op.intent: 操作意图。将O_CREAT, O_EXCL等标志转换为LOOKUP_CREATE, LOOKUP_EXCL等内部意图。
        • op.lookup_flags: 路径查找标志。决定了路径解析的行为,如是否跟随符号链接(LOOKUP_FOLLOW)、是否要求目标是目录(LOOKUP_DIRECTORY)等。
  3. 执行打开操作 (do_filp_open): file_open_name在调用build_open_flags成功后,会调用do_filp_open(此函数未在此代码段中显示)。do_filp_open是VFS中真正执行路径查找(path walking)和文件打开操作的函数,它使用build_open_flags精心准备好的op结构作为其所有决策的依据。

代码分析

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
// ... (WILL_CREATE, O_PATH_FLAGS 宏定义) ...

// build_open_how: 将flags和mode打包成一个open_how结构体。
inline struct open_how build_open_how(int flags, umode_t mode)
{
struct open_how how = {
.flags = flags & VALID_OPEN_FLAGS, // 只保留有效的open标志位。
.mode = mode & S_IALLUGO, // 只保留有效的文件权限位。
};

// 如果设置了O_PATH,则清除所有与O_PATH不兼容的标志。
if (how.flags & O_PATH)
how.flags &= O_PATH_FLAGS;
// 仅当flags包含创建类标志时,mode才有意义。
if (!WILL_CREATE(how.flags))
how.mode = 0;
return how;
}

// build_open_flags: 将open_how结构翻译成VFS内部使用的open_flags结构。
inline int build_open_flags(const struct open_how *how, struct open_flags *op)
{
// ... (变量定义) ...

// 执行大量的有效性检查,拒绝无效的标志或模式组合。
if (flags & ~VALID_OPEN_FLAGS) return -EINVAL;
if (how->resolve & ~VALID_RESOLVE_FLAGS) return -EINVAL;
// ... (更多检查,如互斥的resolve标志,非创建时mode必须为0等) ...

// 关键安全检查:阻止同时使用O_DIRECTORY和O_CREAT来创建普通文件。
if ((flags & (O_DIRECTORY | O_CREAT)) == (O_DIRECTORY | O_CREAT))
return -EINVAL;

// ... (处理O_TMPFILE, O_PATH, O_SYNC等特殊标志的逻辑) ...

// 将清理和验证后的标志存入open_flags结构。
op->open_flag = flags;

// 根据标志推断访问模式(access mode)。
if (flags & O_TRUNC)
acc_mode |= MAY_WRITE; // O_TRUNC 暗示需要写权限。
if (flags & O_APPEND)
acc_mode |= MAY_APPEND; // O_APPEND 暗示一种特殊的写权限。
op->acc_mode = acc_mode;

// 根据标志设置操作意图(intent)。
op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN;
if (flags & O_CREAT) {
op->intent |= LOOKUP_CREATE;
if (flags & O_EXCL)
op->intent |= LOOKUP_EXCL;
}

// 根据标志设置路径查找标志(lookup_flags)。
if (flags & O_DIRECTORY)
lookup_flags |= LOOKUP_DIRECTORY;
if (!(flags & O_NOFOLLOW))
lookup_flags |= LOOKUP_FOLLOW; // 默认跟随符号链接。
// ... (根据how->resolve设置更多查找标志) ...
op->lookup_flags = lookup_flags;

return 0; // 成功
}

// file_open_name: 从一个struct filename打开文件。
struct file *file_open_name(struct filename *name, int flags, umode_t mode)
{
struct open_flags op;
// 1. 将flags和mode打包。
struct open_how how = build_open_how(flags, mode);
// 2. 验证并翻译成VFS内部结构。
int err = build_open_flags(&how, &op);
if (err)
return ERR_PTR(err); // 如果翻译失败,返回错误指针。
// 3. 调用VFS核心的路径查找和打开函数。
return do_filp_open(AT_FDCWD, name, &op);
}

// filp_open: 内核空间打开文件的主要入口API。
// @filename: 要打开的文件的路径字符串。
// @flags: 打开标志。
// @mode: 创建模式。
struct file *filp_open(const char *filename, int flags, umode_t mode)
{
// 安全地将内核空间的路径字符串拷贝到struct filename中。
struct filename *name = getname_kernel(filename);
struct file *file = ERR_CAST(name); // 先假定失败。

if (!IS_ERR(name)) { // 如果拷贝成功
// 调用下一层函数执行打开操作。
file = file_open_name(name, flags, mode);
// 释放为filename分配的内存。
putname(name);
}
return file;
}
// 导出符号,使内核模块可以调用此函数。
EXPORT_SYMBOL(filp_open);

VFS文件打开的最后一步:连接路径与文件对象

本代码片段展示了VFS层中,当路径名已经被成功解析为一个dentry之后,如何完成“打开”这个动作的最后环节。do_o_path函数是一个上层协调者,它首先执行路径查找,然后调用vfs_open来完成真正的打开操作。vfs_open的核心任务是将一个已分配但“空”的struct file对象,与代表物理文件的inode进行最终的绑定,并调用底层文件系统的特定open方法。

您可以将这个过程想象成:

  1. path_lookupat:您(进程)拿着一张地址(路径名),在大楼里(文件系统)找到了正确的房间门(dentry)。
  2. do_o_path:这是门口的接待员。他先核对您的地址(调用path_lookupat),确认无误后,拿出了一张空白的访客通行证(一个已分配的struct file对象)。
  3. vfs_open:接待员拿着您的访客证,刷卡开门。这个“刷卡开门”的动作(调用do_dentry_open),会通知房间的管理员(底层文件系统驱动),“有人要进来了”,管理员可能会做一些准备工作(执行文件系统自己的->open方法)。最后,接待员把通行证交给您,您现在就可以凭证进出这个房间了。

实现原理分析

这个流程清晰地划分了VFS的职责:路径解析与文件打开是两个独立的阶段。

  1. 协调者 (do_o_path):

    • 输入: 它接收一个nameidata结构体,这是路径查找过程的上下文。
    • 第一步:路径查找: 它做的第一件事就是调用我们之前分析过的path_lookupat。这一步将字符串路径名转换为一个内核可识别的struct path对象(包含了dentryvfsmount)。
    • 第二步:审计: 在成功找到路径后,audit_inode会被调用。这是一个安全钩子,用于内核的审计子系统(Audit subsystem)。它会记录下“哪个进程(current)尝试访问了哪个文件(path.dentry)”这样的日志,这对于安全监控和事后分析(例如SELinux的日志)至关重要。
    • 第三步:执行打开: 它调用vfs_open,将查找的结果path和之前分配好的file对象传递过去,委托它完成最后的打开步骤。
    • 第四步:资源清理: path_lookupat成功后,会增加path对象的引用计数。path_put(&path)的作用就是在使用完毕后,递减这个引用计数。这是Linux内核中至关重要的内存管理规则,确保资源在没有被使用时能被正确释放。
  2. 执行者 (vfs_open):

    • 输入: 它接收一个精确的path对象和一个半初始化的file对象。
    • 核心操作: vfs_open的核心工作全部委托给了do_dentry_open函数(此函数未在代码片段中提供,但极其重要)。do_dentry_open会执行以下关键操作:
      a. 从file->f_path.dentry中获取inode
      b. 进行最终的权限检查(例如,文件是否可读、可写)。
      c. 最关键的一步:调用inode中文件操作表(i_fop)里的->open方法。正是这一步,VFS将控制权交给了具体的、底层的 filesystem driver(例如ext4, FATfs, NFS等)。底层文件系统驱动可以在这里执行它特有的初始化操作。
      d. 如果一切顺利,它会将inodei_fop等信息正式填充到file结构体中,完成绑定。
    • 事件通知: 在do_dentry_open成功返回后,fsnotify_open(file)被调用。这会向内核的事件通知系统(如inotify)广播一个“文件被打开”的事件,允许监视文件系统活动的应用程序(如桌面搜索索引器、杀毒软件)做出响应。

代码分析

vfs_open 函数

这是执行最终打开操作的函数。

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
/**
* vfs_open - 打开给定路径的文件
* @path: 要打开的文件的路径结构体 (path_lookupat的结果)
* @file: 一个新分配的、f_flag已初始化的file结构体
*/
int vfs_open(const struct path *path, struct file *file)
{
int ret;

// 步骤1: 将查找到的路径结果(*path)保存在file结构体中。
// 从此刻起,这个file对象就知道了它将要代表哪个文件。
file->f_path = *path;

// 步骤2: 调用核心的打开函数。这是真正“做事”的地方。
// 它会检查权限,并最终调用底层文件系统的 ->open 方法。
ret = do_dentry_open(file, NULL);

// 步骤3: 如果打开成功...
if (!ret) {
/*
* 一旦我们返回一个设置了FMODE_OPENED标志的file对象,
* 当它最终被关闭时(__fput),内核会调用fsnotify_close()。
* 因此,为了对称性,我们在这里需要调用fsnotify_open()。
*/
fsnotify_open(file);
}
return ret; // 返回操作结果。
}

do_o_path 函数

这是一个高层封装,将路径查找和文件打开两个步骤串联起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int do_o_path(struct nameidata *nd, unsigned flags, struct file *file)
{
struct path path; // 声明一个局部的path结构体,用于接收查找结果。
int error;

// 步骤1: 调用路径查找函数,将路径名(nd->name)解析为path结构体。
error = path_lookupat(nd, flags, &path);

// 步骤2: 如果路径查找成功...
if (!error) {
// 步骤2a: 调用审计子系统,记录下对这个inode的访问。
audit_inode(nd->name, path.dentry, 0);

// 步骤2b: 调用vfs_open,用查找到的路径来完成文件打开。
error = vfs_open(&path, file);

// 步骤2c: 释放对path的引用。path_lookupat成功时会增加
// path.dentry和path.mnt的引用计数,这里必须将其递减。
path_put(&path);
}
return error; // 返回最终的错误码。
}

VFS 文件对象的核心初始化:赋予文件句柄生命

本代码片段展示了do_dentry_open函数,这是在vfs_open内部调用的、真正执行文件打开核心逻辑的函数。它的职责是接收一个已经关联到dentryfile结构体,然后完成所有必要的检查、状态设置,并最终调用底层文件系统的open方法,从而将一个抽象的file对象彻底转变为一个可用的、代表特定打开文件的内核句柄。辅助函数file_get_write_access则专门负责为写操作获取必要的“通行证”。

您可以将do_dentry_open想象成一个极其严谨的授权和登记仪式

  1. 核实身份: 首先确认要登记的房间(inode)和地址(path)。
  2. 特殊通道: 检查访客(进程)是否有特殊的通行证(如O_PATH),如果是,就走快速通道,只登记最基本的信息,不进行深入检查。
  3. 获取许可: 如果访客想要写入,仪式的主持人(do_dentry_open)会去两个地方申请许可:一是房间本身的管理员(inode),二是整栋大楼的物业(vfsmount)。这就是file_get_write_access的工作。
  4. 签署协议: 主持人会拿出房间的“住户手册”(inode->i_fop),并将其复印一份给访客(f->f_op = fops_get(...))。
  5. 安检: 安全部门(LSM)和事件通知部门(fsnotify)会进行最后的安全检查和登记。
  6. 房间交接: 最后,主持人会请房间的原主人(底层文件系统的->open方法)出来,和访客见个面,交接一些房间的特殊使用说明。
  7. 颁发通行证: 仪式完成,通行证(file结构体)被彻底激活,访客现在可以自由使用房间了。

实现原理分析

do_dentry_open的实现是一个线性的、步骤清晰但细节繁多的过程,每一步都至关重要。

  1. 基本信息绑定: 函数开始时,它首先将inodemapping(用于内存映射)等核心信息从dentryinode中复制到file结构体中。path_get增加路径的引用计数,确保在file对象存在期间,它所指向的dentryvfsmount不会被释放。
  2. O_PATH快速路径: O_PATH是一个特殊的打开标志,它表示调用者只想获得一个指向文件系统位置的引用(一个文件描述符),而不打算进行真正的读写操作。因此,代码在这里走了一个快速通道:不获取写权限,不调用底层open,只设置最基本的标志,然后直接返回成功。这对于某些需要在文件系统树中定位但不操作内容的程序(如stat的某些变种)是极大的性能优化。
  3. 获取写访问权 (file_get_write_access): 这是写操作前置检查的核心。
    • 双重检查: 它必须通过两道关卡。第一道是get_write_access(inode),它检查inode本身是否为只读(例如,因为chattr +i)。第二道是mnt_get_write_access(mount),它执行我们上一节分析过的、复杂的VFS挂载点只读状态同步协议。必须同时获得inode和mount的写许可,才能继续
    • 错误处理: C语言的goto语句在这里得到了非常经典和优雅的运用。如果第二步mnt_get_write_access失败了,goto cleanup_inode会确保第一步成功获取的inode写权限被正确释放。这种模式避免了复杂的if-else嵌套,使得资源清理路径清晰可控。
  4. 文件操作表 (f_op) 的绑定: f->f_op = fops_get(inode->i_fop);是VFS多态性的核心体现。inode->i_fop是由底层文件系统在创建inode时设置的,它指向一个包含read, write, llseek等函数指针的结构体。这行代码将这个“操作手册”与file对象绑定。从此以后,对这个file对象的所有VFS操作(如vfs_read)都会通过f->f_op自动分派到底层文件系统的正确实现函数上。
  5. 安全和事件钩子: 在调用底层open之前,内核会先通知安全模块(security_file_open)和事件通知系统(fsnotify_open_perm_and_set_mode),给它们一个否决此次打开或进行预处理的机会。
  6. 租约 (Lease) 管理: break_lease处理文件租约。租约是一种服务器-客户端模式的缓存一致性协议(常见于NFS等网络文件系统)。如果一个客户端(如此处的打开操作)想要写入一个被其他客户端租用的文件,这个函数会负责发送“破坏租约”的通知,强制其他客户端刷新它们的缓存。
  7. 调用底层 open: error = open(inode, f);是整个过程的顶峰。VFS将控制权完全交给底层文件系统。底层文件系统可以在这里执行任何它需要的初始化操作,例如,设备驱动可能会检查设备是否就绪,网络文件系统可能会与服务器建立连接等。
  8. 最终状态设置: 在底层open成功后,do_dentry_open会根据文件操作表和打开模式,设置一系列FMODE_*标志(如FMODE_CAN_READ, FMODE_CAN_WRITE)。这些标志是VFS内部的快速查询位,用于高效地判断一个打开的文件是否支持某种操作,避免了每次都去检查函数指针是否为NULL。

代码分析

file_get_write_access 函数

为写操作获取inode和mount的双重许可。

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
static inline int file_get_write_access(struct file *f)
{
int error;

// 第一关: 获取inode的写权限。这会检查inode是否被设为只读。
error = get_write_access(f->f_inode);
if (unlikely(error))
return error;

// 第二关: 获取vfsmount的写权限。这是一个复杂的同步过程。
error = mnt_get_write_access(f->f_path.mnt);
if (unlikely(error))
goto cleanup_inode; // 如果第二关失败,必须回滚第一关的操作。

// 特殊情况: 处理分层文件系统(例如OverlayFS)的写权限。
if (unlikely(f->f_mode & FMODE_BACKING)) {
error = mnt_get_write_access(backing_file_user_path(f)->mnt);
if (unlikely(error))
goto cleanup_mnt; // 如果第三关失败,回滚第二关。
}
return 0; // 所有关卡通过,成功。

cleanup_mnt:
mnt_put_write_access(f->f_path.mnt); // 释放第二关获取的权限。
cleanup_inode:
put_write_access(f->f_inode); // 释放第一关获取的权限。
return error;
}

do_dentry_open 函数

文件打开的核心逻辑实现。

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
static int do_dentry_open(struct file *f,
int (*open)(struct inode *, struct file *))
{
static const struct file_operations empty_fops = {}; // 一个空的、无操作的fops表。
struct inode *inode = f->f_path.dentry->d_inode; // 获取inode。
int error;

path_get(&f->f_path); // 增加路径的引用计数,锁定dentry和mount。
f->f_inode = inode;
f->f_mapping = inode->i_mapping; // 关联地址空间对象。
// ... 采样并记录错误状态 ...

// O_PATH 快速通道:如果用户只想获取一个路径句柄,不进行读写。
if (unlikely(f->f_flags & O_PATH)) {
f->f_mode = FMODE_PATH | FMODE_OPENED;
file_set_fsnotify_mode(f, FMODE_NONOTIFY); // 禁用事件通知。
f->f_op = &empty_fops; // 使用空的fops表。
return 0; // 直接成功返回。
}

// 根据打开模式,处理读/写计数和权限。
if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ) {
i_readcount_inc(inode); // 如果是只读打开,增加inode的只读计数。
} else if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) {
// 如果是写打开,并且不是特殊文件(如设备文件)。
error = file_get_write_access(f); // 获取双重写权限。
if (unlikely(error))
goto cleanup_file;
f->f_mode |= FMODE_WRITER; // 标记这是一个写者。
}

// 为普通文件和目录启用原子化位置更新模式。
if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
f->f_mode |= FMODE_ATOMIC_POS;

// 核心步骤:从inode获取文件操作表并增加其引用计数。
f->f_op = fops_get(inode->i_fop);
if (WARN_ON(!f->f_op)) { // 如果inode没有提供fops,这是个严重错误。
error = -ENODEV;
goto cleanup_all;
}

// 调用安全模块(LSM)的钩子。
error = security_file_open(f);
if (error)
goto cleanup_all;

// 调用fsnotify的权限钩子。
error = fsnotify_open_perm_and_set_mode(f);
if (error)
goto cleanup_all;

// 处理文件租约。
error = break_lease(file_inode(f), f->f_flags);
if (error)
goto cleanup_all;

/* 默认启用seek, pread, pwrite功能,底层open可以再禁用它们。 */
f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;

// 确定最终要调用的open函数。
if (!open)
open = f->f_op->open;

// 如果存在open函数,就调用它。
if (open) {
error = open(inode, f); // *** VFS调用底层文件系统 ***
if (error)
goto cleanup_all; // 如果底层open失败,进行清理。
}
f->f_mode |= FMODE_OPENED; // 标记文件已成功打开。

// 根据fops表中的函数指针,设置FMODE_CAN_*快速标志。
if ((f->f_mode & FMODE_READ) &&
likely(f->f_op->read || f->f_op->read_iter))
f->f_mode |= FMODE_CAN_READ;
if ((f->f_mode & FMODE_WRITE) &&
likely(f->f_op->write || f->f_op->write_iter))
f->f_mode |= FMODE_CAN_WRITE;
// ... 其他类似标志的设置 ...

// 清理掉一些只在打开时有意义的标志。
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);

// ... 初始化预读(read-ahead)状态等 ...

// ... 处理大页缓存(Huge page cache)的特殊逻辑 ...

return 0; // 成功返回。

// 错误清理路径
cleanup_all:
if (WARN_ON_ONCE(error > 0)) // 确保返回的是负数错误码。
error = -EINVAL;
fops_put(f->f_op); // 释放对fops表的引用。
put_file_access(f); // 释放读/写权限。
cleanup_file:
path_put(&f->f_path); // 释放对路径的引用。
// 清空指针,防止悬挂指针。
f->f_path.mnt = NULL;
f->f_path.dentry = NULL;
f->f_inode = NULL;
return error;
}