Linux内核物理内存管理深度实战:从伙伴系统到OOM Killer的完整链路
一、为什么需要了解内存管理?
在高性能系统调优中,内存分配的速度与碎片率直接影响整个系统的吞吐量。许多工程师知道 kmalloc、vmalloc,却不理解它们背后的伙伴系统(Buddy System)与 SLUB 分配器如何协同工作,也不知道 OOM Killer 的评分机制如何决定进程生死。本文将打通从物理页分配到底层硬件的完整链路,并结合 /proc/buddyinfo、slabtop、oom_score 等工具给出实战调优策略。
二、物理内存的组织架构
2.1 Node → Zone → Page 三级模型
Linux 将物理内存建模为 pg_data_t (Node) → zone → page 三级结构:
- NUMA Node:每个 CPU _socket_ 对应一个 Node,
/sys/devices/system/node/可查看拓扑 - Memory Zone:
ZONE_DMA(0-16MB)、ZONE_DMA32(4GB以下)、ZONE_NORMAL(线性映射区)、ZONE_MOVABLE(可迁移页) - struct page:每个物理页帧对应一个
struct page(约 64 字节),全局数组mem_map[]记录所有页的元信息
// 简化的三级关系
pg_data_t numa_node_0
├── ZONE_DMA (0x00000000 - 0x00FFFFFF)
├── ZONE_NORMAL (0x01000000 - 0x3FFFFFFF) -- 线性映射到 PAGE_OFFSET
└── ZONE_HIGHMEM (仅32位系统需要)
2.2 页表映射与线性映射区
内核空间通过线性映射(direct mapping)直接映射所有物理内存。在 x86_64 上,PAGE_OFFSET 为 0xFFFF888000000000,内核可用简单的 __va(phys) = phys + PAGE_OFFSET 实现 O(1) 物理↔虚拟地址转换。这种设计让伙伴系统分配的物理页可以快速被内核访问。
三、伙伴系统(Buddy System)
3.1 核心思想
伙伴系统将物理内存划分为 11 个空闲链表(order 0~10),分别管理 2⁰、2¹... 2¹⁰(即 1、2、4...1024)个连续物理页块。分配时向上递归分裂,释放时向伙伴合并——这是经典的二分策略。
// mm/page_alloc.c 中的核心数据结构
struct free_area {
struct free_list free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct zone {
struct free_area free_area[MAX_ORDER]; // MAX_ORDER = 11
};
3.2 分配与释放的完整流程
alloc_pages(gfp_mask, order) 的执行步骤:
- 快速路径:
get_page_from_freelist()直接从当前 order 的空闲链表取页,O(1) 操作 - 慢速路径:触发直接回收(direct reclaim)、压缩(compact),甚至唤醒 kswapd
- Watermark 检查:
min/low/high三条水位线决定是否需要回收,/proc/sys/vm/min_free_kbytes控制最小空闲水量 - Fallback:使用
__GFP_RECLAIM标志允许阻塞等待回收
释放时的伙伴合并:
// 伙伴计算:page_idx XOR (1 - page block_order)
// 例: order=3 的块 (0x40~0x47),其伙伴为 (0x48~0x4F)
// 因为 0x40 XOR 0x08 = 0x48
void __free_pages(struct page *page, unsigned int order) {
int buddy_idx = page_idx ^ (1 - page order);
// 检查伙伴页是否空闲,若空闲则合并到 order+1 链表
}
3.3 迁移类型:内存碎片化的杀手锏
External fragmentation(外部碎片)是伙伴系统的经典问题。Linux 引入 MIGRATE_TYPES 将页按迁移能力分类:
MIGRATE_UNMOVABLE:内核数据结构(页表、slab),不可移动MIGRATE_MOVABLE:用户空间页、文件缓存,可迁移MIGRATE_RECLAIMABLE:可回收的内核对象(dentries、inodes)MIGRATE_ISOLATE:热插拔或离线隔离
通过将可迁移页聚合,memory compaction 可以将零散页整合为大页块,这是透明大页(THP)成功的关键基础。
3.4 实战:监控伙伴系统健康度
$ cat /proc/buddyinfo
Node 0, zone Normal 28 15 8 4 2 1 2 1 0 0 1
order→ 0 1 2 3 4 5 6 7 8 9 10
各order空闲块数:order=6(2块=128页=512KB) order=10(1块=1024页=4MB连续)
碎片化判断:高阶连续块过少 + 低阶块充足 = 严重碎片。可通过 /proc/sys/vm/compact_memory 手动触发压缩。
四、SLAB / SLUB / SLOB 分配器
4.1 SLAB 设计哲学
伙伴系统以页(4KB~4MB)为粒度分配,但内核大量对象只有几十到几百字节(task_struct ~6KB、inode ~600B、dentry ~200B)。为此引入 SLAB 分配器,在页内预分配固定大小的对象池:
- kmem_cache:管理特定大小的对象池(如
kmem_cache_task_struct) - kmem_cache_create():创建 cache,传入 size/align/flags/constructor
- kmem_cache_alloc() / kmem_cache_free():O(1) 分配/释放
4.2 SLUB:现代内核的选择
Linux 2.6.23 起,SLUB 取代 SLAB 成为默认分配器,解决了 SLAB 的以下问题:p>
- 取消 metadata inline:SLAB 在每个页的
struct page旁维护链表,SLUB 仅在kmem_cache结构中跟踪 - NUMA 感知:每个 Node 维护单独的 partial/full/empty 队列
- Debug 支持:
slab_debug、red_zone、poison可检测越界写入和 UAF
4.3 Per-CPU 缓存:走向 O(1)
SLUB 的核心优化是 per-CPU cpu_slab:每个 CPU 维护一个本地 freelist,分配/释放时无需加锁。__slab_alloc() 直接从 kmem_cache_cpu->freelist 摘节点。本地队列用完时再从 Node 的 partial 队列补充(这时才需要锁):
// 分配路径(简化)
static void *___slab_alloc(...) {
// fast path:per-CPU freelist 直接获取
object = c-next; c-next = get_freepointer(s, object);
if (likely(object)) return object;
// slow path:从 node->partial 补充,若仍空则调 new_slab
object = get_partial(s, node, c);
if (!object)
object = new_slab(s, gfpflags, node);
}
4.4 kmalloc 家族
kmalloc() 不是独立的分配器,而是建立在 SLUB cache 之上的薄封装。内核预先创建了一组 kmem_cache,按 2 的幂次作为桶大小:
kmalloc_caches[8] 覆盖 32B~8B → kmalloc-32/kmalloc-64/.../kmalloc-8192
kmalloc(1500, GFP_KERNEL) → 命中 kmalloc-2048
kmalloc(9000, GFP_KERNEL) → 超出桶范围,降级走 alloc_pages(由 MAX_SIZE 决定)
五、vmalloc vs kmalloc vs kmem_cache_alloc
三种分配方式的关键差异:
- kmalloc(size, flags):返回线性映射的虚拟地址,物理上连续,上限通常 4MB~8MB
- vmalloc(size):返回 vmalloc 区的虚拟地址,物理上可非连续,适合大块分配(如加载内核模块),但有 TLB 效率开销
- kmem_cache_alloc(cache, flags):专用 cache,O(1) 速度 + SLUB 保护,适合高频分配特定对象
六、OOM Killer:最后的防线
6.1 触发条件
当快速分配→慢速分配→direct reclaim→compact 全部失败,且 gfp_mask 允许触发 OOM 时,调用 out_of_memory()。触发路径:alloc_pages → __alloc_pages_may_oom → out_of_memory。
6.2 oom_badness 评分机制
OOM Killer 通过 oom_badness() 为每个进程打分,评分公式:
// mm/oom_kill.c
points = total_vm (RSS + swap + page_table * scale)
adjust = (1 - oom_score_adj)
oom_points = points + (points * oom_score_adj / 1000)
关键规则:
- 占用内存越多,分数越高(越可能被杀)
oom_score_adj范围 -1000~+1000,-1000 表示OOM_DISABLE(永不被杀)- root 进程额外加 3% 加分
- 运行不足 1 秒的进程有保护阈值
/proc/pid/oom_score可实时查看当前分数
6.3 实战保护与调优
# 保护关键服务
echo -1000 - /var/run/sshd/oom_score_adj
echo -17 - /usr/bin/redis-server
# 全局策略:panic on OOM(适合控制节点)
sysctl vm.panic_on_oom=1
sysctl kernel.panic=10
# cgroup 级别限制:
# memory.max 触发 cgroup OOM(不触发全局 killer)
echo "1G" - /sys/fs/cgroup/myapp/memory.max
# 提前触发回收(减少硬阻塞)
sysctl vm.min_ratio=5 # 该 NUMA node 空闲低于 5% 时直接 reclaim
sysctl vm.vfs_cache_pressure=100 # 缓存回收积极度(默认 100,降低可保留更多 dentry cache)
七、高级话题
7.1 透明大页(THP)
THP(Transparent Huge Pages)利用 2MB(x86)或 1GB 大页减少 TLB miss。启用路径:khugepaged 内核线程在后台扫描可合并的连续小页区域。
注意:数据库(如 PostgreSQL、Redis)通常建议关闭 THP(echo never /sys/kernel/mm/transparent_hugepage/enabled),因为大页合并延迟可能导致尾延迟飙升。
7.2 CMA(Contiguous Memory Allocator)
CMA 预留一块可迁移大内存区,平时供可移动页使用,关键时刻归还给需要连续内存的设备(如 Camera、GPU 显存)。核心原理是基于可迁移类型的伙伴系统隔离。
7.3 Memory Cgroup(v2)
cgroup v2 的 memory 控制器提供 memory.min(硬保护)、memory.low(尽力保护)、memory.high(限速触发器)、memory.max(硬上限)四级隔离,是容器化场景的内存 QoS 基石。
八、性能调优清单
# 1. 监控碎片化
watch -n1 "cat /proc/buddyinfo"
cat /proc/pagetypeinfo | grep -E "free|Movable"
# 2. 监控 slab 使用
slabtop -o # 按大小排序
cat /proc/slabinfo | grep -E "dentry|inode|task_struct"
# 3. 查看 OOM 候选
for pid in /proc/[0-9]*/oom_score; do echo "$(cat $pid) $(awk '{print $2}' $(dirname $pid)/comm)"; done | sort -rn | head
# 4. watermark 调优
sysctl -w vm.min_free_kbytes=524288 # 256MB 系统留 512MB 是神话,默认按内存比例
sysctl -w vm.swappiness=10 # 关闭 swap 倾向(默认60)
# 5. 触发压缩
echo 1 - /proc/sys/vm/compact_memory
# 6. NUMA 感知
numactl --cpunodebind=0 --membind=0 ./my_app
numastat -cm | head -5
九、总结
Linux 内核物理内存管理是一个精妙的层级架构:伙伴系统以页为单位管理连续物理块,SLUB 在页内为高频小对象提供 O(1) 池化分配,OOM 则在资源枯竭时通过评分算法做出艰难但合理的牺牲。理解这套机制,是从 "会用 kmalloc" 走向 "能调优系统内存行为" 的关键一步。

发表评论 取消回复