内存分配优化:内核工程slab与vmalloc实战 - 第2节

2.1 对象缓存思想:从通用分配到专用缓存

伙伴系统(Buddy System)以页框(通常 4KB)为最小单位进行内存分配,对于内核中频繁创建和销毁的小对象(如 task_struct、inode、dentry),直接使用伙伴系统会产生两个严重问题:

  • 内部碎片:一个几十字节的对象占用一整页(4096 字节),利用率极低。
  • 分配/释放开销高:每次分配都要走伙伴系统的页框合并与分裂算法,性能无法满足高频路径。

slab 分配器的核心思想是 对象缓存(Object Caching):预先从伙伴系统申请若干连续页框,将其划分为多个相同大小的对象槽位,并维护空闲链表。分配时直接从缓存中取出一个对象,释放时归还到缓存,避免频繁与伙伴系统交互。

关键洞察: 对象缓存将“页框级分配”降级为“对象级分配”,分配时间从微秒级降至纳秒级,同时消除了内部碎片。

2.1.1 对象缓存的工作流程

  1. 缓存创建:调用 kmem_cache_create(),指定对象大小、对齐方式、构造函数等。
  2. 页框预取:slab 分配器从伙伴系统申请 2n 个连续页框,构成一个 slab。
  3. 对象划分:将 slab 的连续内存划分为等长对象槽位,每个槽位对应一个空闲对象。
  4. 分配:从空闲链表中摘取一个对象,返回给调用者;若当前 slab 已满,则分配新的 slab。
  5. 释放:对象归还到空闲链表;若整个 slab 全空闲,可释放回伙伴系统。
对象缓存工作流程 kmem_cache_create() 创建缓存描述 伙伴系统申请页框 2ⁿ 个连续页 划分对象槽位 等长对象 分配对象 摘取空闲对象 释放对象 → 归还缓存 核心优势 • 分配/释放 O(1) 时间复杂度,无伙伴系统页框分裂开销 • 对象大小与 slab 对齐,内部碎片降至最低(通常 < 1%) • 支持着色(coloring)优化,减少 CPU 缓存行冲突 • 可附加构造函数/析构函数,实现对象初始化与清理

2.2 slab / slub / slob 三种实现对比

Linux 内核历史上先后出现了三种 slab 分配器实现:slab(原始实现)、slub(当前默认,针对大型系统优化)、slob(针对小型嵌入式系统优化)。三者的核心目标一致,但在数据结构、锁粒度、内存开销和可扩展性上存在显著差异。

特性 slab(原始) slub(默认) slob(嵌入式)
核心数据结构 每 CPU 缓存 + 共享 slab 链表 + 三色链表(满/部分/空) 每 CPU 空闲指针 + 每个 slab 的 freepointer 链表 简单空闲链表 + 页框内偏移管理
元数据开销 高(每个 slab 有 kmem_slab 描述符) 低(slab 描述符嵌入页框或外部) 极低(几乎无额外描述符)
锁粒度 全局 slab 链表锁 + 每 CPU 缓存锁 每 CPU 操作无锁(freelist 原子操作) 全局锁(简单但并发差)
分配/释放复杂度 O(1) 平均,但链表操作较多 O(1) 严格,freelist 头插头取 O(n) 最坏情况(遍历空闲链表)
内存碎片控制 好(着色 + 三色回收) 好(简化着色,依赖伙伴系统) 一般(简单合并,易产生外部碎片)
适用场景 中小型系统,历史兼容 大型服务器、桌面、移动设备(默认) 极低内存设备(< 64MB)
内核配置选项 CONFIG_SLAB CONFIG_SLUB CONFIG_SLOB
选型建议: 对于大多数通用场景(服务器、桌面、Android),保持内核默认的 slub 即可。仅当目标设备内存小于 64MB 时考虑 slob;slab 主要用于需要细粒度着色调试或历史兼容的场合。

2.2.1 三种实现的数据结构对比

slab / slub / slob 数据结构对比 slab(原始) • kmem_cache → 三色链表 • 每 CPU 数组缓存 (array_cache) • kmem_slab 描述符(外部/内部) • 着色偏移 (colour_off) • 全局 slab_mutex 保护 slub(默认) • kmem_cache → 每 CPU freelist • 每个 slab 内嵌 freepointer • page->freelist 原子操作 • 无全局 slab 链表 • 批量操作 (bulk alloc/free) slob(嵌入式) • 全局空闲链表 (slob_list) • 页框内偏移 + 大小标记 • 首次适应 (first-fit) 分配 • 无每 CPU 缓存 • 全局锁 (slob_lock) 关键差异总结 1. 元数据位置:slab 使用独立描述符;slub 将 freepointer 嵌入对象空闲区;slob 无描述符。 2. 并发策略:slab 依赖每 CPU 缓存 + 全局锁;slub 通过原子操作实现无锁路径;slob 全局锁串行。 3. 内存开销:slab 元数据开销约 5-10%;slub 约 1-3%;slob 接近 0%(但外部碎片可能增加)。 4. 调试能力:slab 支持最丰富的调试(红区、毒药、跟踪);slub 支持基本调试;slob 几乎无调试。 5. 批量操作:slub 原生支持 bulk alloc/free,对网络/存储驱动性能提升显著。 6. 着色支持:slab 完整着色;slub 简化着色;slob 不支持着色。

