引言
Linux 内核的内存管理子系统(MM)是操作系统最复杂的核心组件之一,负责管理物理内存、虚拟内存、进程地址空间以及其间的映射关系。理解整个内存管理的全链路,对于性能调优、故障排查和底层开发至关重要。
现代 Linux 内存管理子系统主要包含以下核心层次:伙伴系统管理物理页解决外部碎片、SLUB 分配器提供小对象的高效分配、VMA 管理进程虚拟地址空间、四级/五级页表映射虚拟到物理地址、kswapd 在内存紧张时回收页面、OOM Killer 作为极端内存不足时的最后手段。
一、核心数据结构
1.1 struct page — 物理页描述符
内核中每一个物理页都对应一个 struct page 描述符,这是内存管理最基本的数据结构。struct page 的大小至关重要 —— 因为每个物理页都要分配一个,通常约 64 字节。假设系统有 1GB 内存,则约有 262144 个 page 结构,仅元数据就占用约 16MB。
struct page {
unsigned long flags; /* 原子标志位,如 PG_locked, PG_dirty */
union {
struct address_space *mapping; /* 页面映射信息 */
void *s_mem; /* slab 对象起始地址 */
atomic_t compound_mapcount; /* compound page 映射计数 */
};
atomic_t _refcount; /* 引用计数 */
atomic_t _mapcount; /* 映射到页表的次数 */
unsigned long private; /* 私有数据,各用途共用 */
struct list_head lru; /* LRU 链表节点 */
pgoff_t index; /* 在映射中的偏移 */
};
1.2 Buddy System — 伙伴系统
伙伴系统是物理页分配的核心算法。它将物理内存分为 11 个 order(0~10),每个 order 管理大小为 2^order 个连续页面的内存块。分配时,若当前 order 没有空闲块,则从更大的 order 拆分;释放时,若相邻的"伙伴"块也空闲,则向上合并。这种机制有效地减少了外部碎片。
// 每个 zone 包含 11 个 free_area
struct free_area {
struct free_list free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
1.3 Zone 和 Node 架构
现代 NUMA 系统中,内存被组织为 Node → Zone → Page 的三级架构:ZONE_DMA(0~16MB,用于 DMA 操作)、ZONE_DMA32(16MB~4GB,32 位 DMA 设备)、ZONE_NORMAL(直接映射区,内核线性映射)、ZONE_HIGHMEM(高端内存,32 位系统)、ZONE_MOVABLE(可移动页面区)。
二、SLUB 分配器详解
2.1 SLUB 的设计目标
SLUB(Unqueued Slab Allocator)是 Linux 默认的 slab 分配器,设计目标是简化 SLAB 分配器的复杂性,提高多核扩展性。其核心思想是:每个 CPU 维护独立的 partial slab 链表消除锁竞争,每个 CPU 有一个 active page 独占当前 slab,简化 metadata 减少管理开销,debug 功能内置化。
2.2 kmem_cache 结构
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; /* CPU 私有 slab */
struct kmem_cache_node *node[MAX_NUMNODES]; /* 每个 NUMA node */
unsigned int offset; /* 空闲对象链表指针的偏移 */
unsigned int size; /* 对象实际大小(含对齐) */
unsigned int object_size; /* 对象请求大小 */
unsigned long min_partial; /* 最少保留 partial 数 */
int refcount;
void *freelist; /* 全局 freelist */
struct list_head list; /* slab_caches 链表 */
const char *name; /* 缓存名 */
};
2.3 对象分配流程
kmem_cache_alloc() 的分配路径:优先从 cpu_slab->freelist 取空闲对象(fast path,无锁);如果 freelist 为空,从 cpu_slab->page 的 partial 链表补充;如果 cpu page 也没有,从 node 的 partial slab 取一个补充给 cpu;如果 node 也没有 partial,调用 new_slab() 从 buddy system 分配新页。
2.4 SLUB 调试功能
SLUB 内置了强大的 debug 功能,可以通过 CONFIG_DEBUG_SLUB 启用:Red zoning(对象之间填充 0xAA 字节检测越界写)、Poisoning(释放时填充 0x6B 检测 use-after-free)、Tracking(记录每个对象的分配/释放调用栈)。
三、虚拟内存与进程地址空间
3.1 mm_struct 和 vm_area_struct
每个进程有一个 mm_struct 结构来描述其地址空间。内核 6.x 引入了 Maple Tree(ma_tree)替代了原有的 VMA 红黑树加读写锁,提供了无锁读路径和更高效的区间查询。Maple Tree 是一种 B-tree 变体,支持区间遍历、插入和删除,特别适合 VMA 的区间查找场景。
struct mm_struct {
pgd_t *pgd; /* 全局页目录 */
struct maple_tree mm_mt; /* VMA 的内存布局树(6.x 替换 list+rwlock) */
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; /* 栈起始地址 */
atomic_t mm_users; /* 用户数 */
atomic_t mm_count; /* 引用数 */
};
3.2 缺页异常处理路径
当访问虚拟地址未映射物理页时触发缺页异常。处理路径:查找该地址对应的 VMA 确认访问权限是否合法;若 VMA 存在且权限合法则分配物理页并建立映射(demand paging);若访问文件映射页则从磁盘加载文件内容(filemap_fault);若访问匿名页且首次访问则分配零页;若访问已交换到 swap 的页面则换回;若 VMA 不存在则发送 SIGSEGV 信号。
四、物理页分配全链路
4.1 alloc_pages() 核心路径
内核中最常用的页面分配接口是 alloc_pages()。完整分配链路:get_page_from_freelist() 从 per_cpu_pageset 分配(fast path),如果失败进入 __alloc_pages_slowpath() 执行 direct reclaim、direct compaction,实在不行调用 out_of_memory() 触发 OOM Killer。
alloc_pages(gfp_mask, order)
-> alloc_pages_node(nid, gfp_mask, order)
-> __alloc_pages_node(nid, gfp, order)
-> __alloc_pages(gfp, order, preferred_nid)
-> get_page_from_freelist() /* fast path */
-> __alloc_pages_slowpath() /* slow path */
-> __alloc_pages_direct_reclaim()
-> __alloc_pages_direct_compact()
-> __alloc_pages_may_oom()
-> out_of_memory()
-> select_bad_process()
-> oom_kill_process()
4.2 GFP 标志位详解
- GFP_KERNEL:标准内核分配,可能睡眠等待页面回收
- GFP_ATOMIC:原子分配,不睡眠,从紧急储备中获取
- GFP_NOWAIT:不等待分配,失败立即返回
- GFP_NOFS / GFP_NOIO:不触发文件系统/IO 操作
- __GFP_ZERO:分配时清零页面
- __GFP_THISNODE:仅从指定 NUMA 节点分配
4.3 内存碎片化与页面迁移
内核使用 migrate_type 对页面进行分类以减少碎片:MIGRATE_UNMOVABLE(内核分配的不可移动页)、MIGRATE_MOVABLE(用户态可移动页)、MIGRATE_RECLAIMABLE(可回收但不可移动的页)、MIGRATE_ISOLATE(隔离区用于热插拔)。碎片整理(compaction)通过分类在后台自动将分散的空闲页聚集为连续大块。
五、页面回收机制
5.1 kswapd 后台回收
kswapd 是内核的页面回收守护进程,每个 NUMA node 一个。当某 zone 的水位低于 low watermark 时开始工作:检查是否已平衡,扫描所有 LRU 链表,依次缩减活跃链表和回收非活跃页面。
kswapd 唤醒条件:zone 空闲页 < zone->low_wmark
主要工作流:balance_pgdat()
-> pgdat_balanced() /* 检查是否已平衡 */
-> shrink_node() /* 扫描所有 LRU 链表 */
-> shrink_list()
-> shrink_active_list() /* 缩减活跃链表 */
-> shrink_inactive_list() /* 回收非活跃页面 */
5.2 LRU 算法与双链表设计
Linux 内核使用 LRU(Least Recently Used)算法决定哪些页面可以回收。内核维护 active 和 inactive 两种 LRU 链表。页面第二次被访问时从 inactive 提升到 active(refault);active 链表尾部扫描时若长时间未访问则降级到 inactive;回收时主要扫描 inactive 链表尾部。文件页与匿名页的回收比例由 swappiness 参数控制(默认 60)。
5.3 四种 LRU 链表
系统维护四种 LRU 链表:LRU_INACTIVE_ANONYMOUS(非活跃匿名页)、LRU_ACTIVE_ANONYMOUS(活跃匿名页)、LRU_INACTIVE_FILE(非活跃文件页)、LRU_ACTIVE_FILE(活跃文件页)。
5.4 Multi-Gen LRU(MGLRU)
内核 5.0+ 引入了新一代回收算法,像 Generational GC 一样按"代"分组页面,每一代记录页面的年龄。回收时从最老的一代开始扫描,避免了传统 LRU 中 young page 扫描的低效问题。可通过 /sys/kernel/mm/lru_gen/enabled 启用。
六、OOM Killer 机制
6.1 触发条件
OOM Killer 在内核分配路径中多次尝试失败后触发。完整流程:alloc_pages 经过 fast path → slow path → direct reclaim → direct compaction 均无法获得足够内存,调用 out_of_memory() 判断是否真的需要杀进程。若分配标志包含 __GFP_NOFAIL 或 __GFP_RETRY_MAYFAIL 则跳过 OOM。
6.2 badness score 选择算法
OOM Killer 通过计算每个进程的"OOM 得分"决定杀死哪个进程:基础分为进程占用的物理内存(RSS + 页表 + swap 占用),按比例归一化后乘以 1000,根进程和内核线程豁免。通过 /proc/[pid]/oom_score_adj 可以手动调整(-1000 永不杀死,1000 优先被杀)。
points = resident set size (RSS + page tables + swap entries)
points *= 1000
points /= totalram_pages /* 按比例归一化 */
/* oom_score_adj 手动调权:-1000 ~ 1000 */
6.3 OOM 处理流程
选中目标进程后,内核发送 SIGKILL 信号,释放进程占用的所有内存,并触发 oom_reaper 异步回收线程清理残留的页面结构和内核对象。
七、内存映射(mmap)全链路
7.1 mmap 系统调用入口
mmap() 是将文件或设备映射到进程地址空间的系统调用。核心流程:找到合适的虚拟地址范围(get_unmapped_area),分配 VMA 结构(vm_area_alloc),文件映射调用 fault 回调,匿名映射设置 vma_set_anonymous,最后将 VMA 插入 mm 的树中。
sys_mmap_pgoff(addr, len, prot, flags, fd, pgoff)
-> vm_mmap_pgoff()
-> do_mmap_pgoff()
-> get_unmapped_area() /* 找到合适的虚拟地址范围 */
-> mmap_region()
-> vm_area_alloc() /* 分配 VMA 结构 */
-> 文件映射:file->f_op->mmap(vma) -> 设置 fault 回调
-> 匿名映射:vma_set_anonymous()
-> insert_vm_struct() /* 将 VMA 插入 mm 的树中 */
7.2 mmap 的四种类型
- Shared + File-backed:共享文件映射(IPC 共享内存)
- Private + File-backed:私有文件映射(代码段、数据段)
- Shared + Anonymous:共享匿名映射(fork 后父子共享)
- Private + Anonymous:私有匿名映射(堆分配、栈)
7.3 mmap 与 read/write 的性能对比
mmap 优势:减少一次数据拷贝(无需内核缓冲区到用户缓冲区的拷贝)、支持随机访问通过指针直接访问文件内容、支持多进程共享映射实现零拷贝 IPC。劣势:占用虚拟地址空间、缺页异常的开销不固定、需要注意 page cache 的同步问题。
八、内核 6.x 内存管理新特性
8.1 Memory Folios(大页结构)
内核 5.16+ 引入 struct folio 替代 struct page 管理复合页和文件页。Folio 是 struct page 的超集,解决了"这个 page 是 head page 还是 tail page"的歧义问题,简化了文件系统对 compound page 的处理。
struct folio {
unsigned long flags;
union {
struct list_head lru;
struct {
unsigned long _flags_1;
unsigned long _head_1;
unsigned long _folio_dtor;
unsigned long _folio_order;
};
};
};
8.2 Maple Tree 替代 VMA 红黑树
内核 6.1 引入 Maple Tree 作为新的区间树结构。特点:读操作无锁(RCU 保护)、区间查询高效(B-tree 特性)、插入/删除性能优于红黑树、天然支持区间操作。
8.3 Per-TLB 优化
内核 6.x 引入了 per-TLB 优化,在 TLB shootdown 操作中只 flush 涉及的目标 CPU,而非全局广播,在云原生多核场景中显著降低了 TLB 失效开销。
九、内存管理性能调优
9.1 水位线参数
每个 zone 有三个水位线:min(最低水位,触发 direct reclaim 和 OOM)、low(低水位,唤醒 kswapd)、high(高水位,kswapd 停止工作)。可通过 /proc/sys/vm/min_free_kbytes 控制。
cat /proc/sys/vm/min_free_kbytes /* 查看当前值 */
sysctl -w vm.min_free_kbytes=1048576 /* 设为 1GB */
9.2 NUMA 本地分配策略
numactl --interleave=all 跨节点交错分配,numactl --membind=0 绑定到节点 0 分配,numactl --cpunodebind=0 CPU 和内存都绑定节点 0。
9.3 Transparent Huge Pages (THP)
透明大页将普通 4KB 页面合并为 2MB 大页,减少 TLB miss 和页表开销。但在数据库场景中可能导致延迟抖动,大页面分配需要 compact 操作,建议数据库场景禁用 THP。
echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled /* 数据库场景 */
9.4 swappiness 调优
/proc/sys/vm/swappiness 控制 swap 积极程度:0 除非触发 OOM 否则不使用 swap(默认 60),100 尽可能使用 swap。服务器场景建议设置较低值(10~20)。
十、内存监控与故障诊断工具
10.1 /proc/meminfo 深度解读
/proc/meminfo 提供了系统内存的完整画像:MemTotal/MemFree/MemAvailable(总/空闲/可用内存)、Buffers/Cached(缓冲区和页缓存)、SwapTotal/SwapFree(交换空间)、HugePages(大页使用情况)、Slab(内核 slab 内存)、PageTables(页表占用)。
10.2 /proc/slabinfo 分析
/proc/slabinfo 展示所有 kmem_cache 的活跃对象数、总对象数、每个对象大小等信息。可用于发现内存泄漏(某 cache 的 active objs 持续增长)和内存碎片问题(objects per slab 远小于实际值)。
10.3 vmstat 与 sar
vmstat 提供实时内存统计:si/so(swap in/out)、bi/bo(块 IO)、in(中断数)、cs(上下文切换)。sar -r 和 sar -B 提供更详细的内存和分页历史统计。
vmstat 1 /* 每秒刷新 */
sar -r 1 10 /* 内存使用统计 */
sar -B 1 10 /* 分页统计 */
10.4 bpftrace 动态跟踪
/* 监控 kmalloc/kfree 调用栈 */
bpftrace -e 'kprobe:kmalloc { @[comm, kstack] = count(); }'
/* 跟踪 OOM 事件 */
bpftrace -e 'kprobe:oom_kill_process { printf("OOM kill: pid=%d comm=%s\n", pid, comm); }'
/* 监控页面回收活动 */
bpftrace -e 'kprobe:shrink_node { @pages = hist(arg2); }'
十一、常见踩坑记录
11.1 SLUB 跨 CPU 释放导致性能下降
SLUB 分配器的 slow path 中,如果对象释放到非分配时的 CPU slab 上会触发 CPU 间 slab 迁移。在高频分配释放场景下建议使用 per-cpu 数据缓存、避免频繁在 CPU 间交叉持有对象、启用 CONFIG_SLUB_CPU_PARTIAL 缓解。
11.2 GFP_KERNEL 在持锁/中断上下文误用
GFP_KERNEL 允许睡眠等待页面回收,若在中断上下文或持有 spinlock 时使用会导致内核 BUG()。在这些场景必须使用 GFP_ATOMIC。
11.3 内存碎片导致高阶分配失败
长时间运行后物理碎片化可能导致 order>=5(32KB)的大块分配失败,即使总的空闲内存足够。解决方案:启用 CONFIG_COMPACTION 让 kcompactd 运行碎片整理、使用 vmalloc()(不要求物理连续)、定期 drop_caches 释放可回收页。
11.4 OOM Killer 误杀关键进程
关键服务进程可能被 OOM Killer 选中导致服务中断。防护措施:使用 cgroup memory.limit_in_bytes 限制大内存应用的用量、为关键进程设置 oom_score_adj 为 -1000、监控 dmesg 中的 OOM 日志。
十二、总结与最佳实践
Linux 内核内存管理是一个精密的系统:Buddy System 管理物理页解决外部碎片,SLUB 高效管理小对象,LRU/kswapd 在内存紧张时决定回收哪些页面,OOM Killer 作为最后手段释放内存,mmap 提供文件到内存的高效映射。内核 6.x 进一步引入了 Folios、Maple Tree、MGLRU 等优化。
实际工作中的建议:优先从 /proc/meminfo、/proc/slabinfo 了解系统内存状态;关注 GFP 标志位的使用场景(中断上下文必须 GFP_ATOMIC);理解水位线机制合理设置 min_free_kbytes;NUMA 环境下注意 CPU 和内存的亲和性;使用 cgroup 限制应用的最大内存用量;对关键进程设置 oom_score_adj 保护;使用 bpftrace/bcc 动态跟踪内存分配和回收事件。

发表评论 取消回复