引言

Linux 内核的内存管理是系统性能的核心支柱之一。在物理内存分配层面,伙伴系统(Buddy System)以页(通常为 4KB)为粒度管理内存,但实际应用中,内核经常需要分配远小于一页的对象——如 task_struct(约 1.7KB)、inode、dentry 等。如果直接用页级分配器来处理这些小型对象,将造成严重内存碎片。Slab 分配器应运而生,它作为伙伴系统之上的二级分配器,专门解决内核中小对象的分配与释放问题。

1. Slab(Slab Allocator)的演进历程

1.1 经典 Slab(Linux 2.1.x)

1994 年,Jeff Bonwick 在 Solaris 2.4 中首次提出了 Slab 分配器的设计思想。Linux 在 2.1.x 版本中引入了原版 Slab 分配器,其核心思想是对象缓存(Object Cache)——预先分配并初始化好对象实例,下次分配时直接取出使用,避免重复的构造/析构开销。

1.2 SLUB(Unqueued Slab)—— 现代默认分配器

SLUB(Simple Linear Mapping Unqueued Allocator)由 Christoph Lameter 在 Linux 2.6.23 中引入,现在是大多数 Linux 发行版的默认分配器。SLUB 简化了 SLAB 的队列管理,每个 CPU 维护独立的 slab 列表(kmem_cache_cpu),消除了多处理器环境下的队列锁争用,大幅提升了 SMP 系统的扩展性。

1.3 SLOB(Simple List Of Blocks)—— 嵌入式优化

SLOB 专为内存极度受限的嵌入式系统设计。它使用简单的首次适应(first-fit)算法管理内存块,代码量极小(约 2000 行),但外部碎片严重。适合资源受限的 IoT 设备和嵌入式 Linux 平台。

1.4 选择策略

分配器适用场景特点
SLUB通用服务器、桌面默认选择,性能最佳
SLAB调试场景提供更丰富的调试信息
SLOB嵌入式设备最小代码占用,内存碎片多

2. SLUB 核心数据结构

2.1 kmem_cache

struct kmem_cache 是 SLUB 最核心的数据结构,代表一个"对象缓存"。每个内核子系统在初始化时会创建自己的缓存,例如 task_struct_cachep 用于进程描述符,inode_cachep 用于索引节点。

struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;       // 每CPU的slab页
    unsigned long flags;                     // 标志位
    unsigned int size;                       // 对象实际大小
    unsigned int object_size;                // 用户请求的原始大小
    unsigned int offset;                     // 下一个空闲对象的偏移
    struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA节点管理
    struct kmem_cache_order_objects oo;     // 每页容纳的对象数
    ...
};

2.2 kmem_cache_cpu(每 CPU 热路径)

这是 SLUB 性能优化的关键。每个 CPU 维护自己的 kmem_cache_cpu 结构,分配时直接从 CPU 本地 slab 页取空闲对象,完全不需要加锁。

struct kmem_cache_cpu {
    void **freelist;    // 空闲对象链表头
    unsigned long tid;  // 事务ID,检测并发
    struct page *page;  // 当前活跃slab页
    struct page *partial; // 部分空闲的slab页链表
};

2.3 kmem_cache_node(NUMA 节点级)

当 CPU 本地 slab 页没有空闲对象时,SLUB 会从 NUMA 节点的 partial 链表中获取部分空闲的 slab 页。kmem_cache_node 使用 spinlock 保护,是二级备用路径。

struct kmem_cache_node {
    spinlock_t list_lock;
    unsigned long nr_partial;
    struct list_head partial;  // 部分空闲slab页的链表
    ...
};

3. SLUB 分配与释放流程

3.1 快速分配路径(Hot Path)

SLUB 的分配优化遵循一个原则:尽可能在 CPU 本地无锁完成。快速路径的完整流程:

  1. 检查 kmem_cache_cpu->freelist 是否非空
  2. 若非空,从中取出第一个空闲对象(指针解链),直接返回
  3. 更新 freelist 指针,原子操作完成
  4. 整个过程无锁、无中断禁用,性能极高

3.2 慢速分配路径

当快速路径失败时进入慢速路径:

  1. 检查 kmem_cache_cpu->partial(CPU 本地的部分空闲 slab 页)
  2. 检查 kmem_cache_node->partial(NUMA 节点的部分空闲 slab 页),需要获取 spinlock
  3. 如果仍无可用对象,向伙伴系统申请新的 slab 页(alloc_pages())
  4. 将新页加入 CPU 本地 slab 管理

3.3 释放流程

释放时同样优先操作 CPU 本地:

  1. 将对象链入 freelist(头插法)
  2. 检查 slab 页是否变为"全空闲"状态
  3. 若是,将页从 CPU 本地移除,归还伙伴系统(减少内存占用)
  4. 若变为"部分空闲",链接入 partial 链表

4. 内存碎片对抗策略

4.1 Slab 着色(Slab Coloring)

