4. slab分配器

在嵌入式Linux系统中,内核需要频繁分配和释放大量的小型数据结构(如task_struct、inode、dentry等)。如果直接使用基于页的伙伴系统(Buddy System)来管理这些对象,会产生严重的内部碎片和性能开销。slab分配器正是为了解决这一问题而设计的——它提供了一种对象缓存机制,将内存划分为特定大小的对象池,实现高速、低碎片的动态内存管理。

核心思想: slab分配器将内存划分为“缓存(cache)”和“slab”两级。每个缓存管理一种固定大小的对象(如32字节、64字节),每个slab则包含一个或多个连续物理页框,并进一步划分为多个对象。对象释放后不立即归还给伙伴系统,而是保留在缓存中,实现“随取随用”。

4.1 三种分配器:slab / slub / slob 对比

Linux内核目前支持三种slab分配器实现:slab(传统实现)、slub(默认、精简版)和slob(极简版,用于极小内存系统)。它们在设计目标、内存开销和性能上各有侧重。

特性 slab(传统) slub(默认) slob(极简)
设计目标 通用、功能完整 减少元数据开销、简化代码 极小内存占用(< 64MB)
元数据开销 高(每个slab有描述符) 低(slab描述符合并到页结构) 极低(链表+简单偏移)
对象缓存 每CPU缓存 + 共享缓存 每CPU缓存 + 部分链表 无专用缓存,直接遍历
碎片控制 好(着色、对齐) 较好(简化着色) 一般(首次适应)
适用场景 大型服务器、复杂NUMA 嵌入式、桌面、服务器(默认) 极低内存MCU、SBC
代码复杂度 高(~6000行) 中(~2000行) 低(~600行)
嵌入式选型建议: 对于大多数嵌入式Linux系统(内存64MB~512MB),slub是默认且最优的选择。它平衡了性能和内存开销。只有在内存极度受限(< 64MB)且不需要复杂缓存特性的场景下,才考虑切换到slob。

下图展示了三种分配器在对象分配/释放路径上的核心差异:

slab / slub / slob 分配路径对比 三种slab分配器分配路径对比 slab(传统) kmalloc(size) 查找缓存(cache) 每CPU缓存(array_cache) 共享缓存 / slab链表 伙伴系统(页分配) 元数据多,路径长,缓存层级丰富 slub(默认) kmalloc(size) 查找缓存(kmem_cache) 每CPU部分链表(partial) slab页直接操作 伙伴系统(页分配) 元数据少,路径短,直接操作页结构 slob(极简) kmalloc(size) 全局slob链表遍历 首次适应(first-fit) 无专用缓存 伙伴系统(页分配) 无缓存,直接遍历,适合极小内存

4.2 对象缓存机制

slab分配器的核心优势在于对象缓存。内核为每种常用数据结构创建一个专用的“缓存”(kmem_cache),缓存中预先分配好多个对象。当内核需要该结构时,直接从缓存中取出一个已初始化(或清零)的对象,无需重新分配物理页;释放时,对象返回缓存,下次分配可直接复用。

4.2.1 缓存层次结构

以slub分配器为例,对象缓存分为三个层级:

  1. 每CPU缓存(cpu_slab): 每个CPU维护一个freelist,指向当前CPU可直接使用的空闲对象。分配时无锁操作,性能最高。
  2. 部分链表(partial): 每个节点(node)维护一个部分空闲的slab链表。当每CPU缓存耗尽时,从partial链表批量搬运对象到CPU缓存。
  3. 全满链表(full): 所有对象都被分配的slab,暂时不参与分配,仅用于释放时回退。
嵌入式优化点: 在单核或双核嵌入式系统中,每CPU缓存的数量可以适当减少(通过slab_max_cpus参数),以节省内存。同时,可以通过调整slub_min_objects和slub_max_order来控制每个slab的大小,避免大块连续内存的浪费。

4.2.2 对象状态与着色

每个slab内的对象可以处于以下三种状态之一:

  • 空闲(free): 对象在freelist中,可被分配。
  • 使用中(used): 对象已被分配,正在被内核使用。
  • 缓存(cached): 对象已被释放,但保留在缓存中,下次分配可直接复用。

着色(coloring): 传统slab分配器通过“着色”技术,让不同slab中对象的起始地址在cache line上错开,以减少CPU cache冲突。slub简化了着色,但在嵌入式场景中,着色对性能影响较小,可以忽略。

4.3 kmalloc与kfree实现

kmalloc和kfree是内核中最常用的内存分配/释放接口。它们底层依赖slab分配器(或slub/slob)来管理通用大小的内存块。

