引言
Linux 内核内存管理是整个操作系统最复杂、最核心的子系统之一。从用户空间的 malloc() 到内核空间的 kmalloc()、vmalloc(),从物理页帧分配器 Slab 到伙伴系统 Buddy,从匿名页面到文件映射页面,每一个内存分配决策都直接影响系统性能与稳定性。本文以 Linux 6.x 内核为背景,从工程实战角度深入解析内存管理的完整技术栈。
一、物理内存模型与 zone 架构
1.1 从 UMA 到 NUMA:节点-区域-页三级模型
Linux 采用节点-区域-页(Node-Zone-Page)三级模型管理物理内存。在 include/mm.h 中定义了核心数据结构:
struct pglist_data { // 节点描述符 (typedef struct pglist_data pg_data_t)
struct zone node_zones[MAX_NR_ZONES]; // 本节点的内存区域数组
struct zoneref node_zonelists[MAX_ZONELISTS]; // 分配fallback的节点列表
int nr_zones; // 本节点区域数量
struct page *node_mem_map; // 本节点页描述符数组
unsigned long node_start_pfn; // 起始页帧号
unsigned long node_present_pages; // 可用物理页数
unsigned long node_spanned_pages; // 总页数(含空洞)
int node_id; // 节点ID
// ... kswapd、compact 等字段
};
struct zone { // 区域描述符
unsigned long _watermark[NR_MARK]; // min/low/high 水位
unsigned long nr_reserved_highatomic; // 保留原子页
struct free_area free_area[MAX_ORDER]; // 伙伴系统空闲区
unsigned long vm_stat[NR_VM_ZONE_ITEMS]; // 区域统计
// ...
};
在 x86_64 架构下,典型的 zone 包括:
- ZONE_DMA(0-16MB):用于 ISA 设备的 DMA 传输
- ZONE_DMA32(16MB-4GB):用于 32 位 DMA 设备
- ZONE_NORMAL(内核直接映射区):线性映射到内核地址空间
- ZONE_MOVABLE(可迁移区):用于内存热插拔
1.2 NUMA 架构实战
在多路服务器上,NUMA 拓扑感知至关重要。假设一台双路 EPYC 服务器(2×64 核):
// 查看 NUMA 拓扑
$ numactl --hardware
available: 2 nodes (0,1)
node 0 cpus: 0-63,128-191
node 0 size: 263798 MB
node 1 cpus: 64-127,192-255
node 1 size: 263798 MB
node distances:
node 0 1
0: 10 21
1: 21 10
本地节点访问延迟 80ns,跨节点访问通过 Infinity Fabric 延迟 130ns。对于数据库这类内存密集型应用,策略性绑核与本地分配可提升 30%+ 吞吐量。
二、伙伴系统(Buddy System)
2.1 算法原理与实现
伙伴系统是物理页帧分配的核心算法,解决外部碎片问题。内核维护 11 个空闲链表(order 0~10),分别管理 2^0~2^10 个连续页(即 4KB~4MB 连续物理块):
// mm/page_alloc.c
struct free_area {
struct free_list free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
struct free_list {
struct list_head lru; // 双向链表头(通过 page->lru 嵌入)
unsigned long nr_pages; // 该迁移类型空闲页数
};
分配 order-N 页面的流程:
- 检查 free_area[N] 是否有空闲块 → 有则直接取出
- 向上查找 order N+1, N+2... 直到 order 10
- 找到后逐层"拆分":取出一半返回,另一半插入低一级 free_area
释放时的"合并"操作:检查伙伴块(相邻且同阶)是否空闲 → 若是则合并为更高阶块。伙伴地址计算:buddy_pfn = pfn ^ (1 << order>。
2.2 页面迁移类型(MIGRATE_TYPES)
为减少碎片化内核引入页面迁移分类:
- MIGRATE_UNMOVABLE:内核数据结构、页表
- MIGRATE_MOVABLE:用户页面、缓存页面
- MIGRATE_RECLAIMABLE:slab 缓存、dcache(可回收但不可迁移)
- MIGRATE_HIGHATOMIC:紧急预留原子页
通过 /proc/pagetypeinfo 可观察各迁移类型的页面分布情况,诊断内存碎片。
三、Slab 分配器:从 Slab 到 SLUB
3.1 Slab 分配器演进
Linux 内核经历三代对象级缓存分配器:
- Slab(Linux 2.1):最初实现,包含 full/free/partial 三链表
- SLOB:极简设计,适用于嵌入式(ROM 设备)
- SLUB(默认,Linux 2.6.23+):简化设计,消除 Per-CPU 队列锁竞争
3.2 SLUB 核心数据结构
// mm/slub.c
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; // Per-CPU 快速路径
struct kmem_cache_node *node[MAX_NUMNODES]; // Per-Node 慢速路径
unsigned int size; // 对象实际大小(含对齐)
unsigned int object_size; // 用户请求的原始大小
unsigned long flags; // SLAB flags(如 SLAB_ACCOUNT)
unsigned int align; // 对齐要求
const char *name; // 缓存名(见于 /proc/slabinfo)
// ...
};
struct kmem_cache_cpu {
void **freelist; // 空闲对象链表头指针
unsigned long tid; // 全局事务ID(无锁同步)
struct page *page; // 当前 CPU page slab
struct page *partial; // CPU partial 链表
};
3.3 分配与释放流程
快速路径(无锁 Per-CPU):
// 简化版分配逻辑
static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfp) {
redo:
// 1. 检查 freelist
void *object = s->cpu_slab->freelist;
if (unlikely(!object))
goto new_slab; // 慢速路径
// 2. CAS 原子替换 freelist(无锁)
void *next = get_freepointer(s, object);
if (likely(this_cmpxchg_double_pointer(...))) {
return object;
}
goto redo;
}
Per-CPU 设计消除了多核间的锁竞争,简单分配仅需 5-10 条 CPU 指令。
3.4 Slab 调试接口
// 查看所有 slab 缓存
$ cat /proc/slabinfo
kmalloc-1k 24512 24768 1024 8 8 1 512
kmalloc-512 18432 18432 512 16 16 1 256
dentry 32768 31232 192 21 21 1 128
inode_cache 16384 15984 632 6 6 1 1024
// 列:名称 活跃对象数 总对象数 对象大小 每slab页数 slab数 缓存数
// 跟踪 slab 分配/释放
echo 1 > /sys/kernel/debug/slab/kmalloc-512/alloc_traces
echo 1 > /sys/kernel/debug/slab/kmalloc-512/free_traces
四、虚拟内存与页表管理
4.1 虚拟地址空间布局
x86_64 Linux 采用 48 位虚拟地址(4 级页表),布局如下:
0x0000 0000 0000 0000 ─┬─ 用户空间(128TB)
│ ├── [0] 保留(NULL指针保护)
│ ├── [.text] 代码段
│ ├── [.data/.bss] 数据段
│ ├── [heap] brk 堆区(向高地址增长)
│ ├── [mmap] 共享库 / 匿名映射区
│ └── [stack] 栈区(向低地址增长,8MB默认)
0x0000 7FFF FFFF FFFF ─┤
├─ 未映射区(64TB 空洞)
0xFFFF 8000 0000 0000 ─┤
├─ 内核空间(128TB)
│ ├── [PAGE_OFFSET] 直接物理映射区
│ ├── [VMALLOC] vmalloc 非连续区
│ ├── [MODULES] 内核模块区
│ └── [FIXMAP] 固定映射区
0xFFFF FFFF FFFF FFFF ─┘
4.2 四级/五级页表
Linux 6.x x86_64 支持 4 级页表(PML4→PDPT→PD→PT),Linux 6.5+ 引入 5 级(PML5,57 位地址,128PB 虚拟空间):
// 4级页表虚拟地址分解(48位)
┌──────┬──────┬──────┬──────┬──────────┐
│ PML4 │ PDPT │ PD │ PT │ OFFSET │
│ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │
└──────┴──────┴──────┴──────┴──────────┘
39-47 30-38 21-29 12-20 0-11
// 5级页表虚拟地址分解(57位)
┌─────┬──────┬──────┬──────┬──────┬──────────┐
│PML5 │ PML4 │ PDPT │ PD │ PT │ OFFSET │
│9bit │ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │
└─────┴──────┴──────┴──────┴──────┴──────────┘
48-56 39-47 30-38 21-29 12-20 0-11
4 级页表需 4 次内存访问完成地址转换,配合 TLB 缓存常用映射;5 级页表默认需要 5 次访问。内核通过 __pgtable_l5_enabled() 运行时检测。
4.3 页表加速:TLB 与 PCID
- TLB:缓存最近使用的虚拟→现代映射,分指令 TLB 和数据 TLB,L1 通常 64 项,L2 可达 1536 项
- PCID(Process Context IDentifier):避免进程切换时 TLB 全刷,Linux 4.14+ 默认开启 INVPCID 支持
- HugePage(大页):2MB/1GB 大页减少 TLB miss,数据库场景提升 15-25%
// 配置透明大页
echo always > /sys/kernel/mm/transparent_hugepage/enabled
// 查看 TLB miss 统计
perf stat -e dTLB-load-misses,dTLB-store-misses ./your_database
五、vmalloc 与地址映射
5.1 vmalloc 非连续内存映射
当需要分配较大缓冲区但物理连续性不必要时,使用 vmalloc():
// mm/vmalloc.c
void *vmalloc(unsigned long size) {
return __vmalloc_node_flags(size, NUMA_NO_NODE, GFP_KERNEL);
}
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp, int node) {
// 1. 计算需要多少页
unsigned long nr_pages = get_vm_area_size(area) >> PAGE_SHIFT;
// 2. 分配虚拟内存区域(vm_area)
area = __get_vm_area(size, VM_ALLOC, VMALLOC_START, VMALLOC_END);
// 3. 逐页分配物理页面,建立页表映射
for (i = 0; i < nr xss=removed>
5.2 vmap / vunmap
vmap() 将一组已存在的物理页映射到连续虚拟地址空间,适用于需要操作不连续物理页面的场景:
// 将分散的物理页映射到连续虚拟地址
struct page *pages[10];
for (i = 0; i < 10 xss=removed xss=removed>
六、内存回收与 kswapd
6.1 页面回收算法
Linux 采用 LRU(Least Recently Used)双链表算法回收页面,分为:
- active list:活跃页面链表(最近被访问过)
- inactive list:非活跃页面链表(Candidate for eviction)
页面首次加入 inactive,第二次访问时晋升到 active。当 active 链表尾部页面长时间未再访问,被降级到 inactive 尾部,最终被回收。
6.2 页面类型与回收代价
| 页面类型 | 来源 | 回收代价 |
|---|---|---|
| Anonymous Page | malloc/mmap匿名映射 | 高(需写入swap) |
| File-backed Page | mmap文件、Page Cache | 低(丢弃或写回) |
| Slab Page | 内核对象缓存 | 可回收(dentry、inode_cache) |
| HugePage | 大页分配 | 不可部分回收 |
6.3 kswapd 守护进程
每个 NUMA 节点都有一个 kswapd 内核线程,周期性检查 vm_zone->_watermark:
// mm/vmscan.c
static int kswapd(void *p) {
pg_data_t *pgdat = (pg_data_t*)p;
while (!kthread_should_stop()) {
// 1. 平衡区域水位(异步轻量扫描)
balance_pgdat(pgdat, order, highest_zoneidx);
// 2. 检查是否需要回收
if (pgdat_balanced(pgdat, order, highest_zoneidx)) {
wait_event_interruptible(pgdat->kswapd_wait,
kswapd_should_run(pgdat));
continue;
}
// 3. 执行直接回收或唤醒等待者
pgdat_balanced(pgdat, order, highest_zoneidx);
}
}
6.4 直接回收(Direct Reclaim)
当水位低于 min 且异步回收来不及,分配进程本身被卷入回收流程——这会导致严重的延迟。生产系统中常见 "kswapd0" 或分配进程进入 D 状态(Uninterruptible sleep):
// 诊断直接回收延迟
$ cat /proc/vmstat | grep allocstall
allocstall_dma 0
allocstall_dma32 12
allocstall_normal 89 ← 直接回收发生次数
allocstall_movable 156
七、OOM Killer
7.1 OOM 触发条件
当系统内存耗尽且所有回收手段均无法获取可用页面时,触发 OOM(Out of Memory):
- 所有 zone 低于 min 水位
- kswapd 无法回收足够页面
- 直接回收仍无法满足分配请求
- 调用
out_of_memory()选择牺牲进程
7.2 oom_badness 评分算法
内核根据 oom_badness() 函数计算每个进程的"坏分值",分数越高越容易被 kill:
// mm/oom_kill.c
unsigned long oom_badness(struct task_struct *p, unsigned long totalpages) {
// 1. 计算内存占用(RSS + swap + page_table + socket buffer)
points = get_mm_rss(mm) + get_mm_counter(mm, MM_SWAPENTS)
+ mm_pages(mm); // page tables pages
// 2. 调整系数:CPU 时间开根号(长时间运行进程权重略降)
adj = (long)p->signal->oom_score_adj * totalpages / 1000;
points += adj;
// 3. OOM_DISABLE 进程不参与评选
if (p->signal->oom_score_adj == OOM_SCORE_ADJ_MIN)
return 0;
return points;
}
7.3 保护关键进程
// 保护进程不被 OOM kill(-1000 表示永久豁免)
echo -1000 > /proc/[pid]/oom_score_adj
// 降低某进程的优先级(更容易被牺牲)
echo 500 > /proc/[pid]/oom_score_adj
// systemd 方式:
[Service]
OOMPolicy=continue
OOMScoreAdjust=-500
八、内存 cgroup(memcg)
8.1 核心概念
内存 cgroup 实现容器级别的内存隔离与限制,关键参数:
- memory.limit_in_bytes:硬限制(超过立即触发 OOM)
- memory.soft_limit_in_bytes:软限制(全局内存充裕时允许超额)
- memory.swappiness:单独控制 swap 倾向(0=禁用 swap)
- memory.kmem.limit_in_bytes:内核内存限制(控制 slab、network buffer)
8.2 容器内存调优实战
// Docker 限制 1GB 内存
docker run --memory=1g --memory-swap=2g myapp
// Kubernetes 配置
resources:
requests:
memory: "512Mi"
limits:
memory: "1Gi"
// 诊断容器 OOM
dmesg | grep -i oom
# [189345.234] Memory cgroup out of memory: Killed process 12345 (java) total-vm:2097152kB, anon-rss:1048576kB
九、性能调优与生产实践
9.1 swappiness 调优
内核参数 vm.swappiness(默认 60)控制 swap 倾向:
- 数据库推荐 1-10:优先回收 file cache,避免匿名页面被换出
- 桌面系统 60:默认值,平衡 swap 与 cache
- HPC/实时系统 0:禁用 swap(除非使用 zram)
# 数据库服务器优化
vm.swappiness = 1
vm.dirty_ratio = 15
vm.dirty_background_ratio = 5
vm.dirty_expire_centisecs = 500
vm.dirty_writeback_centisecs = 100
9.2 碎片整理(Memory Compaction)
针对大型块的连续物理内存碎片问题,Linux 提供两种机制:
- 主动整理(/proc/sys/vm/compact_memory):重启时合并可迁移页
- Proactive Compaction(Linux 5.9+):kcompactd 线程周期性触发
- THP Compact Defrag:尝试为透明大页分配聚合碎片
// 查看碎片指数
$ cat /sys/kernel/debug/extfrag/extfrag_index
// 手动触发全局整理
echo 1 > /proc/sys/vm/compact_memory
// /proc/buddyinfo 观察连续内存块
cat /proc/buddyinfo
Node 0, zone Normal 116 201 34 23 8 2 1 1 0 0 0
9.3 Zswap / Zram 压缩交换
对于内存受限场景,压缩 swap 比直接 swap 到磁盘更高效:
// Zswap:在内存中压缩缓存,必要时才写磁盘
echo 1 > /sys/module/zswap/enabled
echo zstd > /sys/module/zswap/compressor
// Zram:内存中的压缩块设备(推荐嵌入式)
modprobe zram num_devices=1
echo zstd > /sys/block/zram0/comp_algorithm
echo 2G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0
9.4 内存热插拔
Linux 支持物理内存的在线添加/移除:
// 添加内存
echo online > /sys/devices/system/memory/memory42/state
// 离线内存
echo offline > /sys/devices/system/memory/memory42/state
// 迁移页面关键逻辑
// 1. 隔离页面(block page allocation on this block)
// 2. 迁移页面到目标节点
// 3. 移除该 block 的 struct page array
十、调试与诊断工具
10.1 vm_stat 与 /proc/meminfo
$ cat /proc/meminfo
MemTotal: 65765244 kB
MemFree: 12453232 kB
MemAvailable: 48291508 kB
Buffers: 1234567 kB
Cached: 34567890 kB
SwapTotal: 8388608 kB
SwapFree: 8123456 kB
HugePages_Total: 0
Hugepagesize: 2048 kB
DirectMap4k: 12345678 kB
DirectMap2M: 52428800 kB
Slab: 34567890 kB
SReclaimable: 2345678 kB
SUnreclaim: 1111111 kB
10.2 perf 内存分析
// 跟踪 kmalloc/kfree 调用栈
perf record -e kmem:kmalloc -e kmem:kfree -g -- your_app
perf report --sort=comm,dso,symbol
// 分析 TLB miss
perf stat -e dTLB-load-misses,iTLB-load-misses,page-faults ./app
// 使用 BPF 分析页面分配延迟
bpftrace -e 'kprobe:__alloc_pages_nodemask { @start[tid] = nsecs; }
kretprobe:__alloc_pages_nodemask /@start[tid]/ {
@lat_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
10.3 Page Owner 追踪
// 编译时开启 CONFIG_PAGE_OWNER
echo 1 > /sys/kernel/debug/page_owner/on
// 查看每个页面的分配调用栈
cat /sys/kernel/debug/page_owner/sorted_pages
// 使用工具分析
./page_owner_sort page_owner.txt
十一、内核内存操作 API 总结
| API | 用途 | GFP标志 | 最大限制 |
|---|---|---|---|
| kmalloc() | 小物件,物理连续 | GFP_KERNEL/GFP_ATOMIC | 4MB(架构相关) |
| kzalloc() | kmalloc + 清零 | 同上 | 同上 |
| kmem_cache_alloc() | 自定义slab缓存 | 可指定节点 | 2MB per object |
| vmalloc() | 大缓冲区,虚拟连续 | GFP_KERNEL | 几乎无限制 |
| __get_free_page() | 单页分配 | 各种 | 一页 |
| alloc_pages() | 多页,指定order | 可指定迁移类型 | 1024页(4MB) |
| get_zeroed_page() | 分配已清零页 | 同上 | 一页 |
十二、总结
Linux 内核内存管理系统是一个经过 30+ 年演进的精密系统。理解它的关键在于:
- 物理层:伙伴系统管理物理页,MIGRATE_TYPES 减少碎片
- 缓存层:SLUB 为频繁分配的小对象提供 O(1) 性能
- 虚拟层:四级/五级页表映射虚拟→现代,TLB/PCID 加速访问
- 回收层:LRU 双链表 + kswapd 守护进程,水位线驱动回收
- 容器层:memcg 实现隔离与限制,保障多租户环境稳定性
生产系统中,内存调优的核心是观察 /proc/vmstat、perf TLB miss 指标、以及 dmesg 中的 OOM 日志,逐步调整 swappiness、dirty ratio、hugepage 配置。配合 eBPF 的 memleak 工具,可以长期追踪内存泄漏问题,构建健康的系统内存管理实践。
本文基于 Linux 6.6.x 内核源码分析,主要参考 mm/page_alloc.c、mm/slub.c、mm/vmscan.c、mm/vmalloc.c 等核心文件。

发表评论 取消回复