[TOC]

fs/dcache.c 目录项缓存(Directory Entry Cache) VFS路径查找加速器

历史与背景

这项技术是为了解决什么特定问题而诞生的?

这项技术以及它所实现的目录项缓存(dcache),是为了解决Linux虚拟文件系统(VFS)中最核心的性能瓶颈之一:路径名到inode的解析过程

  • 消除磁盘I/O:在一个典型的文件系统中,解析一个路径如/home/user/file.txt需要一系列的磁盘读取操作。首先读取根目录/的内容找到home,然后读取home目录的内容找到user,以此类推,直到最后找到file.txt。每一次目录读取都是一次缓慢的磁盘I/O。如果每次open()stat()系统调用都执行这个过程,系统性能将无法接受。
  • 提供快速路径查找:dcache在内存中缓存了目录项(dentry)的树状结构,它直接映射了文件系统的目录层次。当内核需要解析一个路径时,它首先在dcache中查找。如果路径的所有组件都在缓存中,整个解析过程就可以在内存中以极高的速度完成,完全无需访问磁盘。
  • 缓存负面结果(Negative Lookups):dcache不仅缓存了存在的路径,还会缓存不存在的路径查询结果。例如,如果一个程序频繁尝试打开一个不存在的文件/tmp/lockfile,第一次查询会在磁盘上确认其不存在,dcache会将这个“负面”结果缓存起来。后续的查询会直接从dcache得知该文件不存在,避免了无谓的磁盘访问。

它的发展经历了哪些重要的里程碑或版本迭代?

dcache是内核中最古老、最核心的数据结构之一,它的演进主要是为了提升在多核系统上的扩展性(Scalability)。

  • 基本哈希表:dcache最初是一个简单的、由单个锁保护的哈希表。这在单核或双核系统上工作良好,但在多核系统上,这个锁成为了严重的性能瓶颈。
  • RCU(Read-Copy-Update)的引入:这是dcache发展史上最重要的里程碑。内核开发者利用RCU技术重构了dcache的查找路径。这使得路径查找(绝大多数操作)可以无锁进行,多个CPU核心可以同时并发地在dcache中进行查找,而不会相互阻塞。只有在修改dcache(如创建、删除、重命名文件)时才需要获取锁。这极大地提升了dcache在多核环境下的性能。
  • 重命名操作的优化rename操作是dcache中最复杂的操作之一,因为它涉及到在树状结构中移动整个子树。为了在不长时间锁定整个dcache的情况下安全地执行此操作,内核引入了序列锁(seqlocks)和d_seq字段,允许读者(查找者)检测到自己是否在一次不完整的rename操作中途进行查找,并在必要时重试。

目前该技术的社区活跃度和主流应用情况如何?

dcache是Linux VFS的心脏,其重要性无与伦比。

  • 社区活跃度:dcache的代码极其稳定,但作为内核性能的关键,它仍然是内核开发者持续关注和进行微调的对象。任何对VFS性能的重大改进都可能涉及对dcache的调整。
  • 主流应用:dcache是所有文件系统操作的基础。任何涉及文件路径的系统调用,如open(), stat(), chmod(), unlink(), ls命令等,都必须经过dcache。它的性能直接决定了整个系统的文件I/O性能。

核心原理与设计

它的核心工作原理是什么?

dcache是一个结合了哈希表树状结构的复杂内存缓存。

  1. dentry对象:dcache的核心数据结构是struct dentry(目录项)。一个dentry对象代表路径中的一个组成部分(如home),它包含了这个组成部分的名称,并有一个指针指向其父目录的dentry,以及一个指针指向其对应的inode(如果路径存在)。
  2. 树状结构:通过d_parent指针,所有的dentry对象在内存中构成了一个树状结构,这个树精确地反映了磁盘上文件系统的目录层次。这使得路径解析可以沿着树的分支在内存中快速进行。
  3. 哈希表:为了快速在某个目录中查找一个特定的子项(例如在代表/home的dentry下查找user),dcache使用了一个全局的哈希表。通过一个哈希函数d_hash(parent_dentry, child_name)可以快速定位到可能的dentry对象,避免了线性扫描目录下的所有子项。
  4. 状态管理:dentry有几种关键状态:
    • In-use (Positive):dentry被一个或多个进程使用(引用计数大于0),并且它指向一个有效的inode。
    • Unused (Positive):dentry是有效的(指向一个inode),但当前没有进程在使用它(引用计数为0)。它位于LRU(最近最少使用)列表中,当内存不足时可以被回收。
    • Negative:dentry不指向任何inode(d_inode为NULL)。它代表一个不存在的路径。Negative dentry也会被缓存起来,以加速对不存在文件的重复查找。
  5. 内存回收(Shrinking):当系统内存压力增大时,内核的内存回收机制会调用dcache的“收缩器”(shrinker)。收缩器会扫描dcache的LRU列表,释放那些“Unused”状态的dentry对象,从而回收内存。可以通过/proc/sys/vm/vfs_cache_pressure来调整收缩器的积极程度。

