2. 伙伴系统与slab分配器

伙伴系统(Buddy System)与 Slab 分配器是 Linux 内核内存管理的两大基石。伙伴系统负责物理页框的分配与回收,以 2 的幂次阶(order)为单位管理连续物理内存;Slab 分配器(包括其改进版 SLUB)则构建在伙伴系统之上,专门用于内核对象(如 task_struct、inode)的缓存与复用,避免频繁的页分配开销。本章深入剖析两者的核心原理、数据结构与关键路径。

2.1 伙伴系统原理

伙伴系统将物理内存划分为多个 free_area,每个 free_area 管理一种特定阶(order)的空闲页块。阶(order)定义为 2^order 个连续物理页框。系统启动时,所有可用内存被合并到最高阶的 free_area 中;分配时,若当前阶无空闲块,则向高阶分裂;释放时,检查伙伴块是否空闲,若空闲则合并回高阶。

核心数据结构:struct free_area
每个 free_area 包含一个链表(free_list)和位图(map),用于快速查找和合并伙伴。

2.1.1 伙伴系统分配与释放流程

操作 步骤 说明
分配 1. 从请求阶 order 的 free_area 开始查找 若链表非空,直接分配
2. 若当前阶为空,向 order+1 阶查找 递归向上查找
3. 找到高阶块后,分裂为两个伙伴块 一个返回给调用者,另一个插入低阶 free_area
释放 1. 计算伙伴块的物理地址 通过页框号异或 2^order 得到
2. 检查伙伴块是否在 free_list 中 通过位图或 PG_buddy 标志判断
3. 若伙伴空闲,合并并递归向上检查 合并后插入 order+1 的 free_area
伙伴系统分配与合并流程 请求 order=2 order=2 空闲? 直接分配 向 order=3 查找 分裂为两个伙伴 一个分配,一个入 order=2 释放 order=2 块 检查伙伴空闲? 合并入 order=3

2.1.2 伙伴系统的碎片问题

伙伴系统虽然高效,但存在 外部碎片 问题:频繁分配和释放不同阶的块会导致内存碎片化,即使总空闲内存足够,也可能无法满足高阶分配请求。内核通过 内存规整(compaction) 和 虚拟可移动区域(ZONE_MOVABLE) 缓解此问题。

性能陷阱: 当系统长时间运行后,伙伴系统的 free_area 中高阶块可能耗尽,导致 order>3 的分配频繁触发直接内存回收或规整,增加延迟。监控 /proc/buddyinfo 可观察各阶空闲块分布。

2.2 页面分配与回收关键路径

页面分配的核心函数是 alloc_pages(gfp_mask, order),它通过 快速路径 和 慢速路径 两级机制保证分配成功率。

2.2.1 快速路径与慢速路径

路径 触发条件 操作 可能阻塞?
快速路径 当前 zone 的 free_area 有足够空闲块 直接从伙伴系统分配,不唤醒 kswapd 否
慢速路径 快速路径失败,或 gfp_mask 含 __GFP_KSWAPD_RECLAIM 唤醒 kswapd 异步回收;若仍失败,则同步直接回收(direct reclaim) 是(可能等待 I/O)
OOM 触发 慢速路径回收后仍无法满足分配 调用 out_of_memory() 选择 victim 进程并杀死 是(进程被杀死)
// 快速路径核心逻辑(简化)
static inline struct page *rmqueue(struct zone *zone, unsigned int order) {
    struct free_area *area = &zone->free_area[order];
    struct page *page = list_first_entry_or_null(&area->free_list, struct page, lru);
    if (page) {
        list_del(&page->lru);
        set_page_private(page, 0);
        return page;
    }
    // 若当前阶为空,尝试从高阶分裂
    return __rmqueue_smallest(zone, order);
}

2.2.2 页面回收机制

