引言
Linux内核的内存管理系统是操作系统中最复杂也最精妙的子系统之一。在用户态看来,每个进程都独占一块连续而私密的虚拟地址空间,然而在这优雅抽象的背后,是一套由页表、反向映射、slab分配器、页面回收算法和NUMA调度共同构建的精密机制。本文将深入Linux内核源码级别,逐层拆解从虚拟地址到物理页面的完整生命周期,揭示mmap、hugepage、KSM、OOM Killer等核心组件的工程真相。
一、虚拟内存的空间布局
x86-64架构下,Linux将48位虚拟地址空间(256TB)划分为用户态与内核态两大部分:
0x0000 0000 0000 0000 - 0x0000 7FFF FFFF FFFF : 用户空间 (128TB)
0xFFFF 8000 0000 0000 - 0xFFFF FFFF FFFF FFFF : 内核空间 (128TB)
↑
未使用的空洞 (non-canonical hole)
每个进程的 struct mm_struct 通过 mmap 链表和红黑树管理多个虚拟内存区域(VMA)。VMA 是一组连续的虚拟页面,拥有相同的访问权限和后端映射来源(文件、匿名内存或设备)。vm_area_struct 关键字段包括 vm_start、vm_end、vm_flags(PROT_READ/WRITE/EXEC)和 vm_file(指向文件映射的 file 结构)。
关键设计考量:红黑树用于快速判断某个地址是否落在某个VMA中(fast path),链表则适合遍历整个地址空间。find_vma() 使用红黑树查找指定地址的上界VMA,平均时间复杂度 O(log n)。
二、四级页表与地址翻译
x86-64采用四级页表(Page Global Directory → Page Upper Directory → Page Middle Directory → Page Table),将48位虚拟地址拆分为5个字段(实际4级,PML4 PDP PD PT),每级9位索引,共覆盖 2^48 = 256TB 地址空间。
| PML4 (9bit) | PDP (9bit) | PD (9bit) | PT (9bit) | Offset (12bit) |
↓ ↓ ↓ ↓ ↓
Level 4 Level 3 Level 2 Level 1 4KB Page
每次地址翻译需要4次内存访问(约200-400个时钟周期),为此CPU的μTLB(Micro TLB)和主TLB提供了专用缓存。Linux的 proc/self/maps 文件将每个VMA映射以可读形式呈现,而 pagemap 则暴露PFN(Page Frame Number)给调试工具。
值得关注的是57位五级页表(LA57):Ice Lake之后的CPU支持5级页表(PML5),将虚拟地址扩展到57位(128PB)。Linux 5.5 已内置LA57支持,通过 CONFIG_X86_5LEVEL 开启。对数据库和虚拟化大内存场景,这直接决定单机能否突破128TB虚拟内存瓶颈。
三、物理内存模型:NUMA与内存域
Linux内核使用 struct page 描述每一个物理页面(4KB粒度),在系统启动时由 memblock 分配器初始化。物理内存按 内存域(Zone) 划分:
ZONE_DMA : 0-16MB — 兼容旧式DMA设备
ZONE_DMA32 : 16MB-4GB — 32位DMA设备
ZONE_NORMAL : 4GB-896MB (低端内存,线性映射)
ZONE_HIGHMEM : 896MB (32位系统) — 需要kmap动态映射
ZONE_MOVABLE : 可移动页面(用于内存热插拔和碎片整理)
在NUMA架构下,多socket服务器每个节点有独立的 pglist_data(NODE_DATA),包含本地的内存域和13个free_area伙伴链表。分配器优先在请求CPU的本地节点分配以减少跨socket延迟(约1.5-2倍性能差距)。numactl --membind 和 mbind() 提供了显式绑核策略。
四、SLAB/SLUB/SLOB分配器:小对象的精密仓库
内核态无法直接使用用户空间的 malloc(),因为用户堆管理依赖的物理连续假设在内核中不成立。Linux提供三类解决器:
| 分配器 | 特征 | 场景 |
|---|---|---|
| SLAB | 原版,per-cpu/per-node三层缓存 | 兼容性优先 |
| SLUB | 默认,简化元数据、优于SLAB的碎片率 | 现代服务器首选 |
| SLOB | 极简设计(200行代码) | 嵌入式低内存环境 |
SLUB的核心思想:每个 kmem_cache 定义了固定大小的对象池(如 task_struct=1.9KB、dentry=192B)。分配请求优先命中 per-CPU hot cache,其次从 node partial 链表补充,最后才从伙伴系统申请新页。/proc/slabinfo 暴露所有 cache 的活跃对象数、slab页数和碎片率。
性能关键路径:kmem_cache_alloc() 在热缓存命中时只需几条汇编指令(无锁环形缓冲区LIFO语义),在/sys/kernel/slab/

发表评论 取消回复