引言:为什么 Linux 内核内存管理值得深入理解
内存管理是 Linux 内核中最复杂、最核心的子系统之一。从用户态的 malloc() 到底层的物理页面分配,从虚拟地址到物理页框的映射,从 Slab 分配器的精细管理到页面回收的水位线机制 — 整个栈层涉及无数工程智慧和性能权衡。本文将逐层剖析 Linux 内核内存管理的完整架构,帮助你在遇到 OOM、内存泄漏、性能瓶颈时,不仅知其然,更知其所以然。
一、物理内存建模:节点、区域与页框
1.1 NUMA 与 UMA 模型
Linux 内核将物理内存建模为 pg_data_t(pglist_data)结构体链表,每个节点对应一个物理内存区域。在 UMA(统一内存访问)系统中只有一个节点,而 NUMA 系统则根据 CPU 内存控制器的拓扑划分为多个节点。访问"远端"节点的内存延迟可能比本地节点高出 50%~200%,因此合理的 NUMA 感知分配策略至关重要。
struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
struct page *node_mem_map;
unsigned long node_start_pfn;
...
};
1.2 内存区域(Zone)
每个节点内部进一步划分为多个区域,反映硬件限制:
- ZONE_DMA(0-16MB):老旧 ISA 设备只能寻址最低 16MB
- ZONE_DMA32(16MB-4GB):32 位 DMA 设备兼容
- ZONE_NORMAL:线性映射到内核虚拟地址空间(896MB on x86-32,全部 on x86-64)
- ZONE_HIGHMEM(仅 32 位):超过线性映射区的页需动态映射
每个区域维护一个空闲页框链表和一套水位线(pages_min/pages_low/pages_high),驱动页面回收决策。
二、伙伴系统(Buddy Allocator)
2.1 核心算法
伙伴系统解决外部碎片问题。它将内存按 2 的幂次划分为 11 个空闲链表(order 0~10,对应 4KB ~ 4MB 连续块)。分配时向上取整到最近的 2 幂,若该 order 无空闲块则从更高 order 递归分裂为两个"伙伴"(buddy)。释放时检查伙伴是否空闲,若空闲则合并到更高 order。
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
// buddy = page ^ (1 << order)
2.2 per-cpu 页面缓存(PCP)
为了减少热页框上的锁竞争,每个 CPU 维护一个单页(order 0)的冷热缓存列表。热页放在 PCP list 头部,冷页放尾部。Cache 列表的大小由 pageset_batch 控制。
2.3 页面迁移与 CMA
物理页面可以被标记为不同迁移类型(MIGRATE_MOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_UNMOVABLE),使同类页聚集,减少碎片。CMA(Contiguous Memory Allocator)则预留大块可移动内存,需要时通过页面迁移腾出连续空间。
三、Slab 分配器:内核对象的高效复用
3.1 从 SLAB 到 SLUB
内核频繁分配释放大量相同大小的结构体(task_struct、dentry、inode、buffer_head 等),直接使用伙伴系统会造成大量内部碎片。Slab 分配器将一或多个伙伴页面划分为"Slab",其中包含若干等大的对象槽位。
Linux 内核逐步演化出三代分配器:SLAB → SLQM → SLUB(Unqueued Slab,当前默认)。SLUB 去除了每 CPU/每节点的复杂队列,转而使用 per-CPU partial list 和 per-node partial list,简洁而高效。
3.2 kmem_cache 剖析
每个 kmem_cache 代表一种"类型"的对象池。创建后通过 kmem_cache_alloc()/kmem_cache_free() 分配释放。/proc/slabinfo 可以查看所有缓存的活跃对象数。
3.3 SLUB 的 CPU 热路径
快速分配路径(无锁):(per-CPU freelist 命中) → 直接返回;中速路径:补充 per-CPU list 从 partial slab 窃页;慢速路径:从伙伴系统分配新页面创建新 slab。
3.4 kmalloc 背后的 Slab 缓存池
kmalloc(size) 不直接操作伙伴系统,而是选择最接近的 2 幂 kmem_cache(kmalloc-8 ~ kmalloc-8K)。超过 8KB 的对象才直接走伙伴系统以避免过量内部碎片。
四、虚拟内存与分页机制
4.1 四级/五级页表遍历
x86-64 下 Linux 默认使用 4 级页表(PGD→PUD→PMD→PTE)。自内核 5.0 支持 5 级(LA57,57 位虚拟地址)。每个页表项包含物理页框号(PFN)+ 权限位(R/W/U/S、XD、Accessed、Dirty)。
4.2 大页(HugeTLB / THP)
HugeTLB 预分配 2MB/GB 巨页池(/sys/kernel/mm/hugepages/)。Transparent HugePage (THP) 内核自动在后台将连续 4KB 页面合并为 2MB 大页。madvise(MADV_HUGEPAGE) 可对敏感映射显式启用。
THP 回退策略:defrag=always 同步整理碎片;defrag=defer+madvise 仅对 madvise 标记的映射开启。数据库工作负载通常建议关闭全局 THP,改为 madvise 方式。
4.3 mmap 与 VMA
mm_struct 描述进程完整地址空间,包含按地址排序的 VMA(vm_area_struct)红黑树。vmalloc() 在 VMALLOC_START~VMALLOC_END 分配虚拟连续、物理不一定连续的内存。
五、页面回收与交换
5.1 LRU 链表与扫描策略
每个 zone 维护两组 LRU 链表(active/inactive × file/page),页面根据访问频率在 active 与 inactive 间迁移。kswapd 遍历 zone,水位线低于 pages_low 时启动回收。回收路径:unmap→pageout→swap/pagecache drop。
5.2 Swap 与 Zswap
swappiness(0-200)控制页出匿名/文件间的权重。Zswap 在页面写入磁盘前进行内存压缩池暂存,减少 I/O 等待并作为 Swap 的 read-through cache。
5.3 OOM Killer
根据 oom_score(基于内存占用、CPU 运行时间、oom_score_adj)选择进程杀死。oom_kill_allocating_task 内核参数可改为直接杀死触发分配者。
六、内核态与用户态内存分配的衔接
6.1 从 malloc 到内核
glibc 的 malloc(3) 基于 brk() 和 mmap() 两种路径。通过 free list、tcache、fastbin、smallbin/unsorted bin/largebin 多级空闲链表加速;munmap() 归还大于 M_TRIM_THRESHOLD(默认 128KB)的映射。
6.2 内核快速分配与 GFP 标志
__GFP_HIGHMEM:允许高内存;__GFP_ATOMIC:原子分配(不可睡眠,用于 IRQ 上下文);__GFP_IO:允许 I/O 回写;__GFP_DIRECT_RECLAIM:允许直接回收。
6.3 内核内存 cgroup
cgroup 对单一 cgroup 设置 memory.max、memory.high 等。超过上限触发 cgroup OOM,而非全局 OOM。
七、调试与观测工具
| 工具/文件 | 用途 |
|---|---|
| /proc/meminfo | 全局内存使用概览(MemTotal、MemAvailable、Buffers、Cached、SwapTotal、Slab) |
| /proc/buddyinfo | 伙伴系统每 order 空闲块数 |
| /proc/slabinfo | 所有 kmem_cache 对象统计 |
| /proc/vmallocinfo | vmalloc 映射详情 |
| slabtop | slab 缓存实时监控 |
| /vmstat | 页面回收、swap、缺页等统计 |
| dmesg | OOM 消息、内存不足警告、NUMA 异常 |
| perf probe/record | 动态追踪 kmem:* 事件 |
| bpftrace/eBPF | 定制化内存分配火焰图、泄漏追踪 |
7.1 常见排障场景诊断
内存泄漏:watch /proc/meminfo 中 Slab 和 Unevictable 持续上涨 → slabtop 找活跃缓存 → perf record -e kmem:kmem_cache_alloc 定位分配者。
"Page allocation failure":dmesg 看 GFP 标志和分配的 order,判断是连续内存不足还是碎片。
OOM 频繁:结合 sar -r / cgroup memory.stat 定位源头,调整 swappiness 与 cgroup 限制。
NUMA 性能不均衡:numastat -p <pid> + perf c2c 排查远端内存访问热区。
八、性能调优实战
8.1 减少页面错误
启动时 kcompactd 预先整理碎片;应用 madvise(MADV_HUGEPAGE) 针对大内存代码段启用 THP;mprotect 避免频繁读写转换;数据库常用 mmap + MAP_POPULATE 预读。
8.2 降低 reclaim 压力
调节 vm.min_free_kbytes 避免分配死锁;合理设置 swappiness(SSD 低 swappiness + zswap 可得到最佳折衷);vfs_cache_pressure 过高会过度回收文件缓存。
8.3 NUMA 感知分配
使用 numactl --interleave=all 交错分配或 --membind 绑定节点;mbind() 指定 VMA 策略;内核自动 NUMA balancing 可迁移热页到本地节点。
总结
Linux 内核内存管理是一套精密的多层联动系统:伙伴系统处理物理页级的外部碎片,Slab 分配器优化内核对象周转,虚拟地址层按需映射让进程拥有"无限内存"的错觉,页面回收和 OOM 则在整体压力下做出生死决策。通读 mm/ 目录源码并配合上文的观测工具,你就能从"猜着调参"进阶到"有理有据地调优"。下一篇我们将进入虚拟文件系统(VFS)与 page cache 的深度剖析,敬请持续关注。

发表评论 取消回复