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.swappiness601-10(数据库)/ 60(通用)swap 使用倾向
vm.dirty_ratio2010-40pagecache 触发刷盘的脏页比例阈值
vm.dirty_background_ratio103-10后台 pdflush 开始回写脏页的阈值
vm.min_free_kbytes自动计算sqrt(内存GB*1024*1024)*12最小保留空闲内存
vm.vfs_cache_pressure10050-200dentry/inode 缓存回收倾向
vm.overcommit_memory00(启发式)/ 2(严格)内存超分配策略
kernel.numa_balancing11(通用)/ 0(数据库)自动 NUMA 页面迁移
vm.nr_hugepages0按应用需求预留 2MB 大页数量

内存管理是系统与性能之间的平衡艺术。每个工作负载都有其独特的内存访问模式,理解内核机制后,才能做出最适合的调优决策。建议在调整任何参数前,使用 perf、vmstat、sar 等工具收集 baseline 数据,变更后再对比验证调优效果。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部