本页目录

Slab 与 kmalloc

覆盖: SLUB allocator → kmem_cache → kmalloc/kfree → slab merging → 调试 (KASAN/SLUB_DEBUG) → SLOB 退役 (6.4) 内核版本: 2.6 ~ 6.x

概述

Buddy allocator 最小分配单位是 1 页 (4KB)。但内核中绝大多数分配远小于 4KB——task_struct (~4KB)、inode (~600B)、dentry (~200B)。如果每个小对象都占用一整页,浪费将极其严重。

SLUB allocator 解决了这个问题:将一整页(或连续几页)切成多个相同大小的小对象,用 freelist 管理空闲和已分配的对象。SLUB 是当前默认的 slab 实现(6.4 移除了旧的 SLOB),源自 SLAB (Solaris → Linux 2.2) → SLUB (Linux 2.6.23, C. Lameter)。


SLUB 架构

kmem_cache

// include/linux/slub_def.h
struct kmem_cache {
    unsigned int        size;           // 每个对象的实际大小 (含对齐)
    unsigned int        object_size;    // 原始对象大小 (不含元数据)
    unsigned int        offset;         // 下一个空闲对象的指针偏移
    unsigned int        cpu_partial;    // per-CPU partial slab 数量

    const char          *name;          // /proc/slabinfo 中看到的名称

    // 构造函数 (很少用,Linux 倾向于零初始化 + 手动 init)
    void (*ctor)(void *);

    struct kmem_cache_node *node[MAX_NUMNODES];  // per-NUMA node
    struct kmem_cache_cpu __percpu *cpu_slab;    // per-CPU 热 slab (无锁)
};

Per-CPU 热 slab

flowchart TD
    START["SLUB 分配/释放请求"]

    START --> OP{"操作类型?"}

    OP -->|"分配"| ALLOC["从 freelist 头取对象<br/>(原子: this_cpu_cmpxchg)"]
    ALLOC --> FULL{"当前 slab<br/>满了?"}
    FULL -->|"否"| FAST_ALLOC["✅ 返回对象<br/>(无锁, 极快)"]
    FULL -->|"是"| PART{"有 partial<br/>slab?"}
    PART -->|"是"| SWITCH["换下一个 partial slab"]
    SWITCH --> FAST_ALLOC
    PART -->|"否"| BUDDY_ALLOC["从 buddy 拿新 slab"]
    BUDDY_ALLOC --> FAST_ALLOC

    OP -->|"释放"| FREE["对象归还到 freelist 头"]
    FREE --> EMPTY{"slab 变全空?"}
    EMPTY -->|"是"| RETURN["可能归还回 buddy"]
    EMPTY -->|"否"| FAST_FREE["✅ 释放完成<br/>(无锁, 极快)"]

    classDef fast fill:#e8f5e9,stroke:#2e7d32
    classDef slow fill:#ffebee,stroke:#c62828
    classDef decision fill:#fff3e0,stroke:#ef6c00
    class START,ALLOC,FREE fast
    class FULL,PART,EMPTY decision
    class SWITCH,BUDDY_ALLOC,RETURN slow
    class FAST_ALLOC,FAST_FREE fast

分配与释放路径

// mm/slub.c

// 分配 (fast path → slow path):
kmem_cache_alloc(s, flags)slab_alloc()slab_alloc_node()
    ├─ fast path: 从 cpu_slab 的 freelist 取
    │     object = this_cpu_read(s->cpu_slab->freelist);if (likely(object))// 更新 freelist 指针 (cmpxchg)
return object;
    │
    └─ slow path: __slab_alloc()
        ├─ 尝试 CPU partial list 中的 slab
        ├─ 尝试 node partial list 中的 slab
        ├─ 都失败 → new_slab()allocate_slab()
        │     → alloc_pages(GFP_KERNEL, order) → buddy allocator
        │     → 构造 freelist
        └─ 返回第一个对象

// 释放 (fast path):
kmem_cache_free(s, object)slab_free()
    ├─ fast path: 对象放回 cpu_slab 的 freelist
    │     this_cpu_cmpxchg(s->cpu_slab->freelist, old, object);
    │
    └─ slow path: __slab_free()
        ├─ slab 变全空闲? → 如果 per-node partial 不多 → 转为 partial
        │                → 否则 free_slab() → 释放回 buddy allocator
        └─ 更新 node 统计

kmalloc: 通用分配接口

// include/linux/slab.h
// kmalloc 实际上是预定义的 kmem_cache 的包装