2.3 高速缓存(cache)与 kmem_cache 结构

在 slab 分配器中,高速缓存(cache) 是管理同一类对象的逻辑容器。每个 cache 对应一个 struct kmem_cache 实例,包含对象大小、对齐、slab 管理策略、每 CPU 缓存等全部元数据。

2.3.1 kmem_cache 核心字段

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  /* 每 CPU 缓存 */
    unsigned long size;                        /* 对象大小(含对齐填充) */
    slab_flags_t flags;                        /* 标志位(如 SLAB_HWCACHE_ALIGN) */
    unsigned int object_size;                  /* 原始对象大小(不含填充) */
    unsigned int offset;                       /* freepointer 在对象内的偏移 */
    unsigned int cpu_partial;                  /* 每 CPU 批量分配阈值 */
    struct kmem_cache_node *node[MAX_NUMNODES];/* NUMA 节点链表 */
    int align;                                 /* 对齐要求 */
    const char *name;                          /* 缓存名称(如 "task_struct") */
    int refcount;                              /* 引用计数 */
    void (*ctor)(void *);                      /* 构造函数 */
    struct list_head list;                     /* 全局 cache 链表 */
};
关键字段解读:
  • cpu_slab:每 CPU 的 kmem_cache_cpu 结构,包含当前正在使用的 slab 指针和空闲对象链表头。这是 slub 实现无锁分配的核心。
  • size 与 object_size 的差值:对齐填充和元数据预留空间,直接影响内存利用率。
  • offset:在 slub 中,空闲对象的 freepointer 存储在对象内部的 offset 位置,复用对象本身的空间。
  • cpu_partial:控制每 CPU 缓存中部分空闲 slab 的数量,平衡本地分配与全局回收。

2.3.2 高速缓存的层次结构

高速缓存层次结构(以 slub 为例) struct kmem_cache (高速缓存描述符,全局唯一) struct kmem_cache_node (每个 NUMA 节点一个,管理 slab 链表) struct page(slab 页框) 每个 slab 对应一个或多个连续页框,内含多个对象槽位 对象 0 对象 1 对象 2 对象 3 对象 4 对象 5 对象 6 对象 7 每个 kmem_cache 管理一类对象;kmem_cache_node 按 NUMA 节点组织 slab;每个 slab 包含多个连续对象槽位。

2.3.3 高速缓存的创建与销毁 API

函数 功能 关键参数
kmem_cache_create() 创建新的高速缓存 name, size, align, flags, ctor
kmem_cache_destroy() 销毁高速缓存(需确保所有对象已释放) s (kmem_cache 指针)
kmem_cache_alloc() 从缓存分配一个对象 s, gfpflags
kmem_cache_free() 将对象归还缓存 s, x (对象指针)
kmem_cache_shrink() 收缩缓存,释放空闲 slab 回伙伴系统 s
常见陷阱:
  • 使用 kmem_cache_alloc 分配的对象必须用 kmem_cache_free 释放,不能混用 kfree,否则会导致 slab 元数据损坏。
  • 销毁缓存前必须确保所有对象已释放,否则会造成内存泄漏。
  • 构造函数(ctor)在对象首次分配时调用,但 slub 在批量分配时可能跳过构造函数以提升性能,需注意初始化逻辑。

2.3.4 实战示例:创建并使用专用缓存

#include <linux/slab.h>
#include <linux/string.h>

struct my_object {
    int id;
    char name[32];
    unsigned long flags;
};

static struct kmem_cache *my_cachep;

int init_my_cache(void)
{
    /* 创建专用缓存:对象大小 40 字节,8 字节对齐,无构造函数 */
    my_cachep = kmem_cache_create("my_object_cache",
                                  sizeof(struct my_object),
                                  8, 0, NULL);
    if (!my_cachep)
        return -ENOMEM;
    return 0;
}

void cleanup_my_cache(void)
{
    kmem_cache_destroy(my_cachep);
}

struct my_object *alloc_my_object(gfp_t gfp_flags)
{
    struct my_object *obj;

    obj = kmem_cache_alloc(my_cachep, gfp_flags);
    if (obj) {
        obj->id = 0;
        obj->flags = 0;
        memset(obj->name, 0, sizeof(obj->name));
    }
    return obj;
}

void free_my_object(struct my_object *obj)
{
    if (obj)
        kmem_cache_free(my_cachep, obj);
}
性能提示: 对于高频分配路径,建议使用 kmem_cache_alloc_bulk() 和 kmem_cache_free_bulk()(slub 特有),一次操作可分配/释放多个对象,分摊锁开销和 slab 遍历成本。网络驱动和块设备驱动中广泛使用此接口。