它的主要优势体现在哪些方面?

  • 极高的性能:将绝大多数路径查找操作从磁盘I/O转换为了内存访问。
  • 通用性:作为VFS的一部分,dcache对所有文件系统(ext4, XFS, NFS, tmpfs等)都有效。
  • 可扩展性:基于RCU的无锁读取路径,使得其在拥有大量CPU核心的现代服务器上也能高效工作。
  • 效率:缓存负面查询结果,避免了对不存在文件的重复、昂贵的磁盘检查。

它存在哪些已知的劣势、局限性或在特定场景下的不适用性?

  • 内存消耗:这是dcache最主要的“成本”。在一个拥有数千万甚至上亿个文件的系统上,dcache可能会消耗GB级别的内存。如果内存配置不当,dcache本身可能成为内存压力的来源。
  • 复杂性:dcache的内部实现,特别是其并发控制和与内存管理的交互,是内核中最复杂的部分之一。不正确的交互很容易导致系统死锁或崩溃。
  • 缓存一致性(针对网络文件系统):对于NFS等网络文件系统,dcache中的内容可能与服务器上的实际内容存在短暂的不一致。NFS驱动需要额外的机制来确保缓存的有效性或在必要时使其失效。

使用场景

在哪些具体的业务或技术场景下,它是首选解决方案?请举例说明。

dcache是内核的底层机制,不是一个可选方案,而是所有文件路径操作的必经之路。它的好处在以下场景中体现得尤为明显:

  • Web服务器:一个繁忙的Web服务器需要为每个请求频繁地stat()open()相同的静态文件(如图片、CSS、JS文件)。dcache使得这些重复的操作几乎没有I/O开销。
  • 软件编译:在编译大型项目时,编译器会反复打开相同的头文件。这些头文件的路径会被dcache牢牢记住,极大地加速了编译过程。
  • 任何大量使用小文件的应用:例如,邮件服务器(Maildir格式)、某些数据库或版本控制系统(如Git的早期对象存储)。dcache对于这类工作负载至关重要。

是否有不推荐使用该技术的场景?为什么?

不存在不使用dcache的场景。但是,在某些特定工作负载下,其默认行为可能不是最优的:

  • 一次性大规模文件扫描:例如,一个备份程序或find /命令,它会遍历文件系统中的每一个文件,但可能再也不会访问它们。这种工作负载会用大量“一次性”的dentry填满dcache,可能会将之前缓存的热点数据(如常用程序和库的路径)挤出缓存,造成性能下降。这种现象称为“缓存污染”。在这种情况下,可以通过echo 2 > /proc/sys/vm/drop_caches手动清理dcache(仅限非生产环境调试!),或者期待内核的LRU机制能尽快淘汰这些冷数据。

对比分析

请将其 与 其他相似技术 进行详细对比。

对比一:dcache vs. Page Cache (页面缓存)

这是内核中最重要但也最容易混淆的一对缓存。它们协同工作,但职责完全不同。

特性 dcache (目录项缓存) Page Cache (页面缓存)
缓存对象 文件系统元数据:文件名、目录结构、路径名与inode的映射关系。 文件数据:文件实际的内容。
核心数据结构 struct dentry struct page
解决的问题 路径查找性能:“我如何找到文件?” 文件读写性能:“文件的内容是什么?”
工作流程 open("/path/to/file")时,内核在dcache中查找路径,最终找到文件的inode。 read()write()一个已打开的文件时,内核在Page Cache中查找或缓存文件的内容页。
关系 它们是上下游关系。必须先通过dcache找到一个文件的inode,然后才能通过该inode的地址空间(address_space)去访问其在Page Cache中的数据。

对比二:dcache vs. Inode Cache (inode缓存)

特性 dcache Inode Cache
缓存对象 文件名和目录层次结构。 文件的元数据属性。
核心数据结构 struct dentry struct inode
包含的信息 文件名、父子关系指针、指向inode的指针。 文件模式(权限)、所有者、大小、时间戳、数据块位置指针等。
关系 多对一。多个dentry可以指向同一个inode(例如,通过硬链接)。dcache是通往inode的“路标”。 inode代表了文件的本质。inode缓存在内存中,避免了每次stat()都要从磁盘读取inode信息。
生命周期 dentry是易失的,当内存不足时可以被回收。 只要一个dentry或一个打开的文件引用了一个inode,该inode就会一直存在于inode缓存中。

include/linux/dcache.h

