Linux内存管理深度实战:从伙伴系统到OOM治理的全方位解析
本文深入剖析Linux内核内存管理的完整技术栈,从物理页面分配器、虚拟内存管理、页面回收与交换、OOM Killer机制到cgroup内存控制,结合生产环境诊断工具与调优实践,为系统工程师提供全方位的内存管理知识体系。
一、内存管理子系统架构总览
Linux内存管理(MM)子系统是内核中最复杂的子系统之一,它负责CPU页表管理、物理内存分配、虚拟地址空间映射、页面回收与交换等核心功能。理解MM子系统的整体架构是排查内存相关问题的基石。
从用户态视角看,每个进程拥有独立的虚拟地址空间(x86_64下通常为128TB用户空间 + 128TB内核空间);从内核态视角,物理内存被划分为节点(NUMA node)→ Zone → Page Frame的三级层次。整个内存管理的核心数据流如下:
| 层级 | 核心结构 | 职责 |
|---|---|---|
| NUMA Node | pglist_data | 隔离不同节点的内存资源 |
| Zone | zone | DMA/DMA32/NORMAL/HIGHMEM分区 |
| Page Frame | struct page | 物理页面的元数据描述 |
关键内核参数:/proc/meminfo、/proc/buddyinfo、/proc/pagetypeinfo、/proc/zoneinfo。生产环境中我们首先通过这些proc文件快速掌握系统内存全局状态。
二、伙伴系统(Buddy System):物理页面分配的核心
2.1 算法原理
伙伴系统解决的是外部碎片问题。它将所有空闲页面按阶(order,即2的幂次)分成11个链表(order 0 ~ 10),order N的块包含 2^N 个连续物理页。当申请 order N 的块时:
- 从 order N 的链表中查找,若存在直接分配
- 若不存在,向上查找高阶(order N+1, N+2 ...),找到后递归拆分
- 拆分后的"伙伴"(buddy)块放入低阶链表
- 释放时检查伙伴是否空闲,若空闲则合并升高阶
伙伴(buddy)的判定依据是物理地址:两个大小相同的块,若它们的起始地址只在第 N+1 位不同,则互为伙伴。这意味着伙伴块是连续的、地址对齐的。
2.2 核心数据结构
// mmzone.h
struct free_area {
struct free_list free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct zone {
struct free_area free_area[MAX_ORDER]; // 11个阶的链表
...
};
free_list按迁移类型(migrate type)分组,这是Linux为解决碎片而引入的关键机制。
2.3 迁移类型(Migrate Types)
Linux为每个Zone的free_list按迁移类型分类,减少不同类型页面混合导致的无法合并问题:
| 迁移类型 | 含义 | 典型场景 |
|---|---|---|
| MIGRATE_UNMOVABLE | 不可移动 | 内核数据结构、页表 |
| MIGRATE_MOVABLE | 可移动 | 用户态内存、文件缓存 |
| MIGRATE_RECLAIMABLE | 可回收不可移动 | 内核缓存( dentries、inodes ) |
| MIGRATE_HIGHATOMIC | 高阶预留 | GFP_ATOMIC紧急分配 |
| MIGRATE_ISOLATE | 隔离使用 | 内存热插拔 |
| MIGRATE_CMA | 连续内存分配器 | 视频/音频大段连续需求 |
2.4 页面分配流程(__alloc_pages)
- 分配标志检查:解析 gfp_mask(__GFP_HIGHMEM、__GFP_ATOMIC、__GFP_IO、__GFP_DIRECT_RECLAIM等)
- 快速路径:alloc_pages_fast_path 直接从PCP(per-cpu pageset)和伙伴系统分配
- 慢速路径:alloc_pages_slow_path 执行内存回收(page reclaim)、压缩 compaction、直接回收等
- OOM Killer:所有唤醒式回收均失败时触发OOM
2.5 碎片整理(Memory Compaction)
当高阶连续物理页不足时,kcompactd 内核线程被唤醒,扫描Zone中的MIGRATE_MOVABLE页面,将其迁移到一端,腾出连续大块物理内存。关键参数:
/proc/sys/vm/compact_memory # 手动触发全局压缩
/proc/sys/vm/extfrag_threshold # 外部碎片阈值
/proc/buddyinfo # 各阶空闲块数量
三、Slab分配器:内核小对象分配优化
3.1 为什么需要Slab
伙伴系统以页(4KB)为粒度分配,而内核中大量对象小于一页(如 task_struct ≈ 7KB,dentry ≈ 256B)。若直接用伙伴系统分配小对象,严重浪费内存且增加TLB压力。Slab分配器在伙伴系统之上构建对象缓存池,实现高效的细粒度分配与回收。
3.2 三代分配器
| 分配器 | 内核版本 | 特点 |
|---|---|---|
| Slab | 2.1 | 原始实现,SLAB_ALIGN,每CPU缓存 |
| SLOB | 2.6 | 极简实现,面向嵌入式 |
| SLUB | 2.6.23+ | 当前默认,放弃SLAB的复杂队列,简化元数据 |
SLUB作为当前默认分配器,核心思想是将元数据嵌入页面本身,利用 struct page 的 lru 链表部分来串联空闲对象。
3.3 SLUB核心实现
// SLUB的关键结构
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每CPU快速分配
struct kmem_cache_node *node[MAX_NUMNODES]; // 每Node半满/全满管理
unsigned int object_size; // 对象大小
unsigned int size; // 包含元数据的总大小
unsigned int offset; // 空闲指针偏移
struct kmem_cache_order_objects oo; // 每slab的页数和对象数
...
};
分配优先级:CPU partial → Node partial → 新页分配。释放时优先放回CPU缓存,避免多CPU竞争。
3.4 Slab信息查看与调试
cat /proc/slabinfo # 所有缓存的活跃/总对象数、页数
slabtop # top式实时监控
cat /sys/kernel/slab/<cache>/aliases # 该缓存的别名数
echo m > /proc/sysrq-trigger # 触发Slab内存转储
生产经验:发现某缓存的num_objs远小于active_objs时,可能存在slab泄漏或缓存过大。/proc/slabinfo中的active_objs * obj_size与num_objs * obj_size对比可以计算浪费比例。
四、页表与虚拟内存映射
4.1 多级页表架构
x86_64 Linux采用四级页表(开启五级时为PGD→P4D→PUD→PMD→PTE),48位虚拟地址布局:
16 bits | 9 bits | 9 bits | 9 bits | 9 bits | 12 bits PGD | P4D/PUD | P4D/PUD | PMD | PTE | Offset
5级页表(LA57)扩展至57位:PGD→P4D→PUD→PMD→PTE→Offset,每级9位。
4.2 反向映射(Reverse Mapping)
页面回收时需要快速找到映射某物理页的所有PTE(反向映射)。Linux实现了两种机制:
- 匿名页反向映射(anon_vma):每个匿名页通过 anon_vma 链表连接所有映射它的VMA,fork时通过链表复制共享关系
- 文件页反向映射(address_space):通过基树(radix tree)或XArray直接查找
4.3 TLB管理
TLB(Translation Lookaside Buffer)缓存页表项,避免每次访存都走页表遍历。TLB miss的代价极高(数十至数百个CPU周期)。Linux对TLB的管理策略:
- Lazy TLB:上下文切换时不刷新内核空间TLB
- TLB Shootdown:修改页表后通过IPI通知其他CPU刷新TLB条目
- PCID(Process Context ID):避免每次切换CR3时全量刷新TLB
五、页面回收(Page Reclaim)
5.1 LRU算法实现
Linux使用双链LRU(Two-list LRU)算法,将页面分为活跃(Active)和非活跃(Inactive)两个链表。页面回收时优先扫描非活跃链表。
// 主要LRU链表
enum lru_list {
LRU_INACTIVE_ANON, // 匿名页非活跃
LRU_ACTIVE_ANON, // 匿名页活跃
LRU_INACTIVE_FILE, // 文件页非活跃
LRU_ACTIVE_FILE, // 文件页活跃
LRU_UNEVICTABLE, // 不可回收(mlock等)
NR_LRU_LISTS
};
页面活跃度通过引用位(reference bit)维护:被访问的页移到活跃链表尾部,长时间未访问的页降级到非活跃链表尾部。回收扫描器从非活跃链表头部开始回收。
5.2 kswapd 与直接回收
- kswapd:后台内核线程,当free_page低于
pages_high阈值时唤醒,异步回收直到高于pages_high。回收触发通过wakeup_kswapd()。 - 直接回收(Direct Reclaim):分配路径发现内存不足时同步阻塞回收,严重损害延迟。触发条件: allocations 低于
pages_low且 kswapd 忙不过来。
5.3 交换机制(Swapping)
匿名页(Anonymous Page)不像文件页那样可以直接写入底层存储,因此需要专用的Swap区域(分区或文件)。回收匿名页时:
- 选择非活跃匿名链表尾部的页面
- 分配swap slot,写入磁盘
- 更新PTE指向swap entry(编码type + offset到PTE)
关键参数:
/proc/sys/vm/swappiness # 回收倾向(0=倾向文件,100=均衡,200=倾向匿名)
/proc/sys/vm/vfs_cache_pressure # 文件缓存回收压力系数
/proc/sys/vm/min_free_kbytes # 保留最小空闲内存
swappiness 默认60,数据库/Redis 环境常设为 0 或 1 以尽量保留文件缓存;容器中常将 swappiness 视为建议权重而非绝对比例。
5.4 页面压缩(zswap/zram/zsmalloc)
Linux提供了基于压缩的匿名页存储方案:
- zswap:在 swap 路径中增加压缩缓存层,压缩后真正写出的页面更少
- zram:创建块设备,匿名页直接存入内存中压缩存储,不写磁盘
- zsmalloc:zram 的专用分配器,减小内部碎片
六、OOM Killer机制
6.1 触发条件
当所有唤醒式回收和压缩手段都失败,且分配请求不允许使用紧急预留(如 GFP_ATOMIC 的高水位检查也失败)时,内核选择杀害一个进程来释放内存。
6.2 OOM Badness评分算法
OOM Killer通过计算oom_score选择目标进程。评分公式:
points = total_vm (KB) * 1000 / total_ram
adj = oom_score_adj * total_ram / 1000
score = points + adj
- 占用物理内存越多 → 分数越高(更容易被杀)
- oom_score_adj 范围 -1000 ~ +1000,-1000 表示不可杀
- root进程额外获得3%内存加分
- 运行时间长的进程分数相对降低
相关接口:
/proc/<pid>/oom_score # 当前得分
/proc/<pid>/oom_score_adj # 调整值(写-1000可免杀)
/proc/<pid>/oom_adj # 旧版接口
6.3 OOM Killer的演进
- Early OOM:Linux 4.x前,用户态监控进程内存,滞后明显
- ppressure_oom(PSI-based):基于PSI(Pressure Stall Information)内存压力提前触发
- cgroup OOM:v2支持memory.pressure / memory.oom.group配置cgroup级别OOM策略
- user reclaim + systemd-oomd:systemd实现的用户态OOM守护进程,更智能地选择目标
七、cgroup内存控制
cgroup v1 和 v2 提供精细的内存资源控制。生产环境重点配置:
7.1 v1内存子系统
/sys/fs/cgroup/memory/<cgroup>/memory.limit_in_bytes # 硬上限
/sys/fs/cgroup/memory/<cgroup>/memory.soft_limit_in_bytes # 软上限(不一定生效)
/sys/fs/cgroup/memory/<cgroup>/memory.swappiness # 组级别swappiness
/sys/fs/cgroup/memory/<cgroup>/memory.oom_control # OOM Killer开关
/sys/fs/cgroup/memory/<cgroup>/memory.kmem.limit_in_bytes # 内核内存限制
7.2 v2统一控制
/sys/fs/cgroup/<cgroup>/memory.max # 硬限制(max = 不限制)
/sys/fs/cgroup/<cgroup>/memory.high # 触发主动回收的阈值
/sys/fs/cgroup/<cgroup>/memory.low # 尽量保护不被回收
/sys/fs/cgroup/<cgroup>/memory.min # 绝对保留(不会被全局回收器扫描)
/sys/fs/cgroup/<cgroup>/memory.oom.group # 组级别OOM(一杀全杀)
v2相比v1的核心改进:memory.high 实现了渐进式的内存抑制——超过high就回收,超过max才OOM。这使得容器体验更平滑。
八、KSM与内存去重
KSM(Kernel Samepage Merging)内核线程扫描全系统页面,发现内容完全相同的页面时:
- 将多个PTE指向同一物理页
- 原页面标记为COW(Copy-On-Write),写入时再分裂
- 合并率 = 共享页面数 / 扫描页面数
适用场景:KVM虚拟化中多个相同OS实例、容器基础镜像。
/sys/kernel/mm/ksm/run # 1=开启 0=暂停
/sys/kernel/mm/ksm/pages_to_scan
/sys/kernel/mm/ksm/sleep_millisecs
/sys/kernel/mm/ksm/pages_shared
/sys/kernel/mm/ksm/pages_sharing # 已共享的物理页数
cat /sys/kernel/mm/ksm/pages_unshared # 已检测但内容不同的页
九、大页(Huge Pages)技术
9.1 HugeTLB 静态大页
预留固定大小的大页池(通常2MB或1GB),分配给明确请求的进程(如DPDK、QEMU、Databasae)。
/proc/sys/vm/nr_hugepages # 大页总数(启动时预留)
/proc/sys/vm/nr_overcommit_hugepages # 超分配上限
cat /proc/meminfo | grep Huge # 查看HugePages_Total/Free
大页直接操作PMD级页表(2MB)或PUD级页表(1GB),降低TLB miss率。
9.2 透明大页(THP)
THP是HugeTLB的动态版本,khugepaged内核线程自动合并连续4KB页为2MB大页。
/sys/kernel/mm/transparent_hugepage/enabled # always/madvise/never
/sys/kernel/mm/transparent_hugepage/defrag # 碎片整理策略
数据库场景(如Redis、PostgreSQL)通常建议关闭THP(设为never),因为延迟尖刺和内存膨胀的代价大于TLB收益。
十、NUMA内存策略
10.1 NUMA拓扑发现
numactl --hardware # 查看NUMA拓扑
numastat # 各节点的内存分布统计
cat /sys/devices/system/node/node*/meminfo # 单节点内存详情
cat /proc/<pid>/numa_maps # 进程的NUMA策略与页面分布
10.2 内存分配策略
| 策略 | 说明 |
|---|---|
| MPOL_DEFAULT | 本地分配(LocalAlloc) |
| MPOL_BIND | 绑定到指定节点集合 |
| MPOL_PREFERRED | 优先本地,允许回退 |
| MPOL_INTERLEAVE | 交错分布到指定节点 |
通过 set_mempolicy() 系统调用或 numactl --membind=Node 命令行指定。
10.3 NUMA Balancing
Linux内核的自动NUMA均衡机制( numa_balancing 开关默认开启),通过采样缺页中断将页面迁回本地节点。
/proc/sys/kernel/numa_balancing # 自动NUMA均衡开关
/proc/sys/kernel/numa_balancing_scan_delay_ms # 扫描延迟
/proc/sys/kernel/numa_balancing_scan_period_max_ms # 扫描间隔
十一、内存诊断与监控工具
11.1 基础工具
# 整体内存概览
free -h
cat /proc/meminfo
vmstat 1 # 虚拟内存统计
sar -r 1 # 历史内存报告
# Slab
slabtop -o
cat /proc/slabinfo | sort -k4 -rn # 按页数排序
# Buddy
cat /proc/buddyinfo # 各Zone各阶空闲块
# Page Table
cat /proc/<pid>/maps # 进程VMA列表
pmap -x <pid> # 详细RSS/PSS
# OOM相关
dmesg | grep -i "out of memory" # OOM事件日志
journalctl -k | grep -i oom # systemd journald版
# NUMA
numastat -p <pid> # 按进程统计
numastat -m # 按Zone统计
11.2 eBPF/BCC 内存追踪
# 页面分配追踪
funclatency mm_page_alloc # 页面分配延迟分布
stackcount mm_page_free # 页面释放调用链
# Slab分配追踪
slabratetop # Slab分配/释放速率
funclatency kmem_cache_alloc # Slab分配延迟
# OOM追踪
trace 'oom_kill_process struct oom_control *oc' # OOM事件输出
# 缺页中断统计
funclatency handle_mm_fault # 缺页延迟
biosnoop # 磁盘交换IO
11.3 perf 内存分析
perf stat -e dTLB-load-misses,dTLB-store-misses -p <pid>
perf record -e page-faults -g -p <pid> # 采样缺页调用栈
perf record -e kmem:kmem_cache_alloc -e kmem:kmem_cache_free
11.4 BPFTrace 脚本示例
// 追踪直接回收事件
kprobe:direct_reclaim_begin {
@start[tid] = nsecs;
}
kprobe:direct_reclaim_end /@start[tid]/ {
@lat_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
// 追踪Slab分配
kprobe:kmem_cache_alloc {
@alloc_bytes[comm, kstack(5)] = sum(arg1);
}
十二、生产环境调优实践
12.1 通用服务器调优
# 减少交换倾向
vm.swappiness = 10
# 增加文件缓存压力回收
vm.vfs_cache_pressure = 50
# 增加脏页刷新频率(减少突发的写峰值)
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
# 防止意外OOM杀死系统关键进程
vm.panic_on_oom = 0
vm.oom_kill_allocating_task = 0
12.2 数据库/Redis优化
# 关闭THP
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 极低swappiness
vm.swappiness = 0 # Redis官方建议
# 关闭NUMA均衡(若已绑核)
kernel.numa_balancing = 0
# 使用静态大页(防止TLB抖动)
vm.nr_hugepages = ... # 按Redis内存配置
# 启用mlockall(Redis配置中设置)<怕冷
# cgroup限制
echo "8G" > /sys/fs/cgroup/memory/redis/memory.max
echo "0" > /sys/fs/cgroup/memory/redis/memory.swap.max
12.3 容器/云原生环境
# cgroup v2 内存配置
echo "1G" > /sys/fs/cgroup/workload/memory.max # 硬上限
echo "800M" > /sys/fs/cgroup/workload/memory.high # 主动回收触发
echo "200M" > /sys/fs/cgroup/workload/memory.min # 绝对保留
# PSI监控
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=123456
# full avg10=0.00 avg60=0.00 avg300=0.00 total=123456
# eBPF追踪容器内存
bpftrace -e 'cgroup:memory: { @[args->cgroup_path] = sum(args->nr_pages); }'
十三、前沿演进:Rust、BPF与内存管理
Linux内存管理领域正在经历两个重大变革:
- Rust in MM:Linux 6.8+ 引入 Rust 支持,内存管理子系统是Rust重写的优先目标(Binder驱动已开始),减少use-after-free、double-free等内存安全问题
- eBPF热力图:通过eBPF实时追踪页面分配分布、TLB miss热点,BCC/bpftrace已经是标配,新的memleak.bpf.c可自动发现内存泄漏
- damon:Data Access MONitor,基于访问热力图的自动页面放置与回收策略,已在机器学习训练场景中显著提升性能
总结
Linux内存管理是一个多层次、多策略协同的复杂系统。物理层通过伙伴系统解决外部碎片,通过SLUB解决内部碎片;回收层通过双链LRU+Swap保证系统持续运行;控制层通过cgroup实现资源隔离;诊断层通过PSI、eBPF、perf提供全方位可观测性。
理解这些机制之后,我们面对内存相关故障时不再只是"加内存"三板斧,而是能够:精确定位症状(TLB miss高?直接回收慢?OOM评分异常?)→ 给出因果解释 → 实施有效调优。这就是深度理解内存管理的工程价值所在。

发表评论 取消回复