4.3.1 kmalloc实现流程

kmalloc(size, flags)的工作流程如下:

  1. 确定缓存索引: 根据请求的size,查找对应的通用缓存(如kmalloc-32、kmalloc-64等)。内核预定义了多个2的幂次大小的缓存(8B、16B、32B、64B、128B、256B、512B、1KB、2KB、4KB、8KB)。
  2. 调用slab分配: 从对应的kmem_cache中分配一个对象。如果缓存为空,则从伙伴系统分配新的slab页。
  3. 返回虚拟地址: 返回对象的虚拟地址(线性映射区或vmalloc区,取决于大小和标志)。
// 简化版 kmalloc 核心路径(slub实现)
void *kmalloc(size_t size, gfp_t flags)
{
    struct kmem_cache *s;
    void *ret;

    // 1. 根据size找到对应的通用缓存
    if (size >= KMALLOC_MAX_CACHE_SIZE)
        return kmalloc_large(size, flags);  // 大块走伙伴系统

    s = kmalloc_caches[kmalloc_index(size)];

    // 2. 从slab缓存分配对象
    ret = slab_alloc(s, flags, _RET_IP_);

    // 3. 追踪调试(可选)
    trace_kmalloc(_RET_IP_, ret, size, s->size, flags);

    return ret;
}

4.3.2 kfree实现流程

kfree(ptr)的流程相对简单:

  1. 获取对象所属slab: 通过虚拟地址找到对应的page结构(通过virt_to_page或直接地址运算)。
  2. 确定缓存: 从page结构中的slab_cache指针获取对应的kmem_cache。
  3. 释放对象: 将对象放回slab的freelist,并更新slab状态(可能从full变为partial,或从partial变为free)。
// 简化版 kfree 核心路径(slub实现)
void kfree(const void *x)
{
    struct page *page;
    struct kmem_cache *s;

    if (unlikely(ZERO_OR_NULL_PTR(x)))
        return;

    // 1. 获取对象所在的page
    page = virt_to_head_page(x);

    // 2. 从page获取对应的slab缓存
    s = page->slab_cache;

    // 3. 将对象释放回slab
    slab_free(s, page, (void *)x, _RET_IP_);

    trace_kfree(_RET_IP_, x);
}
常见陷阱:
  • kmalloc分配的内存必须用kfree释放,不能混用vfree或直接释放页。
  • kmalloc最大分配大小受限于KMALLOC_MAX_SIZE(通常为4MB),超过此大小应使用vmalloc或alloc_pages。
  • 在嵌入式系统中,频繁的kmalloc/kfree会导致slab缓存碎片,建议使用内存池(mempool)或对象池来管理高频分配的对象。

4.4 嵌入式场景下的slab调优

在嵌入式Linux中,slab分配器的默认配置可能不适合低内存环境。以下是一些实用的调优参数:

参数 默认值 嵌入式建议 说明
slub_min_objects 4 2 或 1 减少每个slab的最小对象数,降低内存浪费
slub_max_order 3(8页) 1 或 2 限制slab最大连续页数,避免大块分配失败
slab_max_cpus NR_CPUS 实际CPU数 减少每CPU缓存数量,节省内存
slub_debug 关闭 关闭(默认) 开启调试会大幅增加内存开销,生产环境务必关闭
实战案例: 在某ARM Cortex-A7(256MB RAM)的IoT网关项目中,默认slub配置导致kmalloc-128缓存占用超过8MB。通过设置slub_min_objects=2和slub_max_order=1,将kmalloc-128的占用降低到3MB,同时分配性能仅下降5%。这是典型的“以时间换空间”策略。

下图展示了slab分配器在嵌入式系统中的完整工作流:

slab分配器嵌入式工作流 slab分配器嵌入式工作流(slub) 内核驱动 / 文件系统 / 网络栈 调用 kmalloc(size) 查找 kmalloc_caches[索引] CPU缓存有对象? 是 从freelist取对象 否 从partial链表搬运 伙伴系统分配新slab页 返回对象虚拟地址

4.5 本章小结

  • slab/slub/slob 三种分配器分别适用于不同场景:slab功能完整但开销大,slub是嵌入式首选,slob适合极低内存。
  • 对象缓存 是slab分配器的核心,通过每CPU缓存、部分链表和全满链表三级结构实现高速分配。
  • kmalloc/kfree 底层依赖通用缓存(kmalloc-xx),分配路径短,释放路径直接。
  • 嵌入式调优 应重点关注slub_min_objects、slub_max_order和slab_max_cpus,在内存和性能之间找到平衡点。