引言:为什么内存管理是内核最复杂的子系统

Linux 内核内存管理子系统(MM)是整个内核中最复杂、涉及面最广的子系统之一。它不仅要管理物理页面的分配与回收,还要处理虚拟地址空间的映射、页面缓存、交换空间、NUMA 拓扑感知、OOM 紧急回收等场景。在云计算和大内存(TB 级)服务器时代,内存子系统的性能直接影响应用的吞吐和延迟。本文从 Buddy 分配器切入,深入剖析 SLAB/SLUB 对象分配器、LRU 页面回收、OOM Killer、NUMA 内存策略、KSM 去重机制,最后总结生产环境中的调优实践。

一、Linux 内存管理架构全景

1.1 物理内存模型

Linux 内核使用 struct page 描述每一个物理内存页帧(通常 4KB)。内核支持三种内存模型:

  • FLATMEM:平坦内存模型,适用于连续物理内存(大多数单/双路服务器)
  • DISCONTIGMEM:不连续内存模型,处理物理内存有较大空洞的场景
  • SPARSEMEM:稀疏内存模型,支持内存热插拔(现代服务器主流),以 section(通常为 128MB/2GB)为粒度管理

在 NUMA 架构中,内存被组织为节点(pg_data_t),每个节点包含一个或多个 Zone(DMA/DMA32/Normal/HighMem/Movable)。Zone 的类型决定了内存的用途限制,例如 DMA32 只允许 32-bit DMA 设备访问。

1.2 虚拟地址空间布局

x86_64 Linux 虚拟地址空间(48 位有效):

区域地址范围用途
用户空间0x0000_0000_0000 ~ 0x0000_7FFF_FFFF_FFFF进程私有:代码、堆、栈、mmap 映射
内核空间0xFFFF_8000_0000_0000 ~ 0xFFFF_FFFF_FFFF_FFFF直接映射区(物理内存线性映射)、vmalloc 区、模块区

内核空间的直接映射区(PAGE_OFFSET 开始)将物理内存线性映射到内核虚拟地址,访问时通过 __va()/__pa() 快速转换,省去了动态映射开销。

1.3 内存管理核心层次

顶层是虚拟内存管理(VMM),它将进程的虚拟地址映射到物理页;底层是物理页分配器(Buddy)和对象缓存(SLUB)。两者的交互通过 per-CPU page cache(PPL)和 LRU 链表完成。整个架构形成:进程/VMA → 页表 → Buddy/SLUB → 物理页 的分层管理体系。

二、Buddy 分配器:物理页框分配的核心

2.1 设计原理

Buddy 分配器将物理内存划分为不同阶(order)的连续块,每个块大小为 2^order 个物理页。分配时从最接近的申请阶向上查找,找到后逐对分裂(buddy splitting);释放时检查相邻块(buddy)是否空闲,合并为更大块。核心数据结构为 free_area 数组(每个 Zone 一份),维护 0~MAX_ORDER(通常 11)个空闲链表。

2.2 伙伴(Buddy)关系计算

两个页帧号为 buddy 的条件是:page_idx XOR (1 << order) == buddy_idx。这种异或运算保证了在一个块中,每一对伙伴块的 XOR 结果都为 2^order,使得合并检测只需要一次异或操作。

2.3 页面迁移与 compaction

当系统运行一段时间后,Buddy 链表中的高阶块变得稀缺(内存碎片化),导致大页(HugePage、透明大页 THP)无法分配。内核通过以下策略缓解:

  • 页面迁移类型(migrate type):MIGRATE_MOVABLE(用户匿名页)、MIGRATE_RECLAIMABLE(页面缓存)、MIGRATE_UNMOVABLE(内核对象),减少不同类型的混叠
  • kswapd + compaction:内核线程 kswapd 后台扫描并迁移页面,使同类型页面连续
  • 手动触发:echo 1 > /proc/sys/vm/compact_memory 强制压缩全系统内存

2.4 分配标志(gfp_t)

Buddy 分配器的行为由 gfp(get free page)标志控制,常见标志:

标志含义
GFP_KERNEL标准内核分配,允许睡眠等待页面回收
GFP_ATOMIC原子上下文分配,允许从紧急 reserves 池中取页,不睡眠
GFP_HIGHUSER用户空间分配,包含 __GFP_HIGHMEM(访问 ZONE_HIGHMEM)
__GFP_ZERO返回清零后的页面
__GFP_THISNODE仅从当前 NUMA 节点分配(无 fallback)
GFP_NOIO / GFP_NOFS限制分配时做 I/O 或文件系统操作,防止递归回收

