[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
的发展主要体现在对路径解析和文件打开过程的不断增强和安全加固上。
open
和creat
:最初的POSIX接口。creat(path, mode)
在功能上等同于open(path, O_CREAT|O_WRONLY|O_TRUNC, mode)
。- 路径查找的重构:内核的路径查找逻辑(
path_lookup
)经历了多次重构,以提高性能(例如通过dcache)和修复安全漏洞(如符号链接攻击)。 *at()
系列系统调用的引入:这是一个重大的安全里程碑。openat(dirfd, path, flags)
被引入,以解决传统open()
存在的**TOCTOU (Time-of-check to time-of-use)**竞争条件漏洞。它允许相对于一个已打开的目录文件描述符dirfd
来打开一个文件,这使得操作更加原子化和安全。O_TMPFILE
标志:为了更安全、高效地创建临时文件,引入了O_TMPFILE
标志。它允许创建一个匿名的、不可见的inode,只有当通过linkat(2)
赋予其一个目录链接后,它才会成为文件系统中的一个持久文件。open_how
和openat2
:为了进一步扩展open
的功能,引入了一个新的struct open_how
结构和一个新的openat2
系统调用。这提供了一个可扩展的、面向未来的接口,可以传递比简单flags
更多的参数,例如RESOLVE_*
标志,用于更精细地控制路径解析的行为(如禁止跟随符号链接、禁止穿越挂载点等)。
目前该技术的社区活跃度和主流应用情况如何?
fs/open.c
是内核VFS层最核心、最稳定、最关键的部分之一。
- 绝对核心:任何需要访问文件系统对象的程序,其生命周期的第一步几乎总是调用
open
或其变体。 - 社区状态:其核心逻辑非常稳定。社区的活跃度主要体现在:
- 持续的安全加固,特别是对路径解析逻辑的审查。
- 通过
openat2
等新接口,提供更强大、更灵活的文件打开控制。 - 为支持新的VFS特性而进行的适配性修改。
核心原理与设计
它的核心工作原理是什么?
fs/open.c
的核心是一个多阶段的、涉及VFS多个组件(dcache, inode, file)的协作流程。
一个典型的openat()
系统调用的内核路径如下:
- 系统调用入口:用户空间调用
openat()
,进入内核。fs/open.c
中的do_sys_openat2()
或类似的函数是其入口。 - 路径查找与权限检查:这是最复杂的部分。内核会调用
filename_lookup()
等VFS路径解析函数。
a. 这个函数会从一个起始点(由dirfd
决定,如果是AT_FDCWD
则为当前工作目录)开始。
b. 它会逐级地在**dcache(目录项缓存)**中查找路径组件。如果命中缓存,则快速获取对应的dentry
和inode
。
c. 如果未命中,则需要调用底层文件系统的.lookup
操作,从磁盘读取目录内容来查找。
d. 在遍历的每一步,都会进行权限检查(如目录的执行权限)。
e. 它会根据flags
处理符号链接(默认跟随)和挂载点。 - 定位或创建文件:路径查找的结果可能是:
- 文件存在:返回了目标的
dentry
和inode
。 - 文件不存在:如果
flags
中有O_CREAT
,则会在最后一个存在的目录的inode->i_op->create
或.mknod
被调用,以在磁盘上创建新的inode和dentry。
- 文件存在:返回了目标的
- 分配
struct file
:一旦目标inode
确定,内核会调用get_empty_filp()
来分配一个struct file
对象。这个对象是这次打开会话的“句柄”。 - 调用具体实现 (
f_op->open
):struct file
被填充,包括将其f_op
指针设置为inode->i_fop
。然后,关键的一步是调用f_op->open()
。- 这是具体文件系统或设备驱动介入的机会。例如,一个字符设备驱动的
.open
函数可能会在这里初始化硬件。 - 对于大多数常规文件系统,这个
.open
函数通常很简单,或者直接为空。
- 这是具体文件系统或设备驱动介入的机会。例如,一个字符设备驱动的
- 分配文件描述符:最后,内核在当前进程的文件描述符表中找到一个空闲的位置,将
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 ,但其后端是socket 或pipefs_inode 。 |
核心用途 | 文件系统持久化存储访问。 | 进程间通信 (IPC)。 |
查找方式 | 通过文件系统路径。 | 无需查找,直接在内核中创建。FD可以通过fork() 继承或通过UNIX域套接字传递。 |
VFS文件打开接口:内核空间的文件打开实现
本代码片段是Linux VFS(虚拟文件系统)层为内核其他部分提供的、用于打开文件的核心接口。其主要功能是提供一个内核空间的等价物 filp_open
,其行为类似于用户空间的open(2)
系统调用。这段代码的核心在于其健壮性和安全性,它负责将用户(这里指内核中的调用者)提供的简单flags
和mode
参数,经过一系列严格的验证和转换,最终构建出一个VFS路径查找和文件创建操作所需的全功能内部数据结构struct open_flags
。
实现原理分析
该功能的实现是一个层次分明、层层递进的调用链,将复杂的标志位逻辑封装在内,为上层提供简洁的API。
- 顶层API (
filp_open
): 这是暴露给内核大部分代码使用的标准接口。- 它接收一个普通的C字符串
filename
作为路径。 - 它的首要职责是安全地处理这个路径字符串。它调用
getname_kernel
将内核空间的字符串拷贝到一个struct filename
结构中。这是一个关键的安全步骤,可以防止在路径查找过程中,原始的filename
指针指向的内存被意外修改或释放。 - 完成拷贝后,它调用下一层的
file_open_name
来执行真正的打开操作。 - 最后,无论成功与否,它都调用
putname
来释getname_kernel
分配的资源。
- 它接收一个普通的C字符串
- 参数构建与验证 (
build_open_how
,build_open_flags
): 这是整个流程的核心逻辑所在。build_open_how
: 这是一个简单的辅助函数,它将flags
和mode
打包进一个较新的struct open_how
结构体中。这个结构体是为了支持更复杂的openat2(2)
系统调用而引入的,它除了flags
和mode
之外,还能包含resolve
等扩展标志。build_open_flags
: 这个函数是验证器和翻译器。它接收open_how
结构,并填充open_flags
结构。其工作包括:- 严格验证: 它执行大量的检查以拒绝无效或危险的标志组合。例如:检查是否有未知的标志位、
O_CREAT
和O_DIRECTORY
不能同时使用(这是一个历史bug的修复)、使用__O_TMPFILE
时必须附带O_DIRECTORY
、O_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
)等。
- 严格验证: 它执行大量的检查以拒绝无效或危险的标志组合。例如:检查是否有未知的标志位、
- 执行打开操作 (
do_filp_open
):file_open_name
在调用build_open_flags
成功后,会调用do_filp_open
(此函数未在此代码段中显示)。do_filp_open
是VFS中真正执行路径查找(path walking)和文件打开操作的函数,它使用build_open_flags
精心准备好的op
结构作为其所有决策的依据。
代码分析
1 | // ... (WILL_CREATE, O_PATH_FLAGS 宏定义) ... |
VFS文件打开的最后一步:连接路径与文件对象
本代码片段展示了VFS层中,当路径名已经被成功解析为一个dentry
之后,如何完成“打开”这个动作的最后环节。do_o_path
函数是一个上层协调者,它首先执行路径查找,然后调用vfs_open
来完成真正的打开操作。vfs_open
的核心任务是将一个已分配但“空”的struct file
对象,与代表物理文件的inode
进行最终的绑定,并调用底层文件系统的特定open
方法。
您可以将这个过程想象成:
path_lookupat
:您(进程)拿着一张地址(路径名),在大楼里(文件系统)找到了正确的房间门(dentry
)。do_o_path
:这是门口的接待员。他先核对您的地址(调用path_lookupat
),确认无误后,拿出了一张空白的访客通行证(一个已分配的struct file
对象)。vfs_open
:接待员拿着您的访客证,刷卡开门。这个“刷卡开门”的动作(调用do_dentry_open
),会通知房间的管理员(底层文件系统驱动),“有人要进来了”,管理员可能会做一些准备工作(执行文件系统自己的->open
方法)。最后,接待员把通行证交给您,您现在就可以凭证进出这个房间了。
实现原理分析
这个流程清晰地划分了VFS的职责:路径解析与文件打开是两个独立的阶段。
协调者 (
do_o_path
):- 输入: 它接收一个
nameidata
结构体,这是路径查找过程的上下文。 - 第一步:路径查找: 它做的第一件事就是调用我们之前分析过的
path_lookupat
。这一步将字符串路径名转换为一个内核可识别的struct path
对象(包含了dentry
和vfsmount
)。 - 第二步:审计: 在成功找到路径后,
audit_inode
会被调用。这是一个安全钩子,用于内核的审计子系统(Audit subsystem)。它会记录下“哪个进程(current
)尝试访问了哪个文件(path.dentry
)”这样的日志,这对于安全监控和事后分析(例如SELinux的日志)至关重要。 - 第三步:执行打开: 它调用
vfs_open
,将查找的结果path
和之前分配好的file
对象传递过去,委托它完成最后的打开步骤。 - 第四步:资源清理:
path_lookupat
成功后,会增加path
对象的引用计数。path_put(&path)
的作用就是在使用完毕后,递减这个引用计数。这是Linux内核中至关重要的内存管理规则,确保资源在没有被使用时能被正确释放。
- 输入: 它接收一个
执行者 (
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. 如果一切顺利,它会将inode
和i_fop
等信息正式填充到file
结构体中,完成绑定。 - 事件通知: 在
do_dentry_open
成功返回后,fsnotify_open(file)
被调用。这会向内核的事件通知系统(如inotify
)广播一个“文件被打开”的事件,允许监视文件系统活动的应用程序(如桌面搜索索引器、杀毒软件)做出响应。
- 输入: 它接收一个精确的
代码分析
vfs_open
函数
这是执行最终打开操作的函数。
1 | /** |
do_o_path
函数
这是一个高层封装,将路径查找和文件打开两个步骤串联起来。
1 | static int do_o_path(struct nameidata *nd, unsigned flags, struct file *file) |
VFS 文件对象的核心初始化:赋予文件句柄生命
本代码片段展示了do_dentry_open
函数,这是在vfs_open
内部调用的、真正执行文件打开核心逻辑的函数。它的职责是接收一个已经关联到dentry
的file
结构体,然后完成所有必要的检查、状态设置,并最终调用底层文件系统的open
方法,从而将一个抽象的file
对象彻底转变为一个可用的、代表特定打开文件的内核句柄。辅助函数file_get_write_access
则专门负责为写操作获取必要的“通行证”。
您可以将do_dentry_open
想象成一个极其严谨的授权和登记仪式:
- 核实身份: 首先确认要登记的房间(
inode
)和地址(path
)。 - 特殊通道: 检查访客(进程)是否有特殊的通行证(如
O_PATH
),如果是,就走快速通道,只登记最基本的信息,不进行深入检查。 - 获取许可: 如果访客想要写入,仪式的主持人(
do_dentry_open
)会去两个地方申请许可:一是房间本身的管理员(inode
),二是整栋大楼的物业(vfsmount
)。这就是file_get_write_access
的工作。 - 签署协议: 主持人会拿出房间的“住户手册”(
inode->i_fop
),并将其复印一份给访客(f->f_op = fops_get(...)
)。 - 安检: 安全部门(LSM)和事件通知部门(fsnotify)会进行最后的安全检查和登记。
- 房间交接: 最后,主持人会请房间的原主人(底层文件系统的
->open
方法)出来,和访客见个面,交接一些房间的特殊使用说明。 - 颁发通行证: 仪式完成,通行证(
file
结构体)被彻底激活,访客现在可以自由使用房间了。
实现原理分析
do_dentry_open
的实现是一个线性的、步骤清晰但细节繁多的过程,每一步都至关重要。
- 基本信息绑定: 函数开始时,它首先将
inode
、mapping
(用于内存映射)等核心信息从dentry
和inode
中复制到file
结构体中。path_get
增加路径的引用计数,确保在file
对象存在期间,它所指向的dentry
和vfsmount
不会被释放。 O_PATH
快速路径:O_PATH
是一个特殊的打开标志,它表示调用者只想获得一个指向文件系统位置的引用(一个文件描述符),而不打算进行真正的读写操作。因此,代码在这里走了一个快速通道:不获取写权限,不调用底层open
,只设置最基本的标志,然后直接返回成功。这对于某些需要在文件系统树中定位但不操作内容的程序(如stat
的某些变种)是极大的性能优化。- 获取写访问权 (
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
嵌套,使得资源清理路径清晰可控。
- 双重检查: 它必须通过两道关卡。第一道是
- 文件操作表 (
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
自动分派到底层文件系统的正确实现函数上。 - 安全和事件钩子: 在调用底层
open
之前,内核会先通知安全模块(security_file_open
)和事件通知系统(fsnotify_open_perm_and_set_mode
),给它们一个否决此次打开或进行预处理的机会。 - 租约 (Lease) 管理:
break_lease
处理文件租约。租约是一种服务器-客户端模式的缓存一致性协议(常见于NFS等网络文件系统)。如果一个客户端(如此处的打开操作)想要写入一个被其他客户端租用的文件,这个函数会负责发送“破坏租约”的通知,强制其他客户端刷新它们的缓存。 - 调用底层
open
:error = open(inode, f);
是整个过程的顶峰。VFS将控制权完全交给底层文件系统。底层文件系统可以在这里执行任何它需要的初始化操作,例如,设备驱动可能会检查设备是否就绪,网络文件系统可能会与服务器建立连接等。 - 最终状态设置: 在底层
open
成功后,do_dentry_open
会根据文件操作表和打开模式,设置一系列FMODE_*
标志(如FMODE_CAN_READ
,FMODE_CAN_WRITE
)。这些标志是VFS内部的快速查询位,用于高效地判断一个打开的文件是否支持某种操作,避免了每次都去检查函数指针是否为NULL。
代码分析
file_get_write_access
函数
为写操作获取inode和mount的双重许可。
1 | static inline int file_get_write_access(struct file *f) |
do_dentry_open
函数
文件打开的核心逻辑实现。
1 | static int do_dentry_open(struct file *f, |