Linux内核分析之内存管理-03

21.1 SLAB/SLUB/SLOB 三代分配器

内核对象分配器的第一版 SLAB 出现于 1996 年,其设计直接借鉴 Solaris 的 Jeff Bonwick 论文;随后 SLOB 与 SLUB 相继加入,三种实现长期以编译期互斥的方式并存,由同一套 kmem_cache_* API 统一供给上层。演进到 7.0,主干只剩 SLUB——本树 mm/Kconfig:168 仅有 config SLUB 一项即为直接证据。本节回顾三代设计,理解它们对同一问题(对象零售的缓存友好与记账成本)的不同答案,也解释了 21.2 节 SLUB 结构为何长成现在的样子。


21.1.1 为什么页分配器之上还需要一层

伙伴系统有三个无法直接服务对象分配的短板:

问题                          SLUB 的解法
──────────────────────────────────────────────────────────────
1. 粒度过粗: 对象 128B,        slab = 1..N 页的"容器",
   最小供货 4KB → 平均浪费      内切 N 个等大对象,
   94% 以上 (若整页单对象)      零售粒度 = 对象大小
2. 初始化重复: 释放/再分配      ctor 构造函数 + SLAB_TYPESAFE_BY_RCU
   间对象状态可复用             (不清零、不重构造, 只重声明)
3. 局部性差: 相邻分配落点      分配点集中在 per-CPU 捆上,
   随机散布于物理内存           同 CPU 热对象物理相邻

量化第一点:一个 128 字节的对象若独占一页,浪费 (4096-128)/4096 = 96.9%;切成 32 个/页后零浪费(除元数据)。而内核每天要创建销毁数百万个这样的对象(进程的 mm_struct、VFS 的 dentry、网络的 skb 描述符……)。

一次 dentry 创建销毁的两种命运:

 无对象缓存:                     有 SLUB:
 alloc_pages(1)  ~1μs+          kmem_cache_alloc(dentry_cache)
 填充初始化 4096B                 └─ main 捆弹出 ~20ns
 用 128B                          (对象在 CPU 缓存 L1 里, 热!)
 释放 → 整页回 buddy             kmem_cache_free → 入捆 ~20ns
 (冷页, TLB miss, 后续         ( freed 状态保留初始化,
  再分配是冷的)                   d_revalidate 等字段天然复用)

另一个动机是同类型对象的规模可预测:内核知道自己会有多少 dentry、多少 skbuff_head,专用缓存按类型统计与调参(/proc/slabinfo、/sys/kernel/slab/<cache>/)成为观测内核内存构成的基本手段——18 章 totalram 之外的"slab 记账"视角。

21.1.2 SLAB:多链表的精细管理(已移除)

原始 SLAB 设计把每个缓存分为三种颜色带、三级链表(full/partial/free),每 CPU 一个数组缓存(array cache),对象内嵌 freelist。它的优点是元数据精细、锁竞争被 per-CPU 数组隔离;缺点也随之而来:

  • 元数据外置:链表头、数组缓存在对象之外,结构复杂;
  • 多队列维护成本:full/partial/free 三链的状态转换在高并发下锁开销显著;
  • NUMA 时代退化:精细的着色(cache coloring)在物理索引缓存上失效。
SLAB 的对象布局与三链 (回忆图):

 kmem_cache
   ├─ arrays[cpu]: 每 CPU 数组缓存 (热对象)
   └─ slabs 链:    slabs_full / slabs_partial / slabs_free
        slab: [slab_head][obj0][obj1]...[objN]
              (着色偏移使 obj0 落在不同 cache line)

SLAB 于 6.8(2024 年初)被移除主干——mm/slab.c 在本树已不存在。移除的理由在当时的合并说明中归纳为"SLUB 已覆盖其全部调试能力且更快、更省元数据,维护两套实现的双倍成本不再值得"。

21.1.3 SLOB:极简首选适应(已移除)

SLOB(Simple List Of Blocks)面向 2MB 以下内存的嵌入式场景:整个分配器就是若干 kmalloc 档的首选适应空闲链,对象与块共用地址空间。它的内存占用最小(几乎没有元数据),但碎片随时间恶化、无调试设施、无 NUMA 感知。SLOB 于 6.4(2023 年)移除——彼时内核最小配置的内存目标已被 SLUB_TINY 覆盖。

SLOB 的碎片恶化示意:

 初始: [128B][128B][128B][128B][128B][128B]  (一个 768B 块)
 分配 A(256B): 首选适应切前两格
 分配 B(128B): 切第三格
 释放 A:       两格空洞 — 小于任何后续请求 → 永久浪费
 时间演化: 碎片率单调上升, 无凝聚机制 (buddy 的
   伙伴合并思想完全缺席)

21.1.4 SLUB:极简元数据 + per-CPU 热路径

SLUB(SLab UBiquitous)的原始设计哲学是把元数据压到极限:

SLUB 的两个原始招式 (6.x 时代形态):

1. 对象即链表节点: freelist 指针直接藏在空闲对象体内
   (21.5 节将看到 7.0 甚至把它藏进对象"中点"防溢出)
2. slab 即 page: struct slab 复用 struct page 的位布局
   (本树 mm/slab.h:74-99 + SLAB_MATCH 断言 :100-112)
   → 无需独立元数据分配, 18.4 节的 page 数组直接可用

每 CPU 缓存演进:
  6.x 及以前: cpu_slab->freelist/tid 单链指针
    分配 = 弹出 freelist 头 (一条 CAS)
  7.0 (本树): cpu_sheaves 成捆对象数组
    分配 = main->objects[--size]  (数组栈顶弹出, 连 CAS 都省)
    (slub.c:420-425 struct slub_percpu_sheaves)

