☰
深入剖析Linux内核dentry结构:VFS路径解析与dcache机制全解
2026/9/26 7:13:49 网站建设 项目流程

1. 项目概述:dentry 到底是什么

1.1 核心需求解析

说实话,很多接触 Linux 内核的人第一次看到struct dentry这个结构体时,多少都会有点懵。它既不像task_struct那样一上来就能明白是进程描述符,也不像file结构体那样有明确的文件操作语义。dentry 全称是 directory entry,中文叫目录项,但这个名字其实挺有迷惑性的——它不单纯代表“目录”,也代表“文件”,本质上描述的是路径中的一个组成部分。

我当年在调一个嵌入式设备上的文件系统性能问题时,发现应用层频繁open同一个路径,但系统响应总是忽快忽慢,后来用perf一看,热点全在d_lookup和dcache相关的函数上。那一刻我才真正意识到,dentry 不仅仅是内核里一个“挂在 dentry hash table 上的节点”,它直接决定了路径解析快不快、并发访问稳不稳、内存占用多不多。

所以这篇文章我打算从零开始,把struct dentry的来龙去脉讲透。不管你是准备内核面试、做嵌入式开发、还是纯粹对操作系统底层好奇,这篇内容都会给你一个比网上大多数资料更完整的视角。我会结合源码、实际调试经验、以及常见误区来展开。

1.2 dentry 在内核中的地位

先给个整体印象:Linux 虚拟文件系统(VFS)有四个核心对象——super_block(超级块)、inode(索引节点)、dentry(目录项)、file(文件对象)。如果拿现实生活类比,inode 像是房子本身(存放真实数据的元信息),dentry 则像是门牌号和地址,file 是打开房门后你手里的钥匙串。

没有 dentry,路径解析就是一场灾难。你想想看,内核拿到一个路径/usr/local/bin/python3,如果不靠 dentry 提供的树形结构和缓存机制,每一步都要从磁盘读取目录内容来确认下一级名字对应哪个 inode,那系统早就慢成幻灯片了。dentry 的存在,让路径解析的大部分工作可以在内存中高效完成,这是 VFS 性能的重要基石。

2. 核心结构拆解:源码级别的逐字段分析

2.1 struct dentry 的关键字段

不同内核版本的struct dentry定义略有差异,但核心字段基本稳定。以 5.x 内核为例,关键字段如下:

struct dentry { unsigned int d_flags; /* 标志位 */ seqcount_spinlock_t d_seq; /* 用于 RCU 走查的序号锁 */ struct hlist_bl_node d_hash; /* 哈希链表节点 */ struct dentry *d_parent; /* 父目录项 */ struct qstr d_name; /* 名字 */ struct inode *d_inode; /* 关联的 inode 指针 */ const struct dentry_operations *d_op; /* 文件系统自定义操作 */ struct super_block *d_sb; /* 所属超级块 */ unsigned long d_time; /* 文件系统自定义时间 */ void *d_fsdata; /* 文件系统私有数据 */ union { struct list_head d_lru; /* LRU 链表 */ wait_queue_head_t *d_wait; /* 等待队列 */ }; struct list_head d_child; /* 挂到父 dentry 的 children 链表 */ struct list_head d_subdirs; /* 所有子 dentry 链表 */ ... struct dentry *d_alias; /* 关联 inode 的别名链表 */ ... };

每个字段背后都有故事。d_name是struct qstr类型,里面包含 name 指针、hash 值和长度。为什么要单独保存 hash 值?因为路径解析时频繁比较名字,如果每次都比较字符串整体,代价太高;先比较 hash 值,不同直接跳过,相同再验证字符串,这是经典的哈希快速路径优化。d_seq则是为了实现无锁的 RCU 路径走查而引入的,配合__d_lookup_rcu使用,这是现代内核路径解析高性能的关键机制之一。

d_hash这个节点比较特别,它用的是hlist_bl_head,也就是带锁位(lock bit)的哈希链表头。这种设计把锁内嵌到链表头里,可以节省内存,同时支持细粒度的并发控制。为什么不是简单的hlist_head?因为 dentry 哈希表的访问频率极高,如果整个表一把大锁,多核环境下必然成为瓶颈,hlist_bl允许不同的桶并行访问,还能通过锁位做无锁快速路径判断。

