Linux内核伙伴系统(Buddy System)与SLUB内存分配器深度实战

Linux内核的内存管理子系统是整个操作系统的核心,而伙伴系统(Buddy System)和SLUB分配器则是其中最为关键的两个组件。理解它们的原理与实战调优,对于系统管理员和内核开发者来说至关重要。本文将从底层数据结构出发,深入剖析这两个核心机制的工作原理、相互作用以及实际调优策略。

一、伙伴系统(Buddy System):物理内存分配的基石

1.1 核心设计理念

伙伴系统的核心思想是将物理内存划分为不同大小的"页块"(page block),每个块包含2的n次方个连续物理页面。这种2的幂次划分方式使得内存分配和释放具有极高的效率。当一个大小为2^n的块被均分为两个2^(n-1)的块时,这两个块互为"伙伴"(buddy)。只有伙伴块才能重新合并为更大的块,这种约束极大地简化了碎片管理。

1.2 核心数据结构

在Linux内核中,每个NUMA节点(pg_data_t)包含一个zones数组,每个zone(free_area)维护11个空闲链表(order 0到order 10),对应从单页(4KB)到1024页(4MB)的不同块大小:

struct zone {
    struct free_area free_area[MAX_ORDER];
};

struct free_area {
    struct list_head free_list[MIGRATE_TYPES];
    unsigned long nr_free;
};

struct pglist_data {
    struct node_zones node_zones[MAX_NR_ZONES];
    struct zonelist zonelists[MAX_ZONELISTS];
};

其中MIGRATE_TYPES的引入是为了解决外部碎片问题。Linux将空闲页面按迁移类型分组:MIGRATE_UNMOVABLE(内核代码等不可移动页)、MIGRATE_MOVABLE(用户空间页等可移动页)、MIGRATE_RECLAIMABLE(可回收页等)。通过在分配时选择对应类型的空闲块,减少碎片合并的发生频率。

1.3 分配流程详解

伙伴系统的核心分配函数是__alloc_pages(),其分配流程为:首先从当前zone的指定order空闲链表中直接获取,若失败则向上级order尝试,找到足够大的块后逐级拆分(buddy splitting),将剩余部分插入到对应order的链表中。关键代码路径如下:

__alloc_pages_nodemask()
  → get_page_from_freelist()
    → rmqueue()
      → __rmqueue()
        → __rmqueue_smallest()
          → expand()
  → __alloc_pages_slowpath()
    → wakeup_kswapd()
    → direct_reclaim()

expand()函数是伙伴系统拆分的核心逻辑:当order=5(32页)的请求到达但free_area[5]为空时,会从order=6(64页)取出一块,拆成两个order=5的块,一个返回给请求者,另一个插入free_area[5]的空闲链表。

1.4 释放与伙伴合并

释放页面时,伙伴系统会检查其"伙伴页"是否也在同一order的空闲链表中。若是,则合并为一个2^(order+1)的块,并递归向上检查是否可以继续合并。伙伴页的计算极其高效:通过物理页号(page frame number)的XOR运算即可实现:

// 伙伴页的pfn计算
unsigned long buddy_pfn = pfn ^ (1UL << order);

// 即 order 0: pfn ^ 1     (相邻页)
//    order 1: pfn ^ 2     (间隔1页的一对)
//    order 2: pfn ^ 4     (间隔3页的一对)

这种XOR计算避免了链表遍历,是伙伴系统高效的关键设计。

二、SLAB/SLUB分配器:内核对象的内存分配

2.1 SLAB分配器的历史与SLUB的诞生

SLAB分配器由Sun公司工程师Jeff Bonwick在Solaris中首先提出,后被引入Linux。早期的SLAB分配器为每个缓存维护三个链表(full/partial/empty),元数据开销较大。SLUB(Queued SLAB)作为SLAB的改进版本自2.6.23内核起成为默认分配器,将元数据简化为partial链表+per-cpu缓存,大幅降低内存开销。

2.2 SLUB核心数据结构

SLUB使用一个slab page作为分配单元,每个slab page包含多个同类型的内核对象:

struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;
    struct kmem_cache_node *node[MAX_NUMNODES];
    unsigned int size;
    unsigned int object_size;
    unsigned long flags;
    unsigned int offset;
    const char *name;
};

struct kmem_cache_cpu {
    void **freelist;
    unsigned long tid;
    struct page *page;
    struct page *partial;
};

struct kmem_cache_node {
    spinlock_t list_lock;
    unsigned long nr_partial;
    struct list_head partial;
};

2.3 分配路径详解

SLUB的分配路径设计为三级缓存架构,性能逐级递减:

kmem_cache_alloc()
  → slab_alloc()
    → __slab_alloc()
      → cpu_slab->freelist
        若成功 → 返回 (最快路径,无锁)
      → __slab_alloc() slow path
        → 检查 cpu_slab->partial
          若存在 → 切换page, 重新尝试 freelist
        → new_slab_objects()
          → 检查 node->partial
            若存在 → 摘取一个给CPU
          → new_slab()
            → alloc_slab_page()
              → __alloc_pages_node()
            → inc_slabs_node()

这种设计确保了大多数分配操作只需一条"mov"指令即可完成(直接从per-cpu freelist取对象),在x86上这是几乎零开销的。

2.4 释放路径与回收策略