dget 获取一个 dentry 的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* dget - 获取一个 dentry 的引用
* @dentry: 要获取引用的 dentry

* 给定一个 dentry 或 %NULL 指针,如果合适则增加引用计数并返回该 dentry。
当 dentry 有引用时不会被销毁。相反,没有引用的 dentry 可能由于多种原因消失,
首先是内存压力。换句话说,该原语用于克隆现有引用;在引用计数为零的情况下使用它是一个错误。

* 注意:如果 @dentry->d_lock 被持有,它将会自旋。从避免死锁的角度来看,
它等同于 spin_lock()/增加引用计数/spin_unlock(),因此在 @dentry->d_lock 下调用它总是一个错误;
在其任何后代的 ->d_lock 下调用它也是一个错误。
*/
static inline struct dentry *dget(struct dentry *dentry)
{
if (dentry)
lockref_get(&dentry->d_lockref);
return dentry;
}

fs/dcache.c

dcache_init_early 目录项缓存初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static struct hlist_bl_head *dentry_hashtable __ro_after_init __used;

static void __init dcache_init_early(void)
{
/* 如果哈希分布在 NUMA 节点之间,请推迟哈希分配,直到 vmalloc 空间可用。
*/
if (hashdist)
return;

dentry_hashtable =
alloc_large_system_hash("Dentry cache",
sizeof(struct hlist_bl_head),
dhash_entries,
13,
HASH_EARLY | HASH_ZERO,
&d_hash_shift,
NULL,
0,
0);
// = 32 - 12 = 20
d_hash_shift = 32 - d_hash_shift;
/* do { } while (0) */
runtime_const_init(shift, d_hash_shift);
runtime_const_init(ptr, dentry_hashtable);
}

dcache_init 目录项缓存初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/* Only NUMA needs hash distribution. 64bit NUMA architectures have
* sufficient vmalloc space.
*/
#ifdef CONFIG_NUMA
#define HASHDIST_DEFAULT IS_ENABLED(CONFIG_64BIT)
extern int hashdist; /* Distribute hashes across NUMA nodes? */
#else
#define hashdist (0)
#endif

static void __init dcache_init(void)
{
/*
* 可以像列表一样为稳定状态添加构造函数,但由于 dcache 的缓存性质,这可能不值得。
*/
/* 创建目录项缓存(dentry_cache),用于存储目录项(dentry)
SLAB_RECLAIM_ACCOUNT 标志允许缓存被回收以节省内存。
SLAB_PANIC 确保在分配失败时触发内核错误,避免系统进入不稳定状态。
SLAB_ACCOUNT 用于内存使用的统计和控制 */
dentry_cache = KMEM_CACHE_USERCOPY(dentry,
SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_ACCOUNT,
d_shortname.string);

/* Hash may have been set up in dcache_init_early */
if (!hashdist)
return;

dentry_hashtable =
alloc_large_system_hash("Dentry cache",
sizeof(struct hlist_bl_head),
dhash_entries,
13,
HASH_ZERO,
&d_hash_shift,
NULL,
0,
0);
d_hash_shift = 32 - d_hash_shift;

runtime_const_init(shift, d_hash_shift);
runtime_const_init(ptr, dentry_hashtable);
}

vfs_caches_init_early 虚拟文件系统缓存初始化

1
2
3
4
5
6
7
8
9
10
void __init vfs_caches_init_early(void)
{
int i;

for (i = 0; i < ARRAY_SIZE(in_lookup_hashtable); i++)
INIT_HLIST_BL_HEAD(&in_lookup_hashtable[i]);

dcache_init_early();
inode_init_early();
}

vfs_caches_init 设置虚拟文件系统(VFS)相关的缓存和数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __init vfs_caches_init(void)
{
/* 创建一个名称缓存(names_cache),用于存储文件路径名
SLAB_HWCACHE_ALIGN 确保缓存对齐以优化硬件性能,
SLAB_PANIC 确保在分配失败时触发内核错误*/
names_cachep = kmem_cache_create_usercopy("names_cache", PATH_MAX, 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, 0, PATH_MAX, NULL);
/* 初始化目录缓存(dcache),用于管理目录项(dentry) */
dcache_init();
/* 初始化索引节点(inode)缓存 */
inode_init();
/* 初始化文件表,用于管理打开的文件 */
files_init();
/* 置系统允许打开的最大文件数 */
files_maxfiles_init();
/* 初始化挂载点管理,用于处理文件系统的挂载和卸载操作 */
mnt_init();
/* 初始化块设备缓存,用于管理块设备的缓存和操作 */
bdev_cache_init();
/* 初始化字符设备管理,用于处理字符设备的注册和操作 */
chrdev_init();
}

