Linux 内核内存管理深度实战:从 buddy system 到 slub allocator 与性能调优
引言
Linux 内核的内存管理子系统是整个系统中最复杂、最核心的组件之一。它不仅负责物理内存的分配与回收,还承担着虚拟内存管理、页面缓存、内存映射等关键任务。理解这些机制,对于系统管理员优化应用性能、解决 OOM(Out of Memory)问题,以及开发高性能的内核模块至关重要。
本文将深入剖析 Linux 内核内存管理的三大核心组件:Buddy System、Slub Allocator 和虚拟内存管理,并通过实际案例展示如何进行性能调优和故障排查。
一、Buddy System 伙伴系统
1.1 核心原理
Buddy System 是 Linux 内核中管理物理内存页的基础算法。它将所有空闲页面组织成 11 个链表,分别对应 20 到 210 个连续页面(即 1 页到 1024 页,4KB 到 4MB)。当请求分配 N 个页面时,系统在对应阶(order)的链表中查找。如果该阶没有空闲块,则从更高阶分裂出一个块,一半用于分配,另一半放入低阶链表。释放时,如果相邻的"伙伴"块也是空闲的,则合并成更大的块。
1.2 碎片化问题
长期运行的系统会面临严重的内存碎片化问题。即使有足够的空闲内存,如果它们分散在不连续的物理地址上,大块连续物理内存的分配也会失败。Linux 为解决这个问题引入了页面迁移(page migration)和规整(compaction)机制。
可以通过 /proc/pagetypeinfo 查看当前系统的页面碎片情况:
# cat /proc/pagetypeinfo
Page block order: 9
Pages per block: 512
Free pages count per migrate type at order 0 1 2 3 4 5 6 7 8 9 10
Unmovable 1 1 2 1 2 0 1 0 1 0 1
movable 12 11 8 6 5 2 3 2 1 1 0
reclaimable 0 0 0 0 0 0 0 0 0 0 0
1.3 实战:调整 min_free_kbytes
min_free_kbytes 参数控制系统保留的最小空闲内存(水位线 mark_min)。设置过低会导致系统内存紧张时直接回收页面,影响性能;设置过高则浪费内存。
# 查看当前值
cat /proc/sys/vm/min_free_kbytes
# 推荐计算公式(以 16GB 内存为例)
# sqrt(16 * 1024 * 1024) * 12 ≈ 49152 KB
# 临时调整
sysctl -w vm.min_free_kbytes=49152
# 永久调整
echo "vm.min_free_kbytes=49152" >> /etc/sysctl.conf
二、Slab / Slub 分配器
2.1 为什么需要 Slab 分配器
Buddy System 以页(通常 4KB)为粒度分配内存,但内核中大量对象远小于一页(如 task_struct、dentry、inode 等)。如果直接通过 Buddy System 分配,将造成严重的内部碎片。Slab 分配器(及其现代替代品 Slub)通过对象缓存(kmem_cache)机制,将一页内存划分为多个固定大小的小对象,大幅提高内存利用率并减少分配延迟。
2.2 Slub 架构设计
Slub 是 Linux 2.6.23 之后默认的 Slab 实现,相比原始 Slab 大幅简化了设计:
- CPU 缓存:每个 CPU 维护一个活跃对象列表,无锁分配,极致性能
- Node 部分列表:NUMA 架构下每个节点维护一个部分分配列表
- 全空闲页面回 Buddy System:当缓存页中所有对象都被释放时,页面归还给 Buddy System
2.3 实战:查看 Slab 使用状况
# 查看 slab 使用概览
cat /proc/slabinfo | head -30
# 推荐使用 slabtop 实时监控
slabtop -o
# 典型输出:
# OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
# 32456 32456 100% 1.19K 956 34 97792K dentry
# 28765 28765 100% 1.00K 736 39 75840K inode_cache
# 18234 18234 100% 0.19K 213 86 32768K kmalloc-192
2.4 Slab Shrinking 与回收
内存紧张时,内核通过 shrinker 机制回收 Slab 缓存。dentry 缓存和 inode 缓存是常见的回收来源。可以通过 /proc/sys/vm/vfs_cache_pressure 调节回收力度:
- 默认值 100:内核默认的回收倾向
- 值 > 100:更激进地回收 dentry/inode 缓存(适合有大量文件但内存紧张的场景)
- 值 < 100:保留更多缓存(适合有大量重复读取文件的应用)
三、虚拟内存管理与页面回收
3.1 地址空间布局
在 64 位系统中,Linux 使用 48 位虚拟地址空间(未来可扩展至 57 位):
用户空间(128 TB) 内核空间(128 TB)
┌──────────────────┐ ┌──────────────────────────┐
│ Stack (向下增长) │ │ Fixmaps / Module Space │
│ ↓ │ │ vmalloc / ioremap Area │
│ Memory Mapping │ │ Direct Mapping Area │
│ ↑ │ │ Contiguous Mapping │
│ Heap (向上增长) │ │ Kernel Text/Data │
│ BSS / Data │ └──────────────────────────┘
│ Text (代码段) │
└──────────────────┘
3.2 LRU 页面回收算法
Linux 使用改进的双链 LLRU(Least Recently Used)算法来管理页面活跃度。每个页面有一个 PG_active 标志位,分别处于 active_list 或 inactive_list。页面首次被访问时进入 inactive_list,第二次被访问时从 inactive 提升到 active。回收时优先从 inactive_list 尾部扫描和回收。
可以通过 /proc/meminfo 查看当前 LRU 状态:
cat /proc/meminfo | grep -E "Active|Inactive"
Active: 6742032 kB
Inactive: 4218944 kB
Active(anon): 5231104 kB
Inactive(anon): 2048000 kB
Active(file): 1510928 kB
Inactive(file): 2170944 kB
3.3 实战:swappiness 调优
vm.swappiness 参数(0-200)控制内核使用 swap 的倾向:
- 0:仅在内存极其紧张时才使用 swap(Linux 3.5+ 语义)
- 60(默认): 平衡策略
- 100+:更积极使用 swap,可能将 inactive 页面换出以保留更多文件缓存
在数据库/大数据场景中,通常希望减少 swap 以保证 IO 性能,建议设置为 1-10:
sysctl -w vm.swappiness=1
echo "vm.swappiness=1" >> /etc/sysctl.conf
四、OOM Killer 机制与防御
4.1 OOM 触发流程
当系统物理内存和 swap 全部耗尽,且页面回收仍无法满足分配需求时,内核触发 OOM Killer。它会计算每个进程的 oom_score(基于内存占用、运行时间、优先级等),选择分数最高的进程杀死以释放内存。
4.2 实战:排查 OOM 事件
# 查看历史 OOM 事件
dmesg | grep -i "out of memory" | tail -10
journalctl -k | grep -i "oom" | tail -10
# 查看各进程的 OOM 评分
ps aux | awk '{print $2}' | xargs -I {} cat /proc/{}/oom_score 2>/dev/null | \
sort -rn | head -20
# 保护关键进程(将其 oom_score_adj 设为 -1000 可免于 OOM 被杀)
echo -1000 > /proc/$(pidof mysqld)/oom_score_adj
4.3 cgroup v2 内存限制
现代容器化环境中,推荐使用 cgroup v2 精细控制内存:
# 创建 cgroup 限制内存为 4GB
mkdir /sys/fs/cgroup/myapp
echo "4G" > /sys/fs/cgroup/myapp/memory.max
echo "3.5G" > /sys/fs/cgroup/myapp/memory.high # 软限制,触发回收但不杀进程
# 将进程加入该 cgroup
echo $(pidof myapp) > /sys/fs/cgroup/myapp/cgroup.procs
# 监控事件
cat /sys/fs/cgroup/myapp/memory.events
# 输出:high 0, max 0, oom 0, oom_kill 0
五、NUMA 架构内存优化
5.1 NUMA 访问延迟
在 NUMA(Non-Uniform Memory Access)系统中,CPU 访问本地节点内存快于访问跨节点内存。不合理的内存分配会导致严重的跨节点访问延迟(通常是 1.5~2 倍)。
# 查看 NUMA 拓扑
numactl --hardware
# 查看进程的 NUMA 内存分布
numastat -p $(pidof myapp)
# 将进程绑定到本地节点内存分配
numactl --membind=0 --cpunodebind=0 myapp
5.2 自动 NUMA 平衡
Linux 4.0+ 引入了 Automatic NUMA Balancing(自动 NUMA 平衡),通过采样页面访问,将频繁远程访问的页面迁移到本地节点:
# 查看是否启用
cat /proc/sys/kernel/numa_balancing
# 1 = 启用,0 = 禁用
# 对于数据库等性能敏感应用,如果已做显式绑定,建议关闭自动平衡
sysctl -w kernel.numa_balancing=0
六、性能调优实战案例
6.1 案例一:Huge Pages 降低 TLB Miss
对于大内存数据库(如 Redis、MySQL、Oracle),常规 4KB 页导致 TLB(Translation Lookaside Buffer)miss 频繁。使用 2MB/1GB 的 Huge Pages 可显著降低 TLB miss。
/proc/sys/vm/nr_hugepages
# 持久化配置
echo "vm.nr_hugepages=512" >> /etc/sysctl.conf
# 在 /etc/fstab 挂载 hugetlbfs
hugetlbfs /dev/hugepages hugetlbfs pagesize=2M 0 0
# MySQL 配置使用 huge pages
# [mysqld]
# large-pages
# 透明大页(THP)适合通用场景,但不推荐数据库使用
cat /sys/kernel/mm/transparent_hugepage/enabled
# [madvise] always ← 推荐设为 madvise 或 never(数据库场景)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
6.2 案例二:内存 cgroup 压力分析
通过 PSI(Pressure Stall Information)监控内存压力:
# /proc/pressure/memory 提供内存压力指标
cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=123456789
full avg10=0.00 avg60=0.00 avg300=0.00 total=123456789
# some = 至少有一个任务因内存压力被阻塞的时间占比
# full = 所有任务同时因内存压力被阻塞的时间占比
基于 PSI 可实现智能的水平扩缩容、提前迁移或优雅降级,比传统"看空闲内存"的方式更准确反映真实压力。
6.3 案例三:slab 缓存暴涨排查
某线上服务运行数天后 RSS 持续增长,排查发现 dentry 缓存未回收:
/proc/sys/vm/drop_caches # 清除 slab 可回收对象
# (echo 1 清除 pagecache, echo 2 清除 slab, echo 3 全清)
# 4. 调整 vfs_cache_pressure 为更激进的值
sysctl -w vm.vfs_cache_pressure=200
# 5. 如果是容器场景,检查是否正确设置了 cgroup 内存限制
cat /sys/fs/cgroup/memory/memory.limit_in_bytes
七、总结与核心参数速查表
Linux 内存管理涉及众多可调整参数,以下是生产环境中高频使用的参数汇总表:
| 参数 | 默认值 | 推荐值 | 说明 |
|---|---|---|---|
| vm.swappiness | 60 | 1-10(数据库)/ 60(通用) | swap 使用倾向 |
| vm.dirty_ratio | 20 | 10-40 | pagecache 触发刷盘的脏页比例阈值 |
| vm.dirty_background_ratio | 10 | 3-10 | 后台 pdflush 开始回写脏页的阈值 |
| vm.min_free_kbytes | 自动计算 | sqrt(内存GB*1024*1024)*12 | 最小保留空闲内存 |
| vm.vfs_cache_pressure | 100 | 50-200 | dentry/inode 缓存回收倾向 |
| vm.overcommit_memory | 0 | 0(启发式)/ 2(严格) | 内存超分配策略 |
| kernel.numa_balancing | 1 | 1(通用)/ 0(数据库) | 自动 NUMA 页面迁移 |
| vm.nr_hugepages | 0 | 按应用需求 | 预留 2MB 大页数量 |
内存管理是系统与性能之间的平衡艺术。每个工作负载都有其独特的内存访问模式,理解内核机制后,才能做出最适合的调优决策。建议在调整任何参数前,使用 perf、vmstat、sar 等工具收集 baseline 数据,变更后再对比验证调优效果。

发表评论 取消回复