2. Linux 内存分配机制:伙伴系统与 Slab 分配器
在深入 OOM Killer 之前,必须理解 Linux 内核如何管理物理内存。本章剖析两大核心机制:伙伴系统 (Buddy System) 解决连续物理页框的分配与回收;Slab 分配器 解决内核小对象的缓存与复用。两者共同决定了内存碎片化的程度,而碎片化正是触发 OOM 的隐性推手。
2.1 伙伴系统 (Buddy System) 原理
伙伴系统是 Linux 物理页框分配的基础算法。它将物理内存划分为 2order 个连续页框的块(order 从 0 到 MAX_ORDER-1,通常 MAX_ORDER=11,即最大块为 2MB)。
2.1.1 核心数据结构
每个内存域 (Zone) 维护一个 free_area 数组,数组索引对应 order 值,每个元素是一个链表,链接相同大小的空闲块。
| Order | 块大小 (页数) | 块大小 (x86_64 4K 页) | 典型用途 |
|---|---|---|---|
| 0 | 1 | 4 KB | 小块分配、Slab 页框 |
| 1 | 2 | 8 KB | 小型 DMA 缓冲区 |
| 2 | 4 | 16 KB | 文件系统元数据 |
| 3 | 8 | 32 KB | 网络协议栈控制块 |
| 4 | 16 | 64 KB | 中等 DMA 缓冲区 |
| 5 | 32 | 128 KB | 内核模块加载 |
| 6 | 64 | 256 KB | 大块连续内存 |
| 7 | 128 | 512 KB | 视频帧缓冲区 |
| 8 | 256 | 1 MB | 大块连续内存 |
| 9 | 512 | 2 MB | 透明大页 (THP) |
| 10 | 1024 | 4 MB | 巨型页 (HugeTLB) |
2.1.2 分配与合并流程
- 分配:请求 order=N 的连续页框。系统从
free_area[N]链表查找空闲块。若找到,直接摘取返回;若未找到,向 order=N+1 链表“借”一个块,将其一分为二(两个伙伴),一半放入 order=N 链表,另一半返回给请求者。 - 回收:释放 order=N 的块时,检查其伙伴(物理地址相邻、大小相同的块)是否空闲。若伙伴空闲,将两者合并为 order=N+1 的块,递归向上合并。
关键约束:伙伴必须满足“地址对齐”条件。一个 order=N 的块,其起始物理地址必须是 2N * PAGE_SIZE 的整数倍。这是合并算法正确性的基础。
2.2 内存碎片化问题
伙伴系统虽然高效,但长期运行后必然产生碎片化。碎片化分为两种:
- 内部碎片 (Internal Fragmentation):分配出去的块大于实际需求,多余部分无法被其他请求使用。例如请求 3KB,伙伴系统只能分配 4KB 的 order-0 块,浪费 1KB。
- 外部碎片 (External Fragmentation):虽然总空闲内存足够,但无法分配连续的物理页框。例如系统有 10 个空闲页框,但分散在 10 个不同位置,无法满足 order=3(8 页连续)的请求。
OOM 关联:外部碎片是触发 OOM Killer 的常见隐性原因。当内核需要连续内存(如 DMA 缓冲区、网络收发包、文件系统元数据)时,即使 MemFree 显示充足,分配仍可能失败,最终调用 OOM Killer 杀掉进程来回收整块内存。
2.2.1 碎片化程度评估指标
| 指标 | 计算方式 | 健康阈值 | 危险信号 |
|---|---|---|---|
| 外部碎片指数 (EFI) | 1 - (最大连续块大小 / 总空闲内存) | < 0.3 | > 0.6 时分配失败风险高 |
| order-9 及以上可用块数 | 从 /proc/buddyinfo 读取 |
> 10 | 接近 0 时 THP 分配失败 |
| 碎片化指数 (FI) | 1 - (空闲页数 / 总页数) | < 0.5 | > 0.8 时系统性能下降 |
2.2.2 查看碎片化状态
# 查看每个内存域的空闲块分布(按 order 排列)
cat /proc/buddyinfo
# 输出示例(DMA 域):
# Node 0, zone DMA 1 0 1 0 2 1 1 0 1 1 3
# Node 0, zone DMA32 1234 567 89 12 3 1 0 0 0 0 0
# Node 0, zone Normal 23456 12345 6789 1234 567 89 12 3 1 0 0
# 解释:从左到右依次为 order 0 到 order 10 的空闲块数量
# 若高阶 order(如 order-9、order-10)长期为 0,说明存在严重外部碎片
2.3 Slab 分配器
伙伴系统以页框为最小单位(4KB),但内核频繁分配远小于 4KB 的对象(如 task_struct ~2KB、inode ~600B、dentry ~200B)。直接使用伙伴系统会造成严重内部碎片。Slab 分配器应运而生,它基于伙伴系统提供的页框,实现内核小对象的缓存与复用。
2.3.1 Slab 分配器架构
Slab 分配器有三个核心概念:Cache、Slab、Object。
- Cache (缓存):管理同类型对象的集合。每个 Cache 有一个名称(如
kmalloc-256、task_struct),包含一个或多个 Slab。 - Slab ( slabs ):由一个或多个连续物理页框组成,被划分为多个等大小的 Object。Slab 有三种状态:Full(全部分配)、Partial(部分空闲)、Empty(全部空闲)。
- Object (对象):内核数据结构实例,如一个
inode、一个dentry。
| 组件 | 粒度 | 数量级 | 管理方式 |
|---|---|---|---|
| Cache | 对象类型 | 几十 ~ 几百个 | 全局链表,按名称索引 |
| Slab | 物理页框 (1 ~ 32 页) | 每个 Cache 几个 ~ 几千个 | 三链表:full、partial、empty |
| Object | 内核数据结构 | 每个 Slab 几十 ~ 几百个 | Slab 内部空闲链表 |
2.3.2 分配与释放流程
- 分配 Object:从 Cache 的 partial 链表取一个 Slab,从其空闲链表头部摘取一个 Object 返回。若 partial 为空,从 empty 链表取一个 Slab 并初始化;若 empty 也为空,向伙伴系统申请新页框创建 Slab。
- 释放 Object:将 Object 归还到所属 Slab 的空闲链表。若 Slab 变为 empty,将其移入 empty 链表;若 Cache 整体空闲 Slab 过多,可销毁部分 Slab 归还页框给伙伴系统。
性能优势:Slab 分配器通过对象复用(避免频繁分配/释放页框)、CPU 缓存着色(减少缓存行冲突)、每 CPU 缓存(避免锁竞争)三大技术,使内核小对象分配速度比伙伴系统快 10~100 倍。
2.3.3 查看 Slab 使用情况
# 查看所有 Slab 缓存统计
cat /proc/slabinfo
# 输出示例(部分):
# slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> ...
# task_struct 12345 15000 2048 16 8
# inode_cache 5678 8000 640 25 4
# dentry 23456 30000 192 42 2
# kmalloc-256 34567 40000 256 32 2
# 关键字段说明:
# active_objs : 当前正在使用的对象数
# num_objs : Slab 中总对象数(含空闲)
# objsize : 每个对象大小(字节)
# objperslab : 每个 Slab 包含的对象数
# pagesperslab: 每个 Slab 占用的页框数
2.4 伙伴系统与 Slab 的协同
两者形成分层架构:Slab 分配器是伙伴系统的“客户”,伙伴系统是 Slab 的“供应商”。
- 向上接口:Slab 分配器通过
alloc_pages()向伙伴系统申请页框,通过__free_pages()归还页框。 - 向下影响:Slab 分配器释放 Empty Slab 时,伙伴系统回收页框并尝试合并,改善碎片化。
- OOM 传导:当伙伴系统无法满足 Slab 的页框请求时,Slab 分配器返回 NULL,内核调用 OOM Killer 杀进程回收内存。
排查要点:在 OOM 事件中,通过
/proc/slabinfo 观察 task_struct、dentry、inode_cache 等关键 Cache 的 active_objs 是否异常增长。若某个 Cache 占用大量内存,可能是内存泄漏的源头。