引言
内存管理是 Linux 子系统中最复杂、最核心的部分之一。它不仅负责在进程之间分配和回收物理内存,还实现了虚拟内存、页面交换、Slab 分配器等关键机制。本文将从内核源码的角度,深入剖析 Linux 内存管理子系统的架构与实现细节。
1. 物理内存模型
1.1 NUMA 与 UMA 架构
现代多核处理器普遍采用 NUMA(非统一内存访问)架构,每个 CPU 节点拥有本地内存,访问远程内存会有额外的延迟开销。Linux 内核通过 pg_data_t 结构体管理每个 NUMA 节点,其中包含该节点的内存区域(zone)、页面列表和交换信息。
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;
...
};
在 UMA(统一内存访问)系统中,内核只维护一个 contig_page_data,所有 CPU 看到的内存拓扑相同,但仍存在 DMA、Normal、HighMem 等内存区域的区别。
1.2 内存区域(Zones)
物理内存根据用途被划分为多个区域:
- ZONE_DMA(0-16MB):用于老式 ISA 设备的 DMA 传输
- ZONE_DMA32(16MB-4GB):32 位 DMA 设备的可寻址区域
- ZONE_NORMAL(直接映射区):内核线性映射可直接访问的物理页面
- ZONE_HIGHMEM(仅 32 位系统):超出内核线性映射范围的物理内存
- ZONE_MOVABLE(可选):用于内存热插拔的可移动页面
2. 虚拟内存与地址空间
2.1 进程地址空间布局
在 x86_64 Linux 上,用户进程拥有 128TB 的虚拟地址空间(47 位有效地址)。地址空间按功能从低到高依次为:代码段(.text)、只读数据(.rodata)、数据段(.data/.bss)、堆(mmap 区域向上增长)、共享库映射区、栈(向下增长),以及最顶端的内核空间。
内核通过 struct mm_struct 结构体管理进程的地址空间,其中 mmap 字段指向进程的所有 VMA(Virtual Memory Area)红黑树根节点:
struct mm_struct {
struct {
struct rb_root rb;
struct rb_node *rb_subtree_last;
} mm_mt; /* VMA 红黑树 */
unsigned long mmap_base; /* mmap 区域基址 */
unsigned long task_size; /* 用户/内核地址分界 */
pgd_t *pgd; /* 页全局目录 */
atomic_t mm_users; /* 活跃用户计数 */
atomic_t mm_count; /* 引用计数 */
...
};
2.2 页表机制
Linux 采用四级页表结构(x86_64 架构):PGD → P4D → PUD → PMD → PTE。每一级页表记录下一级的物理地址,最终在 PTE 级别完成虚拟地址到物理地址的映射。64 位系统下,页面大小为 4KB 时,每级页表使用 9 位索引(共 512 条目),四级即可映射 48 位虚拟地址(256TB)。
内核提供了 follow_page_mask()、walk_page_range() 等辅助函数完成页表遍历。TLB(Translation Lookaside Buffer)缓存最近使用的地址映射,是虚拟内存性能的硬件基础。
3. Slab 分配器
3.1 Slab 分配器原理
内核频繁分配和释放小对象(如 task_struct、inode、dentry 等),如果直接使用页面分配器(buddy system),会造成严重的内存碎片和分配延迟。Slab 分配器基于 Jeff Bonwick 的经典设计,解决了这个问题:
- 对象缓存(Cache):为同一类型对象创建专用缓存
- Slab:从 buddy system 获取一个或多个连续页面,划分为等大的对象槽位
- 状态跟踪:每个 slab 维护 free/used 对象列表,优先从 partial slab 分配
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; /* 每 CPU 快速分配路径 */
struct kmem_cache_node *node[MAX_NUMNODES]; /* NUMA 节点管理 */
unsigned int object_size; /* 对象实际大小 */
unsigned int size; /* 对齐后大小 */
unsigned int offset; /* 空闲指针偏移 */
unsigned long flags; /* SLAB 构造/析构标志 */
const char *name; /* 缓存名称 */
struct list_head list; /* 全局链表 */
...
};
3.2 SLUB:新一代默认分配器
SLUB(Scribble-unrolled level block)是新版 Linux 内核中默认的 Slab 分配器,相比经典 SLAB,它简化了设计并提升了性能:
- 移除了 per-CPU 队列,改用 per-CPU 指针直接嵌入空闲对象链表
- 减少了元数据开销,每个 slab 的 page 结构复用
freelist和inuse字段 - 更好的 NUMA 亲和性和调试支持(如 red zone、poisoning)
- 使用
kmem_cache_alloc()/kfree()接口,用法简洁
3.3 SLOB:嵌入式场景的极简分配器
SLOB(Simple List Of Blocks)适用于内存极度受限的嵌入式系统。它采用简单的首次适应算法(first-fit),将空闲块用简单链表连接。虽然分配效率较低,但代码量极小、无 per-CPU 和元数据开销。
4. 页面分配器(Buddy System)
4.1 伙伴系统算法
伙伴系统是 Linux 内核物理内存分配的基石。它将物理页面按大小分为 11 个阶(order 0-10),分别管理 2^0 到 2^10 个连续页面。分配时从合适的阶开始,如果该阶没有空闲块,就向上借一个更大的块,将其分裂为两个伙伴(buddy)——这保证了物理地址按 2 的幂次对齐,合并时可以快速获取更大块。
/* 分配 2^order 个页面 */
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
/* 释放页面到伙伴系统 */
void free_pages(unsigned long addr, unsigned int order);
4.2 GFP 标志
gfp_t 标志控制分配器的行为,关键标志包括:
- GFP_KERNEL:标准内核分配,可能睡眠等待内存回收
- GFP_ATOMIC:高优先级、不可睡眠(中断上下文使用)
- GFP_HIGHUSER:用户页面,可回收
- GFP_NOFS:不触发文件系统操作(避免递归)
- __GFP_ZERO:分配清零页面(安全/密码学场景)
- __GFP_NOWARN:分配失败不发出警告
4.3 Per-CPU 页面缓存(PCP)
为了避免全局伙伴系统锁竞争,内核为每个 CPU 维护一个 per-cpu 页面缓存。当需要分配单个页面(order 0)时,优先从 PCP 链表中取,完全不加锁。PCP 在空闲页面低于阈值时从 buddy system 批量获取补充。
5. 内存回收与交换
5.1 页面回收机制(kswapd)
当系统内存不足时,内核唤醒 kswapd 后台线程执行页面回收。回收主要针对两类页面:
- 文件页(File Page):有后备存储(文件)的页面,可直接丢弃后重新读取
- 匿名页(Anonymous Page):没有后备存储的页面(如堆、栈),必须写入 swap 区才能释放
- 活跃LRU:最近访问过,暂不回收
- 不活跃LRU:近期未访问,优先回收候选
这种双链表设计(active/inactive)使得 LRU 算法无需全局排序,只需在链表间移动即可完成近似 LRU 统计。
5.2 反向映射(Reverse Mapping)
回收一个物理页面时,需要找到所有映射了该页面的进程 PTE,将其标记为不存在。反向映射(rmap)建立了从物理页面到 VMA 链表的映射关系:
- 文件页 rmap:通过
address_space和page->index在 radix tree 中查找 - 匿名页 rmap:通过
anon_vma链表遍历所有关联的 VMA。KSM(Kernel Samepage Merging)也使用此机制合并相同内容的页面
5.3 OOM Killer
当内存耗尽且回收仍无法满足需求时,内核触发 OOM Killer。它会调用 oom_badness() 函数评估每个进程的"杀生分数",考虑因素包括:占用内存量、运行时间、UID、是否持有特殊权限等,最终杀死得分最高的进程。
现代内核支持 oom_score_adj 调整值,管理员可以为关键服务设置负值(避免被杀)或为批处理任务设置正值(优先被杀)。
6. 内存管理与性能优化
6.1 Transparent Huge Pages (THP)
透明大页技术自动将连续的普通页面合并为大页(2MB 或 1MB),减少 TLB 压力和页表遍历开销。khugepaged 后台线程扫描进程内存区域,将符合条件的连续 4KB 页面合并。对于数据库和科学计算等内存密集型应用,THP 可带来 10-25% 的性能提升。
6.2 内存压缩(zswap/zram)
- zswap:在 swap 前对匿名页面进行压缩存储于内存缓存中,延迟或避免实际磁盘 I/O
- zram:创建一个压缩的块设备作为 swap 设备,适合无磁盘的嵌入式环境或需要加速交换的场景
6.3 NUMA 调优
NUMA 架构下,内存分配的本地性对性能影响显著。策略包括:
- numactl --interleave=all:在节点间交错分配
- numactl --cpunodebind=0 --membind=0:绑定 CPU 和内存到同一节点
- MADV_HUGEPAGE:madvise 提示使用大页
7. 调试与问题排查
内核提供丰富的工具和接口用于内存调试:
- slabtop / /proc/slabinfo:查看 Slab 分配器状态
- vmstat -s:内存使用速览
- /proc/buddyinfo:伙伴系统各阶空闲块数量
- /proc/<pid>/smaps:进程详细内存映射
- kmemleak:内核内存泄漏检测工具
- KASAN (Kernel Address Sanitizer):运行时检测越界访问和释放后重用
- ftrace kprobe:追踪
__alloc_pages等关键函数
结语
Linux 内存管理子系统的精巧设计,使其能在从嵌入式设备到超级计算机的各种场景中高效运转。理解 Slab 分配器、伙伴系统、LRU 回收机制以及反向映射等核心组成部分,不仅有助于定位性能瓶颈,更能启发我们在系统编程中做出更好的架构决策。建议读者结合最新内核源码(kernel.org)和 Brendan Gregg 的《Systems Performance》一书深入学习。

发表评论 取消回复