"着色"是一种减少缓存冲突的精妙技巧。Slab 创建时,在不同 slab 页之间加入随机偏移量(color offset),改变了对象在缓存行中的位置。这避免了多个 slab 中相同偏移的对象映射到同一缓存行,减少缓存抖动(cache thrashing)。

着色范围由 colour_off(通常为处理器缓存行大小,即 64 字节)和总着色空间(colour)决定,每个 slab 页的偏移为 colour_off * n(n = 0, 1, 2, ... colour-1)。

4.2 对象对齐与大小计算

SLUB 根据 kmalloc 请求的大小自动选择最合适的 slab 缓存。对于 kmalloc 系列 API,内核维护了一组预创建的"通用"缓存,覆盖从 8B 到 8KB(通常以 2 的幂次递增)的常用大小。当请求大小不精确匹配时,向上取整到最近的可用大小——例如请求 600 字节会使用 1024 字节的缓存。

5. 调试与诊断工具

5.1 slabinfo 与 /proc/slabinfo

/proc/slabinfo 提供了全局 slab 分配器状态的概览输出,包含每个缓存的名称、活跃对象数、总对象数、每个对象的大小、页数等信息。

$ cat /proc/slabinfo | head -20
slabinfo - version: 2.1
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-1024        1440     1440     1024      4    1 : tunables    0    0    0 : slabdata    360    360
task_struct          380      416     1664      2    1 : tunables    0    0    0 : slabdata    208    208
inode_cache          956     1050      384      8    1 : tunables    0    0    0 : slabdata    132    132
dentry              2880     2880      192     20    1 : tunables    0    0    0 : slabdata    144    144
vm_area_struct      3514     3520       88     45    1 : tunables    0    0    0 : slabdata     79     79

5.2 slabtop 实时监控

slabtop 是 slab 版的 top 命令,可以实时观察各 slab 缓存的变化。按 O 键可按不同列排序,按 c 键可切换着色信息。

5.3 SLUB 调试模式

内核配置中启用 CONFIG_SLUB_DEBUG 后,SLUB 提供丰富的调试功能:

  • Poison 检测:释放后的对象填充特定模式字节(0x5a 用于未初始化,0x6b 用于已释放)。访问这些字节意味着 use-after-free 或未初始化访问。
  • Red Zoning:在对象尾部设置警戒区(Red Zone),检测 buffer overflow。
  • Tracking:记录每次分配和释放的调用栈,帮助定位内存泄漏。

运行时开启调试:slub_debug=UZP(U=用户追踪,Z=Red Zone,P=Poison)。

6. 性能调优实践

6.1 高对象创建/销毁场景的优化

如果发现 /proc/slabinfo 中某个缓存的活跃对象与总对象比值很高(接近 1:1),说明该缓存的对象周转率极高。此时可调整以下参数:

  • batchcount:每次从 Node slab 移动多少对象到 CPU 本地。提高此值可减少锁争用。
  • limit:CPU 本地空闲对象上限。对于高周转率的缓存,适当增加 limit 可减少回 Node 的频率。
  • slab_min_objects:创建 slab 页时至少包含的对象数。增大可减少 partial 页碎片。

6.2 减少 slab 内存回写延迟

对于不需要频繁分配的缓存,可以通过 /sys/kernel/slab/<cache>/ 下的 tunables 减少内存占用。例如降低 cpu_partial 参数可减少 CPU 本地保留的 partial 页数量。

6.3 NUMA 感知策略

在多路服务器上,SLUB 默认优先在本地 NUMA 节点的 partial 页中分配。如果应用的工作集集中在特定节点,可通过 taskset 或 set_mempolicy() 将进程绑定到对应节点,最大化 slab 高速命中率。

7. 新版本内核中的进化

7.1 Linux 5.x 的 TYPESAFE_BY_RCU

引入 SLAB_TYPESAFE_BY_RCU 标志,允许对象在 RCU 读端临界区内被分配和初始化,然后在 RCU 宽限期后提交给读者。这减少了传统 RCU 模式下的分配等待开销。

7.2 Linux 6.x 的 SLUB 性能优化

最新内核(6.x 系列)持续优化 SLUB:

  • LOCAL_NODE_DEFER:引入延迟 NUMA 分配策略,减少跨节点访问
  • per-CPU partial 页扩展:每个 CPU 可保留更多 partial 页,减少 Node 级锁争用
  • 更好地支持 KASAN/KFENCE:在调试模式下减少性能损失

8. 总结

Slab 分配器作为 Linux 内核内存管理的核心子系统,经历了从经典 SLAB 到现代化 SLUB 的演进。其设计精髓在于:通过对象缓存消除重复初始化开销,通过分层管理(CPU → Node → Buddy)实现高效的内存利用率与缓存亲和性。

对于内核开发者而言,理解 SLUB 的数据结构、分配路径和调试手段,是进行内核模块开发和性能调优的基础。无论是编写 kmem_cache_alloc() 上下文中的模块代码,还是诊断内存泄漏、use-after-free 等疑难问题,掌握 Slab 分配器都是必不可少的关键技能。

参考资源

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部