[TOC]

在这里插入图片描述

list_lru: Linux内核的可扩展对象缓存管理器

list_lru 是 Linux 内核提供的一套可扩展的、近似 LRU (Least Recently Used) 缓存列表管理机制。它专门设计用来高效地管理大量、小型、生命周期不一的内核对象,例如目录项缓存(dentries)和索引节点缓存(inodes)。

可以将其想象成一个特殊的“图书馆卡片目录系统”,这个系统需要被许多图书管理员(CPU核心)同时、频繁地访问,并且需要一种高效的方式来找出那些最久未被使用的卡片(对象)以便回收。


一、 核心问题:为什么需要 list_lru

在理解 list_lru 的设计之前,必须先明白它要解决的核心问题:在多核环境下的锁竞争

一个朴素的 LRU 列表实现通常是这样的:

  1. 维护一个全局的双向链表。
  2. 当一个对象被访问时,将它从链表中的当前位置移到链表头(表示最新使用)。
  3. 当需要回收内存时,从链表尾部(表示最久未使用)开始移除对象。

这种实现在单核系统上工作得很好。但在现代多核系统中,会产生一个巨大的性能瓶颈:所有 CPU 核心都必须竞争同一个全局锁来修改这个链表。由于像 dentry 这样的对象被访问得极其频繁,这个全局锁会成为系统扩展性的噩梦,导致 CPU 核心花费大量时间在等待锁上,而不是做有用的工作。

list_lru 的诞生就是为了在保持 LRU 语义的同时,最大限度地减少锁竞争


二、 核心原理与设计:Per-CPU 列表 + 批量操作

list_lru 的天才之处在于它避免了对每个对象的每次访问都去争抢一个全局锁。它通过一个两级结构实现了这一点:

  1. Per-CPU 私有列表:

    • list_lru 不再维护一个单一的全局列表,而是在内部为**每个 CPU 核心(或每个 NUMA 节点)**都创建了一个私有的、本地的“新对象”列表。
    • 当一个对象需要被添加到 LRU 列表时(例如,一个 dentry 被创建或访问),它会被添加到当前正在执行的 CPU 核心的本地列表中。
    • 对这个本地列表的访问只需要获取该 CPU 的本地锁,甚至在某些情况下可以无锁操作。因此,不同 CPU 核心之间添加新对象的操作完全不会相互干扰
  2. 一个全局共享列表:

    • 除了 Per-CPU 列表外,还有一个传统的、由全局锁保护的共享列表。这个列表存放的是“比较旧”的对象。
  3. 批量迁移 (Batching):

    • Per-CPU 列表并不会无限增长。当某个 Per-CPU 列表中的对象数量达到一个阈值(或者在某些其他条件下),list_lru 会执行一次批量迁移操作。
    • 它会一次性地将这个 Per-CPU 列表中的所有对象作为一个整体,移动到全局共享列表的头部。
    • 这个迁移操作虽然需要获取全局锁,但它的开销被摊销了。我们用“一次昂贵的、但 infrequent 的批量操作”替换了“无数次廉价的、但 highly contended 的单次操作”。
  4. 回收/扫描:

    • 当内核需要回收内存(例如,通过 shrinker 机制)时,回收程序只需要扫描全局共享列表
    • 它会从全局列表的尾部开始,获取一批最久未使用的对象进行回收。

设计图示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
           CPU 0           CPU 1           CPU 2           CPU 3
[ Local ] [ Local ] [ Local ] [ Local ]
[ List ] [ List ] [ List ] [ List ]
| | | |
(Fast, lockless add) (Fast, lockless add) ... ...
| | | |
V V V V
+-------------------------------------------------------+
| BATCHING OPERATION |
| (Moves entire local list when full) |
+-------------------------------------------------------+
| (Acquires Global Lock)
V
+--------------------------------------------------------------------------+
| GLOBAL SHARED LIST |
| |
| <-- HEAD (Newer batches from CPUs) . . . . . . TAIL (Oldest objects) --> |
+--------------------------------------------------------------------------+
|
V (Acquires Global Lock)
PRUNING / EVICTION
(Scans from the tail)

