Linux 内核内存管理:从伙伴系统到 SLUB 分配器的深度解析
引言
内存管理是 Linux 内核最核心、最复杂的子系统之一。从物理页面的分配与回收,到内核对象的频繁创建与销毁,再到用户空间内存映射的精细控制,内存管理子系统犹如操作系统的"心脏",为整个系统提供稳定高效的内存服务。理解 Linux 内核内存管理,不仅是内核开发者的必备技能,也是每个系统性能优化工程师必须掌握的底层知识。
本文将深入剖析 Linux 内核内存管理的完整架构,从底层的伙伴系统(Buddy System)到上层的 SLAB/SLUB 分配器,再到虚拟内存管理与反向映射机制,带你构建一个完整的内存管理知识体系。
一、物理内存模型:NUMA 与内存域
1.1 NUMA 架构下的内存拓扑
现代服务器普遍采用 NUMA(Non-Uniform Memory Access)架构,CPU 访问本地内存节点的延迟远低于跨节点访问。Linux 内核通过 pg_data_t 结构体描述每个 NUMA 节点的内存拓扑:
typedef 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; // 起始物理页帧号
unsigned long node_present_pages; // 可用物理页面总数
unsigned long node_spanned_pages; // 总页面数(含空洞)
} pg_data_t;
1.2 内存域(Memory Zone)
每个 NUMA 节点根据用途划分为多个内存域:
- ZONE_DMA:0-16MB,用于传统 ISA 设备的 DMA 操作
- ZONE_DMA32:16MB-4GB,支持 32 位 DMA 设备
- ZONE_NORMAL:直接映射到内核空间,线性映射区
- ZONE_MOVABLE:可迁移页面,支持内存热插拔
- ZONE_DEVICE:设备内存映射区域(持久内存等)
1.3 页面描述符 struct page
内核为每个物理页面维护一个 struct page 描述符(约 64 字节),全局数组 mem_map 按页帧号索引:
// 简化的 struct page 关键字段
struct page {
unsigned long flags; // 状态标志位(PG_locked, PG_dirty 等)
_refcount _refcount; // 引用计数
void *virtual; // 内核虚拟地址(仅 ZONE_NORMAL)
struct { }
struct page *next; // 伙伴系统链表节点
int pages; // 伙伴系统阶数(2^order 个页面)
};
struct address_space *mapping; // 反向映射
pgoff_t index; // 在映射中的偏移
};
在 4GB 物理内存的系统上,约需 400 万个 page 结构体,占用约 256MB 内存。这也是为什么内核引入了 vmemmap 稀疏内存模型和 CONFIG_SPARSEMEM_VMEMMAP 优化。
二、伙伴系统(Buddy System)
2.1 核心原理
伙伴系统是 Linux 物理内存分配的基础算法,采用按阶(order)分组管理的策略。每个 zone 维护 11 个空闲区域数组(order 0-10),分别管理 2^0 到 2^10 个(即 1 到 1024 个,4KB-4MB)连续物理页面块。
分配流程:
- 请求分配 2^order 个连续页面
- 在对应 order 的空闲链表中查找空闲块
- 若该 order 无空闲块,向上级 order+1 查找
- 找到大块后将其分裂:一半满足请求,另一半降入下级空闲链表
- 重复直到刚好满足请求大小
释放流程(伙伴合并):
- 释放 2^order 个页面
- 计算伙伴块地址:buddy_addr = block_addr ^ (1 << order)
- 检查伙伴块是否也在同一 order 的空闲链表中
- 若伙伴空闲,合并为 2^(order+1) 的更大块,插入上级链表
- 递归尝试进一步合并
2.2 伙伴系统内核实现
// mm/page_alloc.c - 核心数据结构
struct free_area {
struct free_area_free_list free_list[MIGRATE_TYPES];
unsigned long nr_free; // 该阶空闲页面总数
};
struct zone {
struct free_area free_area[MAX_ORDER]; // 11 个阶的空闲区域
struct pglist_data *zone_pgdat; // 所属 NUMA 节点
const char *name;
unsigned long zone_start_pfn;
...
};
快速分配路径(fast path):
// __alloc_pages_nodemask() - 快速路径
static inline struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order,
int alloc_flags, const struct alloc_context *ac)
{
struct zoneref *z = ac->preferred_zoneref;
for_next_zone_zonelist_nodemask(zone, z, ac->highest_zoneidx, ac->nodemask) {
// 检查水位线
if (!zone_watermark_ok(zone, order, min_mark, ...))
continue;
// 从伙伴系统分配页面
page = rmqueue(zone, order, gfp_mask);
if (page) {
prep_new_page(page, order, gfp_mask);
return page;
}
}
return NULL; // 快速路径失败,进入慢速路径
}
2.3 页面迁移类型(MIGRATE_TYPES)
Linux 为减少内存碎片,在伙伴系统中引入了页面迁移类型:
- MIGRATE_UNMOVABLE:不可移动页面(内核数据结构、页表等)
- MIGRATE_MOVABLE:可移动页面(用户空间页面)
- MIGRATE_RECLAIMABLE:可回收页面(文件缓存等)
- MIGRATE_PCPTYPES:PCP(Per-CPU Pageset)高速缓存
分配时按指定迁移类型从对应链表取页,释放时按原类型归还,使得同类页面聚集,不同类型分离,为内存整理(Memory Compaction)创造条件。
三、页面分配与回收策略
3.1 分配标志 gfp_mask
gfp_mask 控制页面分配行为和约束,关键标志:
- GFP_KERNEL:标准内核分配,允许睡眠和 I/O 操作
- GFP_ATOMIC:原子分配,不可睡眠,用于中断上下文
- GFP_DMA / GFP_DMA32:限定 DMA 区域分配
- __GFP_HIGHMEM:允许从高端内存分配
- __GFP_ZERO:分配后清零
- __GFP_NOWARN:分配失败不输出警告
- __GFP_RETRY_MAYFAIL:可能失败但尝试回收
3.2 内存分配完整调用链
kmalloc(size, flags)
→ __kmalloc(size, flags)
→ __do_kmalloc(size, flags, _RET_IP_)
→ kmalloc_slab(size, flags) // 选择合适的 kmem_cache
→ slab_alloc(cachep, flags) // SLAB/SLUB 分配
→ __slab_alloc()
→ new_slab() // 从伙伴系统获取页面
→ alloc_pages()
→ alloc_pages_node()
→ __alloc_pages_nodemask()
→ get_page_from_freelist() // 快速分配
→ __alloc_pages_slowpath() // 慢速路径
→ wakeup_kswapd // 唤醒交换线程
→ try_to_free_pages // 页面回收
→ compact_zone() // 内存整理
→ alloc_pages_direct_reclaim() // 直接回收
3.3 内存回收机制
当物理内存不足时,内核通过以下机制回收内存:
(1)页面回收算法(kswapd)
内核线程 kswapd 周期扫描 LRU 链表,根据页面活跃度分为活跃链表和不活跃链表:
- 活跃匿名页面(Active Anonymous)
- 不活跃匿名页面(Inactive Anonymous)→ 交换到 Swap
- 活跃文件页面(Active File)
- 不活跃文件页面(Inactive File)→ 丢弃或写回
(2)页面写回机制
脏页面通过内核线程 flush 和 writeback 异步刷回磁盘。关键参数:
- vm.dirty_ratio:脏页占总内存百分比上限(默认 20%)
- vm.dirty_background_ratio:后台刷回起始百分比(默认 10%)
- vm.dirty_expire_centisecs:脏页过期时间(默认 3000 厘秒=30秒)
3.4 OOM Killer
当内存回收仍无法满足分配需求时,OOM Killer 被触发。其算法为每个进程计算 oom_score:
// 简化的 OOM 评分逻辑
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages)
{
// 基于内存占用、CPU时间、运行时间、oom_score_adj 等
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS);
points += (p->mm->total_vm >> (SHIFT_PAGES + 1));
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN)
return 0;
points = points * 1000 / totalpoints;
return points;
}
可以通过 /proc/[pid]/oom_score_adj(范围 -1000 到 +1000)调整进程被杀优先级。关键进程设为 -1000 可免疫 OOM Killer。
四、SLUB 分配器:内核对象的精密工厂
4.1 SLUB 的设计目标与演进
SLUB(Unqueued Slab Allocator)是 Linux 默认的内核对象分配器,由 Christoph Lameter 于 2007 年引入。其设计目标是解决 SLAB 分配器在 NUMA 系统中的队列管理开销问题。相比 SLAB,SLUB 具有以下优势:
- 移除每 CPU/每节点的复杂队列管理
- 更简洁的数据结构,减少元数据开销
- 更好的调试支持(Red Zoning、Poisoning)
- 与内存控制组(memcg)的紧密集成
- 更高的分配性能
4.2 SLUB 数据结构
// SLUB 核心数据结构
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // 每 CPU 高速缓存指针
slab_flags_t flags; // 分配标志
unsigned long min_partial; // slab 部分空闲的最小数量
unsigned int size; // 对象实际大小(含对齐)
unsigned int object_size; // 对象原始大小
unsigned int offset; // 下一个空闲对象偏移
unsigned int cpu_partial; // cpu 部分列表的 slab 限制
struct kmem_cache_node *node[MAX_NUMNODES]; // 每 NUMA 节点 slab 管理
struct kmem_cache_order_objects oo; // 最佳阶数(2^order * page_size)
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t alloc_gfp; // 分配页面时使用的 gfp
int refcount; // 引用计数
void *freelist; // 空闲对象链表头
const char *name; // slab 缓存名称
struct list_head list; // slab 缓存链表
...
};
4.3 SLUB 分配原理
SLUB 基于 Slab 分配器基本思想:从伙伴系统获取连续页面,划分为多个固定大小对象的内存池:
// SLUB 分配流程
static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, unsigned long addr)
{
struct kmem_cache_cpu *c = raw_cpu_ptr(s->cpu_slab);
struct page *page;
void *object;
retry:
// 1. 首先检查 CPU 高速缓存 freelist
object = c->freelist;
if (unlikely(!object))
goto new_slab; // CPU 缓存无可用对象,进入 slow path
page = virt_to_head_page(object);
if (unlikely(!node_match(page, node)))
goto new_slab; // NUMA 节点不匹配
// 2. 从 CPU freelist 中取出对象
c->freelist = get_freepointer(s, object);
c->page->inuse++; // 已用计数增加
prefetch_freepointer(s, c->freelist);
return object;
new_slab:
// 3. 尝试从 partial slab 获取
if (unlikely(!have_partial_slot))
goto new_objects; // 无 partial slab,需要新页面
// 4. 从 partial slab 获取对象
page = c->page;
...
new_objects:
// 5. 从伙伴系统分配新的 slab
page = new_slab(s, gfpflags, node);
c->page = page;
c->freelist = start; // slab 中的第一个对象
...
}
4.4 CPU 高速缓存与 NUMA 优化
SLUB 的三级缓存架构极大提升了分配性能:
- L1: CPU freelist—— 每 CPU 单链表,无锁分配,零 NUMA 惩罚
- L2: CPU partial—— 每 CPU 部分空闲 slab 列表
- L3: Node partial—— 每 NUMA 节点的部分空闲 slab,需跨节点访问
释放时对象优先归还到分配时的 CPU freelist,保证缓存热度和 NUMA 局部性。
4.5 SLUB 调试功能
SLUB 提供强大的调试机制用于检测内存错误:
- Red Zoning:在对象前后插入红色标记区域,检测越界访问
- Poisoning:释放时填充特定模式(0x5a5a5a5a),检测 use-after-free
- Tracking:记录每次分配/释放的调用栈
- Sanitizers:KASAN(内核地址消毒器)集成
启用方式:内核启动参数添加 slub_debug=FZP(F=Tracking, Z=Red Zone, P=Poison)。
五、虚拟内存与 mm_struct
5.1 进程地址空间
每个进程拥有独立的虚拟地址空间,由 struct mm_struct 描述:
struct mm_struct {
struct vm_area_struct *mmap; // VMA 链表头
struct rb_root mm_rb; // VMA 红黑树根
unsigned long mmap_base; // mmap 区域基址
unsigned long task_size; // 用户空间大小
unsigned long start_code, end_code; // 代码段
unsigned long start_data, end_data; // 数据段
unsigned long start_brk, brk; // 堆区域
unsigned long start_stack; // 栈起始地址
unsigned long arg_start, arg_end; // 参数区
unsigned long env_start, env_end; // 环境变量区
pgd_t *pgd; // 页全局目录
atomic_t mm_users; // 使用者计数(线程组)
atomic_t mm_count; // 引用计数
rwlock_t mmap_lock; // 地址空间读写锁
...
};
典型 x86_64 进程地址空间布局:
- 0x0000 0000 0000 - 0x0000 7FFF FFFF:用户空间(128TB)
- 0xFFFF 8000 0000 - 0xFFFF FFFF FFFF:内核空间
5.2 虚拟内存区域(VMA)
struct vm_area_struct 描述一段连续的虚拟内存区域:
struct vm_area_struct {
unsigned long vm_start; // VMA 起始地址
unsigned long vm_end; // VMA 结束地址
struct mm_struct *vm_mm; // 所属地址空间
pgprot_t vm_page_prot; // 访问权限
unsigned long vm_flags; // 标志(VM_READ/VM_WRITE/VM_EXEC/VM_SHARED)
struct rb_node vm_rb; // 红黑树节点
struct list_head vml; // 链表节点
const struct vm_operations_struct *vm_ops; // 操作函数集
unsigned long vm_pgoff; // 文件映射的偏移
struct file *vm_file; // 映射的文件
...
};
5.3 缺页异常(Page Fault)处理
当访问未映射的虚拟页面时触发缺页异常,内核通过以下步骤处理:
// 缺页异常处理路径
do_page_fault(regs, error_code)
→ __do_page_fault(addr, error_code)
1. 查找 VMA:find_vma(mm, addr)
- 红黑树查找包含 addr 的 VMA
- 找不到 → SIGSEGV
2. 权限检查
- 写操作但 VMA 不可写 → SIGSEGV
- 执行操作但 VMA 不可执行 → SIGSEGV
3. 处理缺页类型:
a) handle_mm_fault(vma, addr, flags)
→ 分配物理页面并建立映射
b) do_anonymous_page()
- 匿名映射:分配零页,建立 PTE
c) do_fault()
- 文件映射:从磁盘读入页面
d) do_swap_page()
- 换出页面:从 Swap 读回
4. 写时复制(COW)
- do_wp_page():检测到写共享页面时复制新页面
5.4 反向映射(Reverse Mapping)
反向映射解决的核心问题是:给定物理页面,快速找到所有映射它的 VMA(页表项)。这在页面回收和内存迁移时至关重要。
匿名页面反向映射(rmap):
struct anon_vma {
struct anon_vma *root; // 根节点(共享时)
atomic_t refcount; // 引用计数
unsigned degree; // 父子层级
struct rb_root rb_root; // VMA 红黑树
struct rw_semaphore rwsem; // 保护锁
};
// 匿名页面的反向映射链
page->mapping = (struct address_space *)anon_vma + PAGE_MAPPING_ANON
page->index = 线性索引(用于定位 VMA 中的 PTE)
5.5 透明大页(Transparent Huge Pages)
Linux 支持 2MB 甚至 1GB 的大页(Huge Page),减少 TLB 压力:
- PMD 级别(2MB):由内核 khugepaged 线程自动提升
- PUD 级别(1GB):需要用户显式配置
TPH 工作流程:
khugepaged 线程:
1. 扫描进程内存,找到连续 2MB 对齐的匿名区域
2. 使用 __collapse_huge_page() 尝试合并
3. 分配 2MB 大页,复制 512 个 4KB 页面内容
4. 替换 512 个 PTE 为单个 PMD 大页映射
5. 释放旧页面回伙伴系统
// 提升成功率相关参数:
/sys/kernel/mm/transparent_hugepage/enabled // always / madvise / never
/sys/kernel/mm/transparent_hugepage/defrag // 碎片整理模式
六、slab 分配器与系统缓存
6.1 kmalloc 与 vmalloc 对比
| 特性 | kmalloc | vmalloc |
|---|---|---|
| 物理连续性 | 保证连续 | 不保证连续 |
| 虚拟连续性 | 连续 | 连续 |
| 分配大小 | <~数MB(取决于碎片) | 几乎无限 |
| 分配速度 | 快(SLUB) | 慢(需修改页表) |
| DMA支持 | 是(GFP_DMA) | 否 |
| 来源 | SLUB 分配器 | vmalloc 区域 |
| 适用场景 | 设备驱动、DMA、小对象 | 大块内存、模块加载 |
6.2 kmalloc 的 slab 缓存层次
kmalloc 维护了一组按大小分级的 slab 缓存:
// 通用 kmalloc 缓存大小(x86_64)
static const size_t kmalloc_sizes[] = {
32, 64, 96, 128, // 小对象(task_struct 等)
192, 256, // 中对象
512, 1024, 2048, // 常用(buffer_head 等)
4096, 8192, // 较大对象
16384, 32768, 65536, // 大块(网络缓冲区)
131072, 262144 ... // 超大块
};
分配时选择满足请求的最小缓存。例如 300 字节请求会使用 512 字节的 kmem_cache(内部碎片 212 字节)。
七、内存碎片整理与反碎片
7.1 碎片类型
- 内部碎片:已分配内存块中未使用的部分(如分配 300 字节使用 512 缓存)
- 外部碎片:空闲内存分散,无足够连续大块(伙伴系统主要敌人)
7.2 Compaction(碎片整理)
当高阶连续页面无法满足时,内核通过 compact_zone() 移动页面创造连续空间:
// 碎片整理核心流程
enum compact_result compact_zone(struct zone *z, struct compact_control *cc)
{
// 1. 扫描 zone 寻找可迁移页面(从低地址到高地址)
isolate_migratepages(cc);
// 2. 尝试迁移页面到高层地址
migrate_pages(&cc->migratepages, ...);
// 3. 成功后,空闲页面向低地址填充,高地址形成大块
// 返回值:
// COMPACT_SUCCESS - 成功整理出连续块
// COMPACT_PARTIAL - 部分成功
// COMPACT_SKIPPED - 无可迁移页面
}
7.3 CMA(连续内存分配器)
CMA 为设备驱动预留连续内存,但不独占——正常内存不足时可回收 CMA 区域供伙伴系统使用:
// 内核启动参数:cma=64M@0-4G
// 分配 CMA 区域:
struct page *cma_alloc(struct cma *cma, size_t count, gfp_t gfp)
struct page_alloc(cma->base_cma + cma->count);
// 设备驱动使用 DMA API 时自动使用 CMA:
dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
八、内存控制组(memcg)
8.1 memcg 的作用
内存控制组(Memory Control Group)实现容器级别的内存配额管理,是 Docker/Kubernetes 的内存隔离基础。
8.2 memcg 层级结构
root (无限制)
├── system.slice
│ ├── systemd.service (无限制)
│ └── ...
├── user.slice
│ └── user-1000.slice
└── kubepods.slice (8GB)
├── kubepods-burstable (弹性)
│ └── pod-abc (2GB)
└── kubepods-besteffort (尽力)
└── pod-xyz (无保证)
8.3 memcg 限制参数
- memory.limit_in_bytes:硬限制,超过直接 OOM
- memory.soft_limit_in_bytes:软限制,内存紧张时优先回收
- memory.swappiness:控制匿名页和文件页的回收比例
- memory.kmem.limit_in_bytes:内核内存限制(已弃用)
8.4 memcg 与回收的交互
当进程分配内存时,完整的回收路径:
alloc_page(gfp)
→ __alloc_pages_nodemask()
→ memcg_charge() // 检查 memcg 配额
→ page_counter_try_charge() // 原子递减计数器
→ 超出限制 → memcg_reclaim() // 触发 cgroup 内回收
→ mem_cgroup_shrink_node()
→ shrink_lruvec()
→ shrink_slab() // 回收 slab 缓存
九、实战:内存性能调优
9.1 检查内存碎片程度
# cat /proc/buddyinfo
Node 0, zone DMA 1 1 0 0 2 1 1 0 1 1 3
Node 0, zone Normal 2345 1567 890 456 234 123 45 23 12 4 2
# 列的含义(从左到右):order 0~10 的空闲页面数
# 如果高阶(order 8-10)数量稀少,说明碎片严重
9.2 查看 slab 缓存状态
# slabtop -o
Active / Total Objects (% used) : 125678 / 145230 (86.5%)
Active / Total Slabs (% used) : 4521 / 4521 (100.0%)
Active / Total Caches (% used) : 156 / 156 (100.0%)
Active / Total Size (% used) : 45.23M / 52.15M (86.7%)
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
12288 11876 96% 0.08K 384 32 1536K vm_area_struct
8192 8023 97% 0.50K 128 64 4096K inode_cache
7680 7456 97% 0.19K 120 64 3072K dentry
5120 5012 97% 0.12K 160 32 2048K filp
# 查看特定 slab 的详细状态
cat /proc/slabinfo | head -20
9.3 调整 vm 参数优化内存行为
# 系统内存参数调优示例
# /etc/sysctl.d/99-memory.conf
# 减少 swap 倾向(SSD 服务器)
vm.swappiness = 10
# 调整脏页刷新策略(高性能场景)
vm.dirty_ratio = 8
vm.dirty_background_ratio = 4
vm.dirty_expire_centisecs = 3000
# vfs 缓存压力(内存充足时增大,减少回收频率)
vm.vfs_cache_pressure = 50
# 内存超售策略
# 0=启发式超售,1=总是超售,2=不超售(严格模式)
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
# NUMA 平衡(关闭可减少跨节点访问)
kernel.numa_balancing = 0
# 透明大页(数据库场景建议关闭)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
9.4 NUMA 优化策略
# 查看 NUMA 拓扑
numactl --hardware
numastat -m
# 绑定进程到特定 NUMA 节点
numactl --cpunodebind=0 --membind=0 ./my_application
# 查看进程的 NUMA 内存分布
numastat -p $(pidof my_app)
# 自动 NUMA 平衡的影响:
# kernel.numa_balancing = 1(默认开启)
# - 优点:自动将页面迁移到访问者节点
# - 缺点:TLB shootdown 开销,延迟抖动敏感应用应关闭
十、总结与展望
Linux 内核内存管理是一个精密的分层系统:伙伴系统提供物理连续页面的分配与合并能力,SLUB 分配器在其之上构建了高效的内核对象池,虚拟内存管理为用户提供隔离的地址空间,而反向映射和碎片整理机制则保证了系统长期运行的高效性。
随着新硬件和工作负载的出现,Linux 内存管理也在持续演进:
- 内存分层管理:CXL、持久内存等新硬件的引入,正在推动新一层的内存域设计
- 用户态内存管理:MGLRU(Multi-Gen LRU)算法显著提升了回收效率
- 安全增强:CFI(Control Flow Integrity)、Shadow Call Stack 等依赖内存隔离特性的安全机制
- AI 推理场景:大模型推理对连续物理内存提出了新的需求,推动了内存预分配和固定技术的发展
深入理解这些机制,不仅让我们能够编写更高效的系统代码,更能在面对内存瓶颈问题时,精准定位根因并制定最佳优化策略。

发表评论 取消回复