引言:为什么 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/vmallocinfovmalloc 映射详情
slabtopslab 缓存实时监控
/vmstat页面回收、swap、缺页等统计
dmesgOOM 消息、内存不足警告、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 的深度剖析,敬请持续关注。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部