三、 关键数据结构与 API

  • struct list_lru:

    • 这是 list_lru 的主容器结构。
    • 它包含一个全局的自旋锁 (lock) 和一个全局的链表头 (list)
    • 最重要的是,它包含一个指向 Per-CPU/Per-Node 列表数组的指针(struct list_lru_node *node[]),这使得它可以根据 CPU 或 NUMA 节点找到对应的本地列表。
  • struct list_lru_node:

    • 代表一个 Per-CPU/Per-Node 的本地存储。它内部也包含一个链表头 (list) 和一个本地锁。

核心 API:

  • int list_lru_init(struct list_lru *lru):
    初始化一个 list_lru 结构,包括分配和初始化其内部的 Per-CPU 列表。

  • void list_lru_add(struct list_lru *lru, struct list_head *item):
    最常用的函数。将一个新对象 item 添加到 LRU 中。这个操作通常只访问本地列表,因此速度非常快,竞争很小。

  • void list_lru_del(struct list_lru *lru, struct list_head *item):
    从 LRU 中删除一个对象。这个操作比 add 复杂,因为它需要判断 item 是在本地列表还是在全局列表中,并可能需要获取全局锁。

  • long list_lru_walk(struct list_lru *lru, list_lru_walk_cb isolate, void *cb_arg, long nr_to_walk):
    这是用于扫描和回收的核心函数。它会遍历全局列表,对每个对象调用一个由用户提供的回调函数 isolate。内存回收器(shrinker)就是通过这个函数来找到并尝试回收旧对象的。


四、 实际应用场景

list_lru 在内核中被广泛用于需要管理大量可回收对象的子系统。

  1. 目录项缓存 (Dentry Cache):

    • 这是 list_lru经典用例。系统中有数以万计甚至百万计的 dentry 对象。d_lru_add() 函数内部就调用了 list_lru_add()
    • 当内存不足时,VFS shrinker 会调用 list_lru_walk 来扫描 dentry LRU,并释放那些未被使用的 dentry。
  2. 索引节点缓存 (Inode Cache):

    • 与 dentry 类似,inode_add_lru() 也是基于 list_lru 实现的。
    • 内存回收时,也会扫描 inode LRU 来回收不活跃的 inode 对象。
  3. 其他: 某些文件系统也可能使用 list_lru 来管理其私有的元数据缓存。


五、 总结

list_lru 是 Linux 内核为了应对多核时代高并发、高竞争环境而设计出的一套精妙的缓存管理方案。

核心思想:

  • 空间换时间: 使用多个 Per-CPU 列表(空间)来避免全局锁竞争(时间)。
  • 批处理摊销: 将高频的、有竞争的单次操作,替换为低频的、无竞争的本地操作,外加一次性的批量同步操作,从而将锁竞争的开销降至最低。

通过这种设计,list_lru 成功地为 dentry 和 inode 等内核中最活跃的缓存对象提供了高度可扩展的近似 LRU 管理,是保证 Linux 在现代多核服务器上依然能保持高性能的关键基础设施之一。

include/linux/list_lru.h

LRU 链表初始化宏: list_lru_init, list_lru_init_memcg, list_lru_init_memcg_key

本代码片段定义了一组用于初始化 list_lru 数据结构的宏和内联函数。其核心功能是为内核中需要使用 “最近最少使用”(LRU)策略管理的对象列表提供标准化的初始化接口。它提供了三种变体:一个通用的初始化宏、一个支持内存控制组(memcg)感知的版本,以及一个为锁依赖分析器(lockdep)提供额外调试信息的版本。

实现原理分析

