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_DMA0-16MBDMA 设备寻址受限区域
ZONE_DMA3216MB-4GB32位 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 回收包含以下步骤:

  1. 扫描 Inactive 匿名页和文件缓存页的比例(swappiness 控制两者权重)
  2. 页面被回收前,若为脏页则写回磁盘(文件页 → inode writeback;匿名页 → swap)
  3. 将"PTE 引用该页但页未上锁"的项清零,后续再次访问触发 PF(Page Fault)重新加载
  4. 页面从 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.maxswap 空间硬限禁用 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 等观测手段的完整链路,帮助我们从宏观水位追踪到微观页面级别,精准定位问题。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部