Linux 内核内存管理机制深度解析:从伙伴系统到 Slab 分配器
内存管理是 Linux 内核最核心的子系统之一,它直接关系到系统的性能、稳定性和安全性。本文将深入剖析 Linux 内核内存管理的核心机制,包括伙伴系统(Buddy System)、Slab/Slob/Slub 分配器、虚拟内存管理以及内存回收策略。
一、物理内存的管理模型
1.1 节点(Node)与区域(Zone)
现代 NUMA 架构系统中,Linux 将物理内存组织为节点(pg_data_t)和区域(zone)的层级结构:
- 节点:每个 NUMA 节点对应一个
pg_data_t结构,包含该节点下的所有内存区域 - 区域:每个节点内按用途划分为 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM 等
// 简化的节点结构
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES]; // 内存区域数组
struct zonelist node_zonelists[MAX_ZONELISTS]; // 分配 fallback 列表
int nr_zones; // 区域数量
struct page *node_mem_map; // 页描述符数组
unsigned long node_start_pfn; // 起始页帧号
} pg_data_t;
1.2 页(Page)— 物理内存的最小管理单位
物理内存以页(通常 4KB)为基本单位管理。每个物理页对应一个 struct page 描述符,保存在全局 mem_map 数组中。
struct page {
unsigned long flags; // 页状态标志(PG_locked, PG_dirty, PG_lru等)
atomic_t _refcount; // 引用计数
atomic_t _mapcount; // 映射计数(被多少个页表引用)
struct {
union {
struct list_head lru; // LRU 链表节点
struct slab_head slab; // Slab 链表
};
};
struct address_space *mapping; // 关联的地址空间
pgoff_t index; // 在映射中的偏移
};
二、伙伴系统(Buddy System)
伙伴系统是内核管理连续物理页框的核心算法,由 Knowlton 于 1965 年提出,Linux 在此基础上进行了优化实现。
2.1 核心思想
伙伴系统将空闲页面按"阶"(order)组织,每个阶 n 对应 2^n 个连续物理页。系统维护 11 个 free_area 链表(order 0~10),分别管理 1、2、4、8、...、1024 个连续页块。
分配过程:
- 从满足大小的最小 order 的 free_list 中取出一个块
- 如果该 order 无空闲,向更大 order 递归查找,将大块分裂为两个"伙伴"
- 一半分配出去,另一半放入低一阶的 free_list
释放过程:
- 释放块时检查其"伙伴"是否也在 free_list 中
- 若伙伴空闲则合并为高一阶的大块
- 递归合并直到最高阶或伙伴不在空闲列表
2.2 伙伴的判定
两个块互为"伙伴"的条件:
- 大小相同(同一 order)
- 物理地址连续
- 合并后的块起始地址是 2^(order+1) 的整数倍
块A起始地址 = 2^order × K (K为偶数)
块B起始地址 = 2^order × (K+1) (K+1为奇数)
合并后起始 = 2^order × K (低 order+1 位为零)
2.3 避免碎片:页面迁移与反碎片
伙伴系统面临的最大挑战是外部碎片(空闲内存不连续)。Linux 引入了基于页面迁移类型的反碎片机制:
| 迁移类型 | 含义 | 可回收性 |
|---|---|---|
| MIGRATE_UNMOVABLE | 无法迁移(如 slab、内核代码) | 不可迁移 |
| MIGRATE_MOVABLE | 可迁移(如用户页、缓存) | 可迁移 |
| MIGRATE_RECLAIMABLE | 可回收但不能迁移 | 仅回收 |
| MIGRATE_PCP | 每-CPU 页面缓存 | 快速分配 |
分配时优先从对应类型的 free_list 取页,从而使同类页面聚集,便于后续整理。
三、Slab 分配器 — 内核对象的内存管理
伙伴系统以页为单位分配,但内核经常需要分配远小于 4KB 的小对象(如 task_struct、dentry、inode)。直接通过伙伴系统分配会造成严重的内部碎片。Slab 分配器应运而生。
3.1 核心思想
由 Jeff Bonwick 在 Solaris 中首创,SunOS 5.4(1994)。Linux 中经历了三代演进:
- Slab:Linux 2.2 引入,原始实现
- Slob:面向嵌入式系统的极简分配器
- Slub:Linux 2.6.23 起成为默认分配器,优化了元数据开销和碎片
Slab 本质上是一个对象缓存(Object Cache):
- 预先从伙伴系统申请若干页面组成一个 slab
- 将这些页面切分为大小固定的槽位(slot)
- 分配时从已 partial 的 slab 中取空闲槽位
- 释放时标记槽位空闲,归还到原 slab
3.2 三层结构
┌──────────────────────────────────────────────────┐
│ kmalloc_cache (Size Cache) │
│ 如 kmalloc-32, kmalloc-64, kmalloc-128, ... │
├──────────────────────────────────────────────────┤
│ kmem_cache (Object Cache) │
│ 如 task_struct_cache, dentry_cache, inode_cache │
├──────────────────────────────────────────────────┤
│ slab (物理内存块) │
│ ┌──────┬──────┬──────┬──────┬──────┐ │
│ │ slot │ slot │ slot │ slot │ ... │ │
│ └──────┴──────┴──────┴──────┴──────┘ │
└──────────────────────────────────────────────────┘
- size cache:kmalloc 使用,按 2 的幂次组织通用大小的缓存
- kmem_cache:特定类型对象缓存,通过
kmem_cache_create()创建 - slab:实际的物理内存块,可能有多个 slab 组成链表
3.3 Slab 着色(Cache Coloring)
Slab 着色通过为每个 slab 预留不同大小的偏移量(colour offset),使得不同 slab 中的对象映射到不同的 cache line。这样可以减少缓存行冲突(Cache Line Thrashing),提升 CPU cache 利用率。
struct kmem_cache {
unsigned int colour; // 可用颜色数量
unsigned int colour_off; // 颜色偏移步长
void *freelist; // 空闲对象链表
struct list_head slabs_partial; // 部分空闲 slab
struct list_head slabs_full; // 全部占用 slab
struct list_head slabs_free; // 完全空闲 slab
};
四、虚拟内存管理
4.1 地址空间布局
Linux 采用分页机制实现虚拟内存。x86_64 架构下虚拟地址空间划分:
0x0000 0000 0000 0000 ┌──────────────────┐
│ User Space │ 128TB
0x0000 7FFF FFFFFFFF ├──────────────────┤
│ Non-canonical │
FFFF 8000 0000 0000 ├──────────────────┤
│ Kernel Space │ 128TB
FFFF FFFF FFFFFFFF └──────────────────┘
用户空间各段:
- text:代码段,只读可执行
- data:数据段,已初始化的全局变量
- BSS:未初始化的全局变量
- heap:动态内存分配区,向高地址增长
- memory mapping:mmap 区域(共享库、匿名映射等)
- stack:栈区,向低地址增长
4.2 页表与地址转换
四级页表(x86_64 默认):
CR3 → PGD → PUD → PMD → PTE → Physical Page
47:39 38:30 29:21 20:12 物理页内偏移(12bit)
- 每级页表占用 4KB 页,512 项(9bit 索引)
- 实际可寻址 2^48 = 256TB 虚拟地址空间
- 五级页表(Linux 4.14+)扩展到 2^57 = 128PB
4.3 缺页异常处理
当访问未映射的虚拟地址时触发 page fault,处理路径:
- do_page_fault() 检查触发原因(读/写/执行、用户态/内核态)
- 查找 VMA:确认地址是否在合法区域内
- 不同场景处理:
- Demand Paging:首次访问匿名映射,分配零页
- Page Cache 回写:文件映射从未加载,从磁盘读入
- Copy-on-Write:fork 后的写操作触发,分配新页复制
- SIGSEGV:访问非法地址(如 NULL 解引用)
五、内存回收与 OOM 处理
5.1 kswapd 后台回收
当空闲内存低于阈值时唤醒 kswapd 内核线程,执行页面回收:
- 活跃/不活跃 LRU 链表:每个 zone 维护 active_list 和 inactive_list
- 页面老化:通过 PG_referenced 标志位判断页面是否被频繁访问
- 回收策略:优先回收 inactive 链表中的干净页(可直接丢弃),其次是脏页(需写回磁盘)
5.2 Direct Reclaim
当 kswapd 回收不及时,分配者进入直接回收(同步回收),会阻塞当前分配请求,严重影响性能。
5.3 OOM Killer
当系统内存极度紧张且回收无效时触发:
// OOM 评分计算
points = total_vm_pages + swapents + oom_score_adj
- 遍历所有进程计算 oom_score
- 选择得分最高的进程终止
- /proc/[pid]/oom_score_adj 可调整(-1000~1000,-1000 表示不杀)
六、内核态动态分配:vmalloc 与 kmalloc
6.1 kmalloc
void *kmalloc(size_t size, gfp_t flags);
- 基于 Slab 分配器
- 物理连续——适合 DMA 等需要连续物理内存的场景
- 大小限制通常 4MB~8MB(早期 128KB,现代内核有所增加)
- GFP flags 控制分配行为:
GFP_KERNEL、GFP_ATOMIC、GFP_DMA等
6.2 vmalloc
void *vmalloc(unsigned long size);
- 基于伙伴系统 + 页表映射
- 虚拟连续,物理可不连续
- 适合只需要虚拟连续大内存的场景(如模块加载、大数组)
- 访问有 TLB miss 开销,不适用于频繁访问的小内存
七、性能调优与实战
7.1 /proc/buddyinfo 诊断碎片
$ cat /proc/buddyinfo
Node 0, zone Normal 10 21 45 30 3 22 7 2 1 0 0
order: 0 1 2 3 4 5 6 7 8 9 10
如果高阶连续页数量很少,说明存在严重的外部碎片。
7.2 /proc/slabinfo 查看 Slab 使用
$ cat /proc/slabinfo | head -20
<name> <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
dentry 123456 200000 192 20 1
inode_cache 45678 50000 648 8 2
7.3 调整内存参数
| 参数 | 路径 | 含义 |
|---|---|---|
| swappiness | /proc/sys/vm/swappiness | 交换倾向 (0-200) |
| min_free_kbytes | /proc/sys/vm/min_free_kbytes | 最小空闲内存 |
| vfs_cache_pressure | /proc/sys/vm/vfs_cache_pressure | 缓存回收倾向 |
| overcommit_memory | /proc/sys/vm/overcommit_memory | 内存超分配策略 |
总结
Linux 内核内存管理子系统通过伙伴系统管理物理页,通过 Slab 分配器高效处理小对象,通过虚拟内存机制隔离和保护进程空间,再配合 LRU 页面回收和 OOM Killer 保障系统整体稳定性。理解这些机制对于系统调优、驱动开发、性能分析都有着重要意义。
随着硬件发展(持久内存、CXL、HBM),Linux 内存管理系统也在持续演进。例如:
- Memory Tiering(Linux 5.15+):自动分层管理不同速度的内存
- MGLRU(Multi-Gen LRU):新型页面回收算法,取代传统双链表
- Landlock:文件系统沙箱安全模块
深入理解内核内存管理,是每一位系统开发者进阶的必经之路。

发表评论 取消回复