# Linux 内核内存管理深度实战:Buddy System 与 SLAB 分配器全解析
## 引言
Linux 内核的内存管理是操作系统中最复杂且最核心的子系统之一。它不仅负责物理内存的分配与回收,还要在性能、碎片、延迟之间做出精妙的平衡。本文将从 Buddy System(伙伴系统)的底层原理出发,深入剖析 SLAB/SLUB/SLOB 分配器的设计哲学,最终给出在生产环境中诊断内存问题的完整实战方案。
## 一、Linux 内核内存管理架构总览
### 1.1 三级内存视图
Linux 内核看待内存有三个层次,自上而下:
```
┌──────────────────────────────────────────┐
│ 用户空间 Virtual Address │ malloc / mmap
├──────────────────────────────────────────┤
│ 内核空间 VMALLOC Region │ vmalloc()
├──────────────────────────────────────────┤
│ 内核空间 LOWMEM (Direct Map) │ kmalloc() ← 物理连续
├──────────────────────────────────────────┤
│ Buddy System (Physical Frame) │ 页框分配
└──────────────────────────────────────────┘
```
- **物理层**:Buddy System 以页(Page,通常 4KB)为单位管理物理内存
- **对象层**:SLAB 分配器在页的基础上缓存内核对象,避免频繁申请释放页
- **虚拟层**:页表将内核/用户虚拟地址翻译为物理地址
### 1.2 关键数据结构
```c
// 每个物理页的元数据
struct page {
unsigned long flags; // PG_locked, PG_slab 等标志
atomic_t _count; // 引用计数
atomic_t _mapcount; // 映射到多少个页表
unsigned long private; // 可供各子系统自由使用
struct address_space *mapping;
struct list_head lru; // LRU 链表节点
unsigned long index;
struct {
struct page *next; // 伙伴系统空闲链表
int pages; // 伙伴系统阶数
};
};
```
### 1.3 NUMA 架构下的内存组织
在 NUMA 系统中,内存被划分为多个 Node,每个 Node 包含若干 Zone:
```
Node 0 Node 1
├── ZONE_DMA (0-16MB) ├── ZONE_DMA
├── ZONE_DMA32 (16M-4G) ├── ZONE_DMA32
├── ZONE_NORMAL (映射区) ├── ZONE_NORMAL
└── ZONE_HIGHMEM └── ZONE_HIGHMEM
```
内核优先从当前 CPU 所在 Node 的 Zone 分配内存,减少跨 Node 访问延迟。
## 二、Buddy System(伙伴系统)深度剖析
### 2.1 核心思想
Buddy System 由 Knowlton 于 1965 年提出,Linux 内核自 1.0 版本即采用。其核心规则:
1. 将物理内存划分为连续的 2^n 页大小的块(n = 0 ~ MAX_ORDER-1)
2. 每个阶 n 维护一个独立的空闲块链表 free_area[n]
3. 分配时向上取整到最近的 2^n,若该阶无空闲则从更高阶拆分为两个"伙伴"
4. 释放时检查伙伴是否空闲,若空闲则合并为 2^(n+1) 块
### 2.2 源码级分析:分配路径
```c
// mm/page_alloc.c
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
// 1. 从指定 migratetype 的空闲链表中查找
page = get_page_from_freelist(gfp_mask, order, zonelist);
// 2. 若失败,从更高阶分割(expand)
if (!page)
page = rmqueue(zone, order, migratetype);
return page;
}
static struct page *
rmqueue(struct zone *zone, unsigned int order, int migratetype)
{
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
if (!list_empty(&area->free_list[migratetype]))
goto found;
}
return NULL; // 所有高阶也无空闲
found:
page = list_entry(area->free_list[migratetype].next, struct page, lru);
list_del(&page->lru); // 从空闲链表移除
__mod_zone_page_state(zone, NR_FREE_PAGES, -(1UL << current_order));
expand(zone, page, order, current_order, migratetype); // 分割
return page;
}
```
### 2.3 伙伴的判定与合并
两个块互为"伙伴"的条件:
```c
static inline int
page_is_buddy(struct page *buddy, struct page *page, unsigned int order)
{
// 地址相邻,且大小相同,且在同一 zone
if (page_zone_id(buddy) != page_zone_id(page))
return 0;
// buddy 的起始地址与 page 的起始地址相差 exactly 2^(order+1) 页
// 即:buddy_pfn == page_pfn XOR (1 << order)
return (page_pfn(buddy) ^ page_pfn(page)) == (1 << order);
}
// 合并时:两个阶为 order 的伙伴合并为一个阶为 order+1 的块
static inline struct page *
__find_buddy_page(struct page *page, unsigned long page_idx, unsigned int order)
{
unsigned long buddy_idx = page_idx ^ (1 << order);
return page + (buddy_idx - page_idx);
}
```
### 2.4 迁移类型(Movable)与反碎片
Linux 2.6.24 引入 MIGRATE 类型,将页分为:
| 类型 | 说明 | 特点 |
|------|------|------|
| MIGRATE_UNMOVABLE | 内核数据、页表等不可移动 | 无法迁移,易造成碎片 |
| MIGRATE_RECLAIMABLE | inode cache、dentry cache | 可回收 |
| MIGRATE_MOVABLE | 用户页、可移动内核页 | 可迁移至其他块 |
| MIGRATE_ISOLATE | 用于CMA/热插拔 | 隔离区 |
| MIGRATE_CMA | 连续内存分配器 | 大页预留 |
通过将 MOVABLE 和 UNMOVABLE 分离开来分配,确保可迁移页可以聚集到一方,另一方留给不可移动对象,从根本上延缓外部碎片化。
### 2.5 直接内存回收与碎片整理
当 Buddy System 无法满足分配请求时的处理流程:
```
分配请求
│
▼
alloc_pages ──→ 空闲页足够?──是──→ 直接分配
│ │
│ 否
│ ▼
│ __alloc_pages_slowpath
│ │
│ ┌────────┼──────────┐
│ ▼ ▼ ▼
│ 页面回收 页面紧凑 OOM Killer
│ (kswapd/ (page (杀死
│ direct migration) 进程)
│ reclaim)
│
▼
返回页面
```
## 三、SLAB/SLUB/SLOB 分配器详解
### 3.1 为什么需要 SLAB
Buddy System 以整页(4KB)为单位分配,但内核对象通常只有几百字节。频繁的小对象分配:
- 严重浪费内存(内部碎片)
- 频繁申请/释放导致性能下降
- 缺少 CPU Cache 热度保持
SLAB 分配器(Jeff Bonwick, 1994, Solaris)通过在页内缓存对象解决这些问题。
### 3.2 SLAB 核心概念
```
┌─────────────────────────────────────────┐
│ SLAB Cache (如 dentry_cache) │
│ ┌──────────┐ ┌──────────┐ │
│ │ Full SLAB │ Partial SLAB│ Empty SLAB│
│ └──────────┘ └──────────┘ │
│ │
│ SLAB (一页或多页) │
│ ┌──────────────────────────────────┐ │
│ │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │ │
│ │ │obj1│ │obj2│ │obj3│ │obj4│ ...│ │
│ │ └────┘ └────┘ └────┘ └────┘ │ │
│ │ bufctl 位图 (INUSE/FREE) │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
- **Cache**:一组相同大小的对象池(如 `dentry_cache`、`inode_cache`)
- **SLAB**:一个或多个连续的物理页,被切分成若干等大小槽位
- **bufctl**:每个槽位的使用状态位图(传统 SLAB),或 SLUB 用 `freelist` 链表
### 3.3 SLUB:现代的默认分配器
SLUB (Unqueued SLAB, Christoph Lameter, 2007) 是 Linux 默认的 SLAB 实现,解决了传统 SLAB 在 NUMA 和大规模多核系统中的扩展性问题。
```c
// SLUB 的核心结构
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // CPU 本地缓存
unsigned long flags;
unsigned long min_partial; // 保持的最少 partial slab 数
size_t size; // 对象实际大小
size_t object_size; // 用户请求大小
unsigned int offset; // freelist 在对象内的偏移
struct kmem_cache_node *node[MAX_NUMNODES]; // 节点级 partial 链表
};
struct kmem_cache_cpu {
void **freelist; // 本地可用对象链表
struct page *page; // 当前使用的 slab 页
struct page *partial; // 本地 partial slabs (CONFIG_SLUB_CPU_PARTIAL)
};
```
SLUB 的关键优化:
| 优化点 | 说明 |
|--------|------|
| CPU Freelist | 无锁快速路径,直接从 CPU 本地链表分配 |
| CPU Partial | 释放到 CPU 本地 partial,避免全局锁竞争 |
| Debug | 红区(Red Zone)、对象毒化(Poison)、Use-after-free 检测 |
| 合并 | 相似大小的 Cache 合并,减少对象种类 |
### 3.4 SLOB 分配器
SLOB(Simple List Of Blocks)是嵌入式系统的轻量级选择,特点:
- 代码极小(约 1000 行)
- 只维护 3 个大小等级的链表(2^n + small extras)
- 内存碎片严重,不适用于大内存系统
- 仅在内核配置 `CONFIG_SLOB` 时使用
### 3.5 kmalloc vs vmalloc 的选择
```c
// kmalloc: 物理连续,GFP 标志灵活,适合 DMA
void *kmalloc(size_t size, gfp_t flags);
// vmalloc: 虚拟连续(不一定物理连续),只需页数,适合大内存
void *vmalloc(unsigned long size);
```
关键区别:
| 特性 | kmalloc | vmalloc |
|------|---------|---------|
| 物理连续性 | ✅ 必须连续 | ❌ 仅虚拟连续 |
| 最大大小 | ~4MB (依赖 MAX_ORDER) | 接近系统总内存 |
| TLB 开销 | 低(连续页框) | 高(逐页映射) |
| 延迟 | ~几十 ns | ~几百 ns |
| DMA 可用 | ✅ | ❌ |
| vmalloc 区域 | 不占用 | 占用 vmalloc region |
**实战原则**:除非需要分配超出连续物理内存能力的大块数据,否则一律使用 `kmalloc`。
## 四、内存分配 GFP 标志详解
### 4.1 分类与常用组合
```c
// 区域修饰符
#define GFP_DMA __GFP_DMA // ZONE_DMA 分配
#define GFP_DMA32 __GFP_DMA32 // ZONE_DMA32 分配
#define GFP_NORMAL // 默认,优先 LOWMEM
#define GFP_HIGH __GFP_HIGHMEM // 可分配 HIGHMEM
// 行为标志
#define GFP_ATOMIC __GFP_HIGH // 绝不睡眠,可访问紧急储备
#define GFP_NOWAIT __GFP_NOWARN // 不回收,不睡眠
#define GFP_NOIO __GFP_IO // 不启动磁盘 I/O
#define GFP_NOFS __GFP_FS // 不启动文件系统操作
#define GFP_KERNEL (__GFP_RECLAIM) // 正常内核分配,可睡眠
#define GFP_USER // 用户空间分配
#define GFP_NORETRY __GFP_NORETRY // 失败直接返回,不重试
```
### 4.2 实战选择原则
| 场景 | 推荐 GFP | 原因 |
|------|----------|------|
| 中断上下文 | GFP_ATOMIC | 不能睡眠 |
| 持有自旋锁 | GFP_ATOMIC | 不能睡眠 |
| 文件系统写回 | GFP_NOIO | 避免递归 I/O |
| 普通内核路径 | GFP_KERNEL | 允许回收和睡眠 |
| 用户空间 | GFP_USER | 允许直接回收 |
| 分配超过 __MAX_ORDER | GFP_NOWAIT + 重试 | 避免 OOM |
## 五、页面回收与 Swap 机制
### 5.1 LRU 链表机制
Linux LRU 采用双时钟算法,分为两组:
- **Active 链表**:最近被访问过,短期内可能被再次使用
- **Inactive 链表**:长时间未被访问,候选回收
```
页面被访问 ──→ Active LRU (头部)
│
▼ (到达尾部,refault 计数)
Inactive LRU (头部)
│
▼ (到达尾部)
回收 (写入 swap/丢弃)
```
### 5.2 kswapd 与 Direct Reclaim
```
空闲内存 < free_low 空闲内存 < free_min
│ │
▼ ▼
轻微回收 Direct Reclaim
(kswapd 异步) (分配者同步阻塞)
│
▼
仍不足?
│
┌───────┴───────┐
▼ ▼
触发 compaction OOM Killer
```
### 5.3 Swap 调优实战
```bash
# 查看当前 swap 使用
cat /proc/meminfo | grep Swap
# 调整 swappiness(0-100)
# 0:尽可能不换出(数据库推荐 1-10)
# 60:默认
# 100:激进换出
sysctl vm.swappiness=10
# 观察回收行为
vmstat 1
# si: swap in, so: swap out
# 如果 si/so 持续非零,说明内存压力严重
```
## 六、OOM Killer 机制与调优
### 6.1 OOM 触发与评分
当所有内存分配尝试(包括 Direct Reclaim 和 Compaction)均失败且无可用内存时,OOM Killer 被调用。
OOM Killer 为每个进程计算 `oom_score`:
```
oom_score ≈ (进程占用内存 / 系统总内存) × 1000
```
评分越高,被 kill 概率越大。调整方式:
```bash
# 查看当前进程的 OOM 评分
cat /proc/self/oom_score
cat /proc/self/oom_score_adj # -1000 到 1000
# 保护关键进程(调整 -1000 表示绝不被 kill)
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj
# 指定高优先级被 kill
echo 1000 > /proc/$(pidof mem-hogger)/oom_score_adj
```
### 6.2 OOM 事件日志分析
```bash
dmesg | grep -i "out of memory\|oom\|killed"
# 或
journalctl -k | grep -i oom
```
典型 OOM 日志包含:
- 触发进程及 GFP 标志
- 各 Node/Zone 的空闲内存
- 被选中 kill 进程的 OOM score
- 内存快照
## 七、CMA (Contiguous Memory Allocator) 与大页
### 7.1 CMA:可迁移的连续内存池
嵌入式设备(GPU、视频编解码)需要大块连续物理内存。CMA 的巧妙之处在于:
- 预留一块物理内存区域(通过 DTB 指定)
- 可由 MOVABLE 页临时借用
- 需要时通过 `migrate_pages` 迁移走借用者,归还连续块
```bash
# 查看 CMA 区域信息
cat /proc/meminfo | grep Cma
# CmaTotal: 262144 kB # 总计预留
# CmaFree: 204800 kB # 当前可用连续
```
### 7.2 大页 (Huge Page / Transparent Huge Page)
标准页 4KB,大页 2MB(x86-64)或 1GB,可大幅减少 TLB 未命中:
```bash
# 透明大页(THP)配置
cat /sys/kernel/mm/transparent_hugepage/enabled
# always / madvise / never
# 数据库通常建议关闭 THP(因为随机大页拆分延迟不可控)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 静态大页(Hugetlbfs)配置
echo 20 > /proc/sys/vm/nr_hugepages
# 同时挂载 hugetlbfs
mount -t hugetlbfs none /mnt/hugetlb
```
## 八、内存泄漏诊断与实战
### 8.1 常用观测工具链
```bash
# 1. 获取系统内存总览
free -h
cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable|Buffers|Cached|Slab"
# 2. Buddy System 状态(判断外部碎片)
cat /proc/buddyinfo
# Node 0, zone Normal 10 8 4 2 1 1 0 0 0 0 0
# 阶数 0 1 2 3 4 5 6 7 8 9 10
# 如果高阶全为 0 而低阶有值,严重外部碎片
# 3. SLAB 分配器状态 / 发现持续增长的缓存
cat /proc/slabinfo | head -30
# 4. 每进程内存
top -p # 或
cat /proc//smaps | grep -E "^(Rss|Private|Size)" | awk '{sum+=$2} END {print sum/1024 " MB"}'
# 5. vmstat 实时观察
vmstat 1 5
# r: 运行队列, b: 阻塞队列
# swpd: swap使用, free: 空闲内存
# si/so: swap in/out (关键指标!)
# bi/bo: block I/O
```
### 8.2 SLAB 缓存泄漏排查
```bash
# 安装 slabtop(实时监控)
apt install procps
slabtop -o
# 找出持续增长的缓存(如 dentry、inode 等)
watch -n 1 'cat /proc/slabinfo | head -50'
# 使用 slab 追踪(需 CONFIG_SLUB_DEBUG)
echo 1 > /sys/kernel/slab//trace
# 通过 kmemleak 探测内核内存泄漏
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
```
### 8.3 ftrace / perf 内存分析
```bash
# 追踪分配热点
perf record -e kmem:kmalloc -ag -p
perf report
# 或使用 bpftrace 实时统计(推荐)
bpftrace -e 'kprobe:__kmalloc { @[comm, args->size] = count(); }'
# 输出每秒各进程按大小段的分配频率
# 追踪页面回收速率
bpftrace -e 'kprobe:shrink_node { @[comm] = count(); }'
```
### 8.4 OOM 前兆告警规则
```yaml
# Prometheus 告警配置示例
groups:
- name: memory
rules:
- alert: HighMemoryUsage
expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes > 0.9
for: 5m
labels:
severity: warning
- alert: HighSlabUsage
expr: node_memory_Slab_bytes / node_memory_MemTotal_bytes > 0.3
for: 10m
labels:
severity: warning
annotations:
summary: "Slab 占用超过 30%"
- alert: SwapThrashing
expr: rate(node_vmstat_pswpin[5m]) + rate(node_vmstat_pswpout[5m]) > 100
for: 2m
labels:
severity: critical
```
## 九、生产环境调优实践与案例
### 9.1 关键内核参数速查表
```bash
# ===== 页面回收策略 =====
# vm.swappiness: 值越低越倾向回收 page cache 而非 swap 匿名页
vm.swappiness = 10 # 数据库宿主机建议 1-10
vm.swappiness = 60 # 默认
# vm.dirty_ratio: 脏页达到总内存此比例时,进程被强制写回
vm.dirty_ratio = 40 # 默认 40
# vm.dirty_background_ratio: 后台 kthread 开始写回的阈值
vm.dirty_background_ratio = 10
# vm.min_free_kbytes: 保留的最小空闲内存(紧急分配储备)
vm.min_free_kbytes = 262144 # 256MB,大内存机器按需增大
# ===== 内存超额分配策略 =====
# vm.overcommit_memory: 0=启发式,1=总是允许,2=严格拒绝
vm.overcommit_memory = 0 # 默认
# vm.overcommit_ratio: 可超额分配的比例(当 overcommit_memory=2 时)
vm.overcommit_ratio = 50
# ===== 同恶性/碎片控制 =====
# 页面紧凑阈值(compact_memory 由外部触发)
/proc/sys/vm/compact_memory = 1
# 透明大页
/sys/kernel/mm/transparent_hugepage/enabled = madvise
/sys/kernel/mm/transparent_hugepage/defrag = defer+madvise
```
### 9.2 案例:数据库服务的内存调优
场景:宿主机 64GB RAM,运行 PostgreSQL,且与其他服务共享。
```bash
# 1. 关闭透明大页(避免大页拆分导致延迟毛刺)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 2. 设置 swappiness = 1,仍允许最后手段换出
sysctl vm.swappiness=1
# 3. 增大脏页阈值,减少 IO 抖动但增加崩溃丢失
sysctl vm.dirty_ratio=30
sysctl vm.dirty_background_ratio=5
# 4. 提升 min_free_kbytes,保证紧急情况有足够储备
sysctl vm.min_free_kbytes=2097152 # 2GB
# 5. 监控 Slab 中的 dentry/inode 缓存,避免内存被缓存占满
# 主动缩小缓存(临时)
echo 2 > /proc/sys/vm/drop_caches # 仅 pagecache
echo 3 > /proc/sys/vm/drop_caches # pagecache + slab
```
### 9.3 案例:容器化环境的 cgroup 内存限制
在容器中(Docker/K8s),内存限制通过 cgroup 实现:
```bash
# 容器的内存子系统
cat /sys/fs/cgroup/memory/memory.limit_in_bytes # 硬限制(触发 OOM)
cat /sys/fs/cgroup/memory/memory.soft_limit_in_bytes # 软限制(尽力满足)
cat /sys/fs/cgroup/memory/memory.usage_in_bytes # 当前使用量
cat /sys/fs/cgroup/memory/memory.stat # 详细统计
# 容器内的 OOM 行为与宿主机不同:
# 1. 容器限制内的 OOM 不会触发宿主机 OOM Killer
# 2. 请求超过容器限制会在分配时直接失败(非 kill)
```
关键指标:`total_cache`(page cache)、`total_rss`(匿名页+共享内存)、`total_slab_reclaimable`(可回收 slab)。
## 十、新一代追踪技术:eBPF 内存分析
### 10.1 BPF 工具链用于内存监控
```bash
# 使用 bcc 工具包
# 1. 观察虚拟内存分配路径
biolatency -m # 查看内存回收延迟
# 2. OOM 事件追踪
oomkill # BCC 工具,实时打印 OOM 事件
# 3. Page Fault 分析
ex top:
ex LAT # 页面故障延迟
ex 或 # 使用 fault.sh
```
### 10.2 bpftrace 实时内存热力图
```bpftrace
#!/usr/bin/env bpftrace
// 按 IP 和大小统计 kmalloc 分配
kprobe:__kmalloc
{
@alloc_size_hist[comm, (uint64)args->size] = count();
}
// 追踪 vmalloc 大分配(>8KB)
kprobe:__vmalloc_node_range
{
$size = args->size;
if ($size > 8192) {
time("%H:%M:%S ");
printf("vmalloc %s size=%d\n", comm, $size);
}
}
```
通过 eBPF,可以在不重新编译内核、几乎零开销的情况下实时观测:
- 各进程按大小段的分配频率
- 某段代码的累计分配量
- 页面回收的触发者和回收速率
- OOM 前完整的内存分配栈回溯
## 总结
Linux 内核内存管理是一个分层的精密系统:
1. **Buddy System** 负责以页为单位的物理内存分配,通过伙伴合并/分裂、迁移类型、页面压缩来对抗碎片
2. **SLUB 分配器** 在页之上构建对象缓存,通过 CPU 本地 freelist 实现无锁快速路径
3. **页面回收与 Swap** 用双时钟 LRU 算法维护内存活性,kswapd 后台回收与直回回收协同工作
4. **CMA 与 Huge Page** 满足大块连续内存的特种需求
在生产实践中,要做好内存管理需要:
- 持续监控 `/proc/buddyinfo`(碎片)和 `/proc/slabinfo`(内核对象泄漏)
- 根据业务类型调整 `swappiness`、`dirty_ratio`、`min_free_kbytes`
- 用 eBPF 工具进行分配路径的实时追踪
- 为关键进程设置 `oom_score_adj` 避免被误杀
理解这套机制,是做好高并发、低延迟服务运维的基础。

发表评论 取消回复