分配成功概率路径:alloc_pages() → get_page_from_freelist() → __alloc_pages_nodemask() → 如果失败则调用的页面回收路径。

三、SLAB/SLUB 分配器:内核对象的高效复用

3.1 为什么需要对象级分配器

Buddy 分配的最小粒度是页(4KB),而内核对象(task_struct、inode、dentry)通常几十到几百字节。若直接通过 Buddy 分配,会严重浪费内存。SLAB 系列分配器基于"对象复用"思想:从 Buddy 端要来整页,在其中预分配一组同类型对象,维护空闲链表。分配/释放对象只需修改链表指针,无需修改页表和 Buddy 元数据。

3.2 SLUB:现代 Linux 默认分配器

SLUB(Unqueued SLAB Allocator)是 SLAB/SLUB/SLOB 三大家族中的现代默认实现在 Linux 2.6.23 之后替代了 SLAB。核心区别:

  • 去除了 per-CPU 队列:SLAB 为每个 CPU 维护 object 链表但管理复杂;SLUB 用每 CPU 的 partial page 简化
  • 更少的元数据开销:SLUB 将 object 空闲链表嵌入 object 体内(第一个 free 指针),无需独立 slab 控制结构
  • CPU partial 与 Node partial 分层:每 CPU partial 链表优先使用;Node partial 作为全局备份

查看 /proc/slabinfo 就能观察到 SLUB 缓存:

kmalloc-256      12000  12300   256   32    2 : tunables  ...

其中列含义为:缓存名 → 活跃对象数 → 总对象数 → 每个对象大小 → 每个 slab 包含对象数 → 每个 slab 占用页数。

3.3 kmalloc 家族

kmalloc() 是内核中最常用的小对象分配器,内部使用一组 kmalloc-* 通用 SLUB 缓存:

分配器大小底层缓存
kmalloc(32)32 byteskmalloc-32
kmalloc(64)64 byteskmalloc-64
kmalloc(128)128 byteskmalloc-128
kmalloc(1024)1024 byteskmalloc-1024
kmalloc(8192)8192 byteskmalloc-8192

注意:kmalloc() 最大可分配大小受 KMALLOC_MAX_SIZE 限制(通常为 8KB×4 = 32KB on 4KB page),特大分配必须用 vmalloc()。

3.4 vmalloc vs kmalloc

vmalloc() 分配虚拟地址连续的内存(物理不一定连续),适用于大缓冲区(如模块加载、do_mmap() 等大型缓冲)。代价是 TLB 抖动较大、访问延迟较高(因为不是线性映射,需要走非直接映射的页表路径)。kmalloc() 分配物理连续的内存,适合小对象和中量 DMA 缓冲。

3.5 SLAB 释放与 RCU 延迟