此代码是典型的 C 语言宏封装和条件编译技巧的应用,旨在提供一个清晰、分层的 API,同时隔离复杂的配置选项。

  1. 统一后端 (__list_lru_init): 两个主要的初始化宏 list_lru_initlist_lru_init_memcg 都是对一个内部函数 __list_lru_init 的简单封装。这种模式将公共逻辑集中在后端函数中,而前端宏则根据不同的使用场景提供固定的参数(falsetrue)。

    • list_lru_init 传入 false,表示这是一个全局的、不区分 cgroup 的 LRU 链表。
    • list_lru_init_memcg 传入 true,表示这是一个 memcg 感知的 LRU 链表。这种链表能够为每个内存控制组维护独立的 LRU 列表,从而实现更精细的内存回收策略。它还必须与一个 shrinker 关联,以便在 cgroup 内存压力下被回调。
  2. 锁调试支持 (list_lru_init_memcg_key):

    • 这是一个内联函数,它进一步封装了 list_lru_init_memcg
    • 其唯一目的是在内核启用了锁依赖分析器(CONFIG_LOCKDEP)时,允许调用者为 list_lru 内部的自旋锁指定一个 lock_class_keylockdep 是一个强大的内核调试工具,它通过给不同上下文中的锁分配唯一的“类”来静态地分析和检测潜在的死锁风险。
    • 通过 #ifdef CONFIG_LOCKDEP 条件编译,这段代码在未开启 lockdep 的生产内核中不会产生任何额外的开销,lru->key = key; 这一行代码会直接被预处理器移除。

代码分析

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
/**
* @brief list_lru_init - 初始化一个标准的、非 cgroup 感知的 LRU 链表。
* @param lru: 指向要被初始化的 list_lru 结构体的指针。
* @note 这是一个宏,它调用 __list_lru_init 并将 memcg_aware 参数设为 false。
*/
#define list_lru_init(lru) \
__list_lru_init((lru), false, NULL)

/**
* @brief list_lru_init_memcg - 初始化一个 cgroup 感知的 LRU 链表。
* @param lru: 指向要被初始化的 list_lru 结构体的指针。
* @param shrinker: 指向与此 LRU 链表关联的 shrinker 实例的指针。
* @note 这是一个宏,它调用 __list_lru_init 并将 memcg_aware 参数设为 true。
*/
#define list_lru_init_memcg(lru, shrinker) \
__list_lru_init((lru), true, shrinker)

/**
* @brief list_lru_init_memcg_key - 初始化一个 cgroup 感知的 LRU 链表,并为其内部锁设置 lockdep key。
* @param lru: 指向要被初始化的 list_lru 结构体的指针。
* @param shrinker: 指向与此 LRU 链表关联的 shrinker 实例的指针。
* @param key: 指向 lock_class_key 结构体的指针,用于锁依赖性验证。
* @return int: 返回 __list_lru_init 的结果,通常为0表示成功。
*/
static inline int list_lru_init_memcg_key(struct list_lru *lru, struct shrinker *shrinker,
struct lock_class_key *key)
{
// 仅在内核配置了 CONFIG_LOCKDEP 调试选项时,以下代码才会被编译。
#ifdef CONFIG_LOCKDEP
// 将外部传入的 lock_class_key 赋值给 LRU 结构体内部的 key 成员,
// 以便 lockdep 工具能够识别和跟踪此 LRU 内部锁的类别。
lru->key = key;
#endif
// 调用 memcg 感知版本的初始化宏来完成剩余的初始化工作。
return list_lru_init_memcg(lru, shrinker);
}

mm/list_lru.c

list_lru_add 将对象(item)添加到 LRU(最近最少使用)列表中

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
/* 调用方必须确保 memcg 生存期。 */
bool list_lru_add(struct list_lru *lru, struct list_head *item, int nid,
struct mem_cgroup *memcg)
{
struct list_lru_node *nlru = &lru->node[nid];
struct list_lru_one *l;

/* 锁定与内存控制组(memcg)关联的 LRU 列表 */
l = lock_list_lru_of_memcg(lru, nid, memcg, false, false);
/* 如果无法锁定(例如 memcg 生命周期已结束),函数返回 false,表示添加失败 */
if (!l)
return false;
/* 如果对象为空,表示它可以安全地添加到 LRU 列表中 */
if (list_empty(item)) {
/* 将对象添加到 LRU 列表的尾部 */
list_add_tail(item, &l->list);
/* 如果添加了第一个元素,则设置 shrinker bit */
if (!l->nr_items++)
/* Shrinker 位用于标记 LRU 列表需要被回收器(Shrinker)处理 */
set_shrinker_bit(memcg, nid, lru_shrinker_id(lru));
unlock_list_lru(l, false);
atomic_long_inc(&nlru->nr_items);
return true;
}
unlock_list_lru(l, false);
return false;
}