看到d_alias时要理解一个重要概念:一个 inode 可能对应多个 dentry,也就是硬链接场景。d_alias将同一个 inode 的所有 dentry 串起来,这样当 inode 状态变化时(比如删除、属性变更),内核可以通过遍历d_alias来同步失效所有相关的 dentry。反向遍历则靠dentry->d_inode指针完成,这就构成了 dentry 与 inode 的双向关联。

2.2 dentry 的状态机:从生到死

dentry 的生命周期并不是简单的“创建-使用-销毁”,它有一套完整的状态机,理解这套状态机是掌握 dcache 的核心。

内核里用标志位加引用计数来管理状态,核心概念有三个:

  • d_inode 不为空的 dentry 是“已连接”状态:表示它与一个真实存在的 inode 关联,路径解析能通过它找到数据。
  • d_inode 为 NULL 的是“负 dentry”:表示路径中存在这个名字,但对应的文件不存在或已被删除。负 dentry 的作用是缓存“不存在”这个结果,避免每次访问都去磁盘确认。比如你反复open一个不存在的路径,负 dentry 会让内核快速返回ENOENT,不会每次都查存储介质。
  • dentry 还可以是“游离”状态:已经从目录树中拆下来,但还被某些进程引用,比如文件已删除但仍有 fd 打开的情况。

状态转换围绕d_lookup、d_add、d_delete、d_drop、d_prune等核心操作进行。d_lookup负责在目录的 children 链表中查找;d_add把 dentry 和 inode 关联,并挂到哈希表;d_delete将 dentry 标记为负状态并释放与 inode 的关联;d_drop则是从哈希表中摘除,使 dentry 无法再被查到;d_prune则是回收 dentry 占用的内存。

这里要单独提一个高频操作d_drop的应用场景:dentry被d_drop之后,路径解析会绕过它,重新走文件系统的真实查找流程。很多文件系统在数据异常或需要强制刷新时会调用这个接口,相当于把缓存的“门牌号”作废,强制系统重新“查地图”。

2.3 dentry_operations:文件系统自定义的钩子

struct dentry_operations是 dentry 结构中容易被忽略、但实际极其重要的部分。它允许各文件系统在 dentry 生命周期关键节点注入自定义行为:

struct dentry_operations { int (*d_revalidate)(struct dentry *, unsigned int flags); int (*d_weak_revalidate)(struct dentry *, unsigned int flags); int (*d_hash)(const struct dentry *, struct qstr *); int (*d_compare)(const struct dentry *, unsigned int, const char *, const struct qstr *); int (*d_delete)(const struct dentry *); int (*d_init)(struct dentry *); void (*d_release)(struct dentry *); void (*d_prune)(struct dentry *); };

d_revalidate主要用于网络文件系统和分布式文件系统,比如 NFS、CIFS。原因是这些文件系统的数据不在本地,服务器端可能随时删改文件,本地缓存的 dentry 可能已经过期。每次路径解析时,内核会调用d_revalidate询问文件系统“这个路径还真实有效吗?”,文件系统根据自身协议判断,比如发送一个轻量级的 GETATTR 请求,有效返回 1,无效返回 0,内核收到 0 后会触发 dentry 失效并重新查找。

d_compare则是用来做自定义名字比较的。最常见的就是案例不敏感的文件系统,比如 FAT 系列。普通文件系统直接按字节比较字符串即可,FAT 则需要忽略大小写比较。ext4、xfs 这类本地文件系统基本不需要实现这些钩子,因为本地状态可信,名字比较也简单。看到这里你应该明白,dentry 并不是一套固定死板的机制,它给文件系统预留了足够多的“插件点”。

3. dcache 的工作原理与设计逻辑

3.1 哈希表与 LRU 的双重管理

dcache 的核心数据结构有两套:一套是全局的 dentry 哈希表(dentry_hashtable),用于按名字快速查找;另一套是 per-superblock 的 LRU 链表,用于内存回收。

先看哈希表。每个 dentry 通过d_hash节点挂到某个哈希桶中,计算哈希时会把父目录的 dentry 指针和名字一起参与计算,大致逻辑是:hash = parent_dentry + name_hash。为什么要混入父目录指针?因为不同目录下可以有同名文件,比如/a/foo和/b/foo,如果不区分父目录,哈希冲突和错误匹配会非常严重。带上父目录指针后,查找时只需知道父 dentry 和待查名字,就能精准定位桶位置。

