引言
Linux 内核的内存管理是操作系统最核心的子系统之一,它直接决定了系统的稳定性、性能和资源利用率。从底层物理页框的分配与回收,到上层 Slab 小对象缓存机制,再到 OOM(Out-Of-Memory)杀手在极端压力下的决策逻辑,每一个环节都经过了数十年的工程打磨。本文将深入剖析 Linux 内存管理的核心机制,涵盖伙伴系统、Slab/Slub 分配器、LRU 页面回收、OOM Killer 决策流程、透明大页(THP)、cgroup 内存控制以及 eBPF 内存追踪等实战技术。
一、物理内存管理:伙伴系统与 Per-CPU 缓存
1.1 伙伴系统(Buddy System)
Linux 内核以物理页框(Page Frame)作为基本管理单位,通常大小为 4KB。伙伴系统负责组织所有物理内存,支持以 2 的幂次(order)分配连续页框。每个 NUMA 节点维护 11 个空闲区域(free_area),分别对应 order-0 到 order-10(即 1 页到 1024 页/4MB)。
核心机制:
- 分裂:当低阶空闲链表无空闲块但高阶链表有可用块时,递归将大块对半分裂。
- 合并:当相邻的伙伴块同时被释放时,自动合并为更大的块并移向高阶链表。
- 水位线:每个 Zone 维护 min/low/high 三条水位线,决定页面回收的触发时机。
1.2 Per-CPU Page Frame Cache
为减少多核竞争伙伴系统锁的开销,每个 CPU 维护一个单页框本地缓存(pcp-1 到 pcp-16)。当申请单页时优先从 PCP 获取,批量补充/回收的阈值由 /proc/sys/vm/percpu_pagelist_fraction 控制。PCP 极大提升了高频小分配场景的吞吐量。
1.3 Zone 与 NUMA 感知
传统 x86 将物理内存划分为 DMA(0-16MB)、DMA32(16MB-4GB)、Normal(4GB 以上)三个 Zone。在 NUMA 架构下,每个节点内部仍保留 Zone 层级。分配优先级遵循 alloc_pages() 的 GFP 标志位链:先从本地节点的合适 Zone 分配,失败时按 fallback 列表向远程节点推进。
查看伙伴系统状态:
cat /proc/buddyinfo
Node 0, zone DMA 1 0 0 0 1 1 1 0 1 1 3
Node 0, zone DMA32 6596 3765 1045 278 35 5 0 0 0 0 0
Node 0, zone Normal 73580 68095 48253 22148 6828 724 78 3 1 0 0
# 查看 Zone 水位线
cat /proc/zoneinfo | grep -E "Node|zone|min|low|high" | head -20
二、Slab/Slub 分配器:小对象高效复用
2.1 三层架构
Slab 系列分配器建立在伙伴系统之上,专门优化内核中频繁分配释放的固定大小对象(如 task_struct、inode、dentry 等):
- kmem_cache:每种对象类型对应一个 cache,内部维护本地/部分/全满三个 Slab 列表。
- kmalloc:基于一组预定义大小的通用 kmem_cache(如 8/16/32/64/96/128/...2048 字节)实现。
- Slub 默认替代 Slab:从 2.6.23 起为默认分配器,设计更简洁,碎片率更低。
2.2 对象生命周期
每个 Slab 由一个或多个连续页框构成,内部等分槽位存放对象。分配时优先使用 CPU 本地缓存(避免锁竞争),其次从 partial Slab 取空闲槽位;若 partial 耗尽则向伙伴系统申请新 Slab。释放时对象回到本地缓存或直接归还 Slab,当 Slab 全部空闲时归还伙伴系统。
2.3 实战:查看与调优 Slab
# 查看 Slab 使用总量
slabtop
# 查看每个 cache 的详细统计
cat /proc/slabinfo
kmalloc-64 10224 5112 64 63 1 : tunables ...
kmalloc-128 15420 7710 128 32 1 : tunables ...
task_struct 1200 600 7104 5 8 : tunables ...
dentry 31824 31240 192 21 1 : tunables ...
# Slab 内存占用量
grep Slab /proc/meminfo
Slab: 1238976 kB
# 手动清理可回收 Slab(谨慎使用)
echo 3 > /proc/sys/vm/drop_caches # 清理 pagecache + slab
Slub 碎片调优参数:
/proc/sys/vm/min_partial # partial 链表中保留的最少页数
/sys/kernel/slab/*/order # 每个 cache 的 slab 阶数
/sys/kernel/slab/*/cpu_partial # 每个 CPU 本地缓存对象上限
三、页面回收与 LRU 算法
3.1 双 LRU 链表机制
Linux 从 2.6 起采用活跃/非活跃双链表(Active/Inactive LRU)设计,每个 NUMA 节点维护 5 类 LRU 链表:
- Anonymous:匿名页(进程堆、栈)
- File-backed:文件映射页(共享库、mmap)
- SWAP/DMA:与硬件直接相关的分类
页面首先进入 Active 链表尾部。如果在下次扫描前被访问(硬件缺页标记 PG_referenced),则晋升到 Active 头部;否则降级到 Inactive。当内存紧张时,kswapd 将 Inactive 链表尾部的页面回收:匿名页写入 Swap,文件页直接丢弃。
3.2 水位线与回收触发
当空闲页低于 wmark_low 时 kswapd 开始后台回收;低于 wmark_min(min_free_kbytes)时进入直接回收(Direct Reclaim),申请进程同步阻塞等待回收完成——这是系统卡顿的常见根因。
# 查看当前水位线
cat /proc/zoneinfo | grep -A2 "pages free"
# 调整 min_free_kbytes(建议按总内存 0.5%~1%)
sysctl -w vm.min_free_kbytes=1048576 # 1GB 系统建议 64MB
# kswapd 回收的统计
grep -E "pgsteal|pgscan" /proc/vmstat
pgscan_kswapd_normal 1234567
pgsteal_kswapd_normal 987654
3.3 Swap 与 Swappiness
vm.swappiness(范围 0-200)控制匿名页与文件页回收的比例偏好。默认值 60 意味着二者权重相当。在数据库等场景中设为 1-10 可显著减少 Swap 抖动(但不会完全禁用 Swap,仅在 OOM 前仍可能触发)。
# 临时设置
sysctl -w vm.swappiness=10
# 永久设置
echo "vm.swappiness=10" >> /etc/sysctl.d/99-sysctl.conf
# 查看 Swap 使用
free -h
swapon --show
vmstat 1 5 # 观察 si/so 是否频繁非零
四、OOM Killer:内存耗尽时的最后防线
4.1 触发条件
当系统所有内存(包括 Swap)耗尽、直接回收无法满足需求、且所有 Zone 都低于 min 水位线时,内核调用 out_of_memory() 选择牺牲进程。panic_on_oom 控制是否在 OOM 时直接触发 kernel panic(常用于关键生产环境)。
4.2 OOM Score 计算
内核通过 oom_badness() 为每个进程计算 OOM Score:
- 比例因子:进程使用的物理内存(含 RSS + Swap)占总内存的比例
- 调整参数:
oom_score_adj(范围 -1000 ~ +1000,-1000 表示永不杀死) - 特权进程、内核线程(PF_NO_SETAFFINITY)通常获得 1/32 的评分折扣
# 查看进程 OOM 评分
cat /proc/1/oom_score # 当前分数
cat /proc/1/oom_score_adj # 调整值(默认 0)
echo -1000 > /proc/1/oom_score_adj # 保护 systemd 不被 OOM 杀死
# 快速查看最容易被杀死的进程
ps aux --sort=-%mem | head -10
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
mysql 1234 2.1 85.5 12.3g 11.8g ? Ssl 10:00 234:56 /usr/sbin/mysqld
4.3 OOM 事件后的分析
OOM Killer 触发的消息会写入 dmesg,包含:触发进程、被杀死进程、释放内存量以及各 Zone 的内存水位状态。这些信息是排查内存瓶颈的关键线索。
dmesg | grep -i "out of memory"
[123456.789] Out of memory: Killed process 5678 (java) total-vm:12345678kB
[123456.789] oom_memcg: memcg=/user.slice, memory: usage 8589934592, limit 8589934592
五、透明大页(THP)
透明大页将 2MB 或 1GB 的巨页透明化,减少 TLB Miss 并提升内存访问连续性。内核后台线程 khugepaged 负责在适当时机将普通页集合并为巨页。
# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled # [madvise|always|never]
cat /sys/kernel/mm/transparent_hugepage/defrag # 碎片整理策略
# 建议:数据库场景设为 madvise,通过 madvise() 按需使用
echo madvise > /sys/kernel/mm/transparent_hugepage/enabled
# 查看 HugePages 使用
grep -i huge /proc/meminfo
HugePages_Total: 64
HugePages_Free: 32
Hugepagesize: 2048 kB
实战建议:虚拟化/数据库等延迟敏感场景适合 always;内存碎片化严重或需要精细控制的场景建议 madvise 或 never。
六、cgroup 内存控制
cgroup v1/v2 提供层级化内存资源隔离,是现代容器内存管理的基石:
- memory.limit_in_bytes:硬限制,超出即触发 OOM 或阻塞
- memory.soft_limit_in_bytes:软限制,仅在全局内存紧张时生效
- memory.kmem.limit_in_bytes:限制内核内存(Slab 等)
- memory.swappiness:cgroup 级 swappiness 偏好
# cgroup v2 内存控制(systemd)
systemctl set-parameter MemoryMax=2G
systemctl set-parameter MemoryHigh=1.5G # 软限制
# 查看容器内存统计
cat /sys/fs/cgroup/memory/docker/.scope/memory.current
cat /sys/fs/cgroup/memory/docker/.scope/memory.stat
anon 536870912 # 匿名页占用 (512MB)
file 1073741824 # 文件页占用 (1GB)
slab 268435456 # Slab 占用 (256MB)
pgfault 9876543 # 缺页异常总数
oom_kill 0 # OOM 触发次数
七、eBPF 内存追踪
eBPF 提供对内存子系统的无损观测能力,覆盖从页分配到 OOM 的完整链路:
7.1 关键 Tracepoint
kmem:mm_page_alloc:每次伙伴系统分配物理页kmem:mm_page_free:页框归还伙伴系统vmscan:mm_vmscan_kswapd_wake:kswapd 唤醒事件oom:mark_victim:OOM Killer 选定牺牲进程
7.2 BCC/bpftrace 实战
# 实时监控 >4 阶的大块分配
bpftrace -e ' tracepoint:kmem:mm_page_alloc /args->order > 4/ {
printf("order=%d gfp=%x comm=%s\n", args->order, args->gfp_flags, comm);
}'
# 统计每个进程的 RSS 变化(每 1 秒)
bpftrace -e ' kprobe:do_anonymous_page {
@rss[comm] = sum(arg1);
} interval:s:1 { print(@rss); clear(@rss); }'
# 监控 Direct Reclaim(高延迟根因)
bpftrace -e ' tracepoint:vmscan:mm_vmscan_direct_reclaim_begin {
@start[tid] = nsecs;
} tracepoint:vmscan:mm_vmscan_direct_reclaim_end /@start[tid]/ {
@lat[comm] = hist(nsecs - @start[tid]); delete(@start[tid]);
}'
7.3 perf 内存剖析
perf record -e kmem:mm_page_alloc -g -p $(pidof myapp)
perf report --sort=comm,dso
# 实时监控缺页异常率
perf stat -e page-faults,dTLB-load-misses -p 1234 5
八、性能调优全景
不同场景下的推荐配置:
# 1. 高吞吐服务器(数据库/缓存)
vm.swappiness=1
vm.dirty_ratio=10
vm.dirty_background_ratio=5
vm.min_free_kbytes=262144 # 256MB on 128GB
sysctl -w kernel.mm.transparent_hugepage.enabled=madvise
# 2. 桌面/开发环境(交互响应优先)
vm.swappiness=60
vm.dirty_ratio=20
vm.dirty_background_ratio=10
kernel.mm.transparent_hugepage.enabled=always
# 3. 嵌入式/低内存设备
vm.swappiness=100
vm.overcommit_memory=0
vm.min_free_kbytes=4096
kernel.mm.transparent_hugepage.enabled=never
OOM 综合防护策略:
- 关键服务(DB、MQ)设置
oom_score_adj=-1000 - 通过 cgroup MemoryMax 限制非关键容器
- 部署 earlyoom/usermode OOM 守护进程提前介入
- 监控
pgscan_kswapd/pgscan_direct比值,提前预警
总结
Linux 内存管理是一个高度层次化的工程杰作:伙伴系统确保物理页框的高效组织,Slub 分配器让内核对象分配零碎片化,LRU 与 kswapd 实现优雅的渐进式回收,OOM Killer 在极端情况下做出最小代价的止损决策。掌握这些机制不仅能帮助我们排查内存泄漏、OOM 突刺等生产问题,更能指导我们在容器化、NUMA 以及性能敏感场景下做出正确的全局配置选择。借助 eBPF 等现代观测工具,内存问题不再是"黑盒",而是完全可量化、可追踪、可优化的透明系统。

发表评论 取消回复