SLUB 的per-NUMA-node 结构struct kmem_cache_node,每个缓存对每个 NUMA 节点都有一份。它管理该节点上所有 slab 页的全局列表,是快速路径耗尽后的慢路径后端。
一、整体定位
SLUB 每个缓存有三层结构:
struct kmem_cache ├─ struct kmem_cache_cpu (per-CPU) ← 快速路径 └─ struct kmem_cache_node (per-NUMA-node) ← 慢路径 / 全局列表
kmem_cache_cpu:本 CPU 的私有 freelist 和当前 slab,无锁快路径。kmem_cache_node:本 NUMA 节点上该缓存的所有 slab 页,用锁保护,供慢路径取用。
当 CPU 的 freelist 用完、当前 slab 也空了,就到这里来拿新的 partial slab;释放时若对象不属于当前 CPU slab,也会落到这里。
二、字段逐个解析
1.spinlock_t list_lock;
spinlock_t list_lock;
保护这个 node 结构里所有列表和计数的锁。
慢路径(
get_partial、__slab_free、deactivate、shrink)都要拿它。与
kmem_cache_cpu->lock的区别:cpu->lock是 per-CPU 本地锁,开销极小。node->list_lock是真正的跨 CPU 竞争锁,多个 CPU 可能同时访问同一个 node。
快速路径不碰它,所以不产生竞争;一旦进入慢路径就要付加锁代价。这也是 SLUB 统计里
ALLOC_SLOWPATH、FREE_SLOWPATH关注的成本来源。
2.unsigned long nr_partial;
unsigned long nr_partial;
记录当前 node 上partial slab 的数量(即
partial列表长度)。用于和阈值比较:
MIN_PARTIAL:少于这个数时,空的 slab 也保留在 partial 列表,不立即归还伙伴系统。MAX_PARTIAL:超过这个数时,kmem_cache_shrink()会按inuse排序并回收空闲 slab。
判断逻辑大致:
if (n->nr_partial < MIN_PARTIAL) // 保留空 slab if (n->nr_partial > MAX_PARTIAL) // 触发排序/收缩
3.struct list_head partial;
struct list_head partial;
部分空闲 slab 的链表头(链表节点是
struct slab的slab_list字段)。“partial” = 该 slab 中既有已分配对象,也有空闲对象。
分配时慢路径从这里取一个有空闲对象的 slab 补充给 CPU:
___slab_alloc → get_partial → 从 n->partial 取 slab → 设为 c->slab
释放时,若某个 slab 由满变部分空闲,可能被加回这个列表。
这是 SLUB 慢路径最核心的列表。
三、调试专用字段
#ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full; #endif
这三个只在编译CONFIG_SLUB_DEBUG时存在,用于调试、统计和一致性检查。
4.atomic_long_t nr_slabs;
当前 node 上该缓存的 slab 页总数。
为什么用
atomic_long_t而不是普通unsigned long:虽然
list_lock保护列表操作,但这个计数可能被不加锁的路径读取或更新(如统计输出、校验、per-node 汇总),需要原子性。atomic_long_t保证读写原子,避免锁外读取到撕裂值。
用途:
/sys/kernel/slab/<cache>/slabs、一致性校验、内存统计。
5.atomic_long_t total_objects;
当前 node 上该缓存的对象总数(
nr_slabs × objects_per_slab,大致)。用途:
计算使用率、空闲率。
kmem_cache_shrink判断回收价值。sysfs/debugfs 统计输出。
同样用原子类型,理由与
nr_slabs相同。
6.struct list_head full;
完全占满的 slab 链表头(所有对象都已分配,无空闲对象)。
为什么调试时才需要:
正常运行时,full slab 不需要被慢路径扫描——它们没有空闲对象可用,留在 partial 列表没意义,可以完全不追踪,节省内存和操作。
调试时,为了能完整校验和统计所有 slab(检查红区、poison、一致性、统计对象总数),必须把所有 slab 都挂起来,包括 full 的。所以单独维护一个
full列表。典型用途:
validate_slab_cache()、/sys/kernel/slab/<cache>/下的详细统计。
这个字段很好地体现了 SLUB 的取舍:正常路径只维护“有用的”数据结构,调试时才维护完整视图,以降低运行时开销。
四、partial与full的关系
| 列表 | 何时使用 | 内容 |
|---|---|---|
partial | 正常 + 调试 | 部分空闲的 slab,可被分配复用 |
full | 仅调试 | 全满的 slab,正常时不追踪 |
| (无列表) | 正常 | 全空的 slab 可能直接被归还伙伴系统 |
一个 slab 在生命周期里可能:
新分配 → (满) → 释放一个对象 → partial → 全部释放 → free → 归还伙伴系统
正常运行时 SLUB 主要关心partial;full只在调试时需要完整枚举。
五、与阈值宏的呼应
#define MIN_PARTIAL 5 #define MAX_PARTIAL 10
就是作用在nr_partial上的:
// 释放路径中,slab 变空时 if (n->nr_partial >= MIN_PARTIAL) discard_slab(s, slab); // 归还伙伴系统 else add_partial(n, slab); // 保留在 partial 列表 // shrink 时 if (n->nr_partial > MAX_PARTIAL) // 按 inuse 排序,回收空闲 slab
六、在慢路径中的角色
分配慢路径
slab_alloc_node 快速路径失败 └─ ___slab_alloc └─ get_partial(s, node, ...) ├─ 遍历 n->partial 找有空闲对象的 slab └─ 找不到 → new_slab() → 伙伴系统 └─ 成功后加到 n->partial 或直接设为 CPU slab
释放慢路径
slab_free 快速路径失败(对象不属于当前 CPU slab) └─ __slab_free ├─ 加 n->list_lock ├─ 对象放回 slab freelist ├─ slab 由满变 partial → add_partial ├─ slab 全空 → 可能 discard_slab 归还伙伴系统 └─ slab 由 partial 变满 → (调试时) 加入 full
跨节点
分配时若指定 node,SLUB 优先用该 node 的kmem_cache_node;ALLOC_NODE_MISMATCH统计的就是请求节点与实际 slab 节点不一致、需要切换的情况。
七、与 per-CPU 结构的分工
| 维度 | kmem_cache_cpu | kmem_cache_node |
|---|---|---|
| 粒度 | per-CPU | per-NUMA-node |
| 锁 | local_lock(本地) | spinlock_t list_lock(跨 CPU) |
| 数据 | freelist、当前 slab、CPU partial | partial 列表、full 列表(调试)、计数 |
| 路径 | 快速路径 | 慢路径 |
| 目标 | 零锁、低延迟 | 全局池、跨 CPU 复用、回收 |
八、总结
struct kmem_cache_node是 SLUB 的per-NUMA-node 全局池:list_lock保护慢路径并发,nr_partial+partial维护部分空闲 slab 列表供分配复用,并在超过MAX_PARTIAL时触发收缩;调试配置下额外用nr_slabs、total_objects、full维护完整视图以支持校验和统计。它是快速路径耗尽后的后端,也是跨 CPU 复用和内存回收的关键。