页面回收由 kswapd 内核线程和 直接回收(direct reclaim) 共同完成。kswapd 在后台异步扫描 LRU 链表,回收匿名页和文件页;直接回收则在分配路径中同步执行。

  • LRU 链表:分为活跃匿名页、不活跃匿名页、活跃文件页、不活跃文件页。
  • 回收策略:优先回收不活跃页;若内存压力大,则扫描活跃页并降级。
  • Swap:匿名页回收时写入交换分区;文件页回收时若为脏页则写回磁盘。
页面回收流程 kswapd 唤醒 扫描不活跃 LRU 回收页框 直接回收触发 同步扫描 LRU 分配成功或 OOM 关键指标:/proc/vmstat 中的 pgscan_kswapd、pgscan_direct、pgsteal_* 反映回收效率

2.3 Slab/SLUB 分配器机制

Slab 分配器(以及其改进版 SLUB)用于管理内核中频繁创建和销毁的小对象(如 struct file、struct inode)。它通过预分配对象缓存,避免每次分配都调用伙伴系统,从而降低开销并减少碎片。

2.3.1 Slab 分配器核心概念

概念 说明
kmem_cache 每个对象类型对应一个缓存,包含多个 slab
slab 一个或多个连续物理页框,划分为多个对象槽位
对象(object) 内核数据结构实例,如 task_struct
着色(coloring) 通过偏移调整对象起始地址,优化 CPU 缓存行对齐

2.3.2 SLUB 分配器改进

SLUB(Unqueued Slab Allocator)是 Slab 的现代替代,自 Linux 2.6.22 起成为默认分配器。主要改进:

  • 去除 per-CPU 队列:Slab 使用 per-CPU 对象池,SLUB 改用 per-CPU 空闲指针,减少锁竞争。
  • 简化 slab 管理:SLUB 将 slab 元数据存储在页框的 struct page 中,不再需要独立的 slab 描述符。
  • 批量操作:支持批量分配和释放,提升吞吐量。
// SLUB 分配核心:从 per-CPU 空闲指针获取对象
static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node) {
    struct kmem_cache_cpu *c = per_cpu_ptr(s->cpu_slab, raw_smp_processor_id());
    void *object = c->freelist;
    if (unlikely(!object)) {
        // 当前 CPU 空闲列表为空,从 slab 中获取新批
        object = __slab_alloc(s, gfpflags, node, c);
    } else {
        // 直接分配
        c->freelist = get_freepointer(s, object);
    }
    return object;
}

2.3.3 Slab 与 SLUB 对比

特性 Slab SLUB
元数据存储 独立 slab 描述符 嵌入 struct page
per-CPU 队列 有,需加锁 无,使用 freelist 指针
批量分配 不支持 支持
内存开销 较高(描述符 + 着色) 较低
调试能力 较弱 支持红区(redzone)和毒化(poison)
SLUB 分配器结构 kmem_cache (inode) per-CPU freelist 对象分配 partial slab 链表 full slab 链表 伙伴系统 SLUB 关键优化:每个 CPU 维护一个 freelist 指针,避免锁竞争 当 freelist 为空时,从 partial slab 中批量获取对象 监控 /proc/slabinfo 可查看各缓存使用情况

2.4 故障排查要点

排查伙伴系统碎片: 使用 cat /proc/buddyinfo 观察各 zone 的 free_area 分布。若高阶(order>3)块长期为 0,说明碎片严重,可触发内存规整:echo 1 > /proc/sys/vm/compact_memory。
排查 Slab 泄漏: 使用 slabtop 或 cat /proc/slabinfo 观察缓存对象数量是否持续增长。若某个缓存(如 kmalloc-128)的 active_objs 不断上升且不回落,可能存在内存泄漏。进一步使用 kmemleak 工具扫描未释放对象。

伙伴系统与 Slab 分配器共同构成了内核内存分配的两级缓存体系:伙伴系统管理物理页框的宏观分配,Slab/SLUB 则在内核对象层面实现高效复用。理解这两者的协同工作方式,是诊断 OOM 和内存泄漏问题的前提。