新闻详情

新闻详情

首页 / 资讯中心 / 详情

深入理解Linux内核struct dentry:路径解析与dcache机制

发布时间:2026/9/26 18:14:53来源:尧图网络
深入理解Linux内核struct dentry:路径解析与dcache机制
跟内核、文件系统这类问题打交道久了你会发现一个很有意思的现象很多看起来高级的性能问题、死锁问题、内存问题追到最后都落在几个基础结构体身上。struct dentry就是其中之一。它管的是路径解析和目录项缓存也就是 dcache 的核心整个 VFS 层的翻译官。不管你做的是嵌入式、存储、容器还是内核安全只要你跟文件路径、open、stat、mkdir 这类操作沾边就绕不开它。这篇文章我不打算给你抄一遍源码注释而是想从这玩意儿到底解决什么问题讲起把它的字段、状态机、生命周期、并发控制以及我在实际调试和写文件系统驱动时踩过的坑一条条拆开讲清楚。1. dentry 到底是什么一次 open() 背后的完整链路1.1 VFS 为什么需要 dentry而不是直接用 inode你先想一个问题用户态执行open(/etc/nginx/nginx.conf, O_RDONLY)的时候内核是怎么找到这个文件的最朴素的做法是逐级查找从根目录/开始查etc目录再查nginx目录最后查到nginx.conf这个文件。每一次目录查询最终都会对应到一个 inode 上。但这里有个性能问题如果每次都从磁盘读取目录数据来解析路径一次 open 可能要触发好几次磁盘 I/O那系统根本跑不动。于是内核在内存里加了一层缓存专门缓存路径组件到 inode的映射关系。这个缓存里的每一个节点就是struct dentry。你可以把 dentry 理解成一个路径名组件的代言人它记住了自己叫什么名字、父目录是谁、对应哪个 inode以及自己在 hash 表里的位置。整个路径树在内存里的形态就是由这些 dentry 互相链接而成的。这里要特别澄清一个容易混淆的概念dentry 不等于目录。平常说的目录是一个 inode 类型S_ISDIR。而 dentry 是路径解析过程中的一个目录项directory entry它既可以代表一个目录也可以代表一个普通文件、软链接、设备文件。/etc/nginx这个路径里etc和nginx各有一个 dentry它们指向的 inode 是目录类型的nginx.conf也有一个 dentry指向的 inode 是普通文件类型。所以 dentry 是路径名组件inode 才是文件或目录本身。1.2 dentry 与 inode 的对应关系多对一dentry 和 inode 是多对一的关系。硬链接就是最好的例子同一个 inode 可以有多个名字比如/data/a.log和/data/b.log都指向同一个 inode那么在 dcache 里就有两个 dentry分别指向同一个 inode。inode 内部通过i_dentry链表维护所有指向它的 dentry这个链表在别的地方都以d_alias的名义出现。这个多对一的关系在删除文件时有个坑你想删除某个名字但 dentry 还在被别的进程引用或者它被硬链接到了别处这时候 inode 的释放会被延迟典型的现象就是文件删了但磁盘空间没释放或者进程还持有 fd文件却已经 unlink 了。这些行为根子都在 dentry 和 inode 的生命周期纠缠上。2. struct dentry 核心字段逐项拆解我在调试代码时喜欢把结构体打印出来逐字段看因为很多问题就藏在字段语义里。struct dentry在include/linux/dcache.h中定义我挑几个关键字段讲。2.1 d_name、d_parent、d_inode最核心的三件套struct dentry { ... const struct qstr d_name; // 当前组件名称如 nginx.conf struct dentry *d_parent; // 父目录的 dentry struct inode *d_inode; // 指向该 dentry 对应的 inode ... };d_name是struct qstr里面除了name和len还带一个hash字段。这个hash是路径查找时快速比较的凭据不用每次都逐个字符比较字符串先比 hash比不过就直接跳过。d_parent指向父目录的 dentry所以通过dentry-d_parent可以一路向上回溯到根。d_inode可能为 NULL这个情况代表 negative dentry下面会专门讲。我在实际调试时经常用/proc的一些接口反推 dentry 树关系但要小心 d_parent 的引用计数。如果你在内核模块里遍历 d_parent必须持有d_lock或者在 RCU 读锁保护下访问否则你看到的父节点可能已经被人释放了。2.2 d_lockref、d_seq、d_flags状态与并发控制struct lockref d_lockref; // 引用计数 自旋锁的组合 seqcount_spinlock_t d_seq; // 用于 RCU pathwalk 的序列计数 unsigned int d_flags; // DCACHE_* 状态标志d_lockref是 Linux 3.12 之后引入的一个优化把引用计数refcount和一把自旋锁塞进同一个 64-bit 字里一次原子操作就能完成加锁 检查计数 修改计数。这样频繁的dget()/dput()路径不需要每次都开锁关锁缓存行竞争小很多。你用lockref结构体时无法把锁和计数分开操作必须用它的专用 API比如lockref_get_not_zero()。d_seq是 seqlock 的序列号典型用在 RCU pathwalk 里做乐观并发控制。你可以在不持锁的情况下读 dentry然后检查序列号有没有变化如果变了就重新来一遍。这是 VFS 路径解析最快路径的核心机制。写者比如修改 d_name、d_inode 指向会持有锁并递增序列号读者靠read_seqcount_retry()判断期间有没有被修改。d_flags是一堆DCACHE_*标志位比如DCACHE_DISCONNECTED从目录树中断开了、DCACHE_UNUSED不再被引用在 LRU 上等。调试时用d_flags判断 dentry 处于什么状态非常有用后面状态机部分我会展开。2.3 d_hash、d_lru、d_subdirs如何组织这棵大树struct hlist_bl_node d_hash; // 全局 dcache hash 表节点 struct list_head d_lru; // 未使用 dentry 的 LRU 链表节点 struct list_head d_child; // 挂到父 dentry 的 d_subdirs 上 struct list_head d_subdirs; // 本目录包含的子 dentry 链表头d_hash把 dentry 挂到全局哈希表里。哈希函数是full_name_hash()用dentry-d_name.hash作为键的一部分。查找路径时先在哈希表里找找到再比对字符串和父节点。哈希桶是hlist_bl类型bl表示 bucket list目的是为每个桶的锁和链表头做优化减少内存占用。d_lru则解决缓存淘汰问题当 dentry 不再被使用引用计数归零它会挂到全局 LRU 链表上。内存压力大时内核会从 LRU 尾部回收 dentry走到shrink_dcache_parent()或shrink_dcache_for_umount()这类路径。d_subdirs就是父 dentry 保有的子项链表遍历一个目录所有子项时用的就是它。2.4 d_op、d_sb、d_fsdatadentry 与具体文件系统的接口const struct dentry_operations *d_op; // dentry 操作函数集 struct super_block *d_sb; // 所在文件系统的 super_block void *d_fsdata; // 文件系统私有数据这是 dentry 能适配几十种文件系统的关键。d_op里常见的有d_revalidate路径解析时让文件系统确认缓存是否有效、d_compare自定义的名字比较逻辑、d_delete最后一次引用释放时的钩子、d_releasedentry 完全释放时的钩子。NFS、FUSE、overlayfs 都重度依赖这些钩子。d_fsdata是给文件系统存私有数据的比如有的文件系统会把目录的偏移量或者自有结构指针存在这里。注意它不是d_inode-i_private那个是 inode 的私有数据别混淆。3. dentry 的状态机从分配、命中到回收3.1 从 d_alloc 到 d_adddentry 是怎么诞生的内核解析路径时如果某个路径组件在 dcache 里找不到就需要分配一个新的 dentry。这个过程大致是struct dentry *d_alloc(struct dentry * parent, const struct qstr *name);d_alloc()负责初始化一个 dentry拷贝名字、设置父指针、把 d_subdirs 链表接好、初始化锁和引用计数。刚分配出来的 dentry 只是幽灵节点还没有和 inode 绑定。然后用d_add()把它加进 dcachevoid d_add(struct dentry *dentry, struct inode *inode);d_add()的内部逻辑是把 dentry 插入全局哈希表并设置d_inode inode。此时 dentry 真正进入已连接且可用的状态。如果 inode 传入 NULL这个 dentry 就是 negative dentry表示路径名有效但对应的 inode 不存在或已被删除。我在写文件系统驱动时经常犯错的一个点是调d_add()之前必须确认文件名和 inode 已经对应好否则一旦把这个 dentry 插进去后续路径解析命中了错误映射问题极难排查因为 dcache 的缓存命中日志都很隐蔽。3.2 路径查找的核心路径fast path 与 __d_lookup每次路径解析pathwalk都从__d_lookup()或bpath_lookupat 出发核心逻辑是用 qstr 的 hash 去全局 dcache 哈希表里找找到候选后比较名字是否真的相等相等且父节点匹配就算命中。Linux 3.x 之后的 RCU 优化让这个过程可以完全不持锁靠 seqlock 和 RCU 机制完成。fast path 的整个流程基本是读取d_seq得到 sequence 值 s。在不持锁的情况下读取dentry-d_name、d_parent、d_inode。用read_seqcount_retry(dentry-d_seq, s)验证数据没有变化。如果验证失败退回到持d_lock的慢路径重试。这个设计显著提升了所有文件的 open/stat 性能。我自己做负载测试时开perf stat看 open 的系统调用开销前后对比能差出好几倍全靠这个机制撑着。3.3 dentry 的三种状态used、unused 与 negative这是面试常问的题目但很多人只会背名字。我结合代码捋一下used引用计数大于 0dentry 被某个进程或某段内核代码持有。此时它不在 LRU 上也不会被回收。unused引用计数归零没有任何代码在使用它但还在 dcache 哈希表里。它会被挂到 LRU 上内存压力大时被回收。dput()是进入 unused 状态的关键函数它会先减引用计数归零后根据文件系统是否禁止 negative caching 等情况决定是放进 LRU 还是直接释放。negatived_inode NULL。表示这个路径名在当前文件系统上不存在文件或者文件已删除但 dentry 被负缓存住了。negative dentry 的存在能有效避免对不存在的文件路径反复发起磁盘 I/O。negative dentry 是双刃剑如果文件系统实现不当d_delete没有及时触发或者目录被 remove 之后 dentry 没被清理就会导致文件明明创建了但 open 却一直报 ENOENT。这就是经典的 negative dentry 失效问题。3.4 dentry 的回收与 LRU内存压力下的行为当系统内存吃紧内核要回收 dcache 的时候颗粒度是 dentry 对象本身。shrink_dentry_list()会从全局dentry_lru链表尾部摘一些 dentry 出来先摘掉哈希表节点d_hash删除。从父节点的d_subdirs链表中摘除。调用d_op-d_release()钩子让文件系统做清理。最终释放 dentry 对象和名字占用的内存。回收和引用计数是竞争的关系如果回收过程中发现有人正持有引用就会跳过。所以你经常能看到 dcache 占的内存看着很大但无法回收原因多半是长生命周期进程把一堆 dentry 引用计数养肥了。定位这种问题时/proc/slabinfo里:dentry的 active_objs 和 num_objs 能给出很多线索。4. dcache 并发与锁别在并发模型上翻车4.1 d_lock、seqlock 与 RCU三种机制各管一摊事很多人把 dcache 的并发模型想象成一把大锁其实精确一点说是分成几层d_lock即 d_lockref 中的 lock保护单个 dentry 的字段一致性比如 d_inode 的切换、d_name 的修改、d_child/d_subdirs 链表的改动。d_seq/seqlock保护 RCU pathwalk 的读路径只允许一个写者多个读者廉价且乐观。RCU负责 dentry 对象本身的安全释放。路径解析时能在无锁情况下获取到 dentry 指针读完字段再验证序列号释放时用call_rcu()延迟释放确保所有读者都退出 RCU 临界区后才真正销毁对象。全局哈希锁分布在每个 hash bucket 上hlist_bl_lock(b)不是一把全局锁所以不同目录的查找并发度很高。我在排查死锁时有个心得只要看到 dentry 的代码里有人试图先持 d_lock 再申请别的锁就要高度警惕。d_lock是自旋锁持有期间不能睡眠一旦嵌套持锁顺序错了很容易出现 ABBA 死锁。标准做法是能用lockrefAPI 就用lockref_get_not_zero不要自己尝试在内核里维护引用计数。4.2 RCU 遍历 dentry 树的经典注意事项遍历一个目录的d_subdirs在并发环境里特别容易踩雷。list_for_each_entry_rcu不是万能药你必须记住用hlist_bl_for_each_entry_rcu遍历哈希表时只保证对象本身不释放不保证字段不被并发修改。因此遍历取到的 d_name 可能是旧值。要安全读取 d_name需要持有 d_lock 或者在 seqlock 保护下读取并验证。遍历不是快照它看到的是一个动态变化的链表。即使你在遍历时目录里新增了一个文件链表里也可能没出现这是正常的。我在 fuzz 测试中就碰到过一次并发遍历读取 d_name 导致内核崩溃的问题根因就是没持锁读取字符串瞬间字符串被并发 rename 修改了。排查手段是用 KASAN 开起来跑压力测试崩一次就能抓到 UAF 点。4.3 d_delete 和 d_revalidate文件系统最容易写错的两个钩子d_op-d_delete()在 dentry 引用计数最后一次归零时被调用。它返回值很关键返回 0表示可以负缓存dentry 会以 negative 或 unused 状态挂在 LRU 上之后还能复用。返回 1表示不要缓存了直接释放掉。如果实现一个日志型文件系统文件删除后 dentry 还缓存着 inode而磁盘上的 name 已经没了那 open 同一路径就会命中旧 dentry行为完全错误。NFS 那类文件系统则依赖d_revalidate来做强一致校验。用简单类比来说d_delete决定死了要不要留坟d_revalidate决定每次上坟要不要验 DNA两者不能混。5. 实操怎么观察和调试 dentry/dcache5.1 用 /proc 接口查看 dcache 状态内核提供了一些接口直接暴露 dcache 健康状况/proc/sys/fs/dentry-state输出四列数字分别是最小未用数、未用 dentry 数、总 dentry 数、以及曾经创建过的 dentry 总数可以用来观察缓存增长趋势。/proc/slabinfo找到dentry行看active_objs实存对象数和num_objs总对象槽数。如果两者差值巨大且一直不降说明 dcache 回收受阻。slabtop交互式工具里能看到 dentry slab cache 占用的内存大小排在前几名的通常就是它。在排查内存泄漏或 dcache 无限膨胀时这几个文件是我必看的第一现场。如果/proc/sys/fs/dentry-state的未用 dentry 数持续上涨说明某个路径上在不停创建和释放临时文件而且文件系统没有正确抑制 negative dentry。5.2 用 ftrace 追踪 d_lookup 和 dput 调用要精准知道哪些进程在做什么路径解析、dentry 的分配释放是不是异常频繁可以用 ftrace 的 function tracer# 先看可用的函数列表 grep d_lookup /sys/kernel/tracing/available_filter_functions # 设置过滤函数并开启 tracing echo __d_lookup dput d_alloc d_add /sys/kernel/tracing/set_ftrace_filter echo function /sys/kernel/tracing/current_tracer echo 1 /sys/kernel/tracing/tracing_on # 结束抓取后查看结果 cat /sys/kernel/tracing/trace注意函数名字在不同内核版本有区别旧内核用的可能是d_lookup新内核是__d_lookup。追踪结果里每个记录都有时间戳和进程 PID可以配合kallsyms和addr2line定位到具体内核模块的路径。我有一个小技巧追踪前先把/sys/kernel/debug/tracing/buffer_size_kb加大到几十 MB否则高并发下 trace 文件会被冲掉什么都查不到。5.3 开 KASAN 和 slub_debug 定位 dentry UAFdentry 的 UAFuse-after-free问题很难肉眼看出全靠工具。如果你重新编内核做调试建议开这几个配置CONFIG_KASANy或CONFIG_KASAN_GENERICyCONFIG_SLUB_DEBUGy并在启动参数里加slub_debugP对普通对象检查 poisoningCONFIG_DEBUG_OBJECTS_DENTRYy它会用 debug objects 对 dentry 做额外的生命周期检查能在 dentry 释放后再被引用时打印出道栈。我遇到过一次真实案例某个文件系统驱动在d_release里没有清空 filp 的 private_data 指针之后 open 被杀后另一个进程复用该 filp 导致 final_put 时重复调用dput直接 double-free 崩溃。打开CONFIG_DEBUG_OBJECTS_DENTRY之后内核很明确地报出了第一个释放点和第二次引用操作的栈几分钟就定位根治了。5.4 内核模块里安全遍历 dentry 树这里给一个极简的内核模块示例展示安全遍历目录下所有子目录项的方法。注意这里演示的只是结构真实场景请考虑用vfs_*或 readdir 代替手写遍历。#include linux/dcache.h #include linux/spinlock.h #include linux/rcupdate.h /* 在给定父 dentry 的目录下安全统计未使用子项数量 */ static int count_unused_children(struct dentry *parent) { struct dentry *child; int count 0; /* * 先 RCU 读锁定再遍历 d_subdirs。 * 注意 d_subdirs 链表的每个节点都是 dentry释放受 RCU 保护。 */ rcu_read_lock(); list_for_each_entry_rcu(child, parent-d_subdirs, d_child) { /* 读取字段前拿 d_lock保证字符串和指针稳定 */ spin_lock(child-d_lockref.lock); if (d_unhashed(child) d_is_negative(child)) count; spin_unlock(child-d_lockref.lock); } rcu_read_unlock(); return count; }只要记住铁律RCU 保证对象不释放d_lock 保证字段不变遍历逻辑基本不会错。如果你想彻底做目录快照更省心的方案是走iterate_dir/readdir接口而不是自己去啃 dcache 链表因为后者的所有细节都需要你自己处理。6. 常见问题排查与避坑心得6.1 典型问题速查表现象根因方向建议排查手段文件删除后磁盘空间未释放dentry 或 inode 仍被引用dput未执行到位查进程 fd、lsof用 slabtop 看 inode/dentry 对象数量open 一个新创建文件返回 ENOENTnegative dentry 缓存未失效确认文件系统是否实现d_delete返回 0/1检查 dcache 负缓存条目目录重命名后旧路径仍可访问dcache 未刷新或文件系统d_revalidate未实现检查是否有d_drop/d_invalidate被真正调用路径解析耗时异常增大dcache 命中率下降或 hash 被恶意冲突攻击用 ftrace 统计__d_lookup调用次数观察 dentry-state 数值模块加载后 panic栈指向 dentry 释放UAF / double-free开启 KASAN slub_debug debug objects dentry内核死锁栈里多个 CPU 都锁在d_lockref.lock锁顺序错误或持锁睡眠检查代码中持锁是否嵌套使用 lockdep 复现6.2 印象最深的两个 dentry 问题第一个是 overlayfs 场景。有人报容器启动后文件一直显示旧内容查到最后是 overlayfs 的 upper/lower 层切换时dentry 的 d_op 没切换旧层缓存一直生效。解决方法是底层切换时主动d_drop相关 dentry让路径解析重新走lookup。这个理解起来很简单但要在成百上千个容器并发启动时做好涉及的工程细节不少。第二个是网络文件系统场景。NFS 的 dentry 默认要求d_revalidate检查 attr timeout但有些第三方客户端实现得比较粗糙把 result 写死为 1始终失效导致每一台客户机上的每个路径访问都会穿透到服务端。这时的表现是网络文件系统访问慢到不可用。排查方式就是在客户端开 ftrace 抓__d_lookup的 miss 率一抓就看出d_lookup把流量压在了 lookup 慢路径上。6.3 给内核模块开发者的建议写文件系统或相关 module 时我对 dentry 的代码有几点长期遵守的规则算是拿崩溃换来的无论什么时候不要随手直接赋值dentry-d_inode必须走d_add()、d_splice_alias()或d_instantiate()这类接口。使用dget()之前一定确认 dentry 是 hashed 且在路径树上否则引用计数会保护到一个隔离节点上白保护。dput()是可以睡眠的可能触发 LRU shrink所以不要在原子上下文或持有自旋锁时调用它。代码里如果看到spin_lock(); dput(); spin_unlock();这种写法几乎一定有隐患。遍历 dcache 不持锁时读字符串早晚会吃到错觉。想取 d_name 必须先拿 d_lock 或走 seqlock 校验。坦白说struct dentry的坑不是看一两天源码就能全部摸清的但把这几个核心链路和字段真正理解透后面再碰 pathwalk、dcache shrink、NFS/FUSE/overlayfs 都会轻松很多。这篇文章也只是把骨架撑起来真上了生产环境还是要带着 KASAN 和 lockdep 去现实中磨一轮。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

3步搞定wordpressnginx伪静态保姆级建站教程 2026/9/27 2:34:41

3步搞定wordpressnginx伪静态保姆级建站教程

3步搞定wordpressnginx伪静态保姆级建站教程 自己不会代码,却想搭个像样的网站?别慌,很多创业团队负责人都卡在第一步。这篇保姆级建站教程,专为小白设计,让你避开坑,快速上线。 为什么伪静态是SEO命门…

阅读更多 →
[极客大挑战 2019]Havefun_CTF2 2026/9/27 2:34:35

[极客大挑战 2019]Havefun_CTF2

靶场环境:一起来撸猫 页面展示 解题过程 步骤一: 右击查看源代码或Ctrl u 键,下翻发现在408---414行出现以下php代码。 步骤二: 分析代码,构造语句 ?catdog 步骤三: 回到原环境在url后面拼接语句 -…

阅读更多 →
识光CIOE亮相:SPAD-SoC三大产品线如何勾勒单光子感知的量产路径 2026/9/27 2:34:03

识光CIOE亮相:SPAD-SoC三大产品线如何勾勒单光子感知的量产路径

在刚刚落幕的中国国际光电博览会(CIOE)上,苏州识光芯科技术有限公司(识光,Sophoton)携多款SPAD-SoC新品亮相。从单点、线阵到面阵,三条产品线并非孤立展示,而是呈现出一种清晰的技术逻辑:以全芯片化架构为底座,用不同形态的芯片去接住高度分化的产业需求。 统一技术…

阅读更多 →
3步搞定app开发人员网站被黑挂马怎么选安全方案 2026/9/27 2:34:03

3步搞定app开发人员网站被黑挂马怎么选安全方案

3步搞定app开发人员网站被黑挂马怎么选安全方案 半夜三点,手机突然弹出短信:“您的网站www.yourapp.com存在恶意代码”。你慌了神,点开后台,发现首页被替换成了博彩广告,SEO收录瞬间归零。这种 网站被黑挂马不知道怎么办…

阅读更多 →
PADS Logic到OrCAD转换全流程:工具迁移、网表关联与高频问题排查 2026/9/27 2:33:56

PADS Logic到OrCAD转换全流程:工具迁移、网表关联与高频问题排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
国企干部民主评议场景,衡识人才测评等360评估系统适配 2026/9/27 2:33:56

国企干部民主评议场景,衡识人才测评等360评估系统适配

引文/摘要又到年终干部考核季。不少国企组织人事部门都在面对同一道题:民主评议怎么搞,才能既合规又高效,还能真正沉淀出有用的数据?传统纸票模式下,评议结果常常“评完就归档”,难以支撑干部选拔与梯队建设…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