Linux内核裁剪:内存管理精简与调试工具链应用 - 第4节
4.1 slab分配器概述:三种实现与设计哲学
Linux内核的slab分配器是管理小对象(通常小于一页)内存的核心机制,直接服务于kmalloc、task_struct、inode等频繁分配/释放的内核对象。内核提供了三种实现:slab(原始实现)、slub(默认分配器)和slob(精简分配器)。三者在内存开销、分配速度、可扩展性上各有取舍,适用于不同场景。
4.1.1 三种分配器核心对比
| 特性 | slab | slub | slob |
|---|---|---|---|
| 设计目标 | 通用、硬件缓存着色 | 简化、高并发、低内存开销 | 极小内存占用 |
| 元数据开销 | 高(每个 slab 页头 + 描述符) | 低(页头精简,描述符复用) | 极低(链表 + 空闲指针) |
| 分配/释放速度 | 中等(着色计算) | 快(无着色,批量操作) | 慢(首次适配遍历) |
| CPU/NUMA 扩展性 | 好(per-CPU 缓存) | 优秀(per-CPU 缓存 + 批量) | 差(全局链表锁) |
| 着色支持 | 完整 | 无 | 无 |
| 适用场景 | 实时系统、特殊着色需求 | 通用、嵌入式(内存 > 8MB) | 极小内存(< 8MB) |
| 内核配置宏 | CONFIG_SLAB | CONFIG_SLUB | CONFIG_SLOB |
4.2 slab分配器:对象缓存与着色机制
slab 分配器将物理页划分为多个相同大小的对象槽,并引入着色(coloring)技术来优化 CPU 硬件缓存(L1/L2/L3)的利用率。着色通过在每个 slab 的起始位置添加偏移量,使不同 slab 中相同索引的对象映射到不同的缓存行,从而减少缓存冲突。
4.2.1 slab 核心数据结构
struct kmem_cache {
const char *name; // 缓存名称(如 "task_struct")
unsigned int object_size; // 对象实际大小
unsigned int size; // 对齐后大小(含填充)
unsigned int align; // 对齐要求
unsigned int colour; // 着色范围(缓存行数)
unsigned int colour_off; // 每个着色单位的偏移量(通常为缓存行大小)
struct list_head slabs_partial; // 部分空闲 slab 链表
struct list_head slabs_full; // 完全满 slab 链表
struct list_head slabs_free; // 完全空闲 slab 链表
unsigned int num; // 每个 slab 的对象数
gfp_t allocflags; // 底层页分配标志
void (*ctor)(void *obj); // 构造函数(可选)
};
4.2.2 着色原理与计算
着色偏移量计算公式:
colour_off = cache_line_size(); // 通常 64 字节
colour_max = slab_size / colour_off - num; // 最大着色数
offset = (colour * colour_off) % slab_size; // 当前 slab 的起始偏移
每个 slab 在分配时选择一个 colour 值(0 到 colour_max),将 slab 的起始地址偏移 offset 字节。这样,不同 slab 中第 i 个对象的物理地址模缓存行大小不同,避免多个 slab 的对象竞争同一缓存行。
4.3 slub分配器:精简与高性能
slub 是 Linux 内核自 2.6.22 以来的默认分配器,它移除了 slab 的着色和复杂链表,采用每 slab 页头内嵌空闲指针的方式,大幅降低元数据开销。slub 的核心思想是“让 slab 页本身成为描述符”,不再需要额外的 kmem_slab 结构。
4.3.1 slub 页头布局
struct page (slub 复用部分) {
unsigned long freelist; // 空闲对象链表头(偏移量)
unsigned long counters; // 包含 inuse(已用对象数)和 frozen(冻结标志)
struct kmem_cache *slab_cache; // 指向所属缓存
struct list_head lru; // 用于 partial/full 链表
};
slub 将空闲对象指针直接存储在对象内存中(空闲对象的前 8 字节指向下一个空闲对象),形成嵌入式空闲链表。分配时从 freelist 头部取出对象,释放时将对象插回 freelist 头部。
4.3.2 slub 批量操作与 per-CPU 缓存
slub 为每个 CPU 维护一个本地缓存(kmem_cache_cpu),包含当前正在使用的 slab 页和一批预取对象:
struct kmem_cache_cpu {
void **freelist; // 当前 CPU 的空闲对象列表
struct page *page; // 当前使用的 slab 页
struct page *partial; // 部分空闲 slab 页链表
unsigned int tid; // 事务 ID(用于无锁操作)
};
分配时优先从 freelist 取对象,当本地缓存耗尽时,从 partial 链表批量迁移对象(默认 2 的幂次,如 8、16、32)。释放时如果对象属于当前 CPU 的活跃 slab,直接放回 freelist;否则放回 slab 页的全局 freelist。
4.4 slob分配器:极简主义
slob(Simple List Of Blocks)是专为极小内存系统(如 4MB RAM 的 IoT 设备)设计的分配器。它使用首次适配(first-fit)算法,将所有空闲块组织成一个全局链表,分配时遍历链表找到第一个足够大的块。
4.4.1 slob 数据结构
struct slob_block {
unsigned int size; // 块大小(含头部)
struct slob_block *next; // 下一个空闲块
};
// 全局空闲链表头
static struct slob_block *slob_free_list;
slob 不区分缓存(kmem_cache),所有 kmalloc 请求都从同一个全局空闲链表中分配。每个空闲块头部占用 4 字节(32 位)或 8 字节(64 位),用于记录块大小和下一个空闲块指针。
4.4.2 slob 分配与释放流程
- 分配:遍历 slob_free_list,找到第一个 size >= 请求大小的块;如果块过大,进行分裂(split),剩余部分作为新空闲块插入链表。
- 释放:将释放的块插入空闲链表头部,并尝试与相邻空闲块合并(coalesce),减少外部碎片。
- 页分配:当空闲链表无法满足请求时,调用 alloc_pages() 获取新页,将整页作为一个大空闲块加入链表。
4.5 三种分配器架构对比图
4.6 调试工具链:slabinfo 与 kmemleak
内核提供了多个工具来监控和调试 slab 分配器行为,在裁剪和优化过程中至关重要。
4.6.1 slabinfo 工具
slabinfo 是用户空间工具,读取 /proc/slabinfo 和 /sys/kernel/slab/ 信息,显示每个缓存的详细统计:
# slabinfo 输出示例(精简)
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
task_struct 512 512 3456 11 2
inode_cache 2048 2048 768 21 4
kmalloc-64 8192 8192 64 63 1
kmalloc-128 4096 4096 128 31 1
常用调试命令:
slabinfo -s:按缓存大小排序,找出内存占用最大的缓存。slabinfo -r:显示每个缓存的着色参数(仅 slab 分配器)。slabinfo -v:详细模式,显示每个 CPU 的缓存统计。slabinfo -X:显示 slab 内部碎片(浪费空间)。
4.6.2 kmemleak 内存泄漏检测
kmemleak 是内核内存泄漏检测器,通过扫描内存中的指针来发现未释放的对象。启用方法:
# 内核配置
CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=n
# 运行时控制
echo scan > /sys/kernel/debug/kmemleak # 手动扫描
echo clear > /sys/kernel/debug/kmemleak # 清除当前泄漏报告
cat /sys/kernel/debug/kmemleak # 查看泄漏
4.7 嵌入式系统裁剪实践
在嵌入式 Linux 内核裁剪中,slab 分配器的选择直接影响内存占用和实时性。以下为典型场景的配置建议:
| 系统内存 | 推荐分配器 | 关键配置 | 预期内存节省 |
|---|---|---|---|
| < 4 MB | slob | CONFIG_SLOB=y | 相比 slub 节省 50-100 KB |
| 4 - 32 MB | slub | CONFIG_SLUB=y, CONFIG_SLUB_DEBUG=n | 相比 slab 节省 30-80 KB |
| 32 - 256 MB | slub | CONFIG_SLUB=y, CONFIG_SLUB_CPU_PARTIAL=y | 性能优先,内存开销可忽略 |
| > 256 MB | slub 或 slab | CONFIG_SLUB 或 CONFIG_SLAB | 根据实时性需求选择 |
- 确认内核配置中只启用一种分配器(CONFIG_SLAB / CONFIG_SLUB / CONFIG_SLOB 三选一)。
- 关闭调试选项:CONFIG_SLUB_DEBUG=n, CONFIG_DEBUG_SLAB=n, CONFIG_KMEMCHECK=n。
- 关闭 kmemleak:CONFIG_DEBUG_KMEMLEAK=n。
- 调整 per-CPU 缓存大小:CONFIG_SLUB_CPU_PARTIAL 根据 CPU 数量适当减小。
- 使用 slabinfo 监控实际内存占用,裁剪不必要的 kmem_cache(如文件系统缓存)。
4.8 本章小结
- slab:功能最完整,支持着色优化 CPU 缓存,但元数据开销大,适合实时系统。
- slub:默认分配器,嵌入式首选(内存 > 8MB),嵌入式空闲链表降低内存开销,per-CPU 批量操作提升性能。
- slob:极简实现,全局链表首次适配,适合内存 < 8MB 的深度嵌入式设备。
- 着色:仅 slab 支持,通过偏移量减少缓存冲突,在小型缓存 CPU 上效果显著。
- 调试工具:slabinfo 监控缓存统计,kmemleak 检测内存泄漏,裁剪时务必关闭调试选项。