3、slab分配器:slab/slub/slob对比、对象缓存机制、kmalloc/kfree实现
3.1 为什么需要 slab 分配器?
伙伴系统管理的是物理页面,最小单位是 4KB。但内核里大量对象很小——比如 task_struct、inode、dentry,可能只有几十到几百字节。你想想看,如果每次分配一个 64 字节的对象,都去伙伴系统拿一个 4KB 的页面,那内存浪费得有多严重?
所以内核需要一种更细粒度的内存管理机制。slab 分配器就是干这个的。它从伙伴系统申请整页内存,然后切成大小相等的对象缓存起来。说白了,就是内核自己的「对象池」。
我个人习惯把 slab 分配器理解为「内核的 malloc/free」。不过它比用户态的 malloc 更讲究——它要解决碎片化、缓存着色、对象复用等一系列问题。
3.2 三种实现:slab / slub / slob
内核历史上出现过三种 slab 实现。我刚开始接触内核时,看到这三个名字也是一脸懵。后来在项目中调过几次内存性能问题,才真正搞明白它们的区别。
| 特性 | slab | slub | slob |
|---|---|---|---|
| 设计目标 | 通用、功能完整 | 简化、高性能 | 极小内存占用 |
| 适用场景 | 大型系统(已淘汰) | 主流默认(推荐) | 嵌入式/小内存 |
| 元数据开销 | 较大 | 较小 | 极小 |
| 代码复杂度 | 高 | 中 | 低 |
| 调试能力 | 强(有红区检测) | 中等 | 弱 |
现在大部分 Linux 发行版默认用的是 slub。slab 实现因为代码过于复杂,已经被标记为「维护模式」。slob 则主要出现在内存极小的嵌入式设备上。
核心观点:如果你在做内核调优,重点关注 slub 就够了。slab 和 slob 了解一下设计思想即可。
3.3 对象缓存机制
slab 分配器的核心思想是「对象缓存」。什么意思呢?
内核里很多数据结构会被频繁创建和销毁。比如网络连接、文件描述符。每次创建都去伙伴系统分配页面,销毁时再释放——这太慢了。slab 的做法是:
- 预先从伙伴系统拿一些页面
- 切成固定大小的对象
- 对象用完后不还给伙伴系统,而是放回缓存
- 下次分配时直接从缓存取
嗯,这里要注意:slab 缓存的对象是「初始化过的」。什么意思?比如分配一个 task_struct,slab 会调用构造函数帮你把字段初始化好。下次再拿的时候,直接就是可用的状态。这比每次从零分配快得多。
小技巧:我在项目中遇到过一个问题——频繁创建和销毁网络 socket 导致性能抖动。后来用 slab 的缓存池特性,把 socket 对象池化,抖动就消失了。说白了,对象缓存就是内核版的「对象池模式」。
3.4 kmalloc / kfree 实现
kmalloc 是内核最常用的内存分配函数。它的底层就是 slab 分配器。我们来看一下它的实现路径:
// 简化后的 kmalloc 调用链
void *kmalloc(size_t size, gfp_t flags)
{
// 1. 根据 size 找到对应的 slab 缓存
struct kmem_cache *cache = kmalloc_slab(size, flags);
// 2. 从缓存中分配一个对象
return slab_alloc(cache, flags);
}
void kfree(const void *objp)
{
// 1. 找到对象所属的 slab 缓存
struct kmem_cache *cache = virt_to_cache(objp);
// 2. 将对象归还给缓存
slab_free(cache, objp);
}
kmalloc 支持 2 的幂次大小分配,从 8 字节到 8MB 不等。具体支持哪些大小,取决于架构和配置。你可以通过 /proc/slabinfo 查看当前系统支持的 kmalloc 缓存。
我曾经在调试一个内存泄漏问题时,就是通过 /proc/slabinfo 发现某个 kmalloc 缓存的对象数量异常增长,最终定位到是驱动模块没有正确调用 kfree。
避坑指南:我曾经遇到过一个线上问题——某个驱动模块频繁调用 kmalloc 分配大块内存(超过 8KB),导致 slub 分配器频繁从伙伴系统申请页面,性能急剧下降。后来改成预分配池化,问题解决。记住:kmalloc 适合小对象,大对象请用 vmalloc 或直接页面分配。
3.5 三种实现的分配流程对比
为了让你更直观地理解三者的区别,我画了一张流程图:
3.6 实际使用建议
最后,我结合自己的经验给你几个实用建议:
- 默认用 slub:除非你的系统内存小于 64MB,否则别碰 slob。slab 代码太复杂,调试起来很痛苦。
- 关注 /proc/slabinfo:这是排查内存泄漏的第一站。我每次遇到内存问题,第一件事就是看 slabinfo。
- kmalloc 别分配大块:超过一个页面大小(通常是 4KB)时,考虑用 vmalloc 或直接页面分配。
- 对象池化:如果你在写驱动,频繁创建销毁的对象可以考虑用 kmem_cache_create 创建专用缓存。
个人经验:我曾经在嵌入式项目里用 slob 分配器,结果发现频繁分配释放导致堆碎片化严重。后来换成 slub,配合 per-CPU 缓存,性能提升明显。所以别小看分配器的选择,它直接影响系统稳定性。
好了,slab 分配器的核心内容就这些。下一节我们会深入 slub 的源代码,看看它到底是怎么做到高性能的。