引言
内存管理是 Linux 内核中最核心且复杂的子系统之一,它直接决定了系统的性能、稳定性和可扩展性。在生产环境中,内存管理问题往往表现为系统卡顿、OOM(Out of Memory)杀死关键进程、性能抖动等。本文从架构角度剖析 Linux 内核内存管理的完整知识,从硬件层面的 TLB、Cache 到内核层面的页分配器(Buddy System)、SLUB 分配器、内存控制组(Cgroups)、OOM Killer 机制,再到实战中的参数调优与问题排查,每一位内核开发者和系统运维工程师都能从中汲取到宝贵经验。
一、内存管理架构总览
Linux 内核内存管理层级结构清晰且职责分明,自底向上分为以下几个层次:
| 层次 | 组件 | 职责 |
|---|---|---|
| 硬件层 | 物理内存、DMA、NUMA 节点 | 提供存储基础 |
| 页分配器 | Buddy System (伙伴系统) | 以页 (4KB) 为单位管理物理内存的分配与回收 |
| 对象分配器 | SLAB → SLUB → SLOB | 在内核的内存池之上进行小对象的高效分配 |
| 用户空间 | malloc mmap 等 | 通过系统调用从内核进程地址空间获取内存 |
| 虚拟化 | Ballooning、KSM、virtio-mem | 回收和合并内存供其他虚拟机使用 |
理解这一层级结构是分析内存问题的基础。当应用程序通过 malloc 请求小块内存时,用户态的 glibc 分配器会维护内存池,避免与内核过度交互;当大块页需求时,glibc 则直接通过 mmap 系统调用请求匿名内存。
二、伙伴系统(Buddy System)
伙伴系统是 Linux 物理内存管理的核心算法。它将物理内存按 2 的幂次方大小组织成多个 free_area 链表,分别管理 1, 2, 4, 8...1024 页大小连续的内存块。当无法满足连续内存块时,伙伴系统尝试从其他链表分裂伙伴内存块。
关键特性如下:
| 分类 | 特点 |
|---|---|
| 迁移类型 (MIGRATE_TYPES) | 同一大小内部采用不同的迁移组来降低碎片化 |
| PCP (Per-CPU Pages) | 每 CPU 热路径缓存,避免了 NUMA 远程访问 |
| 管理区 (Zone) | 物理内存被划分为多个 Zone(DMA、DMA32、Normal、Movable、HighMem) |
| 水位 (Watermark) | min/low/high 三级水位控制 kswapd 行为 |
在 slab 分配器引入之前,早期内核直接使用伙伴系统分配小对象,效率极低——频繁地拆分与合并伙伴带来严重的锁争用和碎片化问题。现代系统的解决方案是基于伙伴系统构建 kmem_cache,以对象的方式批量管理内存。
三、SLUB 分配器详解
SLUB (Unqueued Slab Allocator) 目前 Linux 默认使用的对象分配器,是 SLAB 分配器的升级版本,维护更简单且锁争用更少。
SLUB 的核心设计:
| 组件 | 说明 |
|---|---|
| kmem_cache | 每种对象类型对应一个 cache,例如 dentry_cachep、inode_cachep |
| Slab 页 | 每个 slab 管理若干个空闲对象,每个对象包含一个 next 指针 |
| CPU 缓存 | 热路径 freelist 使用每 CPU 变量 |
| NUMA 缓存 | 每个 node 有独立的 partial slab 链表 |
| 调试功能 | Red Zoning/Poisoning/Object Tracking/Freelist Hardened |
通过 /proc/slabinfo 查看当前 cache 状态,SLUB 在处理高并发场景中性能卓越。但有时误用会导致内存泄漏,尤其是当 slab cache 中的对象长期不被释放时。
四、内存管理关键机制
4.1 反向映射 (Reverse Mapping)
当页面被回收时,需要找到指向该页的所有 PTE (页表项) 并将其销毁,这就是反向映射要解决的问题。Linux 早期采用 "对象映射" 方式遍历 anon_vma 链表,RMAP1 性能随进程数的增加呈 O(N) 下降;新的 RMAP2 则通过改进链表结构和引入 interval tree 降低了复杂性。
4.2 页面回收 (Page Reclaim)
页面回收机制有两种:由 kswapd 后台回收和直接回收 (Direct Reclaim)。两者的核心算法都基于两个 LRU 链表 (Active/Inactive),采用 2-bit 时钟算法 (Clock Algorithm) 近似 LRU 行为:
| 链表 | 特点 |
|---|---|
| Active | 频繁访问,不回收 |
| Inactive | 不再频繁访问,优先回收 |
| PG_referenced | 页面被访问过,从 inactive 提升到 active |
当一个 zone 水位低于 low 时,kswapd 唤醒开始异步回收;高于 high 时回收完成。当内存到达 min 水位时,触发直接回收,进程会被阻塞等待回收完成,造成延迟抖动。
4.3 OOM Killer
当内核耗尽内存,直接回收也无法压力释放时,OOM Killer 介入选择一个进程杀死释放内存。选择算法通过 oom_badness 计算得分:
- 通过设置 /proc/[pid]/oom_score_adj (范围 -1000 ~ +1000) 可以调整进程的 OOM 评分
- 设置为 -1000:禁止选择该进程(推荐用于关键服务如 sshd)
- 设置为较高正值:提高被选择概率
五、NUMA 架构下的内存管理
在多路服务器上,不同 CPU 访问不同内存节点的延迟存在差异。Linux 通过以下策略优化内存分配:
| 策略 | 说明 |
|---|---|
| Touch First | 首次访问的页被分配在当前 CPU 所属的本地内存节点 |
| NUMA Balancing | 后台周期性地扫描页表,检测远程访问并决定是否迁移到本地节点 |
| numactl | 通过 --membind/--cpubind 强制进程绑定节点 |
| Automatic NUMA Balancing | 通过 /proc/sys/kernel/numa_balancing 启用主动扫描迁移 |
六、Cgroup 内存控制
v2 内存 Cgroup 允许限制某一组进程的总内存使用量:
| 参数 | 说明 |
|---|---|
| memory.max | 内存使用达到后触发 cgroup 级别的 OOM |
| memory.high | 超过此值后激进回收,尝试不触发 OOM |
| memory.swap.max | 控制 swap 使用量 |
| memory.oom.group | 一组 cgroup 整体触发 OOM |
七、Zswap/Zram 交换优化
当内存不足时,内核将匿名页交换到磁盘。传统 swap 慢是因为磁盘 I/O 延迟大。Zswap 在内核中引入了一个压缩交换缓存,被换出的页先经过压缩算法 (zstd/lzo/lz4) 压缩再换出到磁盘;Zram 则直接在内存中创建一个块设备做压缩交换,避免了磁盘 I/O。
八、大页与透明大页
Huge Pages 可以显著减少 TLB Miss。Linux 支持两种:libhugetlbfs (静态预分配) 和 Transparent Huge Pages (THP, khugepaged 通过扫描合并)。在数据库、KVM 等场景请务必开启 THP;而在某些延迟敏感场景 (Cassandra/Hadoop) 则建议关闭 THP 防止碎片整理抖动。
九、实践工具与调优
常用排查命令与监控工具:
- free -h / cat /proc/meminfo 查看整体内存
- numactl --hardware / numastat 查看 NUMA 拓扑与分布
- cat /proc/slabinfo 查看 slab 缓存状态
- sar -r ALL / vmstat 1 查看内存使用趋势
- perf 追踪 page_fault、compaction、migration
- eBPF 通过 mm_page_alloc/mm_page_free 事件定位热点分配调用栈
- /proc/buddyinfo 查看合伙人分布的碎片情况
性能调优方向
| 问题 | 调优参数 |
|---|---|
| swap 频繁 | 增大 swappiness 阈值或配合 zswap 压缩 |
| NUMA 不均衡 | 开启 numa_balancing 或绑定策略 |
| 直接回收延迟 | 调整 vm.min_free_kbytes |
| 内存碎片 | 开启 compaction 或使用 huge pages |
| OOM 防护 | 关键服务设置 oom_score_adj = -1000 |
总结
Linux 的内存管理系统是一个由伙伴系统、SLUB、反向映射、页面回收、OOM Killer、Cgroup、NUMA 等多组件协同工作的精密引擎。理解其架构和各层分工,能够帮助我们在遇到性能瓶颈、内存泄漏、OOM 事件时快速定位根因并做出高效的调优决策。

发表评论 取消回复