一、引言
Linux内核的内存管理子系统是整个操作系统最核心、最复杂的组件之一。它不仅要高效地管理物理内存,还要为上层应用提供隔离、安全、透明的虚拟内存抽象。本文将深入剖析Linux内核内存管理的两大支柱——SLAB/SLUB分配器和虚拟内存机制,帮助读者从源码级别理解其设计哲学与实现细节。
二、物理内存管理
2.1 页(Page)与页帧(Page Frame)
Linux将物理内存划分为固定大小的页(通常4KB),每个页对应一个struct page结构体。该结构体记录了页的状态信息:引用计数、映射关系、所属的slab缓存等。在64位系统上,一个struct page约64字节,意味着仅管理1GB物理内存就需要约16MB的struct page数组。
2.2 伙伴系统(Buddy System)
伙伴系统是物理页分配的基础算法。它将空闲页按阶(order)分组,第n阶包含2^n个连续页。分配时从最小满足的阶中取出一个块,若不存在则向更大阶“借”并将伙伴块分开;释放时递归合并伙伴块。
源码分析: mm/page_alloc.c中的__alloc_pages()是入口函数,最终调用buffered_rmqueue()从per-CPU缓存或伙伴系统获取页面。free_pages()则负责回收并尝试合并伙伴块。
2.3 Per-CPU 页缓存(PCP)
为减少锁竞争和缓存一致性开销,现代内核为每个CPU维护一组struct per_cpu_pages,预先缓存少量热页。rmqueue_pcplist()在此缓存非空时直接返回页,无需操作zone锁。
三、SLAB/SLUB 分配器
3.1 SLAB 的设计初衷
早期Unix内核频繁分配/释放固定大小的对象(如struct inode、struct dentry),伙伴系统的最小粒度(4KB)会造成大量内部碎片。Jeff Bonwick在Solaris中首次提出SLAB算法,Linux 2.1引入SLAB,后由 Christoph Lameter 开发SLUB成为默认分配器。
3.2 SLAB 核心概念
- 缓存(kmem_cache): 针对特定类型对象的内存池
- Slab: 由一个或多个连续页组成的容器,包含若干对象槽位
- 对象(Object): 缓存中存储的实际数据结构
三种Slab状态:full(全满)、partial(部分空闲)、empty(全空)。新分配优先从partial slab取,缓存不足时分配新slab;释放对象后若slab变empty,在内存紧张时归还伙伴系统。
3.3 SLUB 改进
SLUB(Unqueued Slab Allocator)简化了SLAB的元数据:移除了kmem_bufctl数组和独立的kmem_cachep链表,将freelist指针直接嵌入空闲对象内存中。这减少了元数据开销,提升了NUMA系统的可扩展性——每节点维护独立的partial链表。
3.4 kmalloc 系列
kmalloc是内核最常用的内存分配接口。它维护了一组预定义的通用SLAB缓存(大小从8到8K按2的幂递增)。分配时选择最小的满足需求的缓存;释放时无需指定大小,因为页结构体记录了所属缓存。
void *ptr = kmalloc(128, GFP_KERNEL);
if (!ptr) return -ENOMEM;
// 使用ptr...
kfree(ptr);
3.5 Slab 调试机制
SLAB提供强大的调试功能:Red Zone(在对象前后放置魔数检测溢出)、Poisoning(释放时填充特定模式检测use-after-free)、Tracking(记录分配/释放的调用栈)。开启CONFIG_DEBUG_KMEMLEAK可检测内存泄漏。
四、虚拟内存机制
4.1 多级页表(Multi-level Page Table)
x86-64采用四级页表结构:PGD→P4D→PUD→PMD→PTE。每次遍历都需内存访问,开销巨大。MMU内部集成TLB(Translation Lookaside Buffer)缓存近期映射,命中率通常超99%。当TLB未命中时,硬件自动遍历页表(Hardware Page Walk);若PTE无效则触发Page Fault。
4.2 虚拟内存区域(VMA)
进程的虚拟地址空间由一系列VMA组成,每个VMA是一段具有相同权限和后备的连续区间(如代码段、数据段、堆、栈、mmap映射区)。VMA通过红黑树和链表组织,find_vma()可在O(log n)内定位地址所属区域。
4.3 缺页中断(Page Fault)处理
触发条件:访问无效PTE、权限不足、或页面被换出。do_page_fault()根据场景分发:
- Demand Paging: 匿名映射首次访问,分配零页
- File-backed Miss: 从外部存储读取文件内容
- Minor Fault: 页面在页缓存中,无需IO
- Major Fault: 页缓存未命中,需磁盘IO
- COW(Copy-on-Write): fork后写私有页面时复制
4.4 页面回收:LRU与Kswapd
每个内存维护两条LRU链表:active和inactive。页面首次入inactive链表,被访问两次以上提升到active。Kswapd内核线程周期性扫描,将inactive链表尾的页面回收(写回脏页/丢弃干净匿名页/换出脏匿名页到Swap)。swappiness参数控制匿名页与文件页的回收比例。
4.5 透明大页(THP)
THP(Transparent Huge Pages)自动将连续普通页合并为2MB大页(x86-64)。大页减少TLB压力、降低页表遍历开销,但可能增加内存碎片和延迟。/sys/kernel/mm/transparent_hugepage/enabled允许设为madvise、always或never。数据库场景通常建议禁用,因其主动使用madvise标记热点区。
五、内存压缩与去重
5.1 Zswap 与 Zram
Zram在内存中创建压缩块设备,Swap页被LZ4/ZSTD压缩后驻留RAM,典型压缩比2:1到3:1,显著降低闪存磨损。Zswap作为Swap的前置缓存,压缩后的页面在内存压力达到阈值时才真正写入磁盘,减少IO。
5.2 KSM(Kernel Samepage Merging)
KSM定期扫描已注册区域,对比页面内容。若发现相同的只读/可写页,则合并到同一物理页并标记为COW。KSM对虚拟机宿主机尤其有效(多个VM运行相同OS)。/sys/kernel/mm/ksm/下的参数可调节扫描间隔和页数。
六、OOM Killer 与内存控制
6.1 Out-Of-Memory Killer
当所有回收手段均无法释放足够内存时,OOM Killer被触发。内核为每个进程计算oom_score,基于内存占用、运行时间、优先级、子进程等因素。/proc/[pid]/oom_score_adj允许用户空间调整分数(范围-1000到+1000),-1000可免疫OOM。被选中进程收到SIGKILL,dmesg记录杀进程详情。
6.2 cgroups v2 memory controller
cgroups v2提供memory.max(硬限制)、memory.high、memory.low、memory.min四级阈值。超过memory.high时进程被节流(throttle)并触发回收;达到memory.max则触发cgroup OOM。memory.stat提供详细的数据统计(anon/file/kernel_stack/active/inactive)。
# 创建cgroup并限制为512MB
mkdir /sys/fs/cgroup/myapp
echo 536870912 > /sys/fs/cgroup/myapp/memory.max
echo 4294967296 > /sys/fs/cgroup/myapp/memory.high
echo $$ > /sys/fs/cgroup/myapp/cgroup.procs
七、性能调优实战
7.1 内核参数优化
vm.dirty_ratio / vm.dirty_background_ratio:控制脏页占可用内存的比例。高吞吐写入场景(如数据库)可适当降低,强制更频繁的小型刷盘,避免突发IO;大文件顺序写入则可提高。
vm.min_free_kbytes:保留的最小空闲内存,保证紧急分配(如网络栈)不会失败。过度设置会浪费内存,不足则引发直接回收抖动。
vm.overcommit_memory:=0启发式(默认),=1总是允许,=2严格限制。数据库通常设为2保证OOM能被及时检测。
7.2 NUMA 感知策略
多NUMA节点系统中,跨节点访问延迟可达本地节点的2-3倍。numastat命令展示各节点的内存分配情况,numactl --cpunodebind=0 --membind=0绑定应用与内存到指定节点。内核的NUMA Balancing机制可自动将热点页迁移到访问最频繁的节点。
7.3 实用监控工具
sar -r:查看内存使用率、空闲量、Swap量。free -m 中‘available’字段比‘free’更准确,因为它包含了可回收的页缓存。vmstat 1中的si/so列显示Swap进出,频繁非零值意味着内存压力过大。slabtop 实时监控SLAB缓存分配情况,排查内核内存泄漏尤其实用。
八、总结与展望
Linux内核内存管理是一个经十余年演进的精密系统:伙伴系统处理碎片化,SLAB/SLUB优化小对象分配,虚拟内存提供隔离与透明大页,Kswapd平衡回收效率。v5.16引入MGLRU(Multi-Gen LRU)取代传统的active/inactive双链表,通过世代追踪访问频率,更精准地识别冷页。v6.8的页缓存水位自适应、cgroup v2的递归内存低位压力通知等,持续优化着服务器场景的内存效率。
理解这些机制,是进行性能调优、排查OOM、编写内核模块、以及设计低延迟应用的基础。建议读者结合Documentation/admin-guide/mm/下的官方文档,并通过perf、bpftrace 等工具在本地实践中深入探索。

发表评论 取消回复