VFS Dentry分配:文件路径组件的内存对象工厂

本代码片段是Linux VFS层中负责创建dentry(目录项)的核心工厂函数。其主要功能是从内核的slab缓存中分配一个新的struct dentry对象,并对其进行全面的初始化。Dentry是VFS中用于表示一个路径名组件(即一个文件名或目录名)的内存对象,是dcache(目录项缓存)的基本单元。此代码提供了多个API变体(d_alloc, d_alloc_anon等),以满足不同场景(如创建普通文件、根目录、匿名对象等)对dentry创建的需求。

实现原理分析

该实现的原理是提供一个高度优化的、集中的内部函数__d_alloc来处理所有dentry的共性初始化,然后通过多个简单的包装函数来处理不同用例的特性。

  1. 核心分配与初始化 (__d_alloc):

    • 内存分配: 它不使用通用的kmalloc,而是从一个专门的slab缓存dentry_cache中分配内存。kmem_cache_alloc_lru的使表明它与dcache的LRU(最近最少使用)回收机制紧密集成,有利于高效的内存管理和回收。
    • 名称管理优化: 这是一个关键的性能优化。
      • 短名称 (Inline): 如果dentry的名称长度小于DNAME_INLINE_LEN,名称字符串会直接存储在struct dentry对象内部的d_shortname数组中。
      • 长名称 (External): 如果名称过长,它会额外分配一个external_name结构体来存储,并在dentry中只保存一个指针。这种设计避免了为所有dentry都分配最大长度的名称缓冲区,从而在平均情况下节省了大量内存。
    • 并发安全: 在设置d_name.name指针时,它使用了smp_store_release。这是一个带有内存屏障的写操作,确保了名称字符串的memcpy和NUL终止符的写入,对于使用RCU(Read-Copy-Update)进行无锁路径遍历的其他CPU来说是完全可见的。这是保证dcache并发安全性的关键细节。
    • 字段初始化: 它负责对dentry的所有字段进行初始化,包括引用计数(lockref)、序列计数器(seqcount)、父子关系(初始时d_parent指向自身)、所属文件系统(d_sb)以及各种链表头。
    • 文件系统钩子: 它提供了d_op->d_init回调,允许底层具体的文件系统(如ext4, tmpfs)在dentry分配后立即对其进行特定的初始化。
  2. 包装函数 (Wrappers):

    • d_alloc: 这是最常用的API。它调用__d_alloc创建dentry后,会锁定父dentry,然后将新创建的dentry链接到父dentry的d_children链表中,并正确设置其d_parent指针,从而将其“嫁接”到VFS的dentry树中。
    • d_alloc_anon: 用于创建匿名dentry,如文件系统的根dentry(/),它没有父节点和名称。
    • d_alloc_pseudo: 用于为“无查找”的文件系统(如管道pipefs、套接字sockfs)创建dentry。这些dentry被标记为DCACHE_NORCU,意味着它们的生命周期不通过RCU管理,可以在最后一个引用被释放后立即被销毁,这对于这些临时的内核对象来说更高效。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
// __d_alloc: 分配一个dcache条目的内部核心函数。
// @sb: dentry将属于的文件系统。
// @name: 待分配dentry的名字(qstr结构)。
static struct dentry *__d_alloc(struct super_block *sb, const struct qstr *name)
{
struct dentry *dentry;
char *dname;
int err;

// 从dentry的专用slab缓存中分配一个对象,并将其与超级块的LRU关联。
dentry = kmem_cache_alloc_lru(dentry_cache, &sb->s_dentry_lru,
GFP_KERNEL);
if (!dentry)
return NULL;

// 保证内联名称缓冲区总是以NUL结尾,确保字符串操作的安全性。
dentry->d_shortname.string[DNAME_INLINE_LEN-1] = 0;
if (unlikely(!name)) { // 处理匿名dentry的情况
name = &slash_name; // 默认为 "/"
dname = dentry->d_shortname.string;
} else if (name->len > DNAME_INLINE_LEN-1) { // 处理长名称
size_t size = offsetof(struct external_name, name[1]);
// 动态分配一个外部名称结构体来存储长名称。
struct external_name *p = kmalloc(size + name->len,
GFP_KERNEL_ACCOUNT |
__GFP_RECLAIMABLE);
if (!p) {
kmem_cache_free(dentry_cache, dentry);
return NULL;
}
atomic_set(&p->count, 1);
dname = p->name;
} else { // 处理短名称,使用内联缓冲区。
dname = dentry->d_shortname.string;
}

dentry->d_name.len = name->len;
dentry->d_name.hash = name->hash;
// 将名称字符串拷贝到目标缓冲区。
memcpy(dname, name->name, name->len);
dname[name->len] = 0; // 确保NUL结尾。

// 使用带有release内存屏障的存储操作,确保名称内容对其他CPU可见。
smp_store_release(&dentry->d_name.name, dname);

// 初始化dentry的各个字段。
dentry->d_flags = 0;
lockref_init(&dentry->d_lockref); // 初始化锁和引用计数。
seqcount_spinlock_init(&dentry->d_seq, &dentry->d_lock);
dentry->d_inode = NULL; // 新dentry是negative的,没有关联inode。
dentry->d_parent = dentry; // 初始时,parent指向自身。
dentry->d_sb = sb; // 关联到超级块。
dentry->d_op = sb->__s_d_op; // 继承超级块的默认dentry操作。
dentry->d_flags = sb->s_d_flags;
dentry->d_fsdata = NULL;
// 初始化所有链表节点。
INIT_HLIST_BL_NODE(&dentry->d_hash);
INIT_LIST_HEAD(&dentry->d_lru);
INIT_HLIST_HEAD(&dentry->d_children);
INIT_HLIST_NODE(&dentry->d_u.d_alias);
INIT_HLIST_NODE(&dentry->d_sib);

// 如果文件系统提供了d_init回调,则调用它。
if (dentry->d_op && dentry->d_op->d_init) {
err = dentry->d_op->d_init(dentry);
if (err) { // 如果回调失败,则清理并返回NULL。
if (dname_external(dentry))
kfree(external_name(dentry));
kmem_cache_free(dentry_cache, dentry);
return NULL;
}
}

// 增加全局dentry计数器。
this_cpu_inc(nr_dentry);

return dentry;
}