sheaves(捆) 是 7.0 本树的关键新形态:per-CPU 缓存从"一条 freelist 指针"升级为"两捆对象数组 + 一个 RCU 批量释放捆"(main/spare/rcu_free,21.4 节展开)。批量化的收益有三:单次慢路径补货一次搬一捆对象(摊薄锁与 partial 链操作)、kfree_rcu() 按捆整批延迟(rcu_free 捆,slub.c:5790 rcu_free_sheaf)、热路径的数组栈顶操作对缓存行更友好。

三代 per-CPU 快路径的成本对比 (分配一次):

 SLAB:  数组缓存弹出 + 引用计数 + 可能的 batch 补货
 SLOB:  空闲链首选适应搜索 (O(碎片数))
 SLUB 旧: freelist 指针 CAS 弹出 (一条原子指令)
 SLUB 7.0: objects[--size]        (数组下标减一, 无原子!)
          (local_trylock 失败才走慢路径 —
           21.3.2 节 alloc_from_pcs)

21.1.5 三代对比表

维度 SLAB(已移除) SLOB(已移除) SLUB(7.0)
设计蓝本 Solaris Bonwick 嵌入式极简 极简元数据 + per-CPU
元数据 外置多链表+数组缓存 几乎为零 slab 复用 page + sheaves
per-CPU 数组缓存 无 main/spare/rcu 三捆
碎片控制 着色+三链 差 partial 链 + barn 捆池
调试设施 最丰富 无 red zone/poison/trace/kfence
定位 服务器经典 <2MB 嵌入式 全场景默认

21.1.6 SLUB_TINY:现代的"最小配置"

SLOB 移除后,嵌入式极简需求由 CONFIG_SLUB_TINY 承接。本树的代码大量使用它做裁剪判断:

// mm/slab.h:274-277(要点)
static inline bool cache_has_sheaves(struct kmem_cache *s)
{
    /* Test CONFIG_SLUB_TINY for code elimination purposes */
    return !IS_ENABLED(CONFIG_SLUB_TINY) && s->sheaf_capacity;
}

SLUB_TINY 下:禁用 sheaves(所有分配走慢路径)、合并大部分调试批处理、牺牲性能换最小内存占用——这正是 SLOB 用户的迁移通道。21.2 节的 bootstrap_sheaf 与 cache_has_sheaves() 分支是阅读代码时必须跟踪的编译期岔路。


小结

三代分配器回答同一个问题而取舍各异:SLAB 以精细元数据换管理能力、SLOB 以零元数据换内存下限、SLUB 以"对象即链表、slab 即 page"的极简主义胜出并在 7.0 演进出成捆批发的 sheaves 结构。理解这段历史的意义在于:SLUB 代码中每处看似多余的抽象(sheaf 容量、barn 池、TINY 分支)都是某代设计教训的直接沉淀。下一节进入 7.0 SLUB 的数据结构本体。

21.2 SLUB 数据结构 —— kmem_cache 与 slab

SLUB 的数据结构分三层:kmem_cache 是"对象仓库"的管理者(每类对象一个),struct slab 是"一整页(或几页)的容器"(复用 struct page 位布局),slab_sheaf 是 per-CPU 的"一捆对象"(7.0 新形态)。本节逐层拆解,并解释对象布局 calculate_sizes() 如何把 freelist 指针、调试区与对齐编排进 s->size。


21.2.1 kmem_cache:仓库管理者

// mm/slab.h:197-265(节选, 保留关键注释)
struct kmem_cache {
    struct slub_percpu_sheaves __percpu *cpu_sheaves;   /* :199 每 CPU 捆 */
    /* Used for retrieving partial slabs, etc. */
    slab_flags_t flags;
    unsigned long min_partial;      /* 节点至少保留的 partial slab 数 */
    unsigned int size;      /* Object size including metadata */
    unsigned int object_size;   /* Object size without metadata */
    struct reciprocal_size; /* reciprocal_value: 除法换倒数乘法 */
    unsigned int offset;        /* Free pointer offset */
    unsigned int sheaf_capacity;    /* 每捆对象数 */
    struct kmem_cache_order_objects oo; /* 理想: 一 slab 的阶×对象数 */
    ...
    void (*ctor)(void *object); /* 对象构造函数 */
    unsigned int inuse;     /* Offset to metadata */
    unsigned int align;     /* Alignment */
    unsigned int red_left_pad;  /* Left redzone padding size */
    const char *name;       /* Name (only for display!) */
    struct list_head list;      /* 挂入 slab_caches 全链 */
    ...
#ifdef CONFIG_SLAB_FREELIST_HARDENED
    unsigned long random;       /* freelist 混淆密钥 (21.5 节) */
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
    unsigned int *random_seq;   /* 对象序随机排列 (21.5 节) */
#endif
    ...
    struct kmem_cache_node *node[MAX_NUMNODES]; /* :265 节点级 partial */
};

三个字段值得停顿:size 与 object_size 的差是元数据(freelist 槽、调试区、对齐填充)——用户申请 100 字节,实际 stride 可能是 112 或更多;oo(order-objects) 打包"slab 阶与每 slab 对象数"为一个字(kmem_cache_order_objects,slab.h:188-192 的"Word size structure"注释),使"进货一页组货 N 个对象"成为一个原子量;cpu_sheaves 是 per-CPU 指针(18.2 节 percpu 机制的直接消费者),快路径完全无锁。

min_partial 是碎片治理的核心参数:节点 partial 链至少保留这么多半空 slab,避免"刚释放的对象所在 slab 立刻被还页→马上又要新页"的抖动(21.4 节)。

21.2.2 struct slab:一页容器的元数据

// mm/slab.h:74-99(节选)
/* Reuses the bits in struct page */
struct slab {
    memdesc_flags_t flags;

