Appearance
源码实证:Linux openat() 如何穿过 VFS
本页固定 Linux v6.10。问题是:应用调用 openat() 后,内核怎样把一个用户态路径和 flags 变成一个可用的 file descriptor,又怎样在并发目录修改、mount namespace、权限和不同文件系统之间保持正确。
text
openat(dfd, pathname, flags, mode)
-> do_sys_openat2
-> 预留 fd 槽位
-> do_filp_open
-> 路径遍历与 VFS 检查
-> 具体文件系统的 open 操作
-> fd_install
-> 用户态拿到整数 fd 或 errnoopenat() 返回成功,不代表文件数据已读入内存,更不代表任何数据已写入磁盘。它只建立了进程 fd table 中一个可引用的 struct file,供后续 read、write、fstat、close 等系统调用使用。
1. UAPI 入口和内部实现分开
在 fs/open.c,SYSCALL_DEFINE4(openat, ...) 是用户态 syscall ABI 对应的通用 VFS 入口。具体 CPU 如何从用户态陷入内核属于架构相关代码;进入该函数以后,路径就由 VFS 通用层处理。
text
应用 / libc
-> 架构相关 syscall entry
-> SYSCALL_DEFINE4(openat)
-> do_sys_open
-> do_sys_openat2这就是 Linux 的稳定面与可变面:用户依赖 openat 的参数、返回值与 errno 语义;内核可以重构路径缓存、锁、struct file 内部字段和具体文件系统实现。
2. fd 槽位先保留,文件对象后安装
do_sys_openat2 先校验并转换 open_how flags,再把用户态 pathname 复制为内核可用的 struct filename。之后调用:
text
get_unused_fd_flags()
-> 保留当前进程 fd table 的一个数字槽位
do_filp_open()
-> 建立 struct file,或返回错误
fd_install()
-> 只有成功后才将 file 安装进该 fd 槽位这避免多个线程在同一个进程中争抢同一 fd 编号,也避免失败时把半成品 file 暴露给用户态。若 do_filp_open 失败,代码调用 put_unused_fd 回收预留槽位;只有成功的 struct file 才进入 fd_install。
fd 只是 fd table 中的整数索引,struct file 才承载一次打开实例的 flags、当前位置、路径和操作表。多个 fd 可以引用同一个底层 inode,也可因独立 open 而拥有不同文件位置和 flags。
3. 路径遍历:先走快路径,必要时回退
do_filp_open 用 set_nameidata 建立当前解析上下文,其中包含 dfd、pathname 和调用进程可见的根/当前目录语义。它先执行:
text
path_openat(..., LOOKUP_RCU)这是一条尽量无锁的 RCU 路径遍历。若返回 -ECHILD,代码改用普通引用计数遍历;若返回 -ESTALE,再以 LOOKUP_REVAL 要求重新验证。这里的回退不是“偶发错误重试”,而是在目录项、mount 或网络文件系统状态无法由乐观快路径安全确认时,换用更强的正确性路径。
path_openat 负责:
text
alloc_empty_file
-> path_init
-> link_path_walk(处理中间路径与符号链接)
-> open_last_lookups(处理最后一个名字)
-> do_open
-> 成功返回 file;失败 fput(file)路径不是一个全局字符串查表。它相对于 dfd 或进程根目录解析,受到 mount namespace、chroot 类根边界、符号链接规则、目录项缓存和并发重命名/卸载的影响。VFS 把这些通用语义集中在 namei.c,文件系统不必各自实现一遍。
4. do_open:权限、LSM 与文件系统分派
do_open 处理路径最后一步后,依次覆盖:
text
complete_walk
-> O_CREAT / O_EXCL、目录和 sticky-bit 约束
-> O_TRUNC 所需的 mount 写权限
-> may_open 的访问权限与类型检查
-> vfs_open
-> security_file_post_open(LSM)
-> 必要时 truncatevfs_open 是通用 VFS 到具体文件系统/设备文件操作的分派点。ext4、XFS、NFS、FUSE 或设备驱动可以在自己的 inode/file operations 下实现差异;调用方仍只看到统一 fd 与 errno 语义。LSM hook 在通用路径中出现,说明安全不是“某个文件系统可选附加的检查”,而是打开生命周期的一部分。
5. 错误返回是契约的一部分
| 错误类别 | 典型 errno | 说明 |
|---|---|---|
| 路径不存在 | ENOENT | 中间或最后路径分量无法解析 |
| 权限不足 | EACCES / EPERM | DAC、mount 或 LSM 等访问约束不满足 |
| 类型不符 | ENOTDIR / EISDIR | 路径分量或打开目标与期望类型不一致 |
| 创建冲突 | EEXIST | `O_CREAT |
| 并发解析回退 | ECHILD / ESTALE(内部) | 触发更强查找路径,不一定直接交给用户态 |
用户态程序不应依赖 namei.c 里的中间回退细节;它依赖最终 syscall 返回的 ABI。内核内部可以改变快路径,只要对外语义不被破坏。
6. 与 Deployment controller 的对照
| 问题 | Deployment controller | Linux openat() |
|---|---|---|
| 输入 | API 对象事件 | 同步 syscall 参数 |
| 读取状态 | informer cache、必要时强读 | dentry/mount/inode 与进程 fd table |
| 并发处理 | 同一 key 串行,不同 key 并行 | RCU 快路径、引用计数回退、fd 槽位保留 |
| 完成 | 后续多控制器与 Node 收敛 | 成功安装 struct file 到 fd table |
| 失败表示 | status、Event、限速重试 | syscall errno,必要时内部回退 |
两者都不把第一次观察当作永久事实:Kubernetes 用 reconciliation 应对分布式、异步变化;VFS 用查找验证、引用和回退应对单机高并发变化。时间尺度和正确性契约不同,但“读取事实后在边界内安全行动”的结构相同。
可复现阅读与验证
text
git clone --branch v6.10 --depth 1 https://github.com/torvalds/linux.git
rg -n "do_sys_openat2|SYSCALL_DEFINE4\(openat" fs/open.c
rg -n "do_filp_open|path_openat|do_open" fs/namei.c运行时验证可用 strace -e trace=openat 观察用户态的 syscall/errno,用 perf trace 或 ftrace 观察系统调用路径。它们能证明目标机器的实际行为;静态源码只能证明 v6.10 的实现结构。不要把容器内某个 mount namespace 的结果直接推广到宿主机或另一内核版本。
资料
- Linux v6.10
fs/open.c,访问于 2026-08-04。 - Linux v6.10
fs/namei.c,path walk 与do_filp_open,访问于 2026-08-04。 - Linux VFS documentation,访问于 2026-08-04。
- Linux path lookup documentation,访问于 2026-08-04。
本文的函数和行号固定到 Linux v6.10。讨论某个内核、文件系统、LSM 或容器运行时的问题时,必须再固定目标内核配置、mount namespace 和实际文件系统类型。