内核对象释放常涉及 kfree(),部分场景(如 RCU callback)需要等待 RCU grace period 后才真正释放页面。这种延迟释放策略避免了 use-after-free,但会消耗内存峰值(RCU 批量删除页面)。可通过 /sys/kernel/slab/<cache>/* 节点观察每个 slab 缓存的状态。

四、LRU 页面回收与 kswapd

4.1 LRU 双链结构

Linux 内核将页面分为 Active 和 Inactive 两组,构成 LRU(Least Recently Used)核心:

  • Active list:最近被访问过的页面(通过 PTE Access bit 判断)
  • Inactive list:长时间未访问的页面,回收候选
  • 页面老化:当页面从 Inactive tail 移动到 Active head(二次机会算法),称为"promote";反之为"demote"

每个 LRU 列表按类型(File/Anon)分:因此每个内存 cgroup/NUMA Node 共维护 4 个链表:[Inactive File, Active File, Inactive Anon, Active Anon]。Active Anod 链表中的页面需先 demote 到 Inactive 后才被回收(swap-out 或文件回写)。

4.2 kswapd 与直接回收

kswap_daemon 是内核后台回收线程。当 Zone 空闲页低于 high_wmark 时启动,做异步页回收直到达到 low_wmark 稳定。当分配请求连 min_wmark 的水线都保不住时,触发 直接回收(direct reclaim)——分配进程同步扫描 LRU 链表,造成分配延迟飙升。三种水线关系:free < min → 同步回收,min ≤ free < low → kswapd 运行,low ≤ free < high → kswapd 休眠。

4.3 页面回写与 swap

  • 文件页(File page):脏页通过 writeback 线程刷到磁盘。脏页比例下降后系统更安全(断电无数据丢失),但也增加了回收延迟
  • 匿名页(Anonymous page):没有后备文件,回收只能 swap 到磁盘。vm.swappiness(0-200)控制匿名页 vs 文件页回收的权重。100 表示等权重;设为 0 表示尽量不 swap 匿名页(仅内存不够时);设为 200 表示激进 swap 匿名页

调整 swappiness 对数据库等业务(大文件缓存占主导)至关重要:swappiness=0 可以避免文件缓存被驱逐。

五、OOM Killer:最后一根救命稻草

5.1 触发条件

当系统所有页面回收方式(kswapd + direct reclaim + compaction)均无法释放足够内存,且 alloc_pages() 重试达到上限,触发 Out-Of-Memory Killer。其核心逻辑在 out_of_memory() → select_bad_process() → oom_badness() 中。

5.2 oom_badness 评分公式

进程的"坏分数"计算如下:

points = (进程占用物理内存 + swap使用 + pagetable占用 + HWTE占用) * 1000 / totalmemory

加上 oom_score_adj(用户可配置的偏置,-1000到1000)后,得分最高的进程被 kill。oom_score_adj=-1000 表示不会被 kill(如 systemd、关键守护进程可以通过 /proc/<pid>/oom_score_adj 设为 -1000)。

5.3 cgroup OOM 与全局 OOM

在 cgroup v2 场景中,内存事件受 memory.max 控制器限制:当 cgroup 用量超过限制时,先触发 cgroup OOM(只影响组内进程),而不是全局 OOM。memory.pressure 指标可用于监控组内内存压力。生产经验:容器中的 OOM 多为 cgroup OOM,查看 dmesg | grep -i 'oom' 时注意区分 Memory cgroup out of memory 和 Out of memory: Killed process。

5.4 容器中 OOM 的识别

容器环境下的 OOM 识别链:

kubectl describe pod | grep -A5 Last State
→ OOMKilled: true
→ dmesg | grep oom-kill
→ journalctl -k | grep "Out of memory"

容器已被 kill 但 Pod 重启策略为 Always 时,容器会立刻重启——形成 OOMLoop。需要配合 resources.limits.memory 合理设限并启用 metrics-server 监控内存实际用量。

六、NUMA 与内存策略

6.1 NUMA 架构的影响

多路(multi-socket)服务器中,每个 CPU socket 连接本地内存和远端内存。访问远端内存的延迟通常是本地的 1.5~2 倍,跨 socket 带宽减半。lscpu | grep 'NUMA node' 可以查看 NUMA 拓扑。numastat 显示每个节点上调度和访问的命中率。

6.2 NUMA 内存分配策略

策略定义说明
MPOL_DEFAULT本地分配在当前 CPU 所在 NUMA 节点分配
MPOL_BIND绑定分配从指定节点集合分配,无可用则触发 OOM
MPOL_PREFERRED优先分配优先从指定节点分配,fallback 到其他节点
MPOL_INTERLEAVED交叉分配在指定节点间轮转分配,均匀分布

数据中心常用 numactl --interleave=all 启动应用,使内存访问负载均衡到多个 NUMA 节点,适合 MongoDB/Redis 等大内存应用。

6.3 AutoNUMA Balancing

Linux 内核支持自动 NUMA 平衡(kernel.numa_balancing=1),通过采样进程的内存访问情况,将冷页面迁移到远端,将热页面迁移到本地节点,在不显式指定策略时自动优化。代价是扫描开销,对延迟敏感的 OLTP 数据库通常关闭此功能,改在启动时静态绑定。

七、KSM:内核同页合并(去重)

7.1 工作原理

KSM(Kernel Same-page Merging)由内核线程 ksmd 扫描内存区域,将内容相同的物理页合并为同一个写时复制(CoW)页。特别适合 KVM 虚拟化场景:多个虚拟机运行相同 OS 内核/基础库时,大量内存页相同,合并率可达 50%~80%。

7.2 KSM 参数调优

/sys/kernel/mm/ksm/pages_to_scan   # 每次扫描页数,默认 100
/sys/kernel/mm/ksm/sleep_millisecs # 扫描间隔,默认 20ms
/sys/kernel/mm/ksm/run             # 0=关闭, 1=运行, 2=停止并合并表清空

通过 MADV_MADVICE 标记候选页面:madvise(addr, len, MADV_MERGEABLE) 告诉内核该区域适合扫描;madvise(..., MADV_UNMERGEABLE) 取消标记。

7.3 注意事项

KSM 的扫描开销不可忽视:pages_to_scan=10000 在 64 核机器上可能造成 CPU 资源争抢。在数据库/内存敏感场景下,KSM 的写时复制延迟(首次合并后写入触发 page fault 复制)反而可能导致延迟劣化。

八、生产环境调优实践

8.1 sysctl 内存核心参数

参数推荐值(大内存数据库)说明
vm.dirty_ratio5-10脏页占可用内存的最大比例,触发同步回写
vm.dirty_background_ratio2-5后台回写线程启动阈值
vm.dirty_expire_centisecs1000-3000脏页过期时间,控制回写频率
vm.swappiness1(数据库)或 0(无 swap)swap 倾向
vm.min_free_kbytes总内存 0.5%~1%保留紧急池大小,过低会导致 GFP_ATOMIC 分配失败
vm.overcommit_memory2(严格模式)2=CommitLimit = SwapTotal + overcommit_ratio × RAM
vm.overcommit_ratio80-90允许过量提交的物理内存比例

8.2 透明大页(THP)

透明大页将 2MB(x86_64)的连续页合并为 1 个 TLB entry,减少 TLB miss。但对于数据库(如 PostgreSQL、Oracle),THP 的合并操作可能造成最严重延迟毛刺,Oracle 官方建议关闭(/sys/kernel/mm/transparent_hugepage/enabled = never)。对于大数据/AI/Spark 等顺序访问密集场景,开启 THP 可获得 10%~20% 吞吐提升。

8.3 查看与调试工具链

  • /proc/meminfo:内存统计全貌,MemAvailable = MemFree + 可回收缓存 + SReclaimable
  • /proc/zonelist:NUMA fall-back 顺序
  • /proc/buddyinfo:每个 Zone 各阶 buddy 块数量(碎片诊断关键)
  • /proc/slabinfo:SLAB/SLUB 缓存用量
  • /proc/pid/smaps:进程级内存映射详细统计
  • vmstat -w 1:实时内存 include si/so(swap in/out)
  • sar -r 1:历史内存用量
  • slabtop:实时 slab 缓存查看(如 top)
  • perf probe --add 'mm_page_alloc':通过 tracepoint 跟踪分配热点

8.4 内存碎片化应急处理

如果 /proc/buddyinfo 中高阶(order ≥ 7)块数为 0,说明内存碎片严重。应急处理:

# 1. 强制内存压缩
echo 1 > /proc/sys/vm/compact_memory

# 2. 如果仍不能解决,重启受影响服务(释放大量 unmovable 页)
# 3. 禁用透明大页,避免碎片源
echo never > /sys/kernel/mm/transparent_hugepage/enabled

根治需要调节对端:开启 vm.extfrag_threshold 控制外部碎片风险评分;长期方案是在启动时预留大页(hugepagesz=2M hugepages=2048),消除数据库外碎片来源。

九、进阶话题:内存热插拔与 CXL 内存

9.1 内存热插拔

Linux 支持物理内存的在线添加和移除(memory_hotplug)。核心步骤:离线页 → 迁移 → 删除 section。/sys/devices/system/memory/memory<nr>/state 文件控制 memory block 的 online/offline 状态。在云厂商(阿里云、AWS)的弹性内存功能中已有应用,但要注意 offlining 时必须确保页面已无 Movable 页——这不是所有页面都可以迁移的(如内核锚定页)。

9.2 CXL 内存

Compute Express Link(CXL)定义了一种基于 PCIe 物理层的缓存一致性高速互联协议,允许主机访问连接在 CXL 设备上的 DRAM。在 Linux 内核中,CXL 内存被描述为额外的 NUMA 节点(node_offline 或独立 zone)。CXL 使得 TB 级扩展内存池成为可能(内存分层:本地 DRAM → CXL-DRAM → NVMe Swap),但在内核内存管理层面的统一抽象仍在快速演进中。

结语

Linux 内存管理子系统是一个精密的实时运行协同体:Buddy 管理物理页、SLUB 管理小对象、LRU 做回收策略、OOM 做最后兜底、NUMA 做拓扑感知、KSM 做去重优化。理解这些机制的交互方式,是内核性能调优、故障排查和容量规划的基石。在大内存、多核、NUMA 服务器成为标配的当下,掌握 MM 子系统已不再是"高级话题",而是系统工程师的基本功。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部