    struct kmem_cache *slab_cache;
    union {
        struct {
            struct list_head slab_list; /* partial 链节点 */
            /* Double-word boundary */
            struct freelist_counters;   /* 并发 freelist 计数 */
        };
        struct rcu_head rcu_head;
    };

    unsigned int __page_type;
    atomic_t __page_refcount;
#ifdef CONFIG_SLAB_OBJ_EXT
    unsigned long obj_exts;     /* 对象级扩展(memcg/kasan) */
#endif
};

#define SLAB_MATCH(pg, sl)                      \
    static_assert(offsetof(struct page, pg) == offsetof(struct slab, sl))
SLAB_MATCH(flags, flags);
SLAB_MATCH(compound_head, slab_cache);  /* Ensure bit 0 is clear */
SLAB_MATCH(_refcount, __page_refcount);
...
static_assert(sizeof(struct slab) <= sizeof(struct page));  /* :112 */

struct slab 不是独立分配的元数据——它就是 slab 首页的 struct page 本身,靠 SLAB_MATCH 静态断言保证两者字段布局逐一对齐。这是 18.4.1 节"一页多义"设计在 slab 语境的再现:page 的 mapping/lru 槽位在 slab 语义下是 slab_cache/slab_list。page_slab()(slab.h:132-145)经 page->page_type >> 24 == PGTY_slab 判别一页是否属于 slab——18.4.2 节"page_type 与 _mapcount 互斥"的判别位。

slab 本体是 folio:slab_folio()(slab.h:150-156)与 alloc_frozen_pages() 进货(slub.c:3290-3298 的 __SetPageSlab(page))表明一个 slab 就是一个复合 folio——高阶 slab(单页放不下对象时自动升阶)由 18.4 节的 compound 机制自然承载。

21.2.3 slab_sheaf 与三级缓存

// mm/slub.c:396-425
struct node_barn {          /* :396 节点级捆池 */
    spinlock_t lock;
    struct list_head sheaves_full;
    struct list_head sheaves_empty;
    unsigned int nr_full;
    unsigned int nr_empty;
};

struct slab_sheaf {         /* :407 */
    union {
        struct rcu_head rcu_head;
        struct list_head barn_list;
        /* only used for prefilled sheafs */
        struct { unsigned int capacity; bool pfmemalloc; };
    };
    struct kmem_cache *cache;
    unsigned int size;      /* 当前已存对象数 */
    int node; /* only used for rcu_sheaf */
    void *objects[];        /* 柔性数组: 对象指针栈 */
};

struct slub_percpu_sheaves {        /* :420 */
    local_trylock_t lock;
    struct slab_sheaf *main; /* never NULL when unlocked */
    struct slab_sheaf *spare; /* empty or full, may be NULL */
    struct slab_sheaf *rcu_free; /* for batching kfree_rcu() */
};
7.0 SLUB 三级对象缓存:

 [per-CPU]  pcs->main   满/部分满的捆  ←分配/释放的主战场(无锁 trylock)
            pcs->spare  空捆或满捆    ← main 空了换 spare, 满了换 spare
            pcs->rcu_free  批量 kfree_rcu 缓冲
                │ main 空 & spare 空/满时
                v
 [节点]     node->barn  空捆池+满捆池    ← per-CPU 捆的补给站(自旋锁)
                │ barn 空时
                v
 [节点]     node->partial  半空 slab 链  ← 从 slab 里现切对象
                │ partial 空时
                v
 [伙伴系统]  new_slab() → alloc_frozen_pages()  ← 批发整页

三级各有明确分工:main/spare 是零竞争的数组栈操作(local_trylock 失败说明本 CPU 被中断上下文占着,直接走慢路径而不是自旋等待——21.3 节);barn 以捆为粒度缓冲 per-CPU 与 slab 之间的流量,空捆承接逐出、满捆供给补给;partial 以 slab 为粒度承接 barn 的上游。cache_has_sheaves()(slab.h:273-277)区分"真捆"与 bootstrap/调试/TINY 场景的退化直连。

21.2.4 calculate_sizes():对象布局的总设计

s->size(对象 stride)如何长出来的(mm/slub.c:7665-7800):

// mm/slub.c:7665(节选)
static int calculate_sizes(struct kmem_cache_args *args, struct kmem_cache *s)
{
    unsigned int size = s->object_size;
    ...
    size = ALIGN(size, sizeof(void *));     /* 字对齐 */
    ...
    s->inuse = size;            /* :7705 对象+redzone 的边界 */

    if (((flags & SLAB_TYPESAFE_BY_RCU) && !args->use_freeptr_offset) ||
        (flags & SLAB_POISON) || (s->ctor && ...) || ...) {
        s->offset = size;       /* freelist 槽放对象尾后 */
        size += sizeof(void *);
    } else if (...) {
        s->offset = args->freeptr_offset;   /* 调用者指定 */
    } else {
        /*
         * Store freelist pointer near middle of object to keep
         * it away from the edges of the object to avoid small
         * sized over/underflows from neighboring allocations.
         */
        s->offset = ALIGN_DOWN(s->object_size / 2, sizeof(void *)); /* :7735 */
    }
    ...
    if (flags & SLAB_STORE_USER) {      /* :7744 泄漏跟踪 */
        size += 2 * sizeof(struct track);
        ...
    }
    ...
    if (flags & SLAB_RED_ZONE) {        /* :7765 左 redzone 填充 */
        ...
        s->red_left_pad = sizeof(void *);
        s->red_left_pad = ALIGN(s->red_left_pad, s->align);
        size += s->red_left_pad;
    }
    /* 此后: 按 align 对齐 size, 用 calculate_order() 选 oo 阶 */
}
典型对象布局 (无调试, 100B 请求):

