Skip to content

源码实证:Linux openat() 如何穿过 VFS ​

专题索引 / 上一篇:Deployment controller 如何收敛

本页固定 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 或 errno

openat() 返回成功,不代表文件数据已读入内存,更不代表任何数据已写入磁盘。它只建立了进程 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)
  -> 必要时 truncate

vfs_open 是通用 VFS 到具体文件系统/设备文件操作的分派点。ext4、XFS、NFS、FUSE 或设备驱动可以在自己的 inode/file operations 下实现差异;调用方仍只看到统一 fd 与 errno 语义。LSM hook 在通用路径中出现,说明安全不是“某个文件系统可选附加的检查”,而是打开生命周期的一部分。

5. 错误返回是契约的一部分 ​

错误类别典型 errno说明
路径不存在ENOENT中间或最后路径分量无法解析
权限不足EACCES / EPERMDAC、mount 或 LSM 等访问约束不满足
类型不符ENOTDIR / EISDIR路径分量或打开目标与期望类型不一致
创建冲突EEXIST`O_CREAT
并发解析回退ECHILD / ESTALE(内部)触发更强查找路径,不一定直接交给用户态

用户态程序不应依赖 namei.c 里的中间回退细节;它依赖最终 syscall 返回的 ABI。内核内部可以改变快路径,只要对外语义不被破坏。

6. 与 Deployment controller 的对照 ​

问题Deployment controllerLinux 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。讨论某个内核、文件系统、LSM 或容器运行时的问题时,必须再固定目标内核配置、mount namespace 和实际文件系统类型。

学习规划与研究资料库