一、Linux 内存管理架构总览
Linux 内存管理子系统(mm/)是内核最复杂的核心模块之一,它负责:物理页帧管理、虚拟地址空间映射、按需分配与回收、NUMA 亲和性、页缓存/交换、OOM 处理。整个子系统由 Buddy System + Slab 分配器 + 页表管理 三大支柱组成。
1.1 核心数据结构关系
内存管理核心对象:
task_struct → mm_struct:每个进程通过 mm_struct 拥有独立的地址空间mm_struct → vm_area_struct (VMA):按区域划分代码段、数据段、堆、栈、mmap 区pgd_t → pud_t → pmd_t → pte_t:四级页表索引结构struct page:每个物理页面对应的元数据结构(mem_map 全局数组)struct zone:内存区域(DMA/Normal/HighMem/Movable),NUMA 每个 node 独立
1.2 物理内存组织
Linux 将物理内存组织为 node → zone → page 三级层次:
# 查看系统 NUMA/Zone 信息
$ numactl --hardware
available: 2 nodes (0-1)
node 0 size: 131000 MB
node 0 free: 98000 MB
node 1 size: 131000 MB
node 1 free: 102000 MB
# 查看 Zone 水位线
$ cat /proc/zoneinfo | head -40
Node 0, zone Normal
pages free 2456721
min 200000
low 300000
high 400000
spanned 262144000
present 260000000
二、Buddy System — 物理页面分配器
2.1 算法原理
Buddy System 维护 11 个 free_area 链表(order 0~10),分别管理 2^0 到 2^10 个连续页面(4KB ~ 4MB):
- 分配:在满足要求的最小 order 链表取;若空则向上级"分裂"为两半 Buddy,一半返回一半入较低 order 链表
- 释放:检查 Buddy 页是否也在空闲链表 — 是则合并("buddies 牵手")继续向上级尝试
- 伙伴计算:
buddy_pfn = pfn ^ (1 << order)— 通过 XOR 快速定位物理相邻伙伴页
2.2 分配标志(GFP flags)
| 标志 | 含义 | 典型场景 |
|---|---|---|
| GFP_KERNEL | 标准内核分配,允许阻塞和 I/O | 普通内核路径 |
| GFP_ATOMIC | 禁止阻塞禁止调度 | 中断处理、spinlock 上下文 |
| GFP_DMA | 要求 ZONE_DMA(<16MB) | 传统 ISA 设备 DMA |
| GFP_DMA32 | 要求 ZONE_DMA32(<4GB) | 32-bit PCIe 设备 DMA |
| __GFP_ZERO | 分配后清零 | 安全敏感数据 |
| __GFP_HIGHMEM | 允许分配高端内存(32位) | 用户空间映射 |
| __GFP_NOWARN | 分配失败不打印警告 | 可失败路径 |
| GFP_NOWAIT | 禁止阻塞禁止 reclaim | 实时上下文 |
2.3 水位线(watermark)与 页面回收触发
每个 zone 维护 min/low/high 三条水位线。当空闲页面降至 low 内核线程 kswapd 开始异步回收;降至 min 则同步 direct reclaim 强制执行:
/proc/sys/vm/min_free_kbytes — 控制 min 基准值(建议内存的 4%~6%)
/proc/sys/vm/swappiness — 0~200,值越低越避免 swap
/proc/sys/vm/lowmem_reserve_ratio — zone 间保留比例
/proc/sys/vm/compact_memory — 手动触发内存碎片整理
三、Slab / SLUB 分配器 — 对象缓存
3.1 Slab 设计动机
Buddy System 以页面(4KB)为粒度分配,但内核频繁创建销毁的是几十字节的 struct(如 task_struct、inode、dentry),直接使用 Buddy 会产生严重内部碎片。Slab 在页面之上建立细粒度对象缓存:
kmem_cache:同类型对象的专属缓存(如task_struct_cachep、inode_cachep)- 每个 cache 包含 one/many slab,slab 占用一或多个连续页面
- slab 内分为固定大小 slot 数组 + 空闲链表(free list)
3.2 Slab 状态机
每个 slab 有三种状态:
- full:所有 slot 已分配,挂在 cachep->slabs_full
- partial:部分分配(同时又空闲 slot),挂在 cachep->slabs_partial — 优先从此分配
- empty:全部空闲,挂在 cachep->slabs_free — 可释放回 Buddy System
3.3 SLUB — 现代默认分配器
SLUB(Sane Component Uncontrolled Bloat)替代了原始 SLab,主要改进:
- 移除每个 slab 的元数据管理链表(改用 page 结构体直接追踪)
- CPU 本地缓存(cpu_slab):per-cpu partial 锁-free 快速路径
- NUMA 本地 partial 支持:每个 node 维护独立 partial 链表
- 合并同尺寸 cache 减少碎片
SLUB 分配路径:
- cpu->freelist 快速路径(无锁)→ 命中返回
- cpu->partial(per-cpu partial slab 链表)→ 取备用
- node->partial(node partial 链表,需加 node 锁)→ 取节点级 partial
- new_slab 从 Buddy System 批量订购新页面
3.4 Kmalloc 系列 API
kmalloc() 是建立在 Slab 缓存池之上的通用分配器,其内部使用 19 个预建 cache(kmalloc-8、kmalloc-16、kmalloc-32、...、kmalloc-8K、kmalloc-16K、... kmalloc-4M)按大小分级:
// 查看当前 kmalloc slab 缓存状态
$ cat /proc/slabinfo | grep kmalloc | head
kmalloc-1024 2840 2840 1024 32 8 : tunables ...
kmalloc-512 5600 5600 512 32 16 : tunables ...
kmalloc-256 14300 14500 256 32 16 : tunables ...
kmalloc-128 26200 26250 128 64 32 : tunables ...
kmalloc-64 82000 85000 64 128 64 : tunables ...
vmalloc() 分配虚拟连续但物理不连续的内存,适用于大缓冲区(如内核模块加载);krealloc()、kfree() 对应行为类似用户态 realloc/free。
3.5 缓存调试与调优
SLUB 提供丰富的调试与调优接口:
- SLUB_DEBUG:检测越界/释放后使用(Red Zoning + Poisoning + Tracking)
- /sys/kernel/slab/<cache>/:tunables — cpu_partial、batchcount、order 等
- slub_debug= 内核参数:如
slub_debug=FZPU 强制所有 cache 启用追踪 - /proc/slabinfo:所有 cache 的活跃 slot 数、总 slot 数、对象大小、slab 数
四、虚拟地址与页表管理
4.1 虚拟地址空间布局(x86_64)
0000 0000 0000 0000 ├─ 128TB 用户空间(task_size_max - PAGE_SIZE)
PAGE_OFFSET─┤
ff00 0000 0000 0000 ├─ ... 非canonical hole ...
ffff 8000 0000 0000 ├─ KASAN Shadow (if enabled)
ffff 8880 0000 0000 ├─ 物理内存直接映射区(page_offset_base)─ 所有物理页线性映射
ffff c000 0000 0000 ├─ 固定映射区(fixmap)
ffff ea00 0000 0000 ├─ PCI I/O
ffff ffff 8000 0000 ├─ KASAN / KMSAN(可选)
ffff ffff ff60 0000 ├─ vsyscall 页面
ffff ffff ff80 0000 ├─ vvar
ffff ffff ffe0 0000 ├─ trampoline
ffff ffff ffff ffff └─ 结束
4.2 四级/五级页表机制
x86_64 默认四级页表(Intel 5-level paging 可选):
- PML4(Page Map Level 4):512 entries → 512GB/entry
- PDPT(Page Directory Pointer Table):512 entries → 1GB/entry
- PD(Page Directory):512 entries → 2MB/entry(可用 2MB 大页 PDE)
- PT(Page Table):512 entries → 4KB/entry
物理地址计算:phys = l.x + CR3 逐级索引 + offset:
Virtual Address(48 位):
┌─────────┬─────────┬─────────┬─────────┬──────────┐
│ PML4(9b)│PDPT(9b) │ PD(9b) │ PT(9b) │Offset(12b)│
└─────────┴─────────┴─────────┴─────────┴──────────┘
CR3 → PML4[PML4E] → PDPT[PDPTE] → PD[PDE] → PT[PTE] → Physical Page + offset
4.3 TLB 管理
TLB(Translation Lookaside Buffer)缓存 PTE 以加速地址翻译。关键操作:
INVLPG va:虚拟地址页失效MOV CR3:全 TLB 刷新(进程切换触发)- PCID(Process-Context Identifier):Linux 4.14+ 默认开启(CR4.PCIDE=1),位 0~4095 中唯一标识进程,避免 context-switch 全 flush
- TLB Shootdown:
flush_tlb_mm_range()通过 IPI 通知其他 CPU 失效指定区间 TLB — 修改页表后(mprotect/unmap)必须执行
五、大页(Huge Pages / THP)
5.1 静态 Huge Pages
预分配 2MB 或 1GB 大页,绕过动态分配减少 TLB miss:
# 查看 2MB 大页池
$ grep Huge /proc/meminfo
HugePages_Total: 1024
HugePages_Free: 1024
Hugepagesize: 2048 kB
# /etc/fstab 添加挂载
hugetlbfs /dev/hugepages hugetlbfs pagesize=2M 0 0
5.2 Transparent Huge Pages(THP)
THP(Linux 2.6.38+)使用 khugepaged 后台扫描用户匿名内存,自动合并连续 4KB 页为 2MB:
/sys/kernel/mm/transparent_hugepage/enabled — [always|madvise|never]
/sys/kernel/mm/transparent_hugepage/defrag — 直接回收+压缩以凑大页
生产上:数据库(Redis/PostgreSQL)推荐 madvise 模式并主动 madvise(addr, len, MADV_HUGEPAGE) 标记热区。
六、NUMA 感知内存分配
6.1 NUMA 拓扑建模
Linux 通过 pglist_data (struct pg_data_t) 对每个 NUMA node 建模:
node_zones[MAX_NR_ZONES]:该 node 的 zone 数组node_zonelists[MAX_ZONELISTS]:分配降级顺序(fallback list)— 本节点不够时再跨节点node_start_pfn / node_present_pages:该节点的物理内存范围
6.2 NUMA-aware 分配策略
| 策略 | 系统调用 | 行为 |
|---|---|---|
| MPOL_DEFAULT | set_mempolicy | 分配 local node 内存,fallback |
| MPOL_BIND | set_mempolicy | 强制从指定节点分配,失败触发 local OOM |
| MPOL_INTERLEAVE | mbind/page | 交替分配(stripe)跨多节点 |
| MPOL_PREFERRED | mbind | 优先 local,可 fallback |
| MPOL_LOCAL | Linux 4.8+ | 自动感知 CPU 所在 node(默认) |
Per-node 数据可见性:/sys/devices/system/node/node*/meminfo、/proc/buddyinfo、/proc/zoneinfo。
6.3 Kmalloc NUMA-aware 分配
kmalloc_node()、alloc_pages_node() 在线参数 nodeid 对应的 zone list 分配;GFP_THISNODE 标志要求必须从请求节点分配,无 fallback。
关键内核计数器:pgmigrate_success / pgmigrate_fail(node_reclaim 跨节点迁移,由 kcompactd/kswapd 执行)。
七、KSM 与 ZSMAP — 内存去重与压缩
7.1 KSM(Kernel Same-page Merging)
KSM 通过红黑树扫描全系统匿名页,将多个相同物理页合并为一个 Copy-On-Write 共享页:
/sys/kernel/mm/ksm/run — 0/1/2 停止/运行/停止并unmerge
/sys/kernel/mm/ksm/pages_to_scan — 每次扫描页数
/sys/kernel/mm/ksm/sleep_millisecs — 两次扫描间隔
# 查看节省量
$ cat /sys/kernel/mm/ksm/pages_shared
$ cat /sys/kernel/mm/ksm/pages_sharing
适用:KVM 虚拟机密度优化、多实例容器相同应用镜像场景。代价是 5%~20% CPU 开销。
7.2 ZSWAP / ZRAM / ZSMAP
- zram:内存中的压缩块设备,将 swap page 用 lzo/zstd/lz4 压缩后原地存储
- zswap:swap 写入先压缩,满则落磁盘。/sys/module/zswap/parameters/
- zsmalloc:ZRAM/压缩代理的专用 allocator — 内部虚拟地址视角看同一物理页会被合并,生成<3% 平均压缩率
八、内存回收(Reclaim)与 OOM
8.1 LRU 链表算法
Linux 内核使用双链表(active/inactive)近似 LRU:
- LRU_INACTIVE_ANON / LRU_ACTIVE_ANON:匿名页(进程堆/栈)的冷热区
- LRU_INACTIVE_FILE / LRU_ACTIVE_FILE:文件页缓存(page cache)冷热区
- 页访问通过 PTE 的 Accessed 位触发从 inactive 迁到 active — 类似时钟算法
8.2 回收路径
- kswapd 后台回收:水位 low 触发,按 swappiness 比例扫 anon/file LRU
- Direct Reclaim 同步回收:分配路径上 min 不足 → 分配线程亲自回收
- Memory compact:迁移可移动页以减少外部碎片(/proc/sys/vm/compact_memory)
- OOM Killer:所有回收路径均失败时触发
8.3 OOM Killer 调优
/proc/sys/vm/panic_on_oom — 0=触发OOM; 1=panic; 2=强制panic
/proc/sys/vm/oom_kill_allocating_task — 1=直接杀死触发OOM的进程
/proc/[pid]/oom_score — OOM 评分 ~内存占用比
/proc/[pid]/oom_score_adj — 可调 [-1000, 1000],-1000 永不命中
/proc/[pid]/oom_adj — 旧接口 [-17, 15]
systemd 通过这些接口对关键服务(如数据库主进程)设置 OOMScoreAdjust=-1000 保护。
九、实战案例与调试
9.1 内存泄漏检测
// 使用 kmemleak 追踪未释放的 kmalloc
echo scan > /sys/kernel/debug/kmemleak
cat /sys/kernel/debug/kmemleak
// BPF 追踪 (bcc)
$ funclatency -u kmalloc — 分配延迟分布
$ memleak -p PID — 按pid追踪未释放堆
// Slabtop 实时查看
$ slabtop -s c — 按缓存大小排序
$ echo 1 > /proc/sys/vm/drop_caches — 清理 pagecache/dentries/inodes(仅测试用)
9.2 碎片化排查
// 查看 Buddy 系统碎片化
$ cat /proc/buddyinfo
Node 0, zone Normal 100 50 30 20 10 5 3 2 1 ...
// 越靠低 order 空闲页越多 = 碎片化越严重
// 5 = 32 pages (128KB), 6 = 64 pages (256KB) ...
// 手动 compact
echo 1 > /proc/sys/vm/compact_memory
9.3 NUMA 性能调优
// numactl 绑定进程 CPU + Memory node
$ numactl --cpunodebind=0 --membind=0 ./app
// numastat 查看各节点分配命中率
$ numastat -c java
Node 0 Node 1
Numa_Hit 98000000 200000
Numa_Miss 20000 950000 ← miss 应接近 0
Numa_Foreign 10000 50000
// 自动 NUMA 平衡
echo 0 > /proc/sys/kernel/numa_balancing — 关闭(数据库推荐)
echo 1 > /proc/sys/kernel/numa_balancing — 开启(通用应用)
十、内核参数速查
###### Basics ######
vm.min_free_kbytes = 262144 — 保证紧急分配的最小留白(建议 4% 物理内存)
vm.swappiness = 10 — 越低越倾向回收 file cache(数据库推荐 1~10)
vm.dirty_ratio = 40 — 脏页占总内存 40% 时开始回写
vm.dirty_background_ratio = 10 — 后台 pdflush 启动回写阈值
vm.overcommit_memory = 0 — 0=启发式 1=总是 2=严格(生产推荐 2)
vm.overcommit_ratio = 50 — overcommit 时允许的 "RAM*ratio%+swap" 比例
###### Reclaim ######
vm.vfs_cache_pressure = 100 — 越大越愿意回收 dentry/inode(内存紧张 200+)
vm.zone_reclaim_mode = 0 — 0=node 不够直接 fallback 1=强制 zone reclaim(NUMA 调优)
vm.page-cluster = 3 — Swap readahead 窗口(2^3 页,数据库 0)
###### Huge ######
vm.nr_hugepages = 1024 — 全局静态 2M 大页池
vm.nr_overcommit_hugepages = 64 — 超出预分配的弹性大页
###### NUMA ######
kernel.numa_balancing = 1 — 自动 NUMA 平衡(通用应用开,数据库关)
###### OOM ######
vm.panic_on_oom = 0 — OOM 时触发 killer 而非 panic
kernel.sysrq = 1 — 允许 SysRq 触发手动 OOM kill
10.2 监控指标体系
Prometheus node_exporter 关键指标:
node_memory_MemAvailable_bytes— 真正可用内存(含可回收 page cache)node_memory_MemFree_bytes— 完全空闲node_memory_SwapUsed_bytes— 内核交换node_vmstat_pgfault / pgmajfault— 缺页异常(major 代表读磁盘)node_vmstat_oom_kill— OOM kill 计数node_vmstat_numa_*— NUMA 命中/miss/foreign
十一、关键参考资料
- 书籍:《Understanding the Linux Virtual Memory Manager》— Mel Gorman 经典
- 书籍:《Professional Linux Kernel Architecture》— Wolfgang Mauerer
- 书籍:《Systems Performance: Enterprise and the Cloud》— Brendan Gregg (第 7-8 章)
- 内核源码:
mm/— 内存管理核心实现 - LWN "Anatomy of the Linux page tables" — 深入五级页表变更
- Brendan Gregg's perf page:
perf-tools中的 perf/mem 工具集
总结:Linux 内存管理是性能调优的第一战场。从 Buddy 水位线到 Slab fragmentation、从 TLB shootdown 到 NUMA watermark、从 THP merge threshold 到 OOM score—每一步决策都可能产生 10x 性能差异。建议系统管理员与 DBA 重点掌握:Swappiness、Min Free Kbytes、THP 模式、NUMA 绑定、以及 /sys/kernel/slab 的调优入口。

发表评论 取消回复