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 分配与合并流程

  1. 分配:请求 order=N 的连续页框。系统从 free_area[N] 链表查找空闲块。若找到,直接摘取返回;若未找到,向 order=N+1 链表“借”一个块,将其一分为二(两个伙伴),一半放入 order=N 链表,另一半返回给请求者。
  2. 回收:释放 order=N 的块时,检查其伙伴(物理地址相邻、大小相同的块)是否空闲。若伙伴空闲,将两者合并为 order=N+1 的块,递归向上合并。
关键约束:伙伴必须满足“地址对齐”条件。一个 order=N 的块,其起始物理地址必须是 2N * PAGE_SIZE 的整数倍。这是合并算法正确性的基础。
伙伴系统分配与合并流程 伙伴系统分配与合并流程 请求 order=2 检查 free_area[2] 未找到 向 order=3 借块 分裂为两个伙伴 返回 order=2 块 释放 order=2 块 检查伙伴是否空闲 合并为 order=3 块 分裂时低地址块留在原 order 链表,高地址块返回请求者;合并时伙伴必须满足地址对齐约束

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,说明存在严重外部碎片
外部碎片化示意图 外部碎片化:总空闲足够,但无连续大块 碎片化严重: 已分配 空闲 已分配 空闲 已分配 空闲 已分配 空闲 已分配 空闲块分散:5 个空闲块共 190 单位,但最大连续仅 50 单位,无法满足 80 单位的连续请求 紧凑布局: 已分配 (200 单位) 空闲 (200 单位) 已分配 空闲块集中:1 个连续空闲块 200 单位,可满足任意 ≤ 200 单位的连续请求 伙伴系统通过内存规整 (Compaction) 和页面迁移 (Migration) 缓解外部碎片

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 分配与释放流程

  1. 分配 Object:从 Cache 的 partial 链表取一个 Slab,从其空闲链表头部摘取一个 Object 返回。若 partial 为空,从 empty 链表取一个 Slab 并初始化;若 empty 也为空,向伙伴系统申请新页框创建 Slab。
  2. 释放 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 占用的页框数
Slab 分配器架构 Slab 分配器架构:Cache → Slab → Object Cache: task_struct objsize=2048, objperslab=16 Slab 链表: Full (16/16 已分配) Partial (10/16 已分配) Empty (0/16 已分配) Partial Slab 内部 (16 个 Object): A B C D E F G H I J K L M N O P 已分配 (红色) : A~J 空闲 (绿色) : K~P 分配 Object 时优先从 Partial Slab 取;释放后若 Slab 变 Empty,可销毁并归还页框给伙伴系统 Slab 分配器通过着色 (Coloring) 优化 CPU 缓存行利用率,减少伪共享

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 占用大量内存,可能是内存泄漏的源头。