// d_alloc: 分配一个dcache条目(通用API)。
// @parent: 要分配条目的父dentry。
// @name: 名字的qstr。
struct dentry *d_alloc(struct dentry * parent, const struct qstr *name)
{
struct dentry *dentry = __d_alloc(parent->d_sb, name);
if (!dentry)
return NULL;
spin_lock(&parent->d_lock);
// 将新dentry的父指针指向父dentry (dget_dlock会增加引用计数)。
dentry->d_parent = dget_dlock(parent);
// 将新dentry添加到父dentry的子节点链表中。
hlist_add_head(&dentry->d_sib, &parent->d_children);
spin_unlock(&parent->d_lock);

return dentry;
}
EXPORT_SYMBOL(d_alloc);

// d_alloc_anon: 分配一个匿名的dentry。
struct dentry *d_alloc_anon(struct super_block *sb)
{
return __d_alloc(sb, NULL);
}
EXPORT_SYMBOL(d_alloc_anon);

// d_alloc_cursor: 分配一个用于遍历的“游标”dentry。
struct dentry *d_alloc_cursor(struct dentry * parent)
{
struct dentry *dentry = d_alloc_anon(parent->d_sb);
if (dentry) {
dentry->d_flags |= DCACHE_DENTRY_CURSOR;
dentry->d_parent = dget(parent);
}
return dentry;
}

// d_alloc_pseudo: 为无查找文件系统分配一个dentry。
struct dentry *d_alloc_pseudo(struct super_block *sb, const struct qstr *name)
{
static const struct dentry_operations anon_ops = {
.d_dname = simple_dname
};
struct dentry *dentry = __d_alloc(sb, name);
if (likely(dentry)) {
// 标记此dentry不受RCU延迟释放的管理。
dentry->d_flags |= DCACHE_NORCU;
if (!dentry->d_op)
dentry->d_op = &anon_ops;
}
return dentry;
}

// d_alloc_name: 一个便利的包装函数,通过C字符串名字来分配dentry。
struct dentry *d_alloc_name(struct dentry *parent, const char *name)
{
struct qstr q;

q.name = name;
// 计算名字的哈希和长度。
q.hash_len = hashlen_string(parent, name);
return d_alloc(parent, &q);
}
EXPORT_SYMBOL(d_alloc_name);

VFS Dentry实例化:将文件名与文件数据建立链接

本代码片段是Linux虚拟文件系统(VFS)中一个极为核心的部分,负责dentry(目录项)的“实例化”过程。其核心功能是将一个代表文件或目录名、但尚未关联到具体文件数据的dentry,与一个代表文件元数据和内容的inode(索引节点)进行链接。这个操作是所有文件系统查找、创建和访问操作的基础,它将抽象的文件路径名与底层的、持久化的文件对象联系起来,是dcache(目录项缓存)能够工作的关键所在。

实现原理分析