再看 LRU 链表。缓存的 dentry 不可能无限保留,系统内存紧张时需要回收。dcache 的回收策略是:每个 super_block 维护一个 LRU 链表,dentry 被访问时(比如路径解析命中)会标记为 hot,并移动到 LRU 尾部;当系统触发回收时,shrink_dcache_sb会从 LRU 头部开始遍历,把 cold 状态的 dentry 逐个回收。这里还有个细节:dentry 不能单独被回收,必须先处理它下面的子树。一个目录 dentry 如果还有大量子 dentry 挂在d_subdirs上,回收它之前得先把整棵子树拆掉,否则就会出现悬垂指针。

我调过一个问题:一个嵌入式设备上某目录下有几十万个文件,每次触发内存回收,系统就会卡顿数百毫秒。根本原因就是shrink_dcache_sb需要先遍历整棵子树做解链操作,文件数量多、子目录层级深时,这个递归式的摘除过程代价极高。后来优化方案是调整 dentry 的缓存上限、缩短 LRU 周期,并引导应用层避免一次性创建过多同目录文件,问题才缓解。

3.2 RCU 与无锁路径解析

现代内核路径解析的高性能,很大程度上依赖 RCU(Read-Copy Update)机制。在__d_lookup_rcu这个函数里,查找 dentry 全程不取锁,靠d_seq序号验证一致性。

它的工作原理是:查找前读取d_seq的值 A;遍历哈希链表找到目标 dentry 后,再次读取d_seq,如果值仍是 A,说明节点在查找期间没有被修改过,可以安全使用;如果值变了,说明有并发写者动过这个节点,需要重新走慢路径(加锁重查)。这种机制叫 seqlock,特点是读读者之间不互斥,写者和读者之间通过序号变化来检测冲突。路径解析场景读多写少,使用 seqlock 非常契合。

理解了 RCU 路径解析后,你对内核并发的理解会上升一个层次。它并不是什么神奇的“无锁技术”,而是在绝大多数无竞争的读路径上让我们不等待,遇到竞争时再优雅地退回到安全路径。这也解释了为什么内核代码中到处是unlikely分支——快速通道是常态,慢速通道是例外。

3.3 dentry 内存开销怎么算

dentry 结构体本身在 64 位系统上大约占用 200 字节左右(不同内核版本有差异),再加上名字字符串的内存、inode 等关联结构,一个 dentry 的真实开销可能到 400~500 字节。如果文件系统里有 100 万个文件,光 dcache 就可能吃掉数百 MB 内存。

这里有一个面试常问的问题:/proc/sys/fs/dentry-state里有四个数字,分别代表什么?答案是:第一个是 dentry 总数,第二个是未使用(unused)的 dentry 数,第三个是内存压力下被标记为需要回收的 dentry 数,第四个是系统允许的 dentry 上限。通过监控这个文件,你可以快速判断机器 dcache 是否异常膨胀。我在实际运维中见过一种场景:某个应用频繁创建临时文件,但路径名每次都带随机后缀,导致 dcache 中堆积了大量负 dentry,内存直接被打满。这时候手动清一下缓存(比如echo 2 > /proc/sys/vm/drop_caches)只能救急,根治还得改应用行为。

4. 实操:从内核源码到动态调试

4.1 阅读源码的三个切入路径

很多新人面对庞大的内核源码,尤其是fs/dcache.c这种数千行的文件,不知从何下手。我建议切三条线分别看。

第一条线是“查找路径”。以path_openat为入口,顺着link_path_walk下来的调用链,看walk_component,再到lookup_fast和lookup_slow。这条线回答的是问题“一个路径名是如何一步步解析成 dentry 的”。第二条线是“缓存维护”,聚焦d_lookup、d_alloc、d_instantiate、d_delete、d_drop、__dentry_kill这些函数,理解 dentry 的出生、关联、死亡流程。第三条线是“内存回收”,从shrink_dcache_sb开始,逆着调用链往上层看,理解 dcache 如何与内存管理子系统协作。

