引言
Linux 内核的内存管理子系统是整个系统中最复杂、最核心的组件之一。它不仅负责管理物理内存的分配与回收,还要为上层应用提供高效、透明的虚拟内存抽象。本文将从内核源码层面,深入剖析 Linux 内存管理的核心机制——伙伴系统(Buddy System)、Slab 分配器、虚拟内存管理以及 OOM Killer,并结合实际案例讲解性能调优与问题排查方法。
1. 物理内存模型与 Zone 架构
Linux 内核将物理内存组织为 Node → Zone → Page 的三层结构。在 NUMA 架构中,每个 CPU 节点拥有一个 pg_data_t 结构;每个节点内又划分为多个 Zone,用于解决历史遗留的 DMA 地址限制问题。
// include/linux/mmzone.h
struct zone {
unsigned long _watermark[NR_WMARK]; // 水位线: min/low/high
unsigned long nr_free_pages; // 当前空闲页面数
struct free_area free_area[MAX_ORDER]; // 伙伴系统的空闲区域表
// ...
};
// 水位线含义
enum zone_watermarks {
WMARK_MIN, // 紧急保留内存,分配会触发直接回收
WMARK_LOW, // 启动kswapd回收的阈值
WMARK_HIGH, // 空闲充足,无需回收
};
在 64 位系统中,Zones 的重要性已大幅降低——ZONE_DMA 和 ZONE_DMA32 仅用于兼容老设备,ZONE_NORMAL 才是内存分配的主要来源。
2. 伙伴系统(Buddy System)
伙伴系统用于管理连续物理页面的分配与释放,核心思想是将页面按阶(Order,以 2 的幂次分组)组织为链表,分配时向上分裂,释放时向下合并。
// mm/page_alloc.c: __alloc_pages_nodemask()
// 这是页面分配的入口函数
page = get_page_from_freelist(alloc_mask, order, ...);
if (unlikely(!page))
page = __alloc_pages_slowpath(...);
// fast path: 从 free_area 链表中直接取
// slow path: 触发回收、压缩、甚至调用 OOM Killer
分配过程:
- 从目标 Zone 的
free_area[order]链表取一个空闲块 - 若目标 Order 无空闲,从更高 Order 分裂(
expand())为两个伙伴块 - 空闲块伙伴放回低 Order 链表
- 触发
__zone_watermark_ok()检查水位线,决定是否走 slow path
碎片整理(Memory Compaction):当系统运行较长时间后,伙伴系统可能因碎片化而无法分配高阶连续页面。compaction.c 通过迁移页面来聚合空闲页面:
// mm/compact.c: compact_zone()
// 两个扫描器:
// - migration scanner: 找到可移动的页面
// - free scanner: 找到目标空闲区域
// 两者相向移动,将已分配页面移出形成大块连续空闲空间
3. Slab 分配器:小对象高效复用
伙伴系统以页(通常 4KB)为粒度分配,对于内核中频繁创建的小对象(如 task_struct、inode、dentry),直接分配整页会造成严重的内部碎片。Slab 分配器通过在页面内部预先切分和缓存对象,实现近乎 O(1) 的分配速度。
三层架构:
用户调用 kmalloc()
↓
Slab 层(对象缓存管理)
↓
伙伴系统(底层页面获取)
// mm/slab.h
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // per-CPU 无锁快速分配
struct kmem_cache_node *node[MAX_NUMNODES]; // per-Node 后备存储
unsigned int size; // 对象(含对齐)大小
unsigned int object_size; // 用户请求大小
unsigned long flags; // SLAB_ACCOUNT 等
const char *name;
};
// 快速分配路径:cpu_slab→freelist 直接取
// 慢速路径:Node partial 页补充 或向 Buddy 申请新页
三种变体:
- Slab(Jeff Bonham 设计):原始版本,使用 SLAB/SLAB_FULL 等状态位
- Slob:面向嵌入式,极简实现,仅百余行代码
- Slub(当前默认):简化元数据,per-CPU 无锁设计,碎片更小
kmalloc 与 vmalloc 的核心差异:
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理地址 | 连续(直接映射) | 不一定连续 |
| 虚拟地址 | 线性映射区 | 单独的 vmalloc 区间 |
| 性能 | 高(无 TLB 刷新) | 低(需修改页表) |
| 大小 | 通常 ≤ 8KB | 可达数百 MB |
| 适用场景 | DMA、硬件交互 | 大内存模块加载 |
4. 虚拟内存与页表管理
每个进程拥有独立的虚拟地址空间,由 mm_struct 结构描述。32 位系统用户空间约 3GB,64 位系统可达 128TB(x86-64 使用 48 位虚拟地址)。
// include/linux/mm_types.h
struct mm_struct {
pgd_t *pgd; // 全局页目录(PGD)
struct vm_area_struct *mmap; // VMA 链表(虚拟内存区域)
unsigned long total_vm; // 总映射页面数
unsigned long locked_vm; // mlock 锁定的页面
struct rw_semaphore mmap_sem; // 保护 VMA 操作的信号量
// ...
};
多级页表(以 x86-64 为例):
x86-64 采用 4 级页表:PGD → P4D → PUD → PMD → PTE,每级 9 位(512 项),覆盖 48 位(256TB)虚拟地址空间。
// 虚拟地址分解(4KB 粒度)
// | PGD(9) | P4D(9) | PUD(9) | PMD(9) | PTE(9) | Offset(12) |
// 内核通过硬件遍历(Page Table Walk)逐级查找到物理页面
// TLB(Translation Lookaside Buffer)缓存近期映射以加速遍历
VMA(虚拟内存区域):内核不逐页跟踪映射属性,而是将连续且属性相同的区域合并为 VMA。mmap()、brk()、缺页中断都会创建 VMA。/proc/[pid]/maps 就是 VMA 的导出。
5. 页面回收与 OOM Killer
当系统内存不足时,内核通过以下路径回收内存:
- kswapd:后台守护线程,当 Zone 空闲低于
low水位线时唤醒,异步回收 - Direct Reclaim:分配路径同步回收,分配者会阻塞等待
- OOM Killer:极端情况下直接终止进程以释放大量内存
页面回收的 LRU 算法:
内核使用两个链表(Active/Inactive)实现近似 LRU。页面首次访问加入 Inactive,再次访问提升到 Active。回收时从 Inactive 尾部取页面。Linux 5.0+ 引入了 Multi-Gen LRU(MGLRU),通过世代而非链表实现更高效的页面回收。
// mm/vmscan.c: shrink_lruvec()
// 扫描比例控制:swappiness 参数(0-200)
// swappiness=0: 优先回收 page cache(文件页)
// swappiness=100: 匿名页和文件页相同优先级
// swappiness=200: 激进地回收匿名页(换出)
// 默认值: swappiness=60
OOM Killer 评分机制:
// mm/oom_kill.c: oom_badness()
// 评分公式(简化):
// points = (total_vm * 1000 / total_ram_pages) + oom_score_adj
//
// - total_vm: 进程占用的页数(越大越容易被杀)
// - oom_score_adj: -1000 到 +1000,-1000 表示禁用 OOM
// - 守护进程、systemd 等关键服务通常设为 -1000
//
// 实际还会考虑:子进程内存、运行时间、特权级等修正
OOM 实战分析:通过 dmesg | grep -i "out of memory" 可以看到 OOM Killer 的决策日志,包括被杀进程的评分、内存占用等信息。/proc/[pid]/oom_score 和 /proc/[pid]/oom_score_adj 可用于监控和调整进程的 OOM 优先级。
6. NUMA 架构下的内存分配
在 NUMA(非统一内存访问)系统中,CPU 访问本地节点的内存速度远快于跨节点访问。内核通过以下策略优化 NUMA 性能:
- AutoNUMA Balancing:内核自动检测页面访问模式,将频繁被某 CPU 访问的页面迁移到该 CPU 的本地节点
- zone_reclaim_mode:控制 Zone 回收策略,
=1时优先本地回收而非从远端分配 - numactl:用户态工具,绑定进程内存策略(interleave/strict/preferred)
7. 性能调优与实战案例
7.1 内存碎片化排查
# 查看伙伴系统碎片情况
cat /proc/buddyinfo
# Node 0, zone Normal 100 50 20 10 5 2 1 0 0 0 0
# 各 order 对应的空闲块数,order=10 (4MB) 为 0 表示大块连续内存已耗尽
# 查看 compaction 统计
grep -i compact /proc/vmstat
# compact_stall: 因compaction而阻塞的次数
# compact_fail: compaction 失败的次数
# compact_success: compaction 成功的次数
7.2 OOM 事件调查
# 查看最近的 OOM 事件
dmesg | grep -A 30 "Out of memory"
# 查看进程 OOM 评分
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj
# 免疫关键进程(如数据库)
echo -1000 > /proc/<pid>/oom_score_adj
7.3 Huge Pages 配置
2MB/1GB 大页可以减少 TLB Miss,显著提升数据库等内存密集型应用的性能:
# 查看大页配置
cat /proc/meminfo | grep Huge
# 预留 512 个大页(每个2MB = 1GB)
echo 512 > /proc/sys/vm/nr_hugepages
7.4 Per-CPU 内存分配陷阱
使用 alloc_percpu() 时要特别注意四舍五入问题。不同架构的 alloc_percpu() 可能会对齐到缓存行,导致实际内存消耗远超预期。
8. 总结与选型指南
| 场景 | 推荐方案 | 关键字 |
|---|---|---|
| 频繁创建的小对象 | Slab/kmalloc | kmem_cache_create, kmalloc |
| 大块连续物理内存 | alloc_pages + order | GFP_KERNEL, __get_free_pages |
| 大块无需物理连续 | vmalloc | vmalloc, vfree |
| DMA 操作 | dma_alloc_coherent | GFP_DMA, dma_pool |
| 实时/中断上下文 | GFP_ATOMIC | 不允许睡眠的分配 |
| 用户态内存分配 | malloc/brk/mmap | mmap_threshold 调优 |
Linux 内存管理的核心矛盾是:分配效率与碎片化的平衡。伙伴系统解决了大粒度连续分配问题,Slab 解决了小对象高频分配问题,LRU + compaction 解决了碎片回收问题。理解这三层机制,才能在内核开发和系统调优中做出正确的决策。
对于应用开发者,除了合理设置 oom_score_adj 和 Huge Pages 外,更应关注 cgroup 内存限制、memcg 统计以及 PSI(Pressure Stall Information)等新机制,它们比传统的 free 命令提供更精准的内存压力洞察。

发表评论 取消回复