该机制的实现围绕着将struct inode指针安全地赋值给struct dentry的核心操作展开,并辅以必要的锁、状态转换和安全检查。

  1. 确定Dentry类型 (d_flags_for_inode): 在进行链接之前,此辅助函数会检查inode的元数据(i_mode, i_op),以确定它代表的是目录、普通文件、符号链接还是特殊文件。它返回一组DCACHE_*_TYPE标志,这些标志将被存储在dentry中,用于优化后续的路径查找等操作。

  2. 核心链接操作 (__d_instantiate): 这是实际执行链接的内部函数。它假定调用者已经持有了必要的锁。

    • 它首先将dentry添加到inode的i_dentry哈希链表中。这个链表被称为“别名(alias)”链表,因为一个inode可以有多个dentry指向它(即硬链接)。
    • 然后,它原子地将inode指针和从d_flags_for_inode获得的类型标志设置到dentry中。这个过程使用seqcount(序列计数器)来保护,允许无锁的读者(如路径遍历代码)检测到更新并进行重试,从而获得很高性能。
    • 如果dentry之前是一个“negative” dentry(代表一次失败的查找),此操作会将其转变为“positive” dentry,并更新相应的全局计数器。
  3. 公共API (d_instantiated_instantiate_new):

    • d_instantiate: 这是通用的、导出的API。它负责处理所有外部锁定(inode->i_lock)和安全钩子(security_d_instantiate),然后调用__d_instantiate来执行核心工作。它用于将一个已经完全初始化的inode附加到一个dentry上。
    • d_instantiate_new: 这是一个专门的变体,用于处理新创建的inode。除了执行d_instantiate的所有操作外,它还负责清除inode的I_NEW状态标志,并通过内存屏障和唤醒队列,通知任何正在等待此inode创建完成的进程。
  4. 根Dentry创建 (d_make_root): 这是一个高层的辅助函数,专门用于文件系统挂载过程。它分配一个匿名的、无父目录的dentry,然后调用d_instantiate将其与文件系统的根inode进行链接,从而创建出整个文件系统树的起点(/)。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// d_flags_for_inode: 根据inode的类型确定dentry应有的标志。
static unsigned d_flags_for_inode(struct inode *inode)
{
unsigned add_flags = DCACHE_REGULAR_TYPE; // 默认为普通文件类型。

if (!inode)
return DCACHE_MISS_TYPE; // 如果没有inode,这是一个失败的查找。

if (S_ISDIR(inode->i_mode)) { // 如果是目录
add_flags = DCACHE_DIRECTORY_TYPE;
// 检查并缓存inode是否支持自定义lookup操作,用于优化。
if (unlikely(!(inode->i_opflags & IOP_LOOKUP))) {
if (unlikely(!inode->i_op->lookup))
add_flags = DCACHE_AUTODIR_TYPE;
else
inode->i_opflags |= IOP_LOOKUP;
}
goto type_determined;
}

if (unlikely(!(inode->i_opflags & IOP_NOFOLLOW))) {
if (unlikely(inode->i_op->get_link)) { // 如果是符号链接
add_flags = DCACHE_SYMLINK_TYPE;
goto type_determined;
}
inode->i_opflags |= IOP_NOFOLLOW;
}

if (unlikely(!S_ISREG(inode->i_mode))) // 如果不是普通文件(也不是目录或符号链接)
add_flags = DCACHE_SPECIAL_TYPE; // 则是特殊文件(设备文件、管道等)。

type_determined:
if (unlikely(IS_AUTOMOUNT(inode))) // 检查是否为自动挂载点
add_flags |= DCACHE_NEED_AUTOMOUNT;
return add_flags;
}

// __d_instantiate: 实际将inode链接到dentry的内部函数(无锁)。
static void __d_instantiate(struct dentry *dentry, struct inode *inode)
{
unsigned add_flags = d_flags_for_inode(inode);
WARN_ON(d_in_lookup(dentry)); // 警告:不应在查找过程中实例化dentry。

spin_lock(&dentry->d_lock);
// 如果dentry之前是negative dentry且在LRU链表上,则递减全局计数器。
if ((dentry->d_flags &
(DCACHE_LRU_LIST|DCACHE_SHRINK_LIST)) == DCACHE_LRU_LIST)
this_cpu_dec(nr_dentry_negative);
// 将dentry添加到inode的别名哈希链表中。
hlist_add_head(&dentry->d_u.d_alias, &inode->i_dentry);
// 使用序列计数器保护对inode指针和类型标志的更新。
raw_write_seqcount_begin(&dentry->d_seq);
__d_set_inode_and_type(dentry, inode, add_flags);
raw_write_seqcount_end(&dentry->d_seq);
fsnotify_update_flags(dentry); // 通知文件系统事件子系统。
spin_unlock(&dentry->d_lock);
}

