Linux内核内存管理深度实战:从伙伴系统到内存回收的完整链路
引言
内存管理是Linux内核中最复杂、最核心的子系统之一。它不仅负责物理内存的分配与回收,还构建了从硬件 MMU 到虚拟化 cgroup 的完整抽象层。本文将深入剖析 Linux 内核内存管理的核心机制:伙伴系统如何实现高效的大块物理内存分配、Slab/Slub 分配器怎样优化小对象频繁分配释放的场景、页表映射如何连接虚拟地址与物理地址、LRU 算法如何驱动页面回收、以及 OOM Killer 在内存耗尽时如何做出"生死抉择"。
一、物理内存模型与区域划分
1.1 NUMA 架构下的节点管理
现代多核服务器普遍采用 NUMA(非统一内存访问)架构,CPU 访问本地节点的内存远快于跨节点访问。Linux 内核通过 pg_data_t 结构描述每个 NUMA 节点,每个节点又进一步划分为多个内存区域(Zone),形成 node → zone → page 的三级管理模型。
| 区域 | 说明 | 典型用途 |
|---|---|---|
| ZONE_DMA | 0-16MB | DMA 设备寻址受限区域 |
| ZONE_DMA32 | 16MB-4GB | 32位 DMA 设备 |
| ZONE_NORMAL | 可直接映射区 | 内核线性映射,直接用于分配 |
| ZONE_MOVABLE | 可移动区域 | 内存热插拔、页面迁移 |
1.2 伙伴系统(Buddy System)
伙伴系统是物理内存分配器的核心算法,管理以页(通常4KB)为单位的物理内存块。它将所有空闲页面按"阶(order)"组织为11个链表(order 0-10),对应 1, 2, 4, 8, ..., 1024 个连续页面块(即 4KB 到 4MB)。
分配流程:当请求 order-N 大小的块时,先在 order-N 链表中查找空闲块。若无空闲,则向更高阶链表请求,将大块一分为二:一个分配给请求者,另一个作为"伙伴(buddy)"插入低阶链表。递归执行直到成功或最终失败。
释放流程:释放 order-N 的块时,检查其伙伴块是否也为空闲。若是,则合并为 order-(N+1) 的大块,并向上递归检查合并,直至达到最大阶或伙伴不在空闲状态。
伙伴系统的优雅之处在于:两个大小相同、地址连续且从同一大块拆分而来的块称为"伙伴"。判断伙伴关系仅需简单的位运算:XOR(order) 即可完成,无需遍历树或哈希表,时间复杂度为 O(1)。
1.3 per-cpu 页缓存(PCP)
多核系统中,多个 CPU 同时调用 alloc_pages() 会引发链表锁竞争。为此内核在每个 CPU 上维护了 per-cpu 页缓存(pageset),预分配一批单页供本地 CPU 快速分配。当 PCP 水位低于 batch 阈值时从伙伴系统批量补充;高于 high 时批量归还。这种设计将单页分配的争用降至最低,是 SMP 性能的关键优化。
二、Slab/Slub 分配器
2.1 为什么需要 Slab
伙伴系统最小分配单位是 4KB 页,而内核中大量对象(如 task_struct、inode、dentry 等)远小于 4KB。频繁分配小对象将造成严重内部碎片。Slab 分配器在伙伴系统之上构建对象缓存池,预先分配整页、初始化为固定大小的对象切片,使用时直接从缓存池取用,避免初始化和碎片双重开销。
2.2 Slab 核心架构
Slab 三层架构:
- Cache:针对特定类型对象的内存池,如
kmalloc-64、dentry_cache、inode_cache等 - Slab:从伙伴系统申请的一页或多页内存,被切分为固定大小的槽位
- Object:Slab 中的单个可用槽位
每个 Slab 维护三种链表跟踪使用状态:full(全满)、partial(部分分配)、free(空闲)。分配时优先从 partial 列表取用对象,无可用时从伙伴系统申请新 Slab。释放时对象回到 Slab,若 Slab 从 partial 变为空则释放回伙伴系统。
2.3 SLUB:现代 Slab 实现
Linux 2.6.23 起 SLUB 成为默认分配器。相比传统 SLUB:
- 去除 per-CPU 队列的复杂维护逻辑,使用简单的 per-node partial 链表
- 元数据嵌入页面结构体(
struct page扩展字段),减少独立管理开销 - CPU 本地缓存使用无锁设计(cmove/ptr),释放到本地对象 O(1)
- 调试功能更完善:Red Zone 检测越期写、Poison 检测 use-after-free、Tracking 定位泄漏源
三、虚拟内存与页表管理
3.1 虚拟地址空间布局
x86_64 架构下,Linux 使用 48 位虚拟地址(256TB),用户空间与内核空间按 1:1 划分。内核空间高半区包含线性映射区(直接映射物理内存前 896GB)、VMALLOC 区(虚拟连续但物理不连续)、以及固定映射、PIE 等区域。进程的 mm_struct 通过 VMA(Virtual Memory Area)红黑树管理不同用途的地址区间。
3.2 四级/五级页表
PTE(页表项)将虚拟地址映射到物理页框号(PFN)及其访问权限。x86_64 默认使用四级页表(PGD→PUD→PMD→PTE),57 位地址时启用五级(增加 P4D 级)。每次地址翻译需 4-5 次内存访问,由 TLB(Translation Lookaside Buffer)缓存最近映射以加速。
大页(Huge Pages / THP):标准页 4KB,大页 2MB(PMD 级映射)甚至 1GB(PUD 级)。使用大页大幅减少 TLB Miss 和页表遍历深度。Linux 提供两种方案:
- 静态大页(HugeTLB):启动参数预留固定数量大页,需应用程序显式调用 mmap
- 透明大页(THP): khugepaged 后台线程自动将连续小页合并为 2MB 大页,对应用透明
四、页面回收与 LRU 算法
4.1 页面如何变为可回收
Linux 将所有页面组织为两个 LRU 链表:Active(热页)和 Inactive(冷页)。页面首次加入 Inactive 链表尾部;当被访问时通过硬件置位或软件模拟提升到 Active。页面回收扫描 Inactive 链表尾部,将最久未使用的页面释放。
双时钟算法:每次扫描将 Inactive 页表头移动到 Active 候选池,若页面在下次扫描前被再次访问则提升到 Active,否则标记为可回收。这种机制模拟了第二次机会算法(Second Chance),有效区分真正冷页。
4.2 kswapd 后台回收
kswapd 是每个 NUMA 节点上的后台守护线程,持续监控系统内存水位。当某 Zone 空闲页面低于 low 阈值时唤醒 kswapd 进行异步回收,回收至 high 阈值以下停止。若空闲页面进一步跌破 min 阈值,直接回收(direct reclaim)将同步阻塞在进程上下文中执行,代价更高。
4.3 回收路径全景
一次完整的 kswapd 回收包含以下步骤:
- 扫描 Inactive 匿名页和文件缓存页的比例(
swappiness控制两者权重) - 页面被回收前,若为脏页则写回磁盘(文件页 → inode writeback;匿名页 → swap)
- 将"PTE 引用该页但页未上锁"的项清零,后续再次访问触发 PF(Page Fault)重新加载
- 页面从 LRU 链表移除,释放回伙伴系统
4.4 swap 机制
当匿名页(进程堆栈、匿名 mmap 等无文件后备的内存)被换出时,Linux 使用独立设备(swap partition)或镜像文件(swap file)作为写入目标。swap 性能远低于物理内存(磁盘延迟 ms 级 vs 内存 ns 级),swappiness 默认值 60 意味着倾向回收文件缓存而非直接换出匿名页,适用于大多数数据库场景。数据库常建议关闭 swap 或使用极小的 swap空间。
五、OOM Killer:内存耗尽的最后防线
5.1 oom_badness() 评分机制
当系统内存跌至极低水平且回收无法满足需求时,OOM Killer 被触发。它遍历所有进程,以 oom_badness() 给每个进程计算惩罚分,公式为:
points = (total_vm / total_ram * 1000) * (1 + adj^3 / 8)
- total_vm / total_ram:进程占用物理内存占系统总内存的比例,线性反映内存消耗
- oom_score_adj(adj):用户/管理员可调参数(-1000 到 1000),立方级放大影响。设为 -1000 表示永不 kill(推荐用于关键守护进程)
- 特权进程与 init:特权 root 进程额外乘以 3%;init(PID 1)和内核线程永不 kill
OOM Killer 选择得分最高的进程发送 SIGKILL 信号终止,循环直到释放足够内存。这是内核"两害相权取其轻"策略的体现:牺牲一个进程,保全整个系统的运行。
5.2 OOM 防护实践
生产环境中推荐以下防护措施:
- 关键服务
/proc/self/oom_score_adj设为 -1000 或 -999 - 利用 cgroup memory.limit_in_bytes 限制容器或服务的最大内存用量
- 通过 systemd 设置
OOMPolicy=continue或设置内存限制 - 监控
/proc/[pid]/oom_score实时评分
六、cgroup v2 内存控制
cgroup v2 对内存资源提供了精细化的控制能力:
| 参数 | 说明 | 典型调优 |
|---|---|---|
| memory.max | 硬限制,超限触发 OOM | 容器内存上限 |
| memory.high | 软限,超限触发限流回收 | 低于 max 10-20% |
| memory.low | 尽力保护的内存区间 | 保证服务最低用量 |
| memory.min | 硬保护,不会被回收 | 关键缓存锁定 |
| memory.swap.max | swap 空间硬限 | 禁用 swap 设为 0 |
memory.low 优先级机制:当系统内存紧张时,内核优先回收无 low 保护的 cgroup,再考虑低于 high 的 cgroup。A/B 测试服务可用 memory.low 给稳定版服务更高保障,新版本服务更少。
七、内存管理性能优化实战
7.1 页面缓存与回写调优
# 脏页占系统总内存 10% 时,内核启动后台回写
vm.dirty_ratio=10
# 脏页占 5% 时,进程 fsync 触发直接回写等待
vm.dirty_background_ratio=5
# 回写守护进程唤醒间隔,影响写合并效率 (单位 0.01s)
vm.dirty_writeback_centisecs=500
# 脏页过期时间,超过 30 秒的脏页强制回写 (单位 0.01s)
vm.dirty_expire_centisecs=3000
7.2 TLB 与大页配置
# 静态大页数量(2MB 为单位)
# GRUB: hugepages=256
# 透明大页策略:always(全局启用)/ madvise(仅 madvise 标记区域)
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 数据库场景建议关闭 THP(避免 khugepaged 抖动)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
7.3 OOM 调参与监控
# OOM dump 输出全部分配信息(调试用,生产注意内存开销)
vm.oom_dump_tasks=1
# 进程内存实时监控
cat /proc/[pid]/status | grep -E "VmRSS|VmSwap|VmSize"
# cgroup v2 查看当前用量(systemd-cgtop 或 cgroup fs)
cat /sys/fs/cgroup/[path]/memory.current
cat /sys/fs/cgroup/[path]/memory.stat
八、总结
Linux 内存管理是一个高度层次化的架构:最底层由伙伴系统管理物理页框,Slub 在之上构建小对象缓存效率;虚拟内存通过多级页表将连续的虚拟地址翻译到离散的物理页,由 TLB 和 PCP 优化翻译速度;LRU 算法驱动着页面在 Active/Inactive 间流转,由 kswapd 后台回收维持水位;当所有手段都用尽时,OOM Killer 作为最终防线,牺牲个别进程保障全局。
理解这套机制不仅有助于分析生产环境中的内存瓶颈(抖动、泄漏、swap storms),更能指导系统调优决策:NUMA 亲和性与 THP 对数据库的影响、swappiness 与 cgroup memory.low 的语义差异、如何合理设置 vm.dirty 参数以平滑 I/O 峰值。面对内存相关故障时,从 vmstat、sar -r、slabtop、/proc/meminfo 到 /proc/[pid]/smaps 等观测手段的完整链路,帮助我们从宏观水位追踪到微观页面级别,精准定位问题。

发表评论 取消回复