引言
Linux 内核的内存管理子系统是整个操作系统中最复杂、最核心的模块之一。它负责管理物理内存的分配与回收、虚拟地址空间的映射、页面缓存与交换、以及内存不足时的回收策略。理解这些机制不仅是内核开发者的必修课,也是每一位系统程序员、性能优化工程师和基础设施构建者的基本功。
本文将从物理页面管理的基石——伙伴系统(Buddy System)出发,深入剖析内核内存分配器 SLAB/SLUB/SLOB 的实现原理,解析缺页异常(Page Fault)的处理流程,介绍页面回收与交换机制,最后讨论 OOM Killer 的工作策略与调优方法。
一、物理内存管理:伙伴系统(Buddy System)
1.1 为什么需要伙伴系统
操作系统需要频繁地分配和释放不同大小的物理页面。如果简单地以单页为单位管理,会产生大量外部碎片,导致无法满足连续物理内存的请求。伙伴系统通过将内存划分为不同阶(order)的块来解决这个问题——第 n 阶的块包含 2^n 个连续页面。
1.2 核心数据结构
Linux 内核为每个内存节点(Node)维护一个 zone 结构,每个 zone 包含一个 free_area 数组,数组索引就是阶(order),每个元素是一个自由链表(free list):
struct zone {
struct free_area free_area[MAX_ORDER];
...
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
其中 MAX_ORDER 通常为 11,即最大连续分配 2^10 = 1024 个页面(4MB,假设页大小 4KB)。
1.3 分配与释放流程
分配过程:当请求 order-n 的块时,首先在 free_area[n] 的链表中查找,如果有空闲块则直接返回。如果 free_area[n] 为空,则向更高阶寻找——从 free_area[n+1] 中取出一个块,分裂为两个伙伴(buddy),一个返回请求者,另一个放入 free_area[n]。
释放过程:释放一个 order-n 的块时,检查其伙伴是否也在对应阶的 free_list 中。如果是,则合并为 order-(n+1) 的块,并递归向上检查是否可以继续合并。这就是'伙伴'得名原因——只有地址连续的、大小相同的两个块才能合并。
1.4 页面迁移类型(Page Migration Types)
为减少碎片,Linux 将页面按可迁移类型分组:
- MIGRATE_UNMOVABLE:内核数据、页表等不可移动页面
- MIGRATE_MOVABLE:用户态数据、文件缓存等可移动页面
- MIGRATE_RECLAIMABLE:可以通过页面回收重新获得的缓存
- MIGRATE_ISOLATE:用于内存热插拔的隔离区
这种分类使得不可移动页面不会碎片化可迁移页面,提升了内存紧凑机制的效率。
二、内核内存分配器:SLAB/SLUB/SLOB
2.1 SLAB 的设计思想
伙伴系统以页(4KB)为最小分配单位,但内核经常需要分配小对象(如 task_struct、inode、dentry 等)。频繁分配完整页面会造成严重的内部碎片。SLAB 分配器引入对象缓存(kmem_cache)的概念,预先分配完整页面,将其切分为固定大小的对象槽位(slot),实现小对象的高效分配。
2.2 SLAB 的核心结构
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每CPU快速分配缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // 每个NUMA节点缓存
unsigned int size; // 对象实际大小
unsigned int object_size; // 含元数据的对象大小
unsigned int offset; // 下一个空闲对象的偏移
struct list_head list; // 全局缓存链表
const char *name;
...
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表
unsigned long tid; // 全局唯一事务ID
struct page *page; // 当前使用的slab页
struct page *partial; // 部分空闲slab链表
};
2.3 SLAB 的三级分配策略
SLAB 采用三级缓存体系,优先从最快速的层级分配:
- per-cpu 缓存 (cpu_slab):每CPU独占,无锁分配,最快路径
- per-node 部分空闲链表:当前NUMA节点上的 partial slabs
- 伙伴系统新分配:分配新的 slab 页面
释放时按优先级回收到对应层级:per-cpu free 超量时返还给 node partial,node full slab 全空闲时返还给伙伴系统。
2.4 SLUB 分配器
SLUB(Unqueued SLAB)是 Linux 2.6.23 后的默认分配器,简化了 SLAB 的设计:
- 去除了 per-cpu 队列的复杂管理(如 cold cache、hot cache)
- 每个 slab 页面通过 page 结构体的 freelist 指针维护空闲链表
- 释放时不合并 slab 碎片,仅当页面全空闲时返回伙伴系统
- 更少的元数据开销,更好的 NUMA 扩展性
SLUB 的关键创新是将 slab 元数据嵌入到 struct page 中,而不是使用独立的 slab 管理结构,减少了内存开销和缓存行竞争。
2.5 SLOB 分配器
SLOB(Simple List Of Blocks)是专为嵌入式等内存受限环境设计的极简分配器。它不使用 slab 概念,而是维护一个全局空闲块链表,使用 first-fit 策略分配。SLOB 代码简单、内存开销极小,但分配效率低、碎片严重,仅适用于内存小于数MB的嵌入式场景。
2.6 kmalloc 家族
内核通过 kmalloc() 分配连续物理内存,其内部通过预定义大小缓存实现:
// SLUB 预定义的 kmalloc 缓存大小
static char *cache_names[] = {
'kmalloc-8', 'kmalloc-16', 'kmalloc-32', 'kmalloc-64',
'kmalloc-96', 'kmalloc-128', 'kmalloc-192', 'kmalloc-256',
'kmalloc-512', 'kmalloc-1k', 'kmalloc-2k', 'kmalloc-4k',
...
};
当请求超过 2 * PAGE_SIZE 的内存时,kmalloc 直接回退到伙伴系统(alloc_pages)进行分配,以避免 SLUB 内部碎片的浪费。
三、缺页异常(Page Fault)处理
3.1 缺页异常的产生
当 CPU 访问虚拟地址时,MMU 在页表中找不到对应的物理页表项(PTE),或权限检查失败时触发缺页异常(#PF)。#PF 会压入错误码(error code)并跳转到内核的异常处理例程。
3.2 处理流程总览
page_fault(vector) -> do_page_fault(regs, error_code)
-> user_mode -> do_user_addr_fault()
-> VMA查找 (find_vma)
-> 权限检查 -> SIGSEGV (段错误)
-> 写时复制 (COW) -> do_wp_page()
-> 文件映射 -> filemap_fault()
-> 匿名映射 -> do_anonymous_page()
3.3 写时复制(Copy-On-Write)
fork() 子进程时不立即复制物理页面,而是共享父进程的页面并标记为只读。当任一进程尝试写入页面时触发 COW 缺页,内核才复制页面内容并更新页表。这大大提升了 fork() 的执行速度,同样被用于文件映射的私有映射场景。
3.4 文件映射缺页
当访问 mmap 映射的文件区域时触发 do_fault() -> filemap_fault()。内核首先在 page cache 中查找页面块(radix tree / XArray),如果存在则直接映射(minor fault);如果不存在则从磁盘读入(major fault),通常以预读窗口(readahead)方式加载邻接页面。
3.5 匿名映射缺页
对匿名映射(如 malloc 大内存、MAP_ANONYMOUS)的首次访问触发 do_anonymous_page()。内核调用 alloc_pages() 从伙伴系统分配一个零页(zero page),然后写入进程页表。首次匿名访问分配的是特殊的'零页'(全局共享的零初始化页),仅在实际写入时才真正分配物理页面(demand paging)。
四、内存回收与交换机制
4.1 LRU 算法与双链策略
Linux 使用增强型 LRU(Least Recently Used)算法管理页面回收。每个 zone 维护两个 LRU 链表:
- active 链表:近期被访问过的页面被认为是'热'的
- inactive 链表:近期未被访问的页面,候选回收
页面在两个链表之间移动:active 链表尾的页面经过一段时间未被访问会被移到 inactive 链表;inactive 链表的页面被重新访问两回会被提升到 active 链表(双链策略 / double-clock algorithm)。
4.2 页面类型与回收优先级
内核管理的页面分为四种类型:
enum lru_list {
LRU_INACTIVE_ANON, // 匿名页面(堆、栈)
LRU_ACTIVE_ANON,
LRU_INACTIVE_FILE, // 文件缓存
LRU_ACTIVE_FILE,
};
回收优先级:swappiness 参数控制匿名页面与文件缓存的回收比例。默认值 60 表示两种类型按 LRU 时间公平回收;swappiness=0 表示尽可能不交换匿名页面;swappiness=200 表示积极交换匿名页面。
4.3 直接回收与后台回收
- kswapd 后台回收:当空闲页面低于 zone->low 阈值时唤醒 kswapd 内核线程,按 LRU 顺序回收页面直到达到 high 阈值
- 直接回收(Direct Reclaim):当空闲页面低于 min 阈值时,分配进程同步执行回收,会造成明显的延迟抖动
- 内存碎片整理(Memory Compaction):kcompactd 线程定期整理碎片,合并可移动页面以释放连续大块
4.4 Zswap 与 Zram
Zram:在RAM中创建压缩的块设备用于交换。数据写入 swap 时被实时压缩,比传统磁盘 swap 快得多,适用于低内存设备。
Zswap:作为交换的压缩缓存层。数据先写入 zswap 的内存池(LRU管理),仅在池满时写入真正的磁盘 swap。优点是减少磁盘 I/O,缺点是占用额外 CPU 用于压缩。
五、OOM Killer 与内存不足处理
5.1 OOM Kill 触发条件
当系统所有 zone 的空闲页面均低于 min 阈值,直接回收失败,kswapd 无法及时补充内存,且已完全耗尽 swap 空间时,触发 out_of_memory()。OOM Killer 是内核最后的防崩溃手段。
5.2 OOM 评分机制
OOM Killer 通过 oom_badness() 为每个进程计算分数(oom_score),选择分数最高的进程杀死:
points = total_vm_pages * 1000 / totalram_pages
adj = oom_score_adj // -1000 ~ +1000
oom_badness = points * (1 + adj / 1000)
- adj=-1000 表示 OOM 免疫(如 init 进程、systemd)
- adj=0 表示正常评分
- adj=+1000 表示优先被杀死(如大型内存消耗进程)
5.3 OOM 调优实践
- memory.limit_in_bytes:通过 cgroup v1 memory 子系统限制进程组内存
- Cgroup v2 memory.min:保护关键服务不被回收
- vm.overcommit=2:严格模式限制内存超量分配
- vm.oom_kill_allocating_task=1:直接杀死触发OOM的进程
六、性能调优与监控
6.1 Slab 信息监控
$ slabtop -o # 实时slab分配器内存统计
$ cat /proc/slabinfo # 各kmem_cache详细信息
$ slabinfo -T # 超过指定KB的大缓存
通过 /proc/sys/vm/ 下的可调参数动态调整内存行为:
vm.swappiness = 10 # 降低交换倾向
vm.vfs_cache_pressure = 50 # 控制dentry/inode回收速度
vm.dirty_ratio = 40 # 脏页占总内存40%时写回
vm.min_free_kbytes = 262144 # 保留256MB为紧急使用
6.2 内存 NUMA 优化
NUMA 系统中内存访问存在远近差异:
- numactl --membind=node:将进程内存绑定到特定节点
- numactl --interleave=all:交替分配所有节点的内存
- zone_reclaim_mode:节点内存不足时优先本地回收
6.3 透明大页(THP)
透明大页将连续 4KB 页面合并为 2MB 大页,减少 TLB 不命中。适合数据库、HPC 场景:
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag
七、总结
Linux 内存管理子系统是一个精密的分层体系:伙伴系统管理物理页面,满足大块连续分配需求;SLUB 分配器在上面构建对象缓存,满足小对象高频分配;缺页异常机制实现了按需分配和写时复制;LRU 回收算法平衡了内存利用与性能;OOM Killer 则是最后的防线。
理解这些机制,可以帮助我们更好地诊断内存性能瓶颈——是直接回收造成的延迟抖动?是 slab 缓存持续增长?还是 THP 碎片化导致的大页分配失败?掌握 /proc/meminfo、slabtop、vmstat、perf 等工具也成为了系统工程师日常调优的利器。
随着 Cgroup v2、BPF、持久内存(PMEM)等新技术的引入,Linux 内存管理仍在持续演进。对技术人而言,底层原理始终是应对上层变化的坚实底气。

发表评论 取消回复