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来平衡内存利用率与分配确定性。

发表评论 取消回复