// d_instantiate: 为dentry填充inode信息(公共API)。
// @entry: 要完成的dentry。
// @inode: 要附加到此dentry的inode。
void d_instantiate(struct dentry *entry, struct inode * inode)
{
// BUG_ON检查,确保此dentry尚未被链接。
BUG_ON(!hlist_unhashed(&entry->d_u.d_alias));
if (inode) {
security_d_instantiate(entry, inode); // 调用LSM安全钩子。
spin_lock(&inode->i_lock); // 锁定inode。
__d_instantiate(entry, inode); // 调用内部函数执行链接。
spin_unlock(&inode->i_lock); // 解锁inode。
}
}
EXPORT_SYMBOL(d_instantiate);

// d_instantiate_new: 专用于实例化新创建的inode。
void d_instantiate_new(struct dentry *entry, struct inode *inode)
{
BUG_ON(!hlist_unhashed(&entry->d_u.d_alias));
BUG_ON(!inode);
lockdep_annotate_inode_mutex_key(inode); // 为锁调试器提供注解。
security_d_instantiate(entry, inode);
spin_lock(&inode->i_lock);
__d_instantiate(entry, inode);
WARN_ON(!(inode->i_state & I_NEW)); // 警告:inode应处于I_NEW状态。
// 清除I_NEW和I_CREATING状态,表示inode已完全可用。
inode->i_state &= ~I_NEW & ~I_CREATING;
/*
* 与prepare_to_wait_event()中的屏障配对,确保等待者能看到状态位
* 被清除,或者唤醒者能看到等待队列中的任务。
*/
smp_mb(); // 内存屏障
// 唤醒所有等待此inode创建完成的进程。
inode_wake_up_bit(inode, __I_NEW);
spin_unlock(&inode->i_lock);
}
EXPORT_SYMBOL(d_instantiate_new);

// d_make_root: 为根inode创建一个根dentry。
struct dentry *d_make_root(struct inode *root_inode)
{
struct dentry *res = NULL;

if (root_inode) {
// 分配一个匿名的dentry。
res = d_alloc_anon(root_inode->i_sb);
if (res)
// 将其与根inode实例化。
d_instantiate(res, root_inode);
else
// 如果分配失败,释放对根inode的引用。
iput(root_inode);
}
return res;
}
EXPORT_SYMBOL(d_make_root);

目录缓存统计: 通过Sysctl暴露VFS缓存指标

本代码片段的核心功能是为Linux内核的目录条目缓存(dentry cache, dcache)创建一个监控和调优接口。它利用sysctl机制,在/proc/sys/fs//proc/sys/vm/目录下生成一系列文件,用于向用户空间暴露dcache的实时统计数据(如缓存条目总数、未使用数)和相关的内存回收策略参数(如vfs_cache_pressure)。这为系统管理员和性能分析工具提供了洞察VFS层性能和内存占用的关键途径。

实现原理分析

此功能的实现依赖于per-cpu计数器和sysctl框架,以一种高效且可扩展的方式收集和展示统计信息。

  1. Per-CPU计数器 (DEFINE_PER_CPU):

    • 为了在高并发的多核系统上高效地、无锁地更新dcache的统计信息,代码为nr_dentry(总数)、nr_dentry_unused(未使用数)和nr_dentry_negative(“否定”缓存数)都定义了**每CPU(per-cpu)**的计数器。
    • 当内核在某个CPU上创建、释放或使用一个dentry时,它只会原子地更新当前CPU上的对应计数器。这种操作几乎没有锁竞争,性能极高。
  2. 数据汇总 (get_nr_dentry, etc.):

    • 当需要获取全局总数时(例如,当用户读取sysctl文件时),get_nr_dentry这样的辅助函数会被调用。
    • 这些函数通过for_each_possible_cpu循环遍历所有可能存在的CPU(而不是仅仅是在线的CPU,以避免处理CPU热插拔带来的复杂性),并将每个CPU上的计数值累加起来,得到一个全局的总和。
  3. Sysctl接口 (dentry-state):

    • dentry-state是一个只读的、多值的sysctl接口。它不像之前的例子那样只关联一个变量,而是关联一个dentry_stat_t结构体。
    • 它使用了一个自定义的proc handler proc_nr_dentry。当用户读取/proc/sys/fs/dentry-state时,这个函数被调用。
    • proc_nr_dentry实时调用get_nr_dentry()get_nr_dentry_unused()等函数来汇总最新的统计数据,并将它们填充到dentry_stat结构体的相应字段中。
    • 最后,它调用proc_doulongvec_minmax,这个函数会将dentry_stat结构体中的多个long值格式化成一个由制表符分隔的ASCII字符串,并返回给用户。
  4. 内存回收调优 (vfs_cache_pressure):

    • 代码还在/proc/sys/vm/下注册了vfs_cache_pressure接口。这个参数是一个非常重要的内存管理调优参数。
    • 它控制着内核在面临内存压力时,回收dentry和inode缓存的“积极性”。值越高,内核越倾向于回收这些VFS缓存,为其他用途(如进程内存)释放页面;值越低,内核越倾向于保留VFS缓存以提高文件系统性能。
    • 这个参数的实现和暴露,使得管理员可以根据系统负载的特点(例如,是I/O密集型还是计算密集型)来微调内核的内存回收策略。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// 定义用于dentry统计的结构体。