offset 0                    50      100        112
       ┌────────────────────┬──┬──────┬─────┐
       │   对象可用空间       │FP│ pad  │     │   FP = freelist 槽
       └────────────────────┴──┴──────┴─────┘        藏在对象中点(:7735)
       <─── object_size ──>        <── s->size = 112 ──>
       注: freeptr_outside_object()(slub.c:552) 的判别即 offset >= inuse

带调试布局 (STORE_USER+RED_ZONE):
       ┌─────┬──────────────┬─────┬────────┬────────┬──────┐
       │L-red│    对象       │R-red│ track A│ track F│ kmreq │
       └─────┴──────────────┴─────┴────────┴────────┴──────┘
        red_left_pad (:7765)   alloc/free 轨迹   原始请求大小

freelist 槽中点埋藏(:7727-7740 的注释原文"avoid small sized over/underflows from neighboring allocations")是 7.0 的安全姿态:旧版把它放在对象第一个字,堆溢出恰好写坏链表;藏进中点后,小尺寸越界(最常见的溢出形态)摸不到指针——21.5 节展开这条攻防线。


小结

SLUB 的三层结构各有清晰边界:kmem_cache 管理仓库参数与三级缓存入口(cpu_sheaves/node[]),struct slab 复用 page 位布局零成本承载容器元数据,slab_sheaf 以对象数组捆为粒度承接 per-CPU 流量并用 node barn 池缓冲。calculate_sizes() 把 freelist 槽(默认藏对象中点)、track 区、red zone、对齐编排成 s->size 的最终 stride。下一节沿这三级缓存走通分配与释放的完整路径。

21.3 对象分配与释放流程

本节走通 SLUB 的完整分配/释放链路:kmem_cache_alloc_noprof() 的统一入口如何先尝试 per-CPU 捆快路径(alloc_from_pcs())、失败后落入慢路径(___slab_alloc())的 partial 链与新 slab 进货;释放则对称地先入捆(free_to_pcs())、跨 CPU 时走 __slab_free() 的 CAS 链。通用接口 kmalloc() 在此之上按大小选档转发。


21.3.1 分配总调度:slab_alloc_node

// mm/slub.c:4837-4869(节选)
static __fastpath_inline void *slab_alloc_node(struct kmem_cache *s, struct list_lru *lru,
        gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
    void *object;
    bool init = false;

    s = slab_pre_alloc_hook(s, gfpflags);       /* memcg/kasan 前钩 */
    if (unlikely(!s))
        return NULL;

    object = kfence_alloc(s, orig_size, gfpflags);  /* :4847 kfence 拦截池 */
    if (unlikely(object))
        goto out;

    object = alloc_from_pcs(s, gfpflags, node); /* :4850 快路径 */

    if (!object)
        object = __slab_alloc_node(s, gfpflags, node, addr, orig_size); /* :4852 */

    maybe_wipe_obj_freeptr(s, object);      /* init_on_alloc 配合 */
    init = slab_want_init_on_alloc(gfpflags, s);
out:
    slab_post_alloc_hook(s, lru, gfpflags, 1, &object, init, orig_size);
    return object;
}

slab_pre_alloc_hook/slab_post_alloc_hook 是 memcg 记账(12 章)与 KASAN/调试的统一挂点;kfence_alloc() 以采样概率把分配改道 KFENCE 越界检测池(21.5 节)。三个对外入口共享此调度:

// mm/slub.c:4871-4880
void *kmem_cache_alloc_noprof(struct kmem_cache *s, gfp_t gfpflags)
{
    void *ret = slab_alloc_node(s, NULL, gfpflags, NUMA_NO_NODE, _RET_IP_,
                    s->object_size);
    trace_kmem_cache_alloc(_RET_IP_, ret, s, gfpflags, NUMA_NO_NODE);
    return ret;
}
EXPORT_SYMBOL(kmem_cache_alloc_noprof);

kmem_cache_alloc_node_noprof()(slub.c:4916-4921)仅多传 node 指定 NUMA 归属;__kmalloc_noprof()(slub.c:5270)先查 size <= KMALLOC_MAX_CACHE_SIZE(slab_common.c:785)决定走通用档缓存还是 kmalloc_large 直接向伙伴系统取页。

21.3.2 快路径:alloc_from_pcs

// mm/slub.c:4668-4753(节选)
static __fastpath_inline
void *alloc_from_pcs(struct kmem_cache *s, gfp_t gfp, int node)
{
    ...
    node_requested = IS_ENABLED(CONFIG_NUMA) && node != NUMA_NO_NODE;
    if (unlikely(node_requested && node != numa_mem_id())) {
        stat(s, ALLOC_NODE_MISMATCH);
        return NULL;            /* 跨节点请求不碰本地捆 */
    }

    if (!local_trylock(&s->cpu_sheaves->lock))
        return NULL;            /* 捆被占(中断中): 立即让路 */

    pcs = this_cpu_ptr(s->cpu_sheaves);

    if (unlikely(pcs->main->size == 0)) {       /* main 捆空 */
        pcs = __pcs_replace_empty_main(s, pcs, gfp);
        if (unlikely(!pcs))
            return NULL;
    }

    object = pcs->main->objects[pcs->main->size - 1];   /* 栈顶对象 */

    if (unlikely(node_requested)) {
        /* CPU 迁移窗口: 对象可能来自别的节点, 校验 */
        if (page_to_nid(virt_to_page(object)) != node) {
            local_unlock(&s->cpu_sheaves->lock);
            stat(s, ALLOC_NODE_MISMATCH);
            return NULL;
        }
    }

    pcs->main->size--;

    local_unlock(&s->cpu_sheaves->lock);

    stat(s, ALLOC_FASTPATH);

    return object;
}

