Linux 内存子系统深度实战:从物理页框到虚拟地址全链路解析
内存管理是 Linux 内核最复杂、最核心的子系统之一。它不仅负责物理内存的分配与回收,还构建了进程虚拟地址空间的完整抽象层。本文将从物理内存组织出发,深入剖析伙伴系统、Slab 分配器、虚拟内存区域、页表机制、内存回收与 OOM 杀手,最终落地到生产环境调优实践。
一、物理内存组织:NUMA 节点与内存域
现代多核服务器普遍采用 NUMA(Non-Uniform Memory Access)架构,CPU 访问本地内存节点的延迟远低于跨节点访问。Linux 内核通过以下数据结构组织物理内存:
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; // 总页数(含空洞)
};
每个 NUMA 节点包含多个内存域(Zone),分配请求按优先级遍历节点的 zonelist 回退到其他节点。Zone 的类型包括:
- ZONE_DMA:前 16MB,旧 ISA 设备需要
- ZONE_DMA32:4GB 以内,32 位 DMA 设备
- ZONE_NORMAL:直接映射区,内核线性映射的空间
- ZONE_MOVABLE:可迁移页,用于内存热插拔
- ZONE_HIGHMEM(32 位):高端内存,超过 896MB 的部分
在 x86_64 架构上,由于有巨大的虚拟地址空间,ZONE_HIGHMEM 不存在,物理内存大多在 ZONE_NORMAL 中通过直接映射(PAGE_OFFSET + pfn)访问。
二、伙伴系统:物理页框分配引擎
Buddy System 负责管理连续物理页框的分配与释放,解决外部碎片问题。它将所有空闲页框分组为 11 个链表,分别管理 2^0 到 2^10(即 1 页到 1024 页,4KB 到 4MB)大小的连续块。
2.1 分配与释放机制
分配时从合适阶(order)的空闲链表取块;若该阶无空闲块,则从更高阶链表中"分裂"出一半满足请求,另一半插入低阶链表。释放时检查 buddy 块是否空闲,若空闲则向上合并。
// 分配核心逻辑(简化)
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
{
// 1. 快速路径:从 per-cpu 热缓存直接分配
// 2. 慢速路径:遍历 zone 的空闲链表
// 3. 触发内存回收或直接回收
// 4. 若仍失败且允许,调用 OOM Killer
}
// 释放时合并逻辑(简化)
void __free_pages(struct page *page, unsigned int order)
{
// 计算 buddy 页的 pfn: buddy_pfn = pfn ^ (1 << order)
// 若 buddy 空闲则从当前链表移除,合并后向上一级合并
}
GFP(Get Free Pages)标志控制分配行为:
- GFP_KERNEL:标准内核分配,可能睡眠,可用于进程上下文
- GFP_ATOMIC:原子分配,不睡眠,中断处理程序和持锁时使用
- GFP_NOIO:不发起 I/O,块层分配路径使用
- GFP_NOFS:不发起文件系统操作
- __GFP_HIGHMEM:允许从高端内存分配
- __GFP_ZERO:分配后清零
- __GFP_NOWARN:失败时不打印警告
- __GFP_RETRY_MAYFAIL:允许重试但不触发 OOM
2.2 反碎片与迁移类型
内核 2.6.24 引入了反碎片机制,按页面可迁移性将页框分组:
- MIGRATE_UNMOVABLE:内核对象、页缓存等不可移动
- MIGRATE_MOVABLE:用户页面、可重定位
- MIGRATE_RECLAIMABLE:不可移动但可回收(如 Slab 缓存)
- MIGRATE_ISOLATE:禁止跨组分配,用于热插拔
这种设计使得未来可能实现真正的在线碎片整理——将可移动页面搬走形成连续空间。
三、Slab 分配器:内核对象高效复用
伙伴系统以页(4KB)为粒度分配,而内核中大量对象(inode、dentry、task_struct 等)远小于一页。频繁的页级分配会产生严重内部碎片。Slab 分配器在伙伴系统之上构建对象缓存层,实现细粒度内存复用。
3.1 工作原理
Slab 为每种高频对象类型维护一个 Cache,每个 Cache 包含多个 Slab(通常 1-N 页),Slab 内部通过 freelist 链表管理空闲对象。
struct kmem_cache {
struct kmem_cache_cpu *cpu_slab; // per-cpu 快速分配缓存
struct kmem_cache_node *node[MAX_NUMNODES]; // 节点级半满/空 slab 链表
unsigned int size; // 对象大小(含对齐)
unsigned int object_size; // 原始请求大小
unsigned int offset; // freelist 指针偏移
slab_flags_t flags; // 对齐/清零等标志
const char *name; // 缓存名称
};
分配路径为:per-cpu cpu_slab → kmem_cache_node 半满链表 → 伙伴系统新分配。快速路径直接操作 per-cpu 可用对象链表,无需加锁。
3.2 SLUB vs SLOB
Linux 提供三种 Slab 实现:
- SLUB(默认):将元数据嵌入页结构,简化管理,NUMA 友好,适合大内存系统
- SLAB(传统):独立管理结构,精确的着色和调试支持
- SLOB:极简实现,用于内存极度受限的嵌入式环境
查看当前系统活跃 Slab 缓存:
$ cat /proc/slabinfo | head -20
slabinfo - version 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
dentry 1123423 1123456 192 21 1 : tunables 0 0 0 : slabdata 53498 53498
inode_cache 987654 987700 648 8 2 : tunables 0 0 0 : slabdata 123463 123463
buffer_head 543210 543210 104 38 1 : tunables 0 0 0 : slabdata 14295 14295
task_struct 3456 3456 7736 1 3 : tunables 0 0 0 : slabdata 3456 3456
vm_area_struct 4567 4567 208 19 1 : tunables 0 0 0 : slabdata 240 240
四、虚拟内存与进程地址空间
每个进程拥有独立的虚拟地址空间,由 struct mm_struct 描述,通过 VMA(Virtual Memory Area)树/映射管理不同用途的内存区域。
struct mm_struct {
struct maple_tree mt; // Maple Tree(5.16+ 替代红黑树)管理 VMA
unsigned long mmap_base; // mmap 起始地址
unsigned long task_size; // 用户地址空间大小
pgd_t *pgd; // 页全局目录(顶级页表)
atomic_t mm_users; // 用户数量
atomic_t mm_count; // 引用计数
unsigned long total_vm; // 总映射页数
unsigned long locked_vm; // 锁定页数(不可交换)
unsigned long data_vm; // 数据段页数
unsigned long exec_vm; // 代码段页数
unsigned long stack_vm; // 栈页数
};
4.1 地址空间布局(x86_64)
x86_64 Linux 进程地址空间典型布局:
0x0000_0000_0000_0000 - 0x0000_7fff_ffff_ffff (128TB) 用户空间
└─ 文本段 .text
└─ 数据段 .data / .bss
└─ 堆(向上增长,brk/mmap)
└─ mmap 区域(动态库、共享内存、匿名映射)
└─ 栈(向下增长,固定大小或自动增长)
0xffff_8000_0000_0000 - 0xffff_bfff_ffff_ffff (-128TB) 内核空间
└─ 直接映射区(physmap)所有物理内存
└─ vmalloc 区
└─ 内核代码/数据
└─ fixmap / vsyscall
5.16+ 内核中,VMA 管理从红黑树改为Maple Tree(基数树变体),目的是批量操作友好并减少锁争用。/proc/<pid>/maps 可读出进程的 VMA 布局。
4.2 mmap 与 brk
进程通过两种系统调用向内核申请内存:
- brk/mmap:调整.program break 位置(仅适用于小堆,连续分配)
- mmap:在任意地址建立映射(大分配、共享内存、文件映射)
C 库的 malloc 实现(glibc ptmalloc)策略:小于 128KB 用 brk,大于等于 128KB 用 mmap(避免堆碎片导致无法归还)。jemalloc 和 tcmalloc 使用更细粒度的尺寸分级和 per-thread arena 策略。
五、页表与地址翻译机制
Linux 使用四级(x86_64 早期)或五级(Linux 5.11+ )页表完成虚拟地址到物理地址的翻译。硬件 MMU 中的 Page Table Walker 自动遍历页表,TLU 缓存近期翻译结果。
5.1 五级页表结构
x86_64 五级页表每级 9 bit,共 48 bit 虚拟地址空间(256TB),PKE(Protection Key for Userspace)支持内存域保护:
虚拟地址(48-bit)拆分:
[47:39] PGD index (9 bits, 512 entries)
[38:30] P4D index (9 bits) - 五级页表新增
[29:21] PUD index (9 bits)
[20:12] PMD index (9 bits)
[11:0] Page offset (12 bits, 4KB 页)
[11:0] for 2MB/1GB huge page 相应调整偏移位
每一级页表项(PTE/PMD/PUD 等)包含目标物理地址(页帧号 PFN)和权限位(R/W/U/S、NX、Accessed、Dirty)。
5.2 TLB 机制
TLB(Translation Lookaside Buffer)是 MMU 内的地址转换缓存,现代 CPU 通常分 L1 ITLB/DTLB(约 64-128 项)和 L2 STLB(约 1536-2048 项)。
TLB 相关优化:
- 大页(Huge Pages / THP):单个 TLB 项覆盖更大地址范围,提高 TLB 命中率
- PCID(Process-Context Identifier):避免每次上下文切换刷新 TLB
- INVPCID:选择性无效 TLB 项
- mmap/read 重排序:减少同页重复触发 page walk
5.3 Huge Pages 与透明大页
Linux 支持 2MB 和 1GB 大页。透明大页(THP)功能由 khugepaged 内核线程自动合并相邻的常规页为大页,但可能引入延迟抖动。
# 查看大页配置
$ cat /proc/meminfo | grep -i huge
HugePages_Total: 512
HugePages_Free: 256
Hugepagesize: 2048 kB
HugetlbTotal: 1048576 kB
# 查看 THP 状态
$ cat /sys/kernel/mm/transparent_hugepage/enabled
always [madvise] never # 方括号内为当前值
# 为关键应用固定大页
$ echo 256 > /proc/sys/vm/nr_hugepages
数据库(PostgreSQL/Redis)和 DPDK等场景建议禁用 THP(never),改用显式预分配大页,避免运行时合并延迟。
六、内存回收与交换
当系统物理内存不足时,内核按以下顺序激活内存回收机制:直接回收 → kswapd 后台回收 → 压缩 → OOM Killer。
6.1 页面回收算法
Linux 使用 LRU(Least Recently Used)算法维护活跃/不活跃链表来挑选可回收页。内核 5.14+ 的 MGLRU(Multi-Gen LRU)改进了扫描效率。
页面类型与回收优先级:
- 干净页缓存(未修改的文件页):直接丢弃,代价最低
- 脏页缓存(已修改文件页):先写回磁盘再释放
- 不活跃匿名页:写入 swap 后释放
- 活跃页缓存 / 匿名页:降级为不活跃,待下一轮回收
6.2 Swap 机制
匿名内存(堆、栈、mmap 匿名映射)无法丢弃或回写文件,必须通过 Swap 换出。swappiness 参数控制内核在"丢弃页缓存"和"交换匿名页"之间的偏好:
# 降低 swappiness,倾向丢弃缓存而非换出
$ echo 10 > /proc/sys/vm/swappiness
# 完全禁用 swap(对低延迟服务推荐)
$ swapoff -a
# 或在 cgroup v2 中:
$ echo 0 > /sys/fs/cgroup/memory.swap.max
swappiness=0 的含义是"除非无文件页可回收,否则不 swap",而非彻底禁用——除非配合 cgroup 或 swapoff。
6.3 Zswap / Zram
- Zswap:压缩缓存层,匿名页先压缩存入内存池,满后再写回 swap,兼顾快速换回和小内存占用
- Zram:内存中的压缩块设备,全在内存中,读写不落盘,适合无磁盘 swap 的场景
启用 Zram:
$ modprobe zram num_devices=1
$ echo 4G > /sys/block/zram0/disksize
$ mkswap /dev/zram0
$ swapon /dev/zram0 -p 32767
七、OOM Killer:最后的防线
当所有回收手段均失败、分配请求仍无法满足时,OOM Killer 通过启发式评分选择进程终止,释放其占用的内存。
7.1 oom_score 计算
内核为每个进程维护一个 oom_score(0-1000+),分数越高越可能被 kill。计算方式大致为:
oom_score ≈ (进程占用物理内存比例 × 1000) + 调整量
调整量由 oom_score_adj(-1000 到 1000)控制:
- -1000:不可被 OOM kill(内核线程、关键系统进程)
- -100 到 +100:相对调整
# 查看进程 oom 评分
$ cat /proc/<pid>/oom_score
$ cat /proc/<pid>/oom_score_adj
# 保护关键服务不被 kill
$ echo -1000 > /proc/<pid>/oom_score_adj
# cgroup v2 中通过 memory.max 限制,超限触发 cgroup OOM
$ echo 2G > /sys/fs/cgroup/<cgroup>/memory.max
7.2 cgroup 内存控制与 .oom
cgroup v2 的 memory.max 设置硬限制,超出时对该 cgroup 触发局部 OOM,不影响全系统。配合 PSI(Pressure Stall Information)可提前感知内存压力。
$ cat /proc/pressure/some
some avg10=2.34 avg60=1.23 avg300=0.45 total=987654321
some avg10=0.56 avg60=0.89 avg300=1.12 total=123456789
# some = 任意任务因内存等待
# full = 所有任务同时因内存等待
八、vmalloc 与地址空间管理
kmalloc 分配物理连续的内存(适合 DMA 等硬件需求),vmalloc 分配虚拟连续但物理可不连续的内存(适合仅需内核逻辑访问的大块内存)。vmalloc 的代价是需要额外的页表映射,且非物理连续导致 DMA 不可用。
- kmalloc:物理连续,最大约 4MB(受 MAX_ORDER 限制),GFP 标志控制
- vmalloc:虚拟连续,接近 3GB(VMALLOC_ZONE)可用空间,线性映射
- kmap/kmap_atomic:将高端内存临时映射到内核地址空间,用于 32 位系统
九、内存管理性能调优实战
9.1 生产服务器通用参数
# /etc/sysctl.d/99-memory.conf
# 降低 swap 倾向
vm.swappiness = 10
# 提升脏页刷新阈值(适合大内存服务器)
vm.dirty_ratio = 40
vm.dirty_background_ratio = 10
vm.dirty_expire_centisecs = 3000
vm.dirty_writeback_centisecs = 500
# 提升脏页刷新阈值(高 I/O 场景推荐)
vm.dirty_bytes = 268435456 # 256MB 才开始回写
vm.dirty_background_bytes = 67108864 # 64MB 后台回写
# 保留最小可用内存
vm.min_free_kbytes = 262144 # 256MB,保证紧急分配
# 提升网络/文件缓存的水位
vm.vfs_cache_pressure = 50
# 大内存系统放宽 overcommit
vm.overcommit_memory = 0 # heuristic overcommit
vm.overcommit_ratio = 80
# NUMA 平衡延迟(高负载时适当放宽)
kernel.numa_balancing = 1
9.2 数据库场景(PostgreSQL/MySQL/Redis)
# 禁用透明大页(数据库推荐)
# GRUB: transparent_hugepage=never
$ echo never > /sys/kernel/mm/transparent_hugepage/enabled
$ echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 减少 swap(建议物理内存远大于数据集)
vm.swappiness = 1
# 增大共享内存
kernel.shmmax = 68719476736 # 64GB
kernel.shmall = 16777216
# 锁定大页使用(Redis 可配置)
vm.nr_hugepages = 512
9.3 容器场景(K8s/Docker)
# /etc/sysctl.d/99-k8s-memory.conf
# 容器的内存限制由 cgroup 管理
# 建议关闭跨 NUMA 节点内存交错,避免跨节点访问延迟
vm.zone_reclaim_mode = 0
# K8s 中 memory.limit_in_bytes / memory.max 由 kubelet 设置
# 同时为 Burstable Pod 预留 buffer.memory.max = limit - request
9.4 DPDK/NFV 场景
内核启动参数
default_hugepagesz=1G hugepagesz=1G hugepages=8 default_hugepagesz=2M hugepagesz=2M hugepages=1024
# 隔离 CPU 与内存
isolcpus=2-7,10-15
# iommu 直通
intel_iommu=on
十、观测与故障排查工具
生产环境中需要一套完整的观测手段来诊断内存相关问题。
10.1 基础工具链
# vmstat - 系统内存/si/so/每秒上下文切换
$ vmstat 1
procs -----------memory---------- ---swap-- -----io----
r b swpd free buff cache si so bi bo
2 0 0 523456 345678 1256789 0 0 12 45
# free -h - 查看内存占用总览
$ free -h
total used free shared buff/cache available
Mem: 62Gi 23Gi 5.0Gi 1.2Gi 33Gi 36Gi
Swap: 8.0Gi 0B 8.0Gi
# sar -r - 详细内存指标(SAR 包)
$ sar -r 1
# top/htop - 进程级 RES/SHR 查看
10.2 深度诊断工具
# /proc/buddyinfo - 伙伴系统各阶空闲块
$ cat /proc/buddyinfo
Node 0, zone Normal 32 15 8 4 2 1 0 0 1 0 1
# /proc/pagetypeinfo - 按迁移类型分布(碎片分析)
$ cat /proc/pagetypeinfo
# /proc/vmallocinfo - vmalloc 分配列表
$ cat /proc/vmallocinfo | awk '{sum+=$2} END {print sum " bytes in use"}'
# /proc/zoneinfo - 每个 Zone 的详细统计(free/free_pages/low/high watermark)
$ cat /proc/zoneinfo
# /proc/<pid>/smaps - 进程每段内存详细统计(PSS/RSS/Shared)
$ grep -E "^(Rss|Pss|Private|Shared):" /proc/1/smaps | sort | uniq -c | sort -nr
# slabtop - 实时监控 Slab 分配
$ slabtop -s c # 按大小排序
# bpftrace 跟踪慢速内存分配
bpftrace -e 'km:mm_page_alloc { @[kstack] = count(); }'
# perf 观察 page fault
$ perf stat -e page-faults,minor-faults,major-faults -p <pid>
10.3 常见内存泄漏排查
当观察到可用内存持续下降时,按以下步骤排查:
- SAR 趋势:sar -r 检查 memfree 斜率,判断线性/指数增长
- Slab 异常增长:slabtop 观察 dentry/inode_cache 是否失控(小文件服务器常见)
- PSS 增量:检查进程 smaps 的 PSS 是否在增长(用户态内存泄漏)
- WCHAN/kswapd:确认是否频繁内存回收
- cgroup OOM 日志:dmesg | grep -i "oom\|out of memory" 被 kill 的具体原因
- bpftrace/leak_bt:使用 BCC 的 leak_bt 跟踪内核未释放分配
十一、内存子系统发展趋势
Linux 内存管理仍在持续演进,几个值得关注的方向:
- CXL(Compute Express Link)内存池化:扩展 tiered memory 支持,NUMA 节点可指向外部内存设备,需要内核的 Soft-DLB 平衡策略
- Memory Folios:用 folio(复合页组)替代 struct page,减少大页操作开销,5.16+ 内核正在逐步迁移
- MGLRU(Multi-Gen LRU):5.18+ 改进的 LRU 算法,按世代区分页面活跃度,避免传统突发流量刷掉热页的问题
- PAN(Privileged Access Never)/PAN 强化(ARM64):硬件隔离内核/用户访问,防内核越界读写
- Usenet 零拷贝:slow path bypass,绕过常规分配路径,针对高频大块分配场景
总结
Linux 内存子系统是一组精密协作的层次结构:NUMA 节点组织物理内存,伙伴系统管理连续页框,Slab 复用内核小对象,页表与 TLB 完成虚拟到物理的映射,LRU 算法管理回收,OOM Killer 作为最后防线。理解这些机制的交互关系,才能在生产环境中做出正确的调优决策——无论是降低 swappiness、预分配大页、配置 cgroup 限额,还是诊断内存泄漏,都需要回到这些底层逻辑。
对于性能敏感的关键服务,建议固定大页、合理配置 cgroup 限额、禁用透明大页、监控 PSI 指标,将内存管理从"被动响应"转为"主动预防"。

发表评论 取消回复