list_lru_add_obj 将对象(item)添加到 LRU(最近最少使用)列表中

  • lRU 是一种缓存管理机制,优先保留最近使用的对象并逐步移除较少使用的对象。该函数支持内存控制组(Memory Control Groups,memcg)的感知功能,以优化内存管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool list_lru_add_obj(struct list_lru *lru, struct list_head *item)
{
bool ret;
int nid = page_to_nid(virt_to_page(item));

/* return false; */
if (list_lru_memcg_aware(lru)) {
rcu_read_lock();
ret = list_lru_add(lru, item, nid, mem_cgroup_from_slab_obj(item));
rcu_read_unlock();
} else {
ret = list_lru_add(lru, item, nid, NULL);
}

return ret;
}
EXPORT_SYMBOL_GPL(list_lru_add_obj);

list_lru_del 从 LRU(最近最少使用)列表中删除指定的对象(item)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 调用方必须确保 memcg 生存期。 */
bool list_lru_del(struct list_lru *lru, struct list_head *item, int nid,
struct mem_cgroup *memcg)
{
struct list_lru_node *nlru = &lru->node[nid];
struct list_lru_one *l;
l = lock_list_lru_of_memcg(lru, nid, memcg, false, false);
if (!l)
return false;
if (!list_empty(item)) {
list_del_init(item);
l->nr_items--;
unlock_list_lru(l, false);
atomic_long_dec(&nlru->nr_items);
return true;
}
unlock_list_lru(l, false);
return false;
}