快路径的全部操作是数组栈顶弹出一个指针(objects[--size])——没有 CAS、没有链表遍历,local_trylock 是纯本地的抢占保护(失败即放弃而非自旋,让中断上下文的短暂占用不阻塞任何线程)。__pcs_replace_empty_main() 处理补货:先试 spare 捆(21.4 节),再从 node barn 取满捆,最后从 partial slab 现切一捆——每次补货搬一捆对象,慢路径成本被 1/N 摊薄。

21.3.3 慢路径:___slab_alloc 的三级回退

// mm/slub.c:4374-4451(节选, 保留三级策略注释)
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
               unsigned long addr, unsigned int orig_size)
{
    ...
new_objects:
    pc.flags = gfpflags;
    /*
     * 指定了首选节点且无 __GFP_THISNODE 时:
     * 1) 先只对目标节点找 partial (给 pc.flags 加 __GFP_THISNODE)
     * 2) 失败则以 NOWAIT|THISNODE 试图在目标节点开新 slab
     * 3) 再失败: 恢复原始 gfp, 允许扫其他节点 partial / 他节点进货
     */
    if (unlikely(node != NUMA_NO_NODE && !(gfpflags & __GFP_THISNODE)
             && try_thisnode)) {
        ...
        pc.flags = GFP_NOWAIT | __GFP_THISNODE;
    }

    pc.orig_size = orig_size;
    object = get_from_partial(s, node, &pc);    /* 局部链取对象 :4416 */
    if (object)
        goto success;

    slab = new_slab(s, pc.flags, node);     /* 批发新页 :4421 */

    if (unlikely(!slab)) {
        if (node != NUMA_NO_NODE && !(gfpflags & __GFP_THISNODE)
            && try_thisnode) {
            try_thisnode = false;
            goto new_objects;   /* 放宽节点限制重跑 */
        }
        slab_out_of_memory(s, gfpflags, node);
        return NULL;
    }
    ...
    alloc_from_new_slab(s, slab, &object, 1, allow_spin);
    ...
success:
    if (kmem_cache_debug_flags(s, SLAB_STORE_USER))
        set_track(s, object, TRACK_ALLOC, addr, gfpflags);
    return object;
}

三级策略的骨架是"本地性逐级放宽":目标节点 partial → 目标节点新 slab → 任意节点 partial/新 slab。get_from_partial() 内部经 alloc_single_from_partial()(slub.c:3649)从 partial slab 的 freelist 弹对象并把 slab 在链上重排;new_slab()(slub.c:3285-3300)调用 alloc_frozen_pages()(18.3 节接口,__SetPageSlab(page) 置页类型,:3296)整页进货,随后 alloc_single_from_new_slab()(slub.c:3687)或批量把首批对象喂进 per-CPU 捆。

分配全链路汇总 (无调试、命中快路径为 1 步):

 kmem_cache_alloc(s)
  └─ slab_alloc_node()                    slub.c:4837
      ├─ kfence_alloc 采样改道             :4847
      ├─ alloc_from_pcs                   :4668
      │   ├─ trylock 失败 → 让路慢路径
      │   ├─ main 空 → __pcs_replace_empty_main (spare/barn/切 slab)
      │   └─ objects[--size]  ← 命中: 全程 ~20 条指令
      └─ __slab_alloc_node → ___slab_alloc  :4453; :4374
          ├─ get_from_partial (本节点 → 他节点)
          ├─ new_slab → alloc_frozen_pages (伙伴系统, 18.3)
          └─ slab_out_of_memory 报警

21.3.4 释放:入捆与跨 CPU 链

// mm/slub.c:5768-5788(节选)
bool free_to_pcs(struct kmem_cache *s, void *object, bool allow_spin)
{
    struct slub_percpu_sheaves;

    if (!local_trylock(&s->cpu_sheaves->lock))
        return false;           /* 同快路径让路策略 */

    pcs = this_cpu_ptr(s->cpu_sheaves);

    if (unlikely(pcs->main->size == s->sheaf_capacity)) {
        pcs = __pcs_replace_full_main(s, pcs, allow_spin);
        if (unlikely(!pcs))
            return false;       /* 满捆无处安放: 走慢路径 */
    }

    pcs->main->objects[pcs->main->size++] = object; /* 栈顶压入 */

    local_unlock(&s->cpu_sheaves->lock);

    stat(s, FREE_FASTPATH);

    return true;
}

快路径释放是快路径分配的镜像:满捆时 __pcs_replace_full_main() 把满捆送 barn、换空捆回来。当对象属于非当前 CPU 或非当前捆的 slab 时,kmem_cache_free() 转入 __slab_free()(slub.c:5474-5528):

// mm/slub.c:5474(要点)
static void __slab_free(struct kmem_cache *s, struct slab *slab,
            void *head, void *tail, int cnt,
            unsigned long addr)
{
    ...
    do {
        /* CAS: 把 [head..tail] 插回 slab 的 freelist 头 */
    } while (!slab_update_freelist(s, slab, &old, &new, "__slab_free"));    /* :5528 */

    /* slab 从 full → partial 的转换点:
       这里检查 was_full, 把 slab 挂上 node->partial 链,
       并按 min_partial 决定是否留仓 (21.4 节) */
}

slab_update_freelist() 是一条 cmpxchg 双字(freelist 指针 + 计数,防 ABA——19.3 节 ABx 计数的 slab 版),把 [head..tail] 段插回 slab。释放路径的批量化同样贯穿:kfree_rcu() 的对象先攒进 pcs->rcu_free 捆,满捆后整捆经 RCU 回调 rcu_free_sheaf()(slub.c:5790-5850)一次性解钩、还仓——延迟释放的批处理粒度从对象升到捆。

21.3.5 kmalloc:大小分档的转发层