释放对象时,SLUB首先检查对象是否属于当前CPU的活跃slab。若是,则直接放回freelist(快路径)。若不是(属于其他CPU或不同slab),则需要加node锁,将对象归还原slab。如果释放后slab完全空闲,且系统中空闲内存紧张,则将页面归还给伙伴系统。

三、NUMA内存策略与多节点管理

3.1 NUMA架构下的内存分布

在NUMA(非一致性内存访问)系统中,每个CPU有本地内存节点,访问远程节点的内存会有额外延迟。Linux内核通过zonelist机制定义分配回退顺序:首先尝试本地zone,若失败则按距离远近依次尝试其他zone。

numactl工具可用于设置进程的内存策略:

# 查看NUMA拓扑
numactl --hardware

# 绑定进程到节点0分配内存
numactl --membind=0 --cpunodebind=0 ./my_program

# 设置交错分配(round-robin在所有节点之间)
numactl --interleave=all ./my_program

# 查看进程的NUMA策略
cat /proc/<pid>/numa_maps

3.2 NUMA Balancing自动页迁移

Linux 3.8引入了NUMA Balancing机制,内核会自动将页面迁移到访问它的CPU所在的本地节点。其工作逻辑是:内核定期扫描进程地址空间,标记远程访问的页面,然后通过迁移机制将这些页面搬到本地节点。这由sysctl参数kernel.numa_balancing控制。

四、实战调优策略

4.1 /proc/buddyinfo解读

/proc/buddyinfo是分析内存碎片的核心接口,它展示了伙伴系统中各order的空闲块分布:

$ cat /proc/buddyinfo
Node 0, zone   Normal   238   146   129   106    73    41    26    13     6     3     1

解读: order 0(4KB): 238个, order 1(8KB): 146个, ..., order 10(4MB): 1个。如果高阶(order>=4)块很少,说明存在外部碎片。

4.2 /proc/slabinfo解读

/proc/slabinfo展示了所有SLAB缓存的实时状态:

$ cat /proc/slabinfo | head -10
slabinfo - version: 2.1
# name      <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-128    3420     3456       128        32             1 : tunables    0    0    0 : slabdata    108    108      0
dentry         8542     9216       196        21             2 : tunables    0    0    0 : slabdata    439    439      0

关键指标: active_objs/num_objs表示使用比例,objsize是单个对象大小,objperslab是每页slab包含的对象数量,active_slabs是活跃slab页数。

4.3 关键sysctl参数调优

# 页面回收积极度 (0-100), 默认60, 值越大回收越激进
vm.swappiness = 10

# 触发直接回收的水位百分比, 降低可减少分配延迟
vm.min_free_kbytes = 262144  # 256MB, 预留给紧急分配

# 脏页刷新策略
vm.dirty_ratio = 40              # 脏页占总内存40%时强制阻塞写
vm.dirty_background_ratio = 10   # 后台刷写起始阈值

# NUMA Balancing
kernel.numa_balancing = 1        # 启用自动NUMA页迁移

# SLUB调试(开发环境)
slab_nomerge                     # 禁止不同name的slab合并

4.4 页面压缩(compaction)与碎片整理

当伙伴系统出现外部碎片(高order分配失败)时,可以通过内存规整(compaction)来解决。内核的compaction机制将可移动页面集中,从而释放出连续的高阶块:

# 手动触发碎片整理
echo 1 > /proc/sys/vm/compact_memory

# 查看compact成功率
grep compact /proc/vmstat
compact_migrate_scanned   # 扫描的可移动页数
compact_free_scanned      # 扫描的空闲页数
compact_isolated          # 已隔离(准备迁移)的页数
compact_success           # 成功规整次数
compact_fail              # 失败次数

五、性能分析与调试工具

5.1 slabtop实时监控

slabtop类似于top命令,实时显示SLAB内存使用情况:

$ slabtop -o
 Active / Total Objects (% used)    : 234567 / 345678 (67.9%)
 Active / Total Slabs (% used)      : 4567 / 6789 (67.3%)
 Active / Total Caches (% used)     : 123 / 234 (52.6%)
 Active / Total Size (used)         : 123.45K / 234.56K (52.6%)

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
 8542   8542 100%   0.19K    439       21     1756K dentry
 6230   3115  50%   1.07K    230       27     7360K ext4_inode_cache

5.2 perf工具分析内存分配

# 统计kmem相关事件缓存命中率
perf stat -e kmem:kmalloc,kmem:kfree,kmem:kmem_cache_alloc,kmem:kmem_cache_free ./my_workload

# 跟踪mm_page_alloc事件,分析页面分配热点
perf record -e mm_page_alloc -ag -- sleep 10
perf report

六、总结

Linux内核内存管理是一个精密的分层体系:伙伴系统负责物理页面的粗粒度分配与回收,SLUB分配器在此基础上为内核对象提供高效的细粒度内存管理。理解这两个子系统的交互方式、数据结构和调优参数,对于构建高性能、低延迟的系统至关重要。

在实际运维中,应当持续监控/proc/buddyinfo和/proc/slabinfo的状态,在碎片问题变得严重之前主动触发compaction,同时合理配置NUMA策略以保证内存本地性。对于嵌入式或低延迟场景,可以适当调整min_free_kbytes和swappiness来平衡内存利用率与分配确定性。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部