我特别推荐先看后两条线,因为它们相对独立,不需要一开始就理解完整的 VFS 路径解析框架。等这两条线吃透了,再回去看路径解析,你会觉得轻松很多——因为路径解析本质上就是查找缓存,缓存不中才落到具体文件系统的lookup回调。

4.2 用 ftrace 追踪 dentry 操作

光看代码不动手,印象还是不深。我最常用的动态调试工具是 ftrace,配合 tracefs 使用,可以零侵入地追踪内核函数调用。

给你一个简单的操作流程:

# 挂载 tracefs mount -t tracefs nodev /sys/kernel/tracing # 启用函数追踪,并设置要追踪的函数 echo function > /sys/kernel/tracing/current_tracer echo d_lookup > /sys/kernel/tracing/set_ftrace_filter echo d_add >> /sys/kernel/tracing/set_ftrace_filter echo d_delete >> /sys/kernel/tracing/set_ftrace_filter echo 1 > /sys/kernel/tracing/tracing_on # 触发一些文件访问 ls /usr/bin # 停止追踪并查看输出 echo 0 > /sys/kernel/tracing/tracing_on cat /sys/kernel/tracing/trace

你会看到类似这样的输出:

ls-1234 [001] .... 56789.012345: d_lookup <- lookup_fast ls-1234 [001] .... 56789.012346: d_add <- lookup_slow

这表示路径解析先走了快速查找lookup_fast,调用了d_lookup,没有命中后转入lookup_slow,最终通过文件系统的lookup拿到了真实 inode,再调用d_add把新的 dentry 挂入缓存。

我调过一个 NFS 性能问题,就是用 ftrace 发现某个挂载点上的d_revalidate被高频调用,每次路径解析都会触发一次网络回调,导致大部分时间耗在 RPC 等待上。后来调整挂载参数,配合对应的缓存策略,性能立刻提了上来。

4.3 写一个小内核模块实践

如果想对 dentry 做更深入的实验,可以写一个简单的字符设备驱动,但更直接的是写一个不落地的内核模块,在模块里打印当前进程 cwd 对应的 dentry 信息。参考代码如下:

#include <linux/module.h> #include <linux/fs.h> #include <linux/dcache.h> #include <linux/sched.h> static int __init dentry_demo_init(void) { struct dentry *dentry; struct path path; if (!current->fs || !current->fs->pwd.dentry) return -EINVAL; path = current->fs->pwd; dentry = path.dentry; pr_info("cwd name: %s\n", dentry->d_name.name); pr_info("cwd parent: %s\n", dentry->d_parent->d_name.name); pr_info("d_inode: %px\n", dentry->d_inode); pr_info("d_sb: %px, magic: 0x%lx\n", dentry->d_sb, dentry->d_sb->s_magic); pr_info("d_count: %d\n", atomic_read(&dentry->d_lockref.count)); pr_info("d_flags: 0x%x\n", dentry->d_flags); return 0; } static void __exit dentry_demo_exit(void) { pr_info("dentry demo exit\n"); } module_init(dentry_demo_init); module_exit(dentry_demo_exit); MODULE_LICENSE("GPL");

用来编译的 Makefile:

obj-m += dentry_demo.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean

编译后:

make sudo insmod dentry_demo.ko sudo dmesg | tail -10

输出里你会看到当前进程工作目录的 dentry 信息,比如d_count告诉你这个 dentry 当前有多少个引用。通过修改这段代码去遍历d_subdirs链表,你还能打印出某个目录下的所有子 dentry,直观体会 dcache 组织目录树的方式。

4.4 调试中的常见坑

写这类模块或者在内核里做实验时,最常踩的坑有这些:第一,打印 dentry 的d_name.name时,d_name可能在并发中被修改,需要先持有引用或锁,否则可能读到半更新的字符串。第二,不要在内核模块里用printk打印超大缓冲区,pr_info虽然比printk方便,但输出过大时同样导致日志缓冲区覆盖,问题难定位。第三,%px打印指针时kptr_restrict可能导致输出全为 0,需要打开相关内核参数或使用%p配合调试需求。

第四条也是最重要的一条:不要外引用没有持有引用的 dentry。dentry的生命周期由引用计数保护,如果你只是取了一个指针但没有通过dget增加引用,后续对端的回收操作会让你的指针变成悬垂指针,这种 bug 极难排查,多数表现为不可预期的崩溃。mincore 的内核 oops 日志里,最常见的调用栈之一就是d_lookup拿到一个已被释放的 dentry。