struct dentry_stat_t {
long nr_dentry; // dentry总数
long nr_unused; // 未使用的dentry数
long age_limit; // 回收dentry的年龄限制(秒)
long want_pages; // 系统请求的页面数
long nr_negative; // 未使用的“否定”dentry数
long dummy; // 保留字段
};

// 为dentry总数定义一个per-cpu计数器。
static DEFINE_PER_CPU(long, nr_dentry);
// 为未使用的dentry数定义一个per-cpu计数器。
static DEFINE_PER_CPU(long, nr_dentry_unused);
// 为“否定”dentry数定义一个per-cpu计数器。
static DEFINE_PER_CPU(long, nr_dentry_negative);
// 控制是否缓存“否定”dentry的策略变量。
static int dentry_negative_policy;

// dentry统计结构体的静态实例,部分字段会被动态填充。
static struct dentry_stat_t dentry_stat = {
.age_limit = 45, // 硬编码的年龄限制值。
};

// get_nr_dentry: 汇总所有CPU上的nr_dentry计数器。
static long get_nr_dentry(void)
{
int i;
long sum = 0;
// 遍历所有可能的CPU,并将它们的计数值相加。
for_each_possible_cpu(i)
sum += per_cpu(nr_dentry, i);
return sum < 0 ? 0 : sum;
}

// get_nr_dentry_unused: 汇总所有CPU上的nr_dentry_unused计数器。
static long get_nr_dentry_unused(void)
{
int i;
long sum = 0;
for_each_possible_cpu(i)
sum += per_cpu(nr_dentry_unused, i);
return sum < 0 ? 0 : sum;
}

// get_nr_dentry_negative: 汇总所有CPU上的nr_dentry_negative计数器。
static long get_nr_dentry_negative(void)
{
int i;
long sum = 0;

for_each_possible_cpu(i)
sum += per_cpu(nr_dentry_negative, i);
return sum < 0 ? 0 : sum;
}

// proc_nr_dentry: dentry-state文件的专用proc handler。
static int proc_nr_dentry(const struct ctl_table *table, int write, void *buffer,
size_t *lenp, loff_t *ppos)
{
// 在每次读取时,实时汇总per-cpu计数器,并更新dentry_stat结构体。
dentry_stat.nr_dentry = get_nr_dentry();
dentry_stat.nr_unused = get_nr_dentry_unused();
dentry_stat.nr_negative = get_nr_dentry_negative();
// 调用标准处理函数,将整个结构体格式化为字符串并返回。
return proc_doulongvec_minmax(table, write, buffer, lenp, ppos);
}

// 定义在 /proc/sys/fs/ 目录下的dcache相关sysctl条目。
static const struct ctl_table fs_dcache_sysctls[] = {
{
.procname = "dentry-state",
.data = &dentry_stat,
.maxlen = 6*sizeof(long),
.mode = 0444, // 只读
.proc_handler = proc_nr_dentry,
},
{
.procname = "dentry-negative", // 文件名,控制是否缓存否定dentry
.data = &dentry_negative_policy,
.maxlen = sizeof(dentry_negative_policy),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ZERO, // 允许值为0
.extra2 = SYSCTL_ONE, // 或1
},
};

// 定义在 /proc/sys/vm/ 目录下的VFS缓存压力相关sysctl条目。
static const struct ctl_table vm_dcache_sysctls[] = {
{
.procname = "vfs_cache_pressure", // VFS缓存回收压力
.data = &sysctl_vfs_cache_pressure,
.maxlen = sizeof(sysctl_vfs_cache_pressure),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ZERO, // 最小值0
},
{
.procname = "vfs_cache_pressure_denom", // VFS缓存压力分母
.data = &sysctl_vfs_cache_pressure_denom,
.maxlen = sizeof(sysctl_vfs_cache_pressure_denom),
.mode = 0644,
.proc_handler = proc_dointvec_minmax,
.extra1 = SYSCTL_ONE_HUNDRED, // 最小值100
},
};

// init_fs_dcache_sysctls: 初始化函数,注册上述sysctl表。
static int __init init_fs_dcache_sysctls(void)
{
register_sysctl_init("vm", vm_dcache_sysctls);
register_sysctl_init("fs", fs_dcache_sysctls);
return 0;
}
fs_initcall(init_fs_dcache_sysctls);