list_lru_del_obj 从 LRU(最近最少使用)列表中删除指定的对象(item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool list_lru_del_obj(struct list_lru *lru, struct list_head *item)
{
bool ret;
int nid = page_to_nid(virt_to_page(item));

if (list_lru_memcg_aware(lru)) {
rcu_read_lock();
ret = list_lru_del(lru, item, nid, mem_cgroup_from_slab_obj(item));
rcu_read_unlock();
} else {
ret = list_lru_del(lru, item, nid, NULL);
}

return ret;
}
EXPORT_SYMBOL_GPL(list_lru_del_obj);

init_one_lru 初始化一个 list_lru_one 结构

1
2
3
4
5
6
7
8
9
10
static void init_one_lru(struct list_lru *lru, struct list_lru_one *l)
{
INIT_LIST_HEAD(&l->list);
spin_lock_init(&l->lock);
l->nr_items = 0;
#ifdef CONFIG_LOCKDEP
if (lru->key)
lockdep_set_class(&l->lock, lru->key);
#endif
}

__list_lru_init 初始化一个 list_lru 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int __list_lru_init(struct list_lru *lru, bool memcg_aware, struct shrinker *shrinker)
{
int i;

lru->node = kcalloc(nr_node_ids, sizeof(*lru->node), GFP_KERNEL);
if (!lru->node)
return -ENOMEM;

for_each_node(i)
init_one_lru(lru, &lru->node[i].lru);

// memcg_init_list_lru(lru, memcg_aware);
// list_lru_register(lru);

return 0;
}
EXPORT_SYMBOL_GPL(__list_lru_init);

LRU 链表遍历与收缩: list_lru_shrink_walk_irq 及 __list_lru_walk_one

本代码片段是 Linux 内核通用 LRU 链表子系统的核心执行引擎。其主要功能是提供一个通用的、可重入的机制,用于遍历一个 LRU 链表,并对链表中的每个元素执行一个由调用者提供的回调函数(isolate),以尝试隔离(即选中并准备回收)指定数量的对象。该代码通过一个复杂的、支持重试的状态机,优雅地处理了在内核中普遍存在的复杂锁依赖问题。

实现原理分析

此代码的实现精髓在于其对回调函数返回状态的处理,以及由此构建的一个健壮的、可中断并可重启的遍历机制。

  1. 通用回调接口: __list_lru_walk_one 函数本身不包含任何针对特定对象的处理逻辑。它通过接受一个 list_lru_walk_cb 类型的函数指针 isolate,将决定如何处理链表元素的权力交给了调用者。这使得该遍历框架可以被用于管理任何类型的内核对象(如 dentry、inode 等)。

  2. 可重启的状态机: isolate 回调函数的返回值 enum lru_status 是整个机制的核心。__list_lru_walk_one 函数内部的 switch 语句根据这个返回值来驱动一个状态机:

    • LRU_REMOVED: 表示回调成功隔离了对象。遍历继续。
    • LRU_ROTATE: 表示对象暂时不回收,但要更新其“最近使用”状态,因此将其移动到链表尾部。遍历继续。
    • LRU_RETRY / LRU_REMOVED_RETRY: 这是最关键的两种状态。它们表示回调函数为了执行隔离操作,不得不临时释放了 LRU 链表的锁(通常是为了获取另一个顺序不同的锁,以避免死锁)。一旦锁被释放,当前链表的遍历状态就失效了(可能有其他任务修改了链表)。因此,__list_lru_walk_one 必须放弃当前遍历,使用 goto restart 从链表头部重新开始。这种“乐观遍历-失败重启”的模式是处理复杂并发操作的有效技巧。
  3. 死锁规避与活性保证:

    • 在内存回收(shrinker)的上下文中,isolate 回调可能需要获取文件系统或特定对象的锁,而这些锁的获取顺序可能与 LRU 锁冲突。通过 LRU_RETRY 机制,允许回调函数释放 LRU 锁,尝试获取其他锁,从而化解了潜在的死锁风险。
    • 为了防止因反复遇到需要 LRU_RETRY 的对象而导致的活锁(livelock),代码在调用 isolate 回调之前就递减了计数器 *nr_to_walk。这确保了即使遍历过程不断重启,总的尝试次数也是有限的,循环最终必然会终止。
  4. 上下文抽象: 代码通过 irq_off 布尔参数和两个不同的封装函数 (list_lru_walk_onelist_lru_walk_one_irq),将核心遍历逻辑与具体的执行上下文(是否在中断关闭的环境下)解耦,提高了代码的复用性。

代码分析

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
/**
* @brief list_lru_shrink_walk_irq - 在中断关闭的 shrinker 上下文中遍历并收缩 LRU 链表。
* @param lru: 指向目标 list_lru 结构体的指针。
* @param sc: 指向 shrink_control 结构体的指针,包含了收缩所需的所有参数。
* @param isolate: 处理链表元素的毁掉函数。
* @param cb_arg: 传递给回调函数的私有数据。
* @return unsigned long: 成功隔离的对象数量。
* @note 这是一个内联封装函数,它从 shrink_control 中提取参数并调用 IRQ 安全的遍历函数。
*/
static inline unsigned long
list_lru_shrink_walk_irq(struct list_lru *lru, struct shrink_control *sc,
list_lru_walk_cb isolate, void *cb_arg)
{
// 调用 IRQ 安全的遍历函数,传入 NUMA 节点ID、memcg、回调函数和待扫描数量。
return list_lru_walk_one_irq(lru, sc->nid, sc->memcg, isolate, cb_arg,
&sc->nr_to_scan);
}

/**
* @brief __list_lru_walk_one - 遍历 LRU 链表的核心实现。
* @param lru: 指向目标 list_lru 结构体的指针。
* @param nid: 目标 NUMA 节点的 ID。
* @param memcg: 目标内存控制组的指针,可为 NULL。
* @param isolate: 处理链表元素的回调函数。
* @param cb_arg: 传递给回调函数的私有数据。
* @param nr_to_walk: 一个指针,指向需要遍历的对象数量,此函数会修改其值。
* @param irq_off: 布尔值,为 true 表示应使用 IRQ 安全的锁。
* @return unsigned long: 成功隔离的对象数量。
*/
static unsigned long
__list_lru_walk_one(struct list_lru *lru, int nid, struct mem_cgroup *memcg,
list_lru_walk_cb isolate, void *cb_arg,
unsigned long *nr_to_walk, bool irq_off)
{
// 获取指定 NUMA 节点的 LRU 链表管理结构。
struct list_lru_node *nlru = &lru->node[nid];
struct list_lru_one *l = NULL; /// < 指向具体的 per-memcg 或全局 LRU 链表。
struct list_head *item, *n; /// < 用于安全遍历链表的指针。
unsigned long isolated = 0; /// < 已成功隔离的对象的计数器。

restart:
// 根据 memcg 获取并锁定正确的 LRU 链表。
// irq_off 参数决定是使用 spin_lock 还是 spin_lock_irq。
l = lock_list_lru_of_memcg(lru, nid, memcg, irq_off, true);
// 如果无法获取链表 (例如 memcg 正在被删除),则直接返回。
if (!l)
return isolated;

// 安全地遍历链表,'n' 用于在 'item' 被删除后指向下一个元素。
list_for_each_safe(item, n, &l->list) {
enum lru_status ret;

// 如果已经完成了指定数量的扫描,则退出循环。
if (!*nr_to_walk)
break;
// 在调用回调前先递减计数器,以防止因反复 LRU_RETRY 而导致的活锁。
--*nr_to_walk;

// 调用外部传入的回调函数来处理当前元素。
ret = isolate(item, l, cb_arg);
switch (ret) {
// 如果回调函数释放了锁并要求重试,则必须从头开始遍历。
case LRU_RETRY:
goto restart;
// 如果回调移除了元素、释放了锁并要求重试,则...
case LRU_REMOVED_RETRY:
// fallthrough 关键字明确表示此处是故意穿透到下一个 case。
fallthrough;
// 如果回调成功移除了元素。
case LRU_REMOVED:
isolated++; // 增加成功计数。
atomic_long_dec(&nlru->nr_items); // 原子地减少总数计数。
// 如果是 REMOVED_RETRY,则跳转到 restart。
if (ret == LRU_REMOVED_RETRY)
goto restart;
break;
// 如果回调要求将元素轮转到链表尾部(表示它最近被使用过)。
case LRU_ROTATE:
list_move_tail(item, &l->list);
break;
// 如果回调要求跳过此元素。
case LRU_SKIP:
break;
// 如果回调要求完全停止遍历。
case LRU_STOP:
goto out;
default:
// 不应该出现任何其他返回值。
BUG();
}
}
// 遍历结束,解锁。
unlock_list_lru(l, irq_off);
out:
// 返回成功隔离的元素总数。
return isolated;
}

/**
* @brief list_lru_walk_one - 遍历 LRU 链表的标准接口。
* @note 这是一个封装函数,它调用核心实现并明确指定 irq_off 为 false。
*/
unsigned long
list_lru_walk_one(struct list_lru *lru, int nid, struct mem_cgroup *memcg,
list_lru_walk_cb isolate, void *cb_arg,
unsigned long *nr_to_walk)
{
return __list_lru_walk_one(lru, nid, memcg, isolate,
cb_arg, nr_to_walk, false);
}
// 将此函数导出,以便其他内核模块可以使用。
EXPORT_SYMBOL_GPL(list_lru_walk_one);

/**
* @brief list_lru_walk_one_irq - 遍历 LRU 链表的 IRQ 安全接口。
* @note 这是一个封装函数,它调用核心实现并明确指定 irq_off 为 true。
*/
unsigned long
list_lru_walk_one_irq(struct list_lru *lru, int nid, struct mem_cgroup *memcg,
list_lru_walk_cb isolate, void *cb_arg,
unsigned long *nr_to_walk)
{
return __list_lru_walk_one(lru, nid, memcg, isolate,
cb_arg, nr_to_walk, true);
}

list_lru_isolate 从一个 list_lru_one 链表中原子地移除一个指定的链表元素,并更新该链表的内部计数器

本代码片段提供了一个基础的辅助函数,其核心功能是从一个 list_lru_one 链表中原子地移除一个指定的链表元素,并更新该链表的内部计数器。这是一个在 list_lru 框架中被频繁调用的底层构建块,它本身不包含复杂的逻辑,而是依赖于调用者确保其执行的原子性。

实现原理分析

该函数是 list_lru 子系统中的一个基本操作单元。其设计的核心原则是职责分离

  1. 操作的原子性: 此函数本身并不实现任何锁机制。它假定调用者在调用它之前,已经获取了保护 list 参数所指向链表的自旋锁。在 list_lru 框架中,调用 list_lru_isolate 的代码路径(例如 shadow_lru_isolate)总是已经持有了 lru->lock。这种设计避免了在每个底层函数中重复加锁/解锁的开销,并将同步策略的控制权留给了更高层的逻辑。

  2. 安全的链表删除: 它使用 list_del_init 而不是 list_del。这是一个重要的细节。list_del 仅仅将元素从链表中移除,但元素自身的 prevnext 指针仍然指向旧的邻居。如果此时对该元素再次进行链表操作,可能会破坏原始链表。而 list_del_init 在将元素移除后,会立即将其 prevnext 指针指向它自身,将其重新初始化为一个空链表的头部。这是一种防御性编程,确保了被隔离的元素处于一个已知的、安全的状态,防止了后续的误用。

  3. 元数据同步: 它在移除链表节点的同时,同步递减了 list->nr_items 计数器。这保证了链表的元数据(即缓存的元素数量)与其物理状态(链表中的节点数)始终保持一致。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @brief list_lru_isolate - 从一个 LRU 链表中隔离一个元素。
* @param list: 指向 list_lru_one 结构体的指针,代表一个具体的 LRU 链表。
* @param item: 指向要从链表中移除的元素的 list_head 成员。
* @note 此函数不是线程安全的,调用者必须持有保护该链表的锁。
*/
void list_lru_isolate(struct list_lru_one *list, struct list_head *item)
{
// 将 item 元素从其所在的链表中删除,并将其自身初始化为一个空的链表头。
list_del_init(item);
// 将该 LRU 链表的元素计数器减一。
list->nr_items--;
}
// 将 list_lru_isolate 函数导出,使其可用于其他 GPL 许可的内核模块。
EXPORT_SYMBOL_GPL(list_lru_isolate);

LRU 链表计数: list_lru_shrink_count 及 list_lru_count_one

本代码片段提供了用于获取 list_lru 实例中元素数量的函数。其核心功能是安全地、无锁地读取一个特定 NUMA 节点和特定内存控制组(memcg)所对应的 LRU 链表中的当前对象总数。它通过 RCU (Read-Copy-Update) 机制来保证读取操作的安全性,避免了与修改链表的操作(如添加、删除元素)产生锁竞争。

实现原理分析

此计数机制的关键在于它如何在不使用传统自旋锁的情况下,安全地访问可能被并发修改的计数器。

  1. RCU 保护: 整个读取过程被 rcu_read_lock()rcu_read_unlock() 包围。这是一个经典的 RCU 应用场景。当一个任务正在读取计数器 l->nr_items 时,另一个任务(或中断)可能正在修改这个 LRU 链表,甚至在 cgroup 被销毁时,可能会释放并重新分配 list_lru_one 结构体。RCU 机制保证了在 rcu_read_lock 临界区内,l 指针所指向的内存块不会被释放。读取方可以安全地访问,而修改方在修改完成后,会等待所有现存的 RCU 读临界区全部结束后,才执行最终的内存释放操作。这实现了读取方的“无锁”访问,极大地提高了并发性能。

  2. 安全读取 (READ_ONCE): 直接访问 l->nr_items 可能会被编译器优化(例如,将值缓存到寄存器中),或者在某些CPU架构上,对非原生字长的数据读取可能会被分步执行,导致“撕裂读”。READ_ONCE() 宏解决了这两个问题。它作为一个内存屏障,强制编译器每次都从内存中重新加载该值,并保证了读取操作的原子性,确保读取到的是一个完整、有效的值。

  3. 索引与抽象:

    • list_lru_shrink_count 是一个顶层内联函数,作为 shrinker 框架和 list_lru 子系统之间的适配器,它从 shrink_control 结构中提取 nidmemcg 参数。
    • list_lru_from_memcg_idx 负责将 NUMA 节点 ID 和 memcg ID 映射到具体的 list_lru_one 结构体。在此代码片段的实现中,它直接返回 &lru->node[nid].lru,忽略了 memcg 索引。这表明该实现是针对未启用 CONFIG_MEMCG 的内核配置,或者是一个 memcg 感知 LRU 链表的“根”或“默认”列表。
  4. 健壮性处理: unlikely(count < 0) 检查是一个防御性措施。正常情况下,计数器不应为负。如果出现负值,可能表示一个瞬态的竞态条件或是一个程序错误。此时,函数选择返回 0,这是一个安全的默认值,避免了将无效的负值传播到上层调用者。

代码分析

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
/**
* @brief list_lru_shrink_count - 获取 shrinker 上下文中的 LRU 链表大小。
* @param lru: 指向目标 list_lru 结构体的指针。
* @param sc: 指向 shrink_control 结构体的指针,包含了 NUMA 节点和 memcg 信息。
* @return unsigned long: LRU 链表中的元素数量。
* @note 这是一个内联函数,用作 shrinker 框架的适配层。
*/
static inline unsigned long list_lru_shrink_count(struct list_lru *lru,
struct shrink_control *sc)
{
// 从 shrink_control 中提取 nid 和 memcg,并调用底层的计数函数。
return list_lru_count_one(lru, sc->nid, sc->memcg);
}

/**
* @brief list_lru_from_memcg_idx - 根据 NUMA 节点和 memcg 索引获取具体的 LRU 链表。
* @param lru: 指向 list_lru 结构体的指针。
* @param nid: NUMA 节点的 ID。
* @param idx: memcg 的 ID。
* @return struct list_lru_one*: 指向具体的 LRU 链表实例。
* @note 在此实现中,它忽略了 idx,总是返回该 NUMA 节点的根 LRU 链表。
* 这适用于未启用 CONFIG_MEMCG 的情况。
*/
static inline struct list_lru_one *
list_lru_from_memcg_idx(struct list_lru *lru, int nid, int idx)
{
// 返回指定 NUMA 节点 lru->node[nid] 内的 lru 成员。
return &lru->node[nid].lru;
}

/**
* @brief list_lru_count_one - 安全地获取指定 LRU 链表的元素数量。
* @param lru: 指向 list_lru 结构体的指针。
* @param nid: 目标 NUMA 节点的 ID。
* @param memcg: 目标内存控制组的指针,可为 NULL。
* @return unsigned long: LRU 链表中的元素数量。
*/
unsigned long list_lru_count_one(struct list_lru *lru,
int nid, struct mem_cgroup *memcg)
{
struct list_lru_one *l; /// < 指向具体的 LRU 链表实例。
long count; /// < 用于存储读取到的计数值。

// 进入 RCU 读侧临界区,保护期间对 l 指针的访问。
rcu_read_lock();
// 根据 nid 和 memcg 获取对应的 LRU 链表实例。
l = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(memcg));
// 如果链表实例存在,则原子地、安全地读取其 nr_items 成员;否则为0。
count = l ? READ_ONCE(l->nr_items) : 0;
// 退出 RCU 读侧临界区。
rcu_read_unlock();

// 这是一个健壮性检查。如果计数值因某种原因变为负数,则将其修正为0。
if (unlikely(count < 0))
count = 0;

return count;
}
// 将此函数导出,以便其他内核模块可以使用。
EXPORT_SYMBOL_GPL(list_lru_count_one);