// mm/slab_common.c:785
    if (size && size <= KMALLOC_MAX_CACHE_SIZE) {
        /* 走通用档 kmem_cache */

通用分配 __kmalloc_noprof()(slub.c:5270)把请求大小映射到 kmalloc_caches[type][idx] 档位(8/16/32…4MB,2 的幂,KMALLOC_MIN_SIZE 起步按编译配置可跳过低档,slab_common.c:901-930 的 BUILD_BUG_ON 与循环);超过 KMALLOC_MAX_CACHE_SIZE 转为 kmalloc_large——直接向伙伴系统按页申请并在 page 上记大页类型(18.4 节 page_type)。缓存类型枚举(include/linux/slab.h:629-642)区分 KMALLOC_NORMAL/CGROUP/RECLAIM/DMA 四类:memcg 记账对象落到 CGROUP 副本、可回收对象落 RECLAIM 副本,让 12 章 cgroup 的 slab 记账无需逐对象判断。


小结

SLUB 的分配链是"入口挂钩 → kfence 采样 → per-CPU 捆弹出 → 三级回退(本节点 partial → 本节点新页 → 放宽节点重试)",快路径把成本压到数组栈顶一弹,慢路径的每次进货都按捆或按页批量化。释放链镜像对称:入捆、满捆换仓、跨 CPU 时以 cmpxchg 双字插回 slab freelist 并维护 full/partial 转换。kmalloc 层按 2 的幂分档转发,四类缓存副本承接记账差异。下一节聚焦这套机制如何治理碎片——partial 链、min_partial 与 barn 捆池的协作。

21.4 per-CPU sheaves 与碎片管理

SLUB 的碎片问题有两个维度:slab 内碎片(对象 stride 与请求的差,21.2.4 节 calculate_sizes 的课题)与 slab 间碎片(大量"半空 slab"驻留内存却不供货)。本节分析 7.0 的治理方案:per-CPU sheaves 把热路径流量与 slab 状态解耦,node barn 以捆为粒度缓冲,partial 链以 slab 为粒度保底,min_partial 水位控制何时还页给伙伴系统。


21.4.1 捆的流转:main/spare 与 barn

一次典型的"消耗-补货"循环 (capacity = 32):

 main 捆消耗           __pcs_replace_empty_main() 的补给顺序
 ┌─────────┐
 │#### ▓   │ main 空  ──┬─ 1. spare 满? 交换 main<->spare   (零成本)
 └─────────┘            ├─ 2. barn 满捆池有货? 取一捆      (自旋锁+链摘)
                        ├─ 3. 从 partial slab 现切一捆    (slab 锁)
                        └─ 4. 开新 slab 批发后灌满一捆

 释放侧对称:
 main 满 ──┬─ 1. spare 空? 满捆挪去 spare
           ├─ 2. barn 空捆池有空位? 满捆存入 (换出空捆)
           └─ 3. barn 满? 整捆对象退回各 slab (__sheaf_... / __slab_free)

 main/spare/barn 的库存视角:

 per-CPU:  main[██──]  spare[████]  ← 零成本互转
                    ↕ (捆为粒度的流量)
 node barn: full 池 [捆][捆]   empty 池 [捆][捆]
                    ↕ (满捆吸收/供给)
 slabs:     partial [s1 半空][s2 半空]  ← min_partial 保底

alloc_from_pcs()(slub.c:4700-4710)里 main 空时调用的 __pcs_replace_empty_main() 与 free_to_pcs()(slub.c:5774-5779)里 main 满时调用的 __pcs_replace_full_main() 是两个补给/退换枢纽;slub.c:4660-4666(pcs->spare = full 与 barn_put_full_sheaf(barn, full))展示满捆优先回流 spare、barn 收尾的次序。barn 的双向缓冲把"per-CPU 捆的临时盈缺"与"slab 的实际状态"隔开:CPU 间流量在捆粒度就消化掉大半,真正穿透到 slab 锁的只剩 barn 池空转的场景。

21.4.2 kmem_cache_node:partial 链与保底水位

// mm/slub.c:428-438
struct kmem_cache_node {
    spinlock_t list_lock;
    unsigned long nr_partial;
    struct list_head partial;       /* 半空 slab 链 */
#ifdef CONFIG_SLUB_DEBUG
    atomic_long_t nr_slabs;
    atomic_long_t total_objects;
    struct list_head full;
#endif
    struct node_barn *barn;         /* :437 捆池 */
};

partial 链的"半空 slab"承担对象回访点:释放对象把 slab 从 full 转入 partial(__slab_free() 的转换点,slub.c:5528 后),分配从中取货。何时把半空 slab 还给伙伴系统是碎片治理的裁判点:

  • s->min_partial:节点至少保留的半空 slab 数——低于它时释放侧绝不还页(避免"还页→立即又要页"抖动);
  • 高于水位且 slab 空闲比例足够时,put_cpu_partial/退页路径把 slab 整页还伙伴系统(18.3 节 __free_pages 侧对应 __SetPageSlab 的清除);
  • SLAB_TINY 或调试模式直连场景(cache_has_sheaves() 为假)下,partial 链直接面对每对象操作。
半空 slab 的生命周期状态机:

   新 slab 进货           最后一个对象被取走
  ┌──────────┐  ──────>  ┌──────────┐  ──────>  ┌──────┐
  │  EMPTY   │           │ PARTIAL  │           │ FULL │
  │ (刚批发)  │  <──────  │ (在链上)  │  <──────  │(离链) │
  └──────────┘  放回对象   └──────────┘  放回对象  └──────┘
       │            首个对象        ^   释放首个对象       │
       │ 无人认领(高水位退页)         │                    │
       v                          │                    │
   还给伙伴系统 ────────────────────┘    min_partial 保底 ←┘

21.4.3 批量接口:bulk 与 RCU 捆

对象级流量还有两条批量化通道:

[1] bulk 分配/释放。 alloc_from_pcs_bulk()(slub.c:4756 起)一次从 main 捆连续弹出 N 个对象,捆不足时 ___slab_alloc 的批量变体直接从新 slab 整段划拨;kmem_cache_free_bulk() 走 slub.c:2849-2883 的注释路径——"在 cpu_sheaves->lock 外分批处理"(__kmem_cache_free_bulk 在锁内成段入捆),把 memcg/kasan 钩子的开销同样摊薄。

[2] kfree_rcu 的整捆延迟。 pcs->rcu_free 捆攒满后整捆交 RCU:

// mm/slub.c:5790(节选)
static void rcu_free_sheaf(struct rcu_head *head)
{
    sheaf = container_of(head, struct slab_sheaf, rcu_head);
    s = sheaf->cache;
    ...
    if (__rcu_free_sheaf_prepare(s, sheaf))     /* 解钩(含调试过滤) */
        goto flush;
    n = get_node(s, sheaf->node);
    ...
    /* 满捆直接存回 barn 满捆池: 对象一次都不落 slab */
}

优雅之处在于满捆对象可能一次都不回到 slab:宽限期结束后整捆存入 barn 满捆池,等待下一个 CPU 的 main 补货直接取走——slab 的 freelist 完全未被触碰。读侧(RCU 宽限期)与写侧(补货)天然解耦,这是"捆粒度批发"对延迟释放的红利。

21.4.4 碎片的观测与调参

观测入口:

 /proc/slabinfo        每缓存: 活跃对象/总数, slab 数, 阶
 /sys/kernel/slab/<c>/  order, objects, partial, cpu_sheaves...
   alias 子目录         合并别名缓存共享同一仓库 (21.1 节)

调参入口:

 slub_debug=KPZ,...    启动参数开调试区(红区/毒化/trace)
 min_partial (sysfs)   节点保底半空 slab 数
 slab_merge/nomerge    控制同规格缓存合并 (slab_common.c:60)
 sheaf_capacity        捆容量 (auto: 按对象大小与缓存行折算)

 碎片恶化的典型信号:
 slabinfo 里 "对象总数高而 active 占比低"
 → 大量半空 slab 驻留。治理顺序:
   [1] 查泄漏 (STORE_USER 定位分配点, 21.5.3)
   [2] 评估 min_partial 水位
   [3] cgroup 场景查 memory.kmem 绑死
       (slab 无法跨 cgroup 复用 = 伪碎片, 12 章)

21.4.5 三级流量的量化直觉

一次 1 万次 alloc/free 脉冲的分流估算 (capacity=32, 4 CPU):

 10000 次分配
   ├─ ~9700 次: main 捆命中 (数组弹出, ~20ns)   ← 97%
   ├─ ~250 次: 触发补给 (每次搬 32 个) —
   │           大半由 spare/barn 承接 (捆粒度)
   └─ ~50 次: 穿透到 partial/新 slab (批发生意)
 对比无捆设计: ~10000 次都碰 freelist CAS,
   争用同一缓存行 — 多核扩展性差一个数量级

 慢路径成本 ÷ 捆容量 = 摊薄后的单次成本
 这就是"捆"存在的全部数学

小结

7.0 的碎片治理是三级流量的分层:per-CPU main/spare 捆消化零成本盈缺,node barn 以捆粒度缓冲跨 CPU 流量(满捆 RCU 回流甚至完全不触碰 slab),partial 链以 slab 粒度保底并受 min_partial 水位仲裁。bulk 接口与 rcu_free 捆把批处理粒度从对象提到捆,使"慢路径成本 ÷ 捆容量"的摊薄在所有通道成立。下一节盘点这套机制之上的安全加固——攻击者眼中 SLUB 是堆风水的主战场,防御方在对象布局与 freelist 指针上层层设防。

21.5 SLUB 安全加固 —— freelist 随机化与校验

SLUB 是内核堆攻击的主战场:一个 UAF(Use-After-Free)或堆溢出如果能把 freelist 指针改写成受控值,就能在下次分配时获得"任意地址对象"。SLUB 的加固体系从四个层次设防:指针混淆(XOR 编码让溢出写不出合法指针)、布局随机化(freelist 顺序与槽位位置不可预测)、损坏检测(red zone/poison/track)、以及替代隔离(kfence 采样、init-on-alloc 消息、usercopy 边界)。本节逐层取证,所有行号针对 7.0.10 本树。


21.5.1 freelist 指针混淆:SLAB_FREELIST_HARDENED

// mm/slub.c:494-524(节选)
/*
 * Returns freelist pointer (ptr). With hardening, this is obfuscated
 * with an XOR of the address where the pointer is held and a per-cache
 * random number.
 */
static inline freeptr_t freelist_ptr_encode(const struct kmem_cache *s,
                        void *ptr, unsigned long ptr_addr)
{
    unsigned long encoded;

#ifdef CONFIG_SLAB_FREELIST_HARDENED
    encoded = (unsigned long)ptr ^ s->random ^ swab(ptr_addr);
#else
    encoded = (unsigned long)ptr;
#endif
    return (freeptr_t){.v = encoded};
}

三个异或项各堵一条路:

成分 堵住的攻击
s->random(per-cache 密钥,slab.h:229-231) 攻击者知道地址也编码不出合法指针
ptr_addr(指针自身存放地址) 同一对象被搬到不同槽位时编码不同——把对象 A 的编码值抄给对象 B 无效
swab() 字节翻转 打断"XOR 线性"代数结构,多点采样推断密钥的难度陡增

配套的 set_freepointer()(slub.c:537-548)带一个零成本的双 free 现行捕获:

// mm/slub.c:540-542
#ifdef CONFIG_SLAB_FREELIST_HARDENED
    BUG_ON(object == fp); /* naive detection of double free or corruption */
#endif

"把对象 X 指向自己"是 double free 最常见的第一步——链表成环直接被 BUG_ON 拍死在案发现场。

21.2.4 节的槽位中点策略在此叠加第二层:s->offset = ALIGN_DOWN(s->object_size / 2, ...)(slub.c:7735)让指针藏在对象中部,最常见的"溢出/下溢一个字节到几个字"够不着它。freeptr_outside_object()(slub.c:552-556)维护"offset >= inuse 即指针在对象外"的判别约定供调试代码使用。

21.5.2 分配顺序随机化:SLAB_FREELIST_RANDOM

新 slab 的 freelist 默认按地址顺序串链——攻击者以同样规律请求对象即可推知邻居。随机化把对象在 slab 内的释放顺序打乱:

// mm/slub.c:3307-3341(节选)
static int init_cache_random_seq(struct kmem_cache *s)
{
    unsigned int count = oo_objects(s->oo);
    int err;

    /* Bailout if already initialised */
    if (s->random_seq)
        return 0;

    err = cache_random_seq_create(s, count, GFP_KERNEL);    /* 生成随机排列 */
    if (err) {
        pr_err("SLUB: Unable to initialize free list for %s\n", s->name);
        return err;
    }

    /* Transform to an offset on the set of pages */
    if (s->random_seq) {
        unsigned int i;
        for (i = 0; i < count; i++)
            s->random_seq[i] *= s->size;    /* 序号×stride=偏移 */
    }
    return 0;
}

每个缓存创建时生成一份 random_seq[] 随机排列(slab.h:662-673 声明,cache_random_seq_create 实现于 slab_common),新 slab 建 freelist 时按此排列串链。shuffle_freelist()(slub.c:3371)在 allocate_slab 路径调用。防御本质:堆风水需要"可控的分配/释放序列 → 可预测的物理相邻关系",随机排列让"第 N 次分配会得到哪个槽"不再确定。

21.5.3 损坏检测:red zone、poison 与 track

调试标志族(slub.c:272-288):

SLAB_RED_ZONE    对象前后埋哨兵, free/alloc 时校验   (布局见 21.2.4 图)
SLAB_POISON      释放时填充 0x6b, 分配时校验 0x6b    (UAF 写探针)
SLAB_STORE_USER  对象尾附 track: 分配/释放的调用栈   (泄漏/溢出定位)
SLAB_CONSISTENCY_CHECKS  链表与计数全量校验
slub_debug=FPZU  开启组合; slub_debug=<cachename> 定点开

red_left_pad(slub.c:957-966、7765-7780)的实现细节:左红区不是"对象前的一个字"而是独立填充区,kmem_cache_debug 路径返回对象地址时跳过它(slub.c:237-238 p += s->red_left_pad)——保证越界写"从左边打进来"也先撞红区。alloc_debug_processing/free_debug_processing(slub.c:1999-2002)在进出对象时全量走一遍校验清单。

这些设施的运行成本使其默认关闭、按 slub_debug 参数或 CONFIG_SLUB_DEBUG_ON 开启——生产环境的第一线是下一层的 kfence 与 init-on-free。

21.5.4 采样检测与初始化消毒

KFENCE(采样式检测池):slab_alloc_node() 的第 4847 行 kfence_alloc() 以低概率(默认每 100ms 一个样本)把分配改道独立守护池——对象四周是守护页,越界立即缺页报点。采样率低到生产可开,覆盖面靠时间积累。

init_on_alloc / init_on_free(sysctl 与编译开关):slab_alloc_node() 尾部 slab_want_init_on_alloc()(slub.c:4855)与 maybe_wipe_obj_freeptr()(:4854)组合,在分配时清零对象(或仅清 freeptr 槽)。防的是跨 privilege 边界的信息残留:内核对象里残留的密钥/指针被用户态触发的分配继承。init_on_free 反向在释放时清零,代价更高、防护更彻底。

HARDENED_USERCOPY:check_heap_object() 用 kmem_cache 的 useroffset/usersize(slab.h:252-256)校验 copy_to/from_user() 的范围确实落在"该对象声明可拷贝的区间"内——把"内核对象当缓冲区整块拷给用户"的越界读堵在拷贝层。

21.5.5 加固矩阵总览

层次 机制 配置 防的攻击 运行成本
指针编码 XOR 密钥+地址+swab FREELIST_HARDENED(默认 y) 溢出伪造 freelist 每次指针读写 2 次异或
槽位布局 指针藏对象中点 无开关(calculate_sizes) 小尺寸溢出摸指针 零
顺序随机 random_seq 排列 FREELIST_RANDOM(默认 y) 堆风水可预测性 建 slab 时一次排列
双 free 捕获 set_freepointer BUG_ON FREELIST_HARDENED double free 成环 一次比较
哨兵/毒化 red zone/poison slub_debug(默认关) 越界/UAF 写探针 高,调试用
采样检测 KFENCE 守护池 CONFIG_KFENCE(生产可开) 越界/UAF 现行 每百毫秒一个样本
消毒 init_on_alloc/free sysctl/编译 信息残留 分配/释放清零
边界校验 HARDENED_USERCOPY 默认 y 对象级越界用户拷贝 拷贝前检查

小结

SLUB 加固的哲学是"让默认配置挡住廉价攻击,把昂贵检测留给可选项":freelist 指针的 XOR 混淆(密钥×地址×swab 三重异或)与中点埋藏使溢出伪造指针失去可行性,random_seq 打乱堆风水时序,set_freepointer 的自指 BUG_ON 让 double free 现行;red zone/poison/track 作为 slub_debug 的重武器按需开启,KFENCE 以采样成本换取生产环境的越界现行捕获,init-on-alloc/free 消灭跨边界信息残留。至此 SLUB 的结构与攻防全部展开;下一章回到用户进程视角——虚拟内存区域 VMA 与进程地址空间。