3. Slab 分配器引入:为什么需要 Slab 分配器
在 Linux 内核中,伙伴系统(Buddy System)负责管理物理页框的分配与回收,但其最小分配粒度为 4 KiB(一个页面)。内核中大量频繁分配和释放的小型数据结构(如 task_struct、inode、dentry、mm_struct 等)通常只有几十到几百字节。如果直接使用伙伴系统分配这些对象,将导致严重的内部碎片和性能开销。
Slab 分配器正是为了解决这一问题而设计的——它作为伙伴系统之上的通用缓存层,专门管理大小固定、频繁分配回收的内核对象。
3.1 伙伴系统的局限性
| 问题维度 | 伙伴系统直接分配小对象的缺陷 |
|---|---|
| 内部碎片 | 最小分配单元为 4 KiB,一个 64 字节的对象占用整个页框,浪费 4032 字节(约 98.4%) |
| 分配延迟 | 每次分配/释放都涉及页框合并与分裂,频繁操作导致性能抖动 |
| 对象初始化 | 每次分配后需要重新初始化对象,无法复用已释放对象的构造状态 |
| 缓存局部性 | 小对象散落在不同页框中,CPU 缓存命中率低 |
关键数据:在典型 Linux 系统中,内核对象分配请求中超过 85% 的大小在 32 字节到 2 KiB 之间。如果这些请求全部由伙伴系统直接处理,内存浪费率将超过 60%。
3.2 Slab 分配器解决的问题
Slab 分配器从伙伴系统申请一整页或多页连续内存,然后将其切分为多个大小相同的 对象槽(object slot),并维护每个槽的分配状态。它主要解决以下四个核心问题:
- 消除内部碎片:将大页框切分为与对象大小精确匹配的槽,每个对象只占用其实际大小(加上少量对齐开销)。
- 减少分配/释放开销:对象分配和释放仅需操作 slab 内部链表,无需触发伙伴系统的页框分裂与合并。
- 对象缓存与复用:释放的对象不立即销毁,而是保留其构造状态(如锁、引用计数等),下次分配时直接复用,减少初始化成本。
- 硬件缓存友好:同一 slab 中的对象在物理上连续,且 slab 分配器通过着色(coloring)技术使不同 slab 的起始地址错开,均衡 CPU 缓存行使用。
性能对比:在 2.6 内核的基准测试中,使用 slab 分配器处理 dentry 对象分配时,分配延迟比直接使用伙伴系统降低了约 40 倍,内存利用率从 35% 提升到 92%。
3.3 Slab 分配器与伙伴系统的关系
Slab 分配器并不是伙伴系统的替代品,而是建立在伙伴系统之上的 上层缓存层。两者形成清晰的分层协作关系:
3.4 核心协作流程
Slab 分配器与伙伴系统之间的交互遵循以下典型流程:
- 缓存创建:调用
kmem_cache_create()时,slab 分配器仅初始化元数据结构,不立即从伙伴系统申请内存。 - 懒分配:首次对象分配时,slab 分配器调用
__get_free_pages()从伙伴系统申请一个或多个连续页框,作为 slab 的物理载体。 - 对象切分:将申请的页框按对象大小切分为多个槽,初始化空闲链表。
- 对象分配:从空闲链表头部取出一个对象,返回给调用者。若当前 slab 已满,则申请新的页框创建新 slab。
- 对象释放:将对象放回空闲链表。若整个 slab 的所有对象都已释放,则调用
free_pages()将页框归还给伙伴系统。
注意:Slab 分配器不会在每次对象释放时立即归还页框。它通常保留部分空闲 slab 以应对未来的分配请求,这种行为称为 缓存驻留(cache residency)。过度驻留可能导致内存浪费,但完全禁用驻留又会增加伙伴系统的分裂合并开销。调优时需要在这两者之间找到平衡。
3.5 关键数据结构关系
| 数据结构 | 所属层级 | 作用 | 与伙伴系统的交互 |
|---|---|---|---|
kmem_cache |
Slab 分配器 | 描述一种对象类型的缓存(如 dentry 缓存) | 通过 gfp_t 标志控制页框申请行为 |
slab |
Slab 分配器 | 管理一组连续页框及其中的对象槽 | 包含指向 struct page 的指针 |
struct page |
伙伴系统 | 描述一个物理页框 | slab 分配器通过 alloc_pages() 获取 |
free_area |
伙伴系统 | 维护不同阶的空闲页块链表 | slab 释放页框时插入对应链表 |
3.6 为什么不能直接用伙伴系统替代 Slab
假设内核需要频繁分配 256 字节的对象:
- 伙伴系统方案:每次分配一个 4 KiB 页框,内部碎片 3840 字节(93.75%)。1000 次分配消耗 4 MiB 物理内存,实际只使用 256 KiB。
- Slab 方案:一个 4 KiB 页框可容纳 16 个对象(256 字节 × 16 = 4096 字节),内部碎片为 0。1000 次分配仅需 63 个页框(约 252 KiB),内存利用率提升 16 倍。
结论:Slab 分配器不是伙伴系统的替代品,而是其 必要补充。伙伴系统负责物理页框的宏观管理,Slab 分配器负责内核对象的微观缓存。两者协同工作,构成了 Linux 内核内存管理的核心骨架。