首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Linux内核struct path深度解析:VFS路径查找、引用计数与排障实战
📅 2026/10/22 4:06:49
✍️ 爱科研究院
👁 阅读 3,247
做内核文件系统相关开发或者排查系统调用异常时struct path 是一个绕不过去的结构体。它看起来只有两个成员却串起了 VFS 最核心的两大体系挂载管理mount和目录项缓存dentry。很多刚接触内核路径查找机制的人会对为什么不直接用 struct dentrymnt 和 vfsmount 到底什么关系这类问题感到困惑。这篇文章我就围绕 struct path 本身展开从结构体定义、路径查找流程、引用计数管理到实际排障把它的来龙去脉一次讲明白。适合读这篇文章的主要有三类人一是正在看内核文件系统源码的初学者二是需要在驱动或内核模块里操作文件路径的开发者三是遇到过打开文件失败、路径解析异常但不知道怎么下手的排查者。我尽量用一个实际场景串联所有知识点而不是堆砌字段说明。1. 内容整体设计与思路拆解1.1 一位路径持有者的诞生先想一个问题内核里到处都需要表示某一个文件或目录的位置但在 VFS 层这个位置并不是一个简单的字符串。字符串路径只是用户态的概念内核每次使用路径前都要解析它——这个过程涉及逐级目录查找、符号链接解析、跨挂载点跳转最终落到的结果就是一个 struct path。换句话说struct path 是路径解析的终点产物它把这个路径在哪个挂载点上、对应哪个目录项这两件事绑定在一起。设计意图非常明确路径查找是一次重活不能让每个使用方都自己从字符串开始解析而是要解析一次、之后复用这个结果。只要打开过文件这个结构体就会出现。拿最常见的 open() 来说从用户态传进来一个字符串路径内核经过 do_sys_open - do_filp_open - path_openat 的层层处理最终得到一个 struct path再把它塞进 struct file 里。后续所有读写、mmap、fsync 操作都是基于这个 struct file 里的 f_path 成员继续工作不需要再回头去解析字符串。这也是为什么很多系统调用的时间开销集中在打开阶段而不是读写阶段——路径解析是重头戏。1.2 为什么不是只存一个 dentry一个很自然的问题是既然 dentry 已经能表示目录项为什么还要再加一个 vfsmount直接把 dentry 存下来不就行了吗关键在于同一个 dentry可能是不同挂载视角下的同一个目录。这句话有点绕举一个贴切的例子假设你有个 U 盘挂载在 /mnt/usb同时系统里有另一个根目录视角。此时访问 /mnt/usb 下的文件和通过某个绑定挂载bind mount从另一个路径访问同一个文件dentry 可能是同一个但它们跨越的挂载点、所在的 mount 实例、权限和挂载选项都不同。如果只保存 dentry你就丢失了我是从哪个挂载视图进来的这个信息。文件系统类型可能不同比如同一个底层目录被 reflink、overlayfs 叠了一层读写语义也可能不同。所以 struct path 把 mnt 和 dentry 绑在一起其实是在声明路径的语义 挂载视图 目录项两者缺一不可。这也是 VFS 设计里非常核心的一个思想。从代码层面看struct mount 才是真正描述一次挂载的数据结构vfsmount 只是它暴露给 VFS 使用的内嵌抽象。平时代码里到处传的是 vfsmount 指针但真正的挂载树关系、挂载标志、引用计数都在 struct mount 里。这一点在后面讲引用计数时还会展开。2. 核心细节解析与实操要点2.1 结构体定义逐字段拆解先看 Linux 源码 include/linux/path.h 中的定义struct path { struct vfsmount *mnt; struct dentry *dentry; };两个成员各有分工mnt指向这次路径解析所跨越的挂载实例vfsmount。它描述了这个路径是从哪个文件系统挂载视图下来的包含挂载点、挂载标志、超级块等隐含信息。注意这里类型是 vfsmount而不是更底层的 struct mount因为 VFS 大量代码只需要 vfsmount 层面的信息不需要感知 mount 内部结构。dentry指向路径最后一级的目录项。dentry 绑定的是具体的 inode负责把文件名和 inode 对应起来。它承载了目录项缓存的引用计数、哈希链表、父指针等信息。很多人会把这两个成员对立起来理解其实它们是一个整体mnt 决定从哪个根看dentry 决定看到的具体节点是什么。比如同一个 dentry 出现在两个不同的 mnt 里就代表两个完全不同的打开语义。补充一下 dentry 和 inode 的关系因为这也是初学者常见的盲区。dentry 不是文件本体它只是目录项的名字实体真正的文件元数据在 inode 里数据块在 page cache 和相关文件系统逻辑里。因此 struct path 最终通过 dentry-d_inode 拿到 inode再通过 inode 进入文件系统实际的读写流程。2.2 mnt、vfsmount、mount 三者关系这三个概念经常被混用有必要掰开讲清楚结构体定位说明struct vfsmount瘦身后的挂载描述VFS 对外接口最早是主结构后来被重构为 mount 的内嵌成员struct mount完整挂载描述挂载树管理节点包含 mnt_parent、mnt_child、挂载标志、挂载设备等struct path.mnt类型为 vfsmount*指向 mount 实例内部访问该挂载的通用视图为什么会这样设计早期内核里挂载信息就是 vfsmount 一个结构体搞定随着挂载树语义复杂化绑定挂载、递归挂载、用户命名空间挂载需要维护父子关系、挂载传播关系等大量额外字段但 VFS 大部分代码又不需要这些。于是把 jj 原 vfsmount 保留变成一个对外可见的脑袋新增 struct mount 用来管理身体。代码里常见的 mnt-mnt_root、mnt-mnt_sb 这类访问其实是在访问 vfsmount 上的字段而挂载树的遍历比如 list_empty(mnt-mnt_mounts)操作的则是 struct mount 的成员。这一点在阅读挂载相关代码时会很有帮助。比如你想判断一个 path 是不是挂载点不能只看 dentry还得看 mount 子系统的状态。这时候就需要通过 container_of 把 vfsmount 转成 mount再检查它的挂载点关系。3. 实操过程与核心环节实现3.1 路径查找的整体流程path 是怎么一步步走出来的struct path 不是从天而降的它来自一次完整的路径解析。以 open 一个相对/绝对路径为例内核的解析过程大致可以分三个阶段。第一阶段获取起始位置。如果是绝对路径起点是当前进程的 fs-root 中保存的根 path如果是相对路径起点是 fs-pwd也就是当前工作目录的 path。这两个都是 struct path 类型进程切换、chdir 时都会更新。第二阶段逐级解析路径分量。内核会把路径字符串按 / 切分成多个分量比如 /etc/hosts 会拆成 etc 和 hosts 两个分量。每一级都要在当前 dentry 里查找子 dentry找到后还要检查是否跨越了挂载点。这个过程中有两个关键函数形态开锁路径查找refcount walk和无锁的 RCU walk。RCU walk 主要在快速路径上避免锁竞争但如果遇到挂载点跳转、符号链接、需要权限检查等场景就会从 RCU walk 回退到 refcount walk。第三阶段生成最终 path。当所有分量解析完毕最后的 dentry 和它所在挂载点的 vfsmount 组合在一起就构成了你要的 struct path。整个查找过程中引用计数是一路叠加上去的最后把结果返回给调用方。这里面有一个很值得注意的细节不仅要找到目标 dentry还要确保它的 vfsmount 是对的。比如你要打开 /mnt/usb/file路径查找走到 /mnt/usb 时发现 usb 这个 dentry 是一个挂载点这时就要跨到挂载点对应的 mount 实例的根 dentry。如果没有这一步跨越你解析到的 file 可能是挂载点目录自己的内容而不是 U 盘根目录的内容。挂载点跨越是路径解析最核心、也最容易犯错的地方。3.2 符号链接、挂载点跨越的处理先看符号链接。如果某个路径分量本身是符号链接内核需要读取链接目标再基于目标路径继续解析。这个机制叫跟随链接follow link。这里有几个限制符号链接深度有上限默认 40 层超过就报 ELOOP符号链接内容存在 inode 的数据块里所以解析过程中会触发实际的文件系统读操作这也是为什么符号链接解析比普通目录查找要慢。再看挂载点跨越。路径查找中每当进入一个目录都要检测这个目录是不是某个挂载的挂载点。如果存在就不再继续用当前 mount 解析而是切换到被挂载文件系统的根目录。最简单的判断方式就是看 dentry 的 d_flags 里有没有 DCACHE_MOUNTED 标志。如果设置了就通过 __lookup_mnt 或类似的遍历找到对应的 mount换用它的 vfsmount 和根 dentry 继续向下走。这里要特别提示一个经验路径解析不是逐字符读路径而是逐分量、逐层级变换 path。当你写代码调试一个路径时最好按照一次分量解析 一次挂载检查 一次符号链接检查的心智模型去推演而不是想当然认为整条路径是连续字符串。3.3 手动构造 path 的一个真实场景上面说的是内核自身解析路径的流程。但在驱动或模块开发中有时候你需要自己拼出一个 struct path。一个经典场景是设备驱动里想获取某个固定路径文件的 inode 信息但不想走完整的用户态 open 流程于是直接调用内核路径查找接口。代码大致是这样的struct path p; int err; err kern_path(/etc/example.conf, LOOKUP_FOLLOW, p); if (err) { pr_err(failed to resolve path: %d\n, err); return err; } // 使用完毕后必须释放引用 path_put(p);kern_path 是个非常实用的接口它把字符串路径解析成 path。这里有几个注意点LOOKUP_FOLLOW如果路径最后一级是符号链接加上这个标志才允许跟随不加的话只解析到符号链接本身。使用完 path 之后一定要 path_put。path_get/path_put 成对出现这一点后面专门讲。kern_path 走的是内核线程上下文不会使用用户态内存因此驱动里用它是安全的。但如果路径涉及用户态内存就要用 getname_kernel 等机制或者走普通系统调用入口。3.4 从 path 拿 inode、superblock、挂载标志一旦拿到 struct path就可以很方便地取出关联的 VFS 对象。这是驱动开发中非常高频的一类操作struct path *path ...; // 拿 dentry 和 inode struct dentry *dentry path-dentry; struct inode *inode d_inode(dentry); // 拿 superblock struct super_block *sb path-mnt-mnt_sb; // 检查挂载标志 if (path-mnt-mnt_flags MNT_READONLY) // 这个挂载是只读的 // 判断是不是根目录 if (dentry path-mnt-mnt_root) // 当前处于该挂载的根这些访问在大多数情况下是安全的但有并发风险如果你持有 path 但不持有引用计数或者你已经 path_put 了再访问 path-dentry 和 path-mnt 就会用悬空指针。内核崩溃的常见原因之一就是在没有人持有 path 的情况下继续使用它。4. 引用计数管理path 的生命周期4.1 path_get 与 path_put 背后的双重引用struct path 本身是个复合对象mnt 和 dentry 各自维护独立的引用计数。path_get 其实就是对两个引用同时递增path_put 则是同时递减逻辑单纯但后果很严重——漏一个就泄漏多一个就早释放。看内核的引用计数接口static inline void path_get(const struct path *path) { mntget(path-mnt); dget(path-dentry); } static inline void path_put(const struct path *path) { dput(path-dentry); mntput(path-mnt); }也就是说一个 path 的生命周期由 dentry 和 mount 两个对象的生命周期共同决定。在分析引用计数问题时不能只盯着其中一个。为什么强调这一点因为实践中很多人都只关注 dget/dput忽略了 mntget/mntput。比如你写了一个内核模块拿着 path 去异步任务里使用如果只保存了 dentry 引用而没有对 mnt 做引用那在这个任务执行期间挂载点可能已经被卸载mnt 指向的超级块、挂载标志都变成无效内存接下来的访问就是灾难。4.2 谁在持有 path谁负责释放内核里持有 struct path 的典型位置持有者说明释放时机struct file 的 f_path打开文件时从路径解析结果拷贝文件最后关闭时由 filp 清理逻辑释放进程的 fs-root / fs-pwd每个进程维护的根路径和工作目录路径进程回收、chdir 时切换路径查找过程中的临时 path解析过程中的中间状态每一步查找后立即 path_putVFS 内部的 mount 遍历逻辑操作挂载树时临时持有依附于所在函数周期以 struct file 为例file 里的 f_path 不是自己手动维护的引用而是在 filp 打开时通过 path_get 增加的。因此只要 file 还活着f_path 就有效。反过来当 file 被释放时会调用 path_put 释放两个引用。这也就是为什么我们经常说文件描述符泄漏本质上是 path 引用泄漏。在内核模块里如果你想长期持有某个路径一定要显式 path_get 增加引用并在不再使用时 path_put。这里有个约定俗成的写法struct path saved_path; // 获取 path_get(saved_path); // 使用 ... // 释放 path_put(saved_path);4.3 手动持有 path 的三个铁律基于踩坑经历这里给三条经验不能只拷贝结构体不增加引用。直接把某个 path 赋值给你的局部变量但不调用 path_get这不是持有只是复制了指针值。原持有者一旦释放你的局部变量就成了悬空引用。正确的是先拷贝后 path_get或者先 path_get 再拷贝严格配对。释放顺序有讲究。path_put 内部先 dput 再 mntput这个顺序不是随便定的因为 dentry 的释放有时需要访问挂载信息如果先把 mnt 引用释放掉dput 进入 dentry 释放回调时可能读到无效的 sb。因此自定义逻辑里也应当尽量保持这个顺序。不要在一个 path 上重复 path_put。每次 path_get 对应一次 path_put。如果某段代码路径可能提前 return一定要用 goto 统一清理避免一些分支释放、另一些分支不释放的问题。这一类 bug 平时不报错只在特定路径触发很难复现。5. 常见问题与排查技巧实录5.1 常见问题速查表定位挂载点与路径解析异常现象可能原因排查思路open 返回 ENOENT路径中间某个分量不是目录或不存在检查路径每一级是否都有权限用 strace 确认失败的具体路径分量返回 ELOOP符号链接嵌套过深检查是否存在循环链接确认路径中反复引用自身挂载返回 EXDEV跨设备操作如 rename 跨挂载点检查源和目标是否在同一个挂载点下open 时报权限错误但用 root 可以挂载选项如 ro、nosuid或 LSM 策略限制查看 /proc/mounts 中的挂载标志进程在某目录下卡死路径解析递归或挂载循环检查是否有递归绑定挂载使用 /proc/self/mountinfo 对比挂载树表格里的最后一类挂载循环其实和路径解析关系非常紧密。在一个挂载里又绑定挂载到自身某个子目录可能导致遍历时永远在挂载树上打转。幸好 VFS 对普通路径查找有深度限制和挂载计数保护但识别这类问题的基本方法依然是把挂载树打出来照着路径一项项对。5.2 跟追踪路径查找ftrace 和 kprobe 实战一旦怀疑路径解析出问题或者只是单纯想理解某个路径是怎么一步步查出来的最直接的方式是追踪路径查找的内核函数。我的常用做法是以 ftrace 的 function_graph 追踪 lookup_fast、link_path_walk、follow_managed 这几个关键函数。下面是一个追踪路径查找的示例命令# 切换到 function_graph 追踪器并过滤目标函数 echo function_graph /sys/kernel/debug/tracing/current_tracer echo lookup_fast /sys/kernel/debug/tracing/set_ftrace_filter echo link_path_walk /sys/kernel/debug/tracing/set_ftrace_filter echo follow_managed /sys/kernel/debug/tracing/set_ftrace_filter echo 1 /sys/kernel/debug/tracing/tracing_on # 触发一次路径解析 ls /home/user/testfile # 关闭追踪并查看输出 echo 0 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace通过 trace 输出你能看到路径查找在哪些阶段进入了快路径dcache 命中在哪些阶段慢路径需要真实读取目录项以及符号链接是在哪一级被解析的。这个方法比靠猜靠谱得多。如果函数追踪输出太乱也可以使用 kprobe 直接在 path_get/path_put 上挂载点统计路径引用的增减情况echo p:my_probe path_get mnt%di dentry%si /sys/kernel/debug/tracing/kprobe_events echo p:my_probe_put path_put mnt%di dentry%si /sys/kernel/debug/tracing/kprobe_events echo 1 /sys/kernel/debug/tracing/events/kprobes/my_probe/enable echo 1 /sys/kernel/debug/tracing/events/kprobes/my_probe_put/enable通过对比 path_get 和 path_put 的参数地址你可以初步判断是否有路径引用没有配对释放。5.3 从崩溃调用栈反推 path 使用错误内核崩溃后打印的调用栈里如果出现了 path_get、path_put、dput、mntput 这样的函数往往就意味着 path 的引用计数管理出了问题。我见过一种典型的崩溃场景调用栈大致是 dput - __dentry_kill - shrink_dentry_list - ...此时 panic 信息里能看出 dentry 已经被标记为 dead 但仍有代码在访问。这类问题大概率是某个模块保存了 path 却没有 path_get导致 dentry 被提前回收。排查这类问题的核心思路是找到持有 path 的位置确认它是否拥有引用计数。具体做法分三步通过崩溃堆栈地址找到访问 path 的内核模块函数阅读该函数的源码确认它是临时使用还是长期保存如果是长期保存检查写入路径有没有 path_get退出路径有没有 path_put。实践中有个高效率的做法是给 path 结构体加调试字段或者临时把 path_get/path_put 替换成带调用者记录的版本输出每一次调用来源。由于这个改动比较 invasive通常建议在调试内核版本上做而不是生产内核。5.4 排查挂载相关问题的两条辅助命令第二个是在排查跨挂载点路径问题时查看父挂载和子挂载的关系。第一个是 /proc/mounts它将所有挂载信息以设备 挂载点 文件系统类型 挂载选项的格式列出来。缺点是它不展示挂载树的层级关系对于多层 overlayfs 嵌套的场景非常不直观。这时就需要 /proc/self/mountinfo。它的每一行包含挂载 ID、父挂载 ID、挂载点、根目录在这个挂载视角下的根对应底层目录树的哪个位置以及传播属性。通过父挂载 ID 可以重建挂载树定位某个 path 到底属于哪个挂载实例。拿 overlayfs 举例你在容器里看到 /etc/hosts 这个路径但它在底层其实是宿主机某个文件。用 mountinfo 对比容器内和宿主机上同一路径的挂载 ID 和上级挂载 ID很快就能理清叠加关系。很多文件系统诡异现象最后都落在你以为在 A 挂载实际在 B 挂载上。5.5 一次真实排障ls 慢得离谱问题却出在路径查找最后分享一个真实工作场景。某次在嵌入式设备上排查一个目录 ls 非常慢的问题用户操作一次 ls 要两三秒而相同环境下另一台设备只要几十毫秒。首先用 strace 跟踪 ls 的系统调用发现耗时集中在 getdents64 以及 openat 上且 openat 的路径都是相对路径长度也不长。按理说路径查找不该这么慢。于是用 ftrace 追踪 link_path_walk发现大量时间花在路径分量的目录项 cache 未命中上而每个目录项查找都触发了底层文件系统 I/O。进一步排查才发现这个目录所在的挂载点开启了实时目录更新real-time每次路径解析都要跨越一个挂载边界并且底层存储介质是慢速闪存。核心问题其实是一个挂载点被嵌在了目录树中间所有经过该挂载边界的路径查找都要付出跨挂载检查 底层文件系统访问的代价。这个问题用 struct path 的视角来看就非常清晰路径解析过程中每跨越一个挂载点就要从当前 mount 切换到另一个 mount这个切换逻辑与 dentry 查找分别消耗一次查找成本。跨挂载点过多直接让路径查找变慢。优化手段也直接减少中间挂载层数把频繁访问的挂载点放在路径顶层。6. 实际操作经验总结与后续扩展这章不写泛泛的总结只分享几个我在读源码和调驱动时觉得最有用的手感它们能让 struct path 在脑中的画面立起来。第一个手感遇到任何文件操作函数先问它是基于 path 工作还是基于 file 工作。内核里这两套接口是分开的。基于 file 的接口如 vfs_read、vfs_write天然带着 f_path 的上下文你不用关心路径解析基于 path 的接口如 kern_path、follow_up则要求你显式管理生命周期。码代码前后随手翻一下调用链能少踩很多坑。第二个手感阅读路径查找源码时始终带着两个问题看一是当前走到哪个 mount 了二是当前 dentry 是不是就是目标。路径查找本质上就是不断更新这两个量的过程。理解了这一点再复杂的 lookup 流程都能拆解成简单循环。第三个手感善用内核已有的 trace 点。除了上面提到的 ftrace 和 kprobe路径查找相关的 tracepoint 还有 vfs_open、do_sys_open 等。日常调试不一定要重编内核先试试现有 trace 点往往能快速缩小范围。至于这个内容的后续扩展方向建议可以再深挖几个相关主题一是 struct file 与 path 的完整生命周期包括 fget、fput、flush 和 close 的中间状态二是 VFS 的挂载树管理围绕 struct mount 的父子关系、挂载传播与回收机制三是路径查找的 RCU 慢路径/快路径详细实现里面有许多值得借鉴的无锁化手法。这三个方向顺着 struct path 都可以自然衔接读完后你对 VFS 全局会有更深一层的理解。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/22 4:06:49
基于Python-CNN的会飞昆虫识别毕设设计与实现全解析
2026/10/22 4:01:47
强化学习性能天花板:推断策略才是部署突破口
2026/10/22 4:01:47
Prompt缓存四种策略:从精确匹配到语义检索的实践指南
2026/10/22 6:01:58
Postgres-XL GTM快照管理:分布式事务一致性的核心机制
2026/10/22 6:01:58
Autodesk Maya 2022 下载安装全流程:环境验证与排错指南
2026/10/22 6:01:58
跨平台AI编程智能体生态:多Agent协作架构设计与工程实践
2026/10/22 6:01:58
DSP简答题推导指南:从定义锚点到代码验证
2026/10/22 6:01:58
2026年IM选型指南:AI生成、开源与三方服务如何取舍
2026/10/22 5:56:58
Rust 系统级 Agent 框架 rpi 架构解析与多 Agent 协作实战
2026/10/22 0:01:24
电商智能运营体系从0到1:架构设计、落地步骤与避坑指南
2026/10/22 0:01:24
压缩空气储能与热网热惯性联合优化调度:热电联产系统建模与求解
2026/10/22 0:01:24
若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定
2026/10/21 19:05:16
OpenResearch实战:Claude Code、Codex、OpenCode、Cursor 工作流搭建与故障排查
2026/10/21 14:47:26
知识管理核弹:用Obsidian构建第二大脑的工程化方案(TaoToken 统一 Key 接入版)
2026/10/21 19:05:04
金融AI落地指南:从调研报告到技术选型与工程实践
2026/10/22 5:06:15
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/22 2:07:18
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/21 19:01:08
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)