K01 —— 物理页分配后的对象管理:SLAB/SLUB 分配器

金句:把固定大小的对象放在固定大小的页上,是内存分配器设计的核心。


Buddy System 之后为什么还需要 SLAB

Buddy System 管理的是 4KB、8KB、16KB… 4MB 的物理页框。但内核需要分配更小的对象——Task Struct(几十字节)、inode(几百字节)、dentry(几百字节)。

Bash
单一对象分配的问题(直接用 buddy):

假设需要分配 100 字节对象:
  - buddy 最小单位 4KB
  - 分配 4KB,浪费 3900 字节
  - 1000 个对象 → 浪费 3.9MB

解决方案:SLAB 分配器
  - 在 buddy 分配的页上,建立对象池(object cache)
  - 每个对象大小固定(2^n 字节)
  - 每个 SLAB 包含多个对象(填满整个 4KB 页)
  - 分配和释放都是 O(1)

4KB / 100 bytes = 40 对象/页
1000 个对象 → 只需要 25 页,而不是 1000

SLAB 的数据结构

Bash
SLAB 的结构:

struct slab {
    struct list_head list;       // 链表节点(连接 free/partial/full)
    unsigned long colouroff;     // 颜色偏移(缓存行对齐)
    void *s_mem;                 // 第一个对象的地址
    unsigned int inuse;         // 已分配对象数
    kmem_bufctl_t free;          // 空闲对象链表头
};

每个 SLAB 管理一个或多个 4KB 页:
  - 包含若干相同大小的对象
  - 空闲对象通过 free 链表串联
  - 已满的 SLAB 不参与分配

SLAB 状态:
  - free:所有对象空闲(不在 CPU cache 中分配)
  - partial:部分对象已分配
  - full:所有对象已分配

Bash
kmem_cache 结构(每个对象类型的缓存):

struct kmem_cache {
    const char *name;           // 缓存名称(如 "task_struct")
    size_t object_size;         // 对象大小(如 576 bytes)
    size_t size;                // 对齐后大小(如 640 bytes)
    size_t align;               // 对齐要求(如 64 bytes)
    void (*ctor)(void *);       // 构造函数(可选)

    struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 缓存
    struct list_head *node;     // NUMA 节点链表(每个节点有多个 SLAB)
    struct kmem_cache_order_objects size;
    struct kmem_cache_order_objects order;
};

关键设计:
  - 每个 CPU 有自己的 SLAB(无锁分配)
  - 每个 NUMA 节点有自己的 SLAB 链表
  - 分配优先从本地 CPU 的 SLAB 取

SLUB 分配器(现代 Linux 默认)

Bash
SLUB = SLAB with Minimal Optimization
  - 简化了 SLAB 的实现
  - 移除了 per-SLAB 的 free 链表
  - 使用 page->freelist 直接指向空闲对象

SLUB 的优势:
  1. 更少内存开销(不需要额外元数据)
  2. 更好的缓存友好性
  3. 更好的 NUMA 局部性

SLUB page 结构(嵌入在 struct page 中):

struct page {
    union {
        struct {
            unsigned long flags;          // 页面状态
            atomic_t _refcount;           // 引用计数
            unsigned long:25;             // 空闲对象数
            unsigned long inuse:16;
            void *freelist;              // 第一个空闲对象
        };
    };
    // ...
};

freelist 指向每个对象中的下一个空闲对象:
  freelist[0] → 对象 A
  freelist[A] → 对象 B
  freelist[B] → 对象 C
  freelist[C] = NULL(结束)

Bash
SLUB 分配流程:

__slab_alloc(slab, flags):
  // 1. 从 current CPU 的 slab 取
  c = get_cpu_ptr(cachep->cpu_slab);
  page = c->page;

  if (!page) // CPU slab 为空
    goto new_slab;

  // 2. 从 page->freelist 取对象
  object = page->freelist;
  page->freelist = object[next];  // 移动到下一个空闲对象
  page->inuse++;

  return object;

new_slab:
  // 3. 从 buddy 分配新页面
  page = alloc_pages(flags, order);

  // 4. 初始化 freelist
  page->freelist = objects; // 构建空闲链表
  page->inuse = 0;

  // 5. 更新 cpu_slab
  c->page = page;

  return object;

per-CPU 缓存和锁优化

Bash
SLUB 最大的优化:per-CPU 本地缓存

每个 CPU 维护自己的"当前 SLAB":
  - 分配时不需要获取锁(无竞争)
  - O(1) 时间
  - 避免了跨 CPU 的锁竞争

per-CPU slab 结构:

struct kmem_cache_cpu {
    void **freelist;   // 指向本地 SLAB 的空闲对象
    unsigned long tid; // 事务 ID(用于调试)
    struct page *page; // 当前 SLAB(大部分对象空闲)
    struct page *partial; // 部分空闲的 SLAB(本地)
};

CPU0 的分配:
  if (cpu_cache.frelist != NULL) {
      object = cpu_cache.freelist;
      cpu_cache.freelist = *(void**)object;
      return object;  // 无锁,O(1)
  }

跨 NUMA 节点的优化:
  - NUMA node 的 SLAB 优先分配给本地 CPU
  - 减少跨节点内存访问

kmalloc:通用对象分配

Bash
kmalloc 是 SLUB 之上的通用分配接口:

kmalloc(size, flags):
  - size:分配大小(1B ~ 8KB)
  - 返回:虚拟地址(对象起始地址)

kmalloc 的内部:
  - 把 size 映射到对应的 kmem_cache
  - kmalloc-32, kmalloc-64, kmalloc-128, ... kmalloc-8192
  - 每个 size 类别有一个独立的 kmem_cache

大小类别(Power of 2):
  32 / 64 / 128 / 256 / 512 / 1024 / 2048 / 4096 / 8192 bytes

实际分配示例:
  kmalloc(100, GFP_KERNEL)
  → 找到最接近的 size = 128
  → 从 kmalloc-128 cache 分配
  → 实际分配 128 bytes

slabinfo 查看缓存:
  $ slabtop
  OBJS ACTIVE  USE   SIZE  OBJECTS  SLABS
  12345   12000  97%    64   24567    1234   kmalloc-64
   5678    5600  98%   128    8123     456   kmalloc-128

总结

  • Buddy vs SLAB:Buddy 分配 4KB+ 的块,SLAB 把大页切成小对象池
  • SLAB 结构:slab = 1~多个页 + 对象数组 + free 链表 + 状态(free/partial/full)
  • SLUB(现代):page->freelist 直接指向空闲对象(无额外元数据),page->inuse 计数
  • per-CPU 缓存:每个 CPU 有自己的 current slab,分配无锁 O(1)
  • kmalloc:SLUB 之上的通用接口,size → cache(power of 2),GFP_KERNEL 标志

下篇预告(K02):虚拟地址到物理地址的翻译具体怎么做的?多级页表、页表缓存(TLB)、以及 page fault 的完整处理流程。


关注公众号「AI不着急」,回复”资料”获取内存学习路线图。

最后修改: 2024年3月6日

作者

评论

发表评论

您的邮箱地址不会被公开。