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 的做法是:

  1. 预先从伙伴系统拿一些页面
  2. 切成固定大小的对象
  3. 对象用完后不还给伙伴系统,而是放回缓存
  4. 下次分配时直接从缓存取

嗯,这里要注意: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 三种实现的分配流程对比

为了让你更直观地理解三者的区别,我画了一张流程图:

slab / slub / slob 分配流程对比 分配请求 slab 实现 1. 查找 partial slab 2. 从 freelist 取对象 3. 更新着色偏移 slub 实现 1. 检查 per-CPU 缓存 2. 从 partial 链表取 3. 无则从伙伴系统取页 slob 实现 1. 扫描空闲链表 2. 首次适配分配 3. 无则扩展堆 返回分配的对象 slub 是当前主流实现,slob 仅用于极小内存场景 slab 实现已标记为维护模式,不推荐新项目使用

3.6 实际使用建议

最后,我结合自己的经验给你几个实用建议:

  • 默认用 slub:除非你的系统内存小于 64MB,否则别碰 slob。slab 代码太复杂,调试起来很痛苦。
  • 关注 /proc/slabinfo:这是排查内存泄漏的第一站。我每次遇到内存问题,第一件事就是看 slabinfo。
  • kmalloc 别分配大块:超过一个页面大小(通常是 4KB)时,考虑用 vmalloc 或直接页面分配。
  • 对象池化:如果你在写驱动,频繁创建销毁的对象可以考虑用 kmem_cache_create 创建专用缓存。

个人经验:我曾经在嵌入式项目里用 slob 分配器,结果发现频繁分配释放导致堆碎片化严重。后来换成 slub,配合 per-CPU 缓存,性能提升明显。所以别小看分配器的选择,它直接影响系统稳定性。

好了,slab 分配器的核心内容就这些。下一节我们会深入 slub 的源代码,看看它到底是怎么做到高性能的。


专注资料整理