// 内核中预定义的通用 cache:
// kmalloc-8, kmalloc-16, kmalloc-32, kmalloc-64,
// kmalloc-96, kmalloc-128, kmalloc-192, kmalloc-256,
// kmalloc-512, kmalloc-1k, kmalloc-2k, kmalloc-4k, kmalloc-8k

// kmalloc(size, flags):
//   1. 根据 size 查到对应的 kmem_cache (通过 kmalloc_caches 表)
//   2. 调用 kmem_cache_alloc(s, flags)
//   3. 返回对象指针

// kfree(ptr):
//   1. 从对象 metadata 中找到它属于哪个 kmem_cache
//      → 通过 page->slab_cache (struct page 中的字段)
//   2. kmem_cache_free(cache, ptr)

Slab Merging

// mm/slab_common.c
// 为了节省内存,尺寸相同或相近的 kmem_cache 可以合并:
//   "dentry" (256B) 和 "inode_cache" (592B)
//   → 都分配自 kmalloc-512? 不合并: 它们不同 size
//   → 但如果两个 cache 的 object_size 相近 (都接近 256B)
//      → 可能共享同一个底层 slab
//
// /sys/kernel/slab/ 下可以看到哪些 cache 被合并

SLOB 退役 (6.4)

历史上 Linux 有三种 slab 实现:
  SLAB: 原始实现 (Solaris 移植 → Linux 2.2), 最复杂
  SLUB: 简化实现 (2.6.23), 当前默认
  SLOB: 极小实现, 用于嵌入式系统 (< 16MB RAM)

6.4 移除了 SLOB:
  - SLUB 经过多年优化, 代码体积已经足够小
  - 嵌入式系统可以用 SLUB (CONFIG_SLUB_TINY)
  - 维持三个 allocator 的维护负担太大

SLOB 的设计思路值得一提:
  - 没有 per-CPU slab
  - 所有分配从单一空闲链表
  - first-fit 分配 (找到第一块够大的)
  - 极致简单, 但碎片化和性能差

kmemleak: 内存泄漏检测

// mm/kmemleak.c
// 类似用户态 valgrind 的 memcheck
// 周期性扫描所有已分配的内核对象,找到无引用的 → 报告泄漏

// 启用:
//   CONFIG_DEBUG_KMEMLEAK=y
//   boot: kmemleak=on

// 手动触发:
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak  # 泄漏报告

KASAN: 越界/UAF 检测

// mm/kasan/
// Kernel Address Sanitizer — 编译时 instrumentation
// 检测 slab out-of-bounds, use-after-free, double-free

// 三种模式:
//   Generic KASAN: 影子内存 (每 8 bytes 用 1 byte 标记) → 1/8 内存开销
//   Software Tag-Based KASAN: ARM64 使用 TBI (Top Byte Ignore) → 低开销
//   Hardware Tag-Based KASAN: ARM64 MTE → 硬件检查 → 用于生产环境

调试与观测

# slab 使用情况
cat /proc/slabinfo
# 列: name, active_objs, num_objs, objsize, objperslab, pagesperslab

# 更好的人类可读版本
slabtop -o       # 按活跃对象数排序
slabtop -s c     # 按 cache 大小排序

# 特定 cache 的详细信息
cat /sys/kernel/slab/dentry/order       # slab order
cat /sys/kernel/slab/dentry/object_size # 对象大小
cat /sys/kernel/slab/dentry/objects     # 已分配对象数
cat /sys/kernel/slab/dentry/slab_size   # 每个 slab 的大小

# 查看谁在用最多 slab (kmem trace)
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kfree/enable
cat /sys/kernel/debug/tracing/trace_pipe | head -100

# 内存泄漏检测
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak | head -50

参考与延伸

  • 内核文档⁠: Documentation/mm/slub.rst, Documentation/dev-tools/kmemleak.rst, Documentation/dev-tools/kasan.rst
  • LWN:
    • "The SLUB allocator" (lwn.net/Articles/229984/)
    • "Removing SLOB" (lwn.net/Articles/967178/)
  • 源码文件⁠:
    • mm/slub.c — SLUB 实现
    • mm/slab_common.c — slab merging, sysfs 接口
    • include/linux/slub_def.h — struct kmem_cache
    • mm/kmemleak.c — 泄漏检测
    • mm/kasan/ — KASAN

关键词: SLUB, kmem_cache, kmalloc, per-CPU slab, freelist, slab merging, SLOB退役, kmemleak, KASAN