5. 常见问题与排查思路

5.1 问题速查表

我把平时工作和调试中遇到的高频问题整理成了一个速查表,方便你在开发或排障时直接对照。

现象可能原因排查方向
路径解析变慢,系统卡顿dcache 过大,LRU 回收频繁查看/proc/sys/fs/dentry-state和/proc/meminfo中 Slab 相关字段
不存在的路径反复访问导致磁盘 IO 异常负 dentry 未生效或频繁失效检查文件系统是否实现d_revalidate,网络文件系统尤其常见
open 文件后删除,df 显示空间未释放进程仍持有 fd,inode 未释放用lsof找到持有 fd 的进程,dcache 中 dentry 处于负状态但 inode 还活着
同目录大量小文件访问慢dentry 哈希冲突严重或子树过大用perf看热点函数,检查 hash 分布情况,调整目录层级
卸载文件系统时卡死dentry 子树过大,回收耗时过长检查是否有进程持有目录 fd 不放,确认是否触发了 unhash 的递归操作

第一条和第四条在容器场景最常见。容器频繁创建和删除文件,用的存储驱动如果产生大量负 dentry,宿主机内存会被蹭蹭吃掉。解决思路包括:限制容器可创建文件数、优化应用层文件缓存策略、调整/proc/sys/vm/vfs_cache_pressure来加快 dentry 回收。vfs_cache_pressure默认值是 100,把它调高到 200,代表内核会更激进地回收 dcache,但这也可能导致频繁的路径解析重查,实际效果需要测试。

5.2 一次 dcache 内存泄漏的排查实录

聊一个典型案例。某台服务器运行着一个长期任务,进程数不多,但 RSS 不算高,整个系统内存却持续被吃光,OOM Killer 频繁出没。第一反应是用户态有内存泄漏,但top里 RSS 加起来远小于 total,那多出来的内存去哪了?free -g一看,used 接近总量,但没哪个用户态进程能对上号。

再看/proc/meminfo,发现Slab字段异常巨大,有十几 GB。继续细分,/proc/slabinfo里dentry那一行的 active_objs 数量高达千万级。此时再看/proc/sys/fs/dentry-state,第一个数字也是千万量级,说明 dcache 确实堆积了海量 dentry。

接下来用perf top锁定热点函数,发现__d_lookup_rcu和shrink_dcache_parent频繁出现。再用ftrace追踪d_alloc的调用栈,发现大量来源是一个三方的文件监控库,它每次创建一个临时文件后立即删除,但路径名带毫秒级时间戳和随机后缀,导致 dcache 中不断生成独一无二的负 dentry,而且因为父目录 dentry 一直存在,这些负 dentry 不会被直接回收。

根治方案是:先把监控库升级到新版本(它后来修复了临时文件命名方式),同时清理当前积压的 dcache。这里注意,对线上环境不要直接重启,可以先用sync && echo 2 > /proc/sys/vm/drop_caches释放在内存中的 dentry 和 inode,但这只是应急。真正的重点是找到源头,否则缓存会以相同速度再次膨胀。最终这台机器经过一轮应用升级和逐步重启,缓存稳定了下来。这个案例也再次印证:dcache 的问题大概率不是内核 bug,而是应用层不合理行为叠加出来的现象。

5.3 关于 dentry 未来演进

现在内核社区对 dentry 的改进方向主要集中在降低内存占用和提升并发能力上,其中比较典型的工作有:

  • 使用更紧凑的结构体布局,通过 RCU 字段归并和位域压缩,在 64 位系统上把 dentry 开销进一步压低。
  • 改进锁粒度,从 per-dentry spinlock 向更细粒度的同步机制演进,减少路径解析的锁竞争。
  • 对负 dentry 的数量做更主动的限制,避免缓存了大量“不存在”的路径。
  • 在文件系统层尝试用更大的哈希表或可变哈希算法来降低碰撞概率。

在实际项目中要想减少 dcache 问题,我总结了几条经验:尽量少用随机文件名;避免在同一个目录下放过多文件,建议按日期或 ID 分子目录;网络文件系统挂载时,选择合理的缓存策略,不要盲目开所有缓存选项;定期监控/proc/slabinfo和/proc/sys/fs/dentry-state,设定异常告警阈值。这些操作对我的线上稳定性帮助非常大。

6. 面试与实战:dentry 知识的价值转化

6.1 内核面试高频问题梳理

如果你正准备内核相关岗位的面试,dentry 几乎是必考知识点。面试官不会直接问“struct dentry 有哪些字段”,而是会从场景出发来考察。常见的问题有:

  • 解释路径解析过程,lookup_fast和lookup_slow的区别是什么?
  • dentry 和 inode 之间是什么关系?硬链接下链接数怎么变化?
  • 为什么删除一个文件后,进程持有 fd 仍能读写?
  • 负 dentry 是什么?有什么好处和问题?
  • 为什么 NFS 文件系统的 dentry 需要d_revalidate?
  • dentry 缓存的回收机制是怎样的?
  • /proc/sys/fs/dentry-state各项参数的含义?
  • dentry 的 RCU 路径解析如何保证正确性?

如果能把每个问题都按“背景机制 + 代码路径 + 具体场景”三层来回答,面试官会觉得你不是背题,而是真的在用过、调过。比如硬链接题,正确的打开方式是从 inode 的i_nlink计数、d_alias链表、dentry->d_inode三个角度做交叉说明,然后引出删除时i_nlink减到 0、但 inode 仍然存活、直到最后一个引用释放才真正销毁的完整过程。

6.2 白皮书学习法:从 dcache.c 中挖宝

如果你真想把 dentry 吃透,我建议选一个内核版本,把这个版本下的fs/dcache.c完整读一遍。读的时候拿着问题去看,而不是逐行刷代码。

我的习惯是第一遍只看 API 注释和函数签名,画出调用关系图(用文本或手绘都可以,不需要工具画流程),第二遍挑核心函数(d_lookup、__d_lookup_rcu、d_alloc、d_add、d_delete、d_drop、__dentry_kill)深入看注释和关键行。第三遍再回头处理疑点,比如d_lockref的具体语义、LRU 和d_lru的引用关系、d_weak_revalidate和d_revalidate的调用区别。

读源码不需要每行都懂,但每段都要能回答两个问题:这段代码在哪种场景下会被执行?它的目的是什么?带着这两个问题去读,效率会高很多。

6.3 工程实践中的几个建议

最后从工程角度分享几点心得。第一个建议是:不要把 dentry 相关优化当作业绩,除非你有真实数据和线上问题支撑。很多团队对 dcache 的调整属于“看着参数很酷”就改了,结果反而降低了缓存命中率,性能不升反降。任何 sysctl 参数调整,先做 A/B 测试。

第二个建议是:应用层尽量做路径规范化。内核虽然会缓存解析结果,但如果一个进程反复用相对路径、软链接、..组合穿透同一目标,路径解析会绕更多弯。虽然结果还是命中了同一个最终 dentry,但解析过程中的中间步骤会消耗更多 CPU。应用层把真实路径一次性规范化并缓存,收益是实打实的。

第三个建议是:做嵌入式开发时需要注意内存预算。嵌入式设备内存有限,如果业务会创建大量小文件,dcache 很容易占满剩余内存。此时可以提前调大vfs_cache_pressure,或者通过 cgroup 和 sysctl 限制 slab 增长。我见过不少嵌入式设备因为没预估 dcache 占用,在长期运行后出现莫名重启,其实就是 OOM 触发 panic。

7. 写在最后

说实话,dentry 是我在内核里最喜欢的一个结构体——它非常小,却串联起了文件系统、内存管理、并发机制三个大领域。把struct dentry弄明白,你对 VFS 的理解、对路径解析的敏感度、对缓存管理的直觉,都会有一个质的提升。

如果你看完这篇文章决定动手验证一下,我的建议顺序是:先编译一个带CONFIG_DEBUG_INFO的内核,用 ftrace 追踪一次简单的路径解析;然后照着源码把d_lookup和d_add的调用链画出来;最后抄写文中的内核模块,在模块里遍历某个目录的子 dentry,打印它们的名字和状态。这个过程走完,你就不需要再背任何面试题了,因为答案已经在你的操作里了。遇到具体问题也不怕,记住作者的经验:先看/proc,再上perf,最后才动代码。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询