Linux内核内存管理深度实战:从伙伴系统到大页NUMA优化
深入理解Linux内存管理机制,掌握从物理页框分配到大页NUMA调优的完整链路
一、物理内存架构:NUMA、Zone与Node
现代多核服务器多采用NUMA(Non-Uniform Memory Access)架构,处理器访问本地内存节点(Local Node)的延迟远低于跨节点访问(Remote Node)。Linux内核将物理内存划分为NUMA Node,每个Node下再按用途划分为不同的Zone:
| Zone | 典型范围(x86_64) | 用途 |
|---|---|---|
| ZONE_DMA | 0–16MB | ISA DMA兼容设备 |
| ZONE_DMA32 | 16MB–4GB | 32位DMA设备 |
| ZONE_NORMAL | 16MB–896MB | 内核直接映射(线性映射) |
| ZONE_HIGHMEM | 896MB以上(仅32位) | 高端内存,动态映射 |
| ZONE_MOVABLE | 可配置 | 可移动页面,支持热插拔和大页 |
在x86_64架构中,ZONE_HIGHMEM已基本退出历史舞台,因为64位地址空间足够庞大(48位或57位虚拟地址),内核通过mem_map[]全局数组直接管理所有物理页框(struct page)。
每个Zone维护一个free_area数组,用于伙伴系统的阶(order)管理:
struct zone {
struct free_area free_area[MAX_ORDER]; // 伙伴系统
unsigned long managed_pages; // 由伙伴系统管理的页数
unsigned long spanned_pages; // 跨度页数
unsigned long present_pages; // 实际存在页数
int node; // 所属NUMA节点
// ...
};
struct free_area {
struct list_head free_list[MIGRATE_TYPES];
unsigned long nr_free;
};
MIGRATE_TYPES 用于抗碎片管理,将页框按可移动性分组:
MIGRATE_UNMOVABLE:内核数据结构(kmalloc、slab等)MIGRATE_MOVABLE:用户空间页面、文件缓存MIGRATE_RECLAIMABLE:可回收的内核缓存(dcache、inode cache)MIGRATE_PCP:per-cpu pageset,高速本地缓存
二、伙伴系统(Buddy System):分配与回收的艺术
2.1 核心原理
伙伴系统按2的幂次组织空闲块(order 0 = 1页,order 10 = 1024页 = 4MB),分配时向上取整,回收时与"伙伴"(Buddy)合并成更大的块。
分配流程:
- 从请求的order开始,在对应free_list的空闲链表中查找
- 若找到,摘除并返回页面
- 若当前order无空闲块,向更高阶拆解——将大块一分为二,一半分配,另一半放入低阶free_list
回收流程:
- 判断要回收页面的"Buddy"是否也在同一阶空闲链表中
- 若伙伴空闲:合并成高一阶块,继续向上检查合并
- 若伙伴已分配:直接放入当前阶free_list
2.2 关键数据结构
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 list_head buddy_list; // 伙伴系统链表
};
unsigned long private; // 各子系统私有数据
void *virtual; // 虚拟地址(高端内存时有效)
};
2.3 per-cpu pageset(PCP)
为避免多CPU竞争Zone锁,每个CPU维护一组本地页缓存(PCP List),仅当本地缓存耗尽时才向伙伴系统批量申请。PCP同时维护冷页和热页两个列表:
- 热页(Hot Page):刚从伙伴系统获取,Cache line可能还在CPU cache中,优先分配给小对象
- 冷页(Cold Page):经过一段时间,Cache冷掉了,保留用于DMA或大块映射
查看PCP参数:
# cat /proc/zoneinfo | head -50
Node 0, zone DMA32
pages free 123456
min 976
low 1220
high 1464
managed 384000
# PCP参数(通过sysctl vm.extra_free_kbytes调整)
2.4 Watermark水位线
每个Zone维护三个水位线——min/low/high,通过kswapd守护进程回收:
- min:如果空闲页低于此值,分配器走slow path,触发直接回收
- low:kswapd被唤醒,持续回收直到达到high水位
- high:kswapd停止回收,系统恢复正常分配
监控命令:
# 各Zone水位信息
cat /proc/zoneinfo
# 内存碎片情况
cat /proc/buddyinfo
# pcp缓存数量(cat /proc/vmstat | grep pcp)
三、Slub分配器:内核小对象分配引擎
3.1 三层架构
Slub是Linux默认的小对象分配器(替代Slab和Slob),采用三层结构:
- kmem_cache:一类对象的缓存池,包含对象大小、对齐、构造函数等元数据
- kmem_cache_cpu:per-cpu高速缓存(fastpath),无锁分配
- kmem_cache_node:per-node后备池,当CPU缓存不足时从Node补充
3.2 分配流程(Fast Path)
static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags)
{
// 1. 优先从cpu_slab->freelist拿空闲对象(无锁)
void *object = cpu_slab->freelist;
if (object) {
cpu_slab->freelist = get_freepointer(s, object);
return object;
}
// 2. 慢路径:从node批次申请或创建新slab
return slab_alloc_node(s, gfpflags, node);
}
每个slab由一个或多个连续页框组成,对象被紧密排列。空闲对象内部存储链表指针,实现O(1)分配。
3.3 SLUB Debug与性能调优
# 查看当前缓存使用情况
cat /proc/slabinfo | sort -k3 -nr | head -20
# 典型输出:
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
# dentry 1234567 1892345 192 21 2
# inode_cache 987654 1456789 584 7 2
# filp 543210 876543 256 16 2
# 内存浪费分析(大量slab对象 = dentry/inode cache占内存过高)
slabtop --once -sc | head -20
常见调优参数:
# 调整slab回收积极性(默认100,越小越积极回收)
sysctl -w vm.vfs_cache_pressure=50
# 主动清空dentry和inode cache(谨慎使用)
echo 2 > /proc/sys/vm/drop_caches
# 限制slab最大order(避免高阶分配失败)
sysctl -w vm.slab_max_order=0
3.4 SLAB_ACCOUNT与cgroup内存控制
较新内核中,SLAB_ACCOUNT标志使slab记账与cgroup关联,实现按cgroup的内存隔离。若某cgroup达到其memory.limit_in_bytes,会触发其自身的shrink回收,而非全局kswapd。
四、虚拟内存:VMA与页表管理
4.1 进程地址空间
每个进程通过struct mm_struct管理虚拟地址空间:
struct mm_struct {
struct rb_root mm_rb; // VMA红黑树根节点
struct vm_area_struct *mmap; // VMA链表
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 total_vm; // 总虚拟页面数
unsigned long locked_vm; // mlock的页面
// NUMA相关
struct mmu_gather *mmlist; // 批量TLB刷新
};
4.2 VMA(Virtual Memory Area)
VMA代表虚拟地址空间中具有相同权限的连续区域,每个vm_area_struct描述一段VMA:
struct vm_area_struct {
unsigned long vm_start; // 虚拟起始地址
unsigned long vm_end; // 虚拟结束地址(不包含)
struct mm_struct *vm_mm; // 所属mm_struct
pgprot_t vm_page_prot; // 访问权限
unsigned long vm_flags; // 标志:VM_READ/WRITE/EXEC/SHARED/MERGEABLE
struct vm_operations_struct *vm_ops; // 缺页处理函数集
};
VMA通过红黑树和链表双重组织,红黑树用于快速地址定位,链表适合顺序遍历。内核还维护VMA Cache(mmap_cache缓存最近访问的VMA),命中时路径可跳过红黑树查找。
4.3 缺页异常处理流程
当CPU访问尚未建立映射的虚拟地址时,触发Page Fault(do_page_fault()→handle_mm_fault()):
- Find VMA:通过红黑树查找包含目标地址的VMA,若未找到 → SEGV(段错误)
- 权限校验:检查
vm_flags是否允许当前访问(读/写/执行)→ 触发COW或SIGSEGV - 匿名页(MAP_ANONYMOUS):分配物理页建立映射
- 文件映射(MAP_FILE):从Page Cache读取,结合
fault()方法 - Demand Paging:页面调入并建立PTE映射
- COW(写时复制):对共享页面的写操作触发COW,分配新页框并复制内容
4.4 多级页表与TLB
x86_64采用4级页表(PGD→PUD→PMD→PTE),地址翻译:
39位虚拟地址 = 9bit(PGD) + 9bit(PUD) + 9bit(PMD) + 9bit(PTE) + 12bit(offset)
TLB(Translation Lookaside Buffer)缓存最近翻译结果:
- 4K页TLB通常64-128 entries
- 2M大页TLB通常32-64 entries
- 2020年后硬件支持5级页表,支持57位虚拟地址(128PB)
查看TLB性能:
perf stat -e dtlb_load_misses.stlb_hit,dTLB-load-misses ./your_app
五、页面回收与LRU算法
5.1 双链LRU
Linux内核维护五种LRU链表(每种可移动性类型各一组):
- Active Anonymous:活跃匿名页(用户堆栈、匿名映射)
- Inactive Anonymous:不活跃匿名页
- Active File:活跃文件页(Page Cache中被频繁访问的)
- Inactive File:不活跃文件页
- Unevictable:不可回收(mlock、Ramfs等)
核心思想:页面先进入Inactive链表,再次被访问时提升到Active链表。回收时优先扫描Inactive链表的头部,实现近似LRU(Second Chance算法)。
5.2 kswapd与直接回收
kswapd是内存回收守护进程,每个NUMA Node有一个kswapd/线程:
- 监测Zone水位线,低于low时唤醒
- 通过扫描LRU链表,将Inactive页面回收
- 回收到high水位线后sleep
直接回收(Direct Reclaim):当进程本身分配内存时触发同步回收,该进程被阻塞等待kswapd工作,极其影响性能。频繁的直接回收是内存压力过大的信号。
5.3 反向映射(Reverse Map)
回收页面时需要断开所有PTE映射:
- 匿名页:通过
anon_vma链表遍历所有映射该页的VMA - 文件页:通过
address_space的radix tree查找所有映射PTE
反向映射使回收代价随映射数量线性增长,影响大型共享进程(如数据库fork出的子进程)的回收效率。
5.4 页面回收调优参数
# swappiness:0=优先回收文件缓存,100=匿名页和文件缓存均衡回收,200=积极回收匿名页
# 数据库场景推荐0-10,文件服务器推荐100+
sysctl -w vm.swappiness=10
# min_free_kbytes:保留给核心内核的最小空闲内存(用于紧急分配)
# 计算公式:sqrt(lowmem_kbytes * 16),或手动指定
sysctl -w vm.min_free_kbytes=262144
# dirty_ratio/dirty_background_ratio:脏页占全局内存的比例触发刷盘
sysctl -w vm.dirty_ratio=40
sysctl -w vm.dirty_background_ratio=10
六、大页(Huge Pages / THP)与NUMA优化
6.1 透明大页(THP)
THP使应用无需修改代码即可使用大页(x86_64下2MB),通过khugepaged守护进程合并普通页面:
# 查看THP状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 可选值:always、madvise、never
# 大页统计
grep -i huge /proc/meminfo
# HugePages_Total: 512
# HugePages_Free: 234
# Hugepagesize: 2048 kB
madvise模式(推荐):应用通过madvise(addr, len, MADV_HUGEPAGE)显式提示合并。
6.2 显式大页(HugeTLB)
显式大页需预留,开机时配置:
# 内核启动参数
hugepagesz=2M hugepages=256 hugepagesz=1G hugepages=4
# 或运行时(仅连续页面足够时成功)
echo 256 > /proc/sys/vm/nr_hugepages
# 查看状态
cat /proc/meminfo | grep -i huge
通过hugetlbfs挂载使用:
mount -t hugetlbfs hugetlbfs /mnt/huge
6.3 NUMA内存策略
Linux提供多种NUMA内存分配策略:
| 策略 | 说明 |
|---|---|
| MPOL_DEFAULT | 默认策略,优先从本地Node分配 |
| MPOL_BIND | 必须从指定Node集合分配,失败则触发OOM |
| MPOL_PREFERRED | 优先从指定Node分配,失败可fallback到其他Node |
| MPOL_INTERLEAVED | Node间交错分配,减少热偏移 |
| MPOL_LOCAL | 本地优先(同DEFAULT,fallback更弱) |
查看进程NUMA策略:
# numastat displays memory allocation per node for each process
numastat -p <pid>
# 查看上次OOM发生在哪个Node
cat /proc/<pid>/numa_maps | grep -i oom
6.4 numactl工具
# 强制进程只在Node0上运行并分配内存
numactl --cpunodebind=0 --membind=0 /path/to/app
# 交错策略绑定多个Node
numactl --interleave=all /path/to/app
# 查看NUMA拓扑
numactl --hardware
七、OOM Killer与内存cgroup
7.1 OOM Killer评分机制
当系统内存严重不足且所有回收手段用尽时,OOM Killer被触发:
// 粗略公式(实际更复杂,含adj调整)
unsigned long oom_badness(struct task_struct *p)
{
points = total_vm; // 进程使用的总页数
// 特权进程(init、内核线程、CAP_SYS_ADMIN)大幅降分
if (has_capability_noaudit(p, CAP_SYS_ADMIN))
points -= 32;
// oom_score_adj调整(-1000到1000)
points = points * (oom_score_adj + 1000) / 1000;
return points;
}
保护关键进程:
# 防止关键进程被OOM杀掉(设为-1000,永不OOM)
echo -1000 > /proc/<pid>/oom_score_adj
# 降低某进程被OOM优先杀掉的概率
echo -500 > /proc/<pid>/oom_score_adj
7.2 内存cgroup v2
cgroup v2提供层次化精细内存控制:
# 查看cgroup树
cat /sys/fs/cgroup/cgroup.controllers
# memory.max:硬限制
echo "1G" > /sys/fs/cgroup/mycgroup/memory.max
# memory.high:软限制(超过触发回收)
echo "900M" > /sysfs/cgroup/mycgroup/memory.high
# memory.low:保障低水位保护
echo "500M" > /sys/fs/cgroup/mycgroup/memory.low
# memory.weight:相对权重(1-10000),竞争时按比例分配
echo 500 > /sys/fs/cgroup/mycgroup/memory.weight
# 当前使用量
cat /sys/fs/cgroup/mycgroup/memory.current
7.3 容器中的内存限制陷阱
- 容器限制4G,JVM
-Xmx设3.5G,但可能因堆外内存(线程栈、直接IO、native库)触发容器OOM /proc/meminfo在容器中会显示宿主机总内存(除非使用lxcfs或cgroup v2视图)- 应用监控看到的"总内存"远高于cgroup限制 → 应读取
memory.current为准
八、实战调优与排障案例
8.1 场景一:数据库mysqld内存暴涨
现象:mysqld进程RSS持续增长,宿主机频繁OOM。
排查链路:
# 1. 确认OOM是谁触发的
dmesg -T | grep -i 'oom\|kill'
# 2. 进程实际内存分布
cat /proc/$(pidof mysqld)/smaps | grep -E '^[0-9a-f]|Rss|Pss' | head -50
# 3. 关注几个大头
# - [heap]: C++分配(插件、存储引擎)
# - [anon]: 客户端线程栈(thread_stack默认256K*Max_connections)
# - /usr/sbin/mysqld: 可执行文件+共享库
# 4. 查看slab占用(可能为mysql的kmalloc导致)
slabtop -sc | head -20
# 5. NUMA均衡分析
numastat -p $(pidof mysqld)
修复方案:
# my.cnf
[mysqld]
innodb_buffer_pool_size = 6G # 设为物理内存70%
innodb_buffer_pool_instances = 8 # 减少锁争用
max_connections = 200 # 限制连接数
thread_cache_size = 50 # 复用线程
performance_schema = OFF # 节省内存(开发环境可开)
8.2 场景二:Java应用频繁Full GC,系统load高
排查链路:
# 1. GC日志分析
java -Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags -jar app.jar
tail -f /var/log/gc.log | grep 'Full GC'
# 2. 堆外内存检查(NMT)
java -XX:NativeMemoryTracking=summary -jar app.jar
jcmd <pid> VM.native_memory summary
# 3. 查看mmap文件数(JVM可能大量mmap)
ls /proc/<pid>/map_files/ | wc -l
# 4. 检查是否触发直接回收
cat /proc/vmstat | grep -E 'compact_|oom|kswapd|compact_stall'
8.3 场景三:嵌入式系统内存仅512MB
配置建议:
# 关闭THP,避免内存浪费和合并延迟
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 减少内核缓冲区
sysctl -w vm.min_free_kbytes=16384
sysctl -w vm.dirty_ratio=20
sysctl -w vm.dirty_background_ratio=5
# 禁用不必要的服务(减少slab)
systemctl disable systemd-journald # 或使用volatile journal
# 限制最大PID数,减少线程开销
kernel.sysctl="kernel.pid_max=2048"
九、内存管理排障三板斧
9.1 宏观监控:vmstat和sar
# 每秒输出,关注si/so(swap in/out,非0表示内存紧张)
vmstat 1 10
# procs -----------memory---------- ---swap-- -----io----
# r b swpd free buff cache si so bi bo in cs
# 2 0 0 123456 65432 789012 0 0 45 12 123 456
# ^^^^
# si=swapin, so=swapout
# 历史数据
sar -r 1 5 # 内存使用
sar -B 1 5 # 页面统计
sar -W 1 5 # Swap统计
9.2 进程级分析:smaps和pmap
# 进程内存详细分布
cat /proc/<pid>/smaps | awk '/^[0-9a-f]/ {name=$6} /^Rss/ {rss+=$2} /^Pss/ {pss+=$2} END {print "Total RSS:", rss, "kB; Total PSS:", pss, "kB"}'
pmap -x <pid> | sort -k2 -nr | head -20
# 关键指标
# Rss: Resident Set Size,实际驻留内存
# Pss: Proportional Set Size,按比例分摊共享库
9.3 内核级追踪:perf和bpftrace
# 追踪页面分配延迟
perf stat -e 'kmem:kmalloc','kmem:kfree','kmem:mm_page_alloc' -p <pid> sleep 10
# 追踪缺页异常
perf record -e faults -p <pid> sleep 10 && perf report
# bpftrace监控慢分配
bpftrace -e 'kprobe:__alloc_pages_nodemask { @start[tid] = nsecs; }
kretprobe:__alloc_pages_nodemask /@start[tid]/ {
$lat = (nsecs - @start[tid]) / 1000;
if ($lat > 1000) { printf("slow alloc %d us order=%d\n", $lat, args->order); }
delete(@start[tid]);
}'
# 监控直接回收(性能杀手)
bpftrace -e 'kprobe:direct_reclaim_begin { @reclaim_start = nsecs; }
kretprobe:direct_reclaim_end {
$lat = (nsecs - @reclaim_start) / 1000;
printf("direct reclaim %d us\n", $lat);
}'
十、未来演进与展望
Linux内存管理仍在快速迭代:内存 Folio(2021+)是大页管理的新抽象,统一了compound page和普通page的接口;Damon(Data Access MONitor)提供硬件级内存访问热力图,实现精准的主动回收和NUMA迁移;MTE(Memory Tagging Extension)在ARMv8.5+上硬件辅助检测Use-After-Free和Buffer Overflow,提升内存安全。
在RISC-V架构上,Sv39/Sv48/Sv57支持多级页表与1GB大页,Linux社区积极适配以保证同等调优能力。
参考资料:
- Linux内核源码
mm/目录(mm/page_alloc.c, mm/slub.c, mm/vmscan.c) - Mel Gorman, *Understanding the Linux Virtual Memory Manager*
- 《Professional Linux Kernel Architecture》, Mauerer
- Brendan Gregg, *Systems Performance: Enterprise and the Cloud*

发表评论 取消回复