内存管理是 Linux 内核最复杂且最核心的子系统之一。它不仅决定了系统资源的利用效率,更直接影响应用的响应延迟和系统稳定性。本文将从内核源码级别,全面剖析 Buddy System(伙伴系统)、Slab/Slob/Slub 分配器的工作原理,并结合实际案例讲解 OOM Killer 的行为机制与调优策略。

一、物理内存的组织架构:Node与Zone

Linux 内核采用 NUMA(Non-Uniform Memory Access)架构来管理物理内存。整个物理内存被组织为以下层次结构:

NUMA Node
  └── Memory Zone
        └── Page Frame (4KB)

每个 NUMA 节点(pg_data_t)包含多个内存区域(Zone),主要类型包括:

  • ZONE_DMA:0-16MB,用于老旧 ISA 设备的 DMA 操作
  • ZONE_DMA32:16MB-4GB,用于 32 位 DMA 设备
  • ZONE_NORMAL:直接映射到内核虚拟地址空间(x86 下通常为 1GB 或 2GB)
  • ZONE_HIGHMEM:超过直接映射范围的物理内存(仅 32 位系统)

可以通过以下命令查看当前系统的内存区域分布:

$ cat /proc/zoneinfo | head -100
Node 0, zone   DMA
  pages free     3960
        min      12
        low      15
        high     18
        spanned  4095
        present  3997
        managed  3975
        protection: (0, 2006, 3007, 3684)

其中 watermark(min/low/high)是页面回收的关键阈值:当空闲页面低于 low 时,内核启动后台回收(kswapd);低于 min 时,直接进行同步回收(direct reclaim)。

二、Buddy System(伙伴系统)详解

2.1 核心思想

Buddy System 是物理页框(Page Frame)的分配器,负责以页(通常 4KB)为单位分配和释放连续物理内存。其核心机制是按需分裂与合并,通过维护 11 个空闲链表(order 0~10),分别管理 2<sup>n</sup> 个连续页面组成的内存块。

// 核心数据结构:free_area
struct free_area {
    struct free_list free_list[MIGRATE_TYPES];
    unsigned long    nr_free;
};

// 每个节点的内存区域中管理分页
struct zone {
    struct free_area free_area[MAX_ORDER];  // MAX_ORDER 在部分版本为11
};

2.2 分配流程

alloc_pages() 是最常用的底层分配接口,其调用链为:

alloc_pages()
  └── alloc_pages_node()
        └── __alloc_pages_node()
              └── __alloc_pages()
                    └── get_page_from_freelist()  // 快速路径
                    └── __alloc_pages_slowpath()  // 慢速路径(触发回收/压缩)

快速路径直接从未分配列表中摘取页面;慢速路径则触发直接内存回收(direct reclaim)、内存规整(compaction),甚至唤醒 OOM Killer。

2.3 伙伴关系的判断

两个页面块被称为"伙伴"需满足三个条件:大小相同(均为 2<sup>n</sup> 页)、物理地址连续、且合并后对齐到 2<sup>n+1</sup> 的边界。判断逻辑如下:

// 计算伙伴块的索引:buddy_pfn = pfn ^ (1 << order)
static inline unsigned long __find_buddy_pfn(unsigned long page_pfn, unsigned int order) {
    return page_pfn ^ (1UL << order);
}

例如,order=2 时(4页=16KB),地址 0x10000~0x14000 的块的伙伴是 0x14000~0x18000,因其索引 X XOR 100 = X+4。

2.4 碎片问题与解决方案

Buddy System 长期运行后会产生外部碎片:虽然总空闲页足够,但没有足够大的连续物理块。内核通过页面迁移类型(page migration type)来缓解这一问题:

  • MOVABLE:可移动页面(用户态内存),可通过页面迁移整理碎片
  • RECLAIMABLE:可回收页面(文件缓存),可直接丢弃重建
  • UNMOVABLE:不可移动页面(内核态 dma_alloc、slab 等)

从内核 5.10 年起,CONFIG_TRANSPARENT_HUGEPAGE 默认开启,通过 khugepaged 后台线程将小页合并为大页(2MB/1GB),有效减少 TLB miss。

三、Slab 分配器:小内存分配的艺术

3.1 为什么需要 Slab?

Buddy System 以页为单位分配对象,但内核频繁创建和销毁的对象(如 task_struct、inode、dentry)往往只有几十到几百字节。若直接调用 alloc_page(),内部碎片率接近 100%。Slab 分配器在 Buddy System 之上构建对象缓存池,实现高效的小对象分配。

3.2 Slab 的三种实现

Linux 支持三种 Slab 变体,当前主流为 SLUB:

实现特点适用场景
Slab最早的实现,复杂的数据结构,维护开销大已废弃
Slob极简实现,用于嵌入式系统内存小于64MB的系统
SLUB为 SMP 优化,默认选项,per-CPU 缓存默认选择

3.3 SLUB 的核心机制

// Slab 缓存描述符
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // per-CPU 快速缓存
    struct kmem_cache_node *node[MAX_NUMNODES]; // Node 级缓存
    unsigned int size;         // 对象实际大小(含对齐)
    unsigned int object_size;  // 用户请求大小
    unsigned int offset;       // 下一个空闲对象的偏移
    struct kmem_cache_order_objects oo; // 每 slab 的 page order 与对象数量
};

分配优先级链:

kmem_cache_alloc()
  └── ____cache_alloc()
        └── cpu_slab       // 第一步:per-CPU 快速缓存(无锁)
        └── cpu partial    // 第二步:per-CPU 部分空闲列表
        └── node partial   // 第三步:Node 级部分空闲列表
        └── alloc_pages()  // 第四步:向 Buddy 申请新页

3.4 kmem_cache 的实际使用

通过 /proc/slabinfo 可以查看所有 slab 缓存的状态:

$ sudo cat /proc/slabinfo | head -20
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : ...
dentry            156823  158000     192   20    1 : tunables ...
inode_cache       24561   25000     648    5    2 : tunables ...
task_struct        256     264    5952    5    2 : tunables ...
signal_cache       380     400    1136   14    4 : tunables ...

各列含义:active_objs 为当前活跃对象数,num_objs 为总对象数(含已释放但缓存的),objsize 为单个对象字节数。

3.5 Slab 调试:检测内存泄漏与越界

CONFIG_DEBUG_KMEMLEAK 可追踪未被释放的分配:

# echo scan > /sys/kernel/debug/kmemleak
# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88800a3b2c00 (size 256):
  comm "test", pid 1234, jiffies 4294901234
  backtrace:
    [<ffffffff81a2b3c4>] kmem_cache_alloc_trace+0x124/0x200
    [<ffffffff81b5c6d7>] my_module_init+0x17/0x1000 [my_module]

四、OOM Killer:最后的防线

4.1 触发条件

当系统内存耗尽且直接回收都无法释放足够页面时,OOM Killer 被激活。触发点在 out_of_memory() 函数中:

alloc_pages()
  └── __alloc_pages_may_oom()
        └── out_of_memory()
              └── select_bad_process()    // 选择"最坏"进程
              └── oom_kill_process()       // 发送 SIGKILL

4.2 oom_score 的计算逻辑

每个进程的 OOM 评分存放在 /proc/[pid]/oom_score,计算方式为:

points = total_vm_pages * 1000 / total_ram_pages + (oom_score_adj * total_ram_pages / 1000 / 100)

其中:

  • 基础分:进程占用内存占总内存比例 * 1000,得分越高越容易被杀
  • 调整值:oom_score_adj 范围 -1000~1000,-1000 表示永不被 OOM杀死

关键影响因素包括:

  • 长时间运行的进程得分更高(内存占用累积效应)
  • root 进程得分降低 3%
  • 直接硬件访问进程得分降低 3%
  • 不能杀死的核心进程:init(pid 1)、内核线程、pid 2

4.3 如何保护关键进程

通过设置 oom_score_adj 保护关键服务:

# 保护数据库,永不被 OOM 杀死
$ echo -1000 > /proc/$(pidof mysqld)/oom_score_adj

# 或 systemd 方式(推荐):
# /etc/systemd/system/mysql.service.d/oom.conf
[Service]
OOMScoreAdjust=-1000

查看当前所有进程的 OOM 评分排名:

$ for pid in $(ls /proc | grep -E '^[0-9]+$'); do
    if [ -f /proc/$pid/oom_score ]; then
        echo "$(cat /proc/$pid/oom_score 2>/dev/null) $(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ')"
    fi
done | sort -rn | head -20

4.4 OOM 行为调优参数

内核 vm.overcommit_memory 控制系统是否允许超额内存分配:

值行为适用场景
0(默认)启发式 overcommit,允许适量超分通用场景
1总是允许 overcommit,永不拒绝科学计算(如 MATLAB)
2严格模式:CommitLimit = Swap + RAM * overcommit_ratio%数据库、金融系统
# 查看当前 CommitLimit
$ grep -i commit /proc/meminfo
CommitLimit:    16565676 kB
Committed_AS:   12367890 kB

五、实战调优案例

5.1 容器内存限制导致频繁 Direct Reclaim

某 Java 虚拟机容器限制为 4GB 内存,访问量大时频繁触发直接回收,P99 延迟飙升。

排查步骤:

# 1. 检查直接回收次数
$ vmstat 1 10
procs -----------memory---------- ---swap-in-----------
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs
 1  4      0 452000 203400 1998100    0    0     0   380 2140 3985

# si/so 不为 0 说明系统压力大
# b 列(不可中断睡眠进程数)> 0 说明存在 IO/内存阻塞

# 2. 检查回收统计
$ sar -B 1 10
pgpgin/s pgpgout/s  fault/s  majflt/s pgfree/s pgscank/s pgscand/s
   128.00    1024.00   1200       4.3      2800      1500     8000

# pgscand/s 持续 > 0 表示前台直接回收频繁

# 3. 检查 watermark
$ cat /proc/zoneinfo | grep -E "Node|zone|high|min|low"

# 解决:增大容器内存限制 + 调整 watermark_scale_factor
$ echo 125 > /proc/sys/vm/watermark_scale_factor  # 默认 10,增大提高 watermark

5.2 OOM 导致数据库被杀的应急排查

凌晨收到报警 MySQL 进程被 OOM Killer 杀死。分析步骤:

# 1. 查内核日志
$ dmesg -T | grep -i "oom\|Out of memory\|Killed process"
[Tue Jan 14 03:15:22 2025] Out of memory: Killed process 1234 (mysqld)
[Tue Jan 14 03:15:22 2025] oom_memcg: memcg=/system.slice/mysql.service ...

# 2. 查看 OOM Killer 详细评分
$ dmesg -T | grep -B20 "Out of memory"
[  +0.000000] oom-kill: constraint=CONSTRAINT_MEMCG,...

# 3. 解决:设置 cgroup memory.high 做软限制 + 保护进程
$ mkdir /sys/fs/cgroup/mysql.slice
$ echo 2147483648 > /sys/fs/cgroup/mysql.slice/memory.high   # 2GB 软限制
$ echo 2684354560 > /sys/fs/cgroup/mysql.slice/memory.max    # 2.5GB 硬限制
$ echo -1000 > /proc/$pid/oom_score_adj                      # 不被杀死

5.3 Slab 内存泄漏导致系统卡顿

排查 dentry 缓存无法释放的问题:

# 直接释放 slab 缓存(临时缓解)
$ echo 2 > /proc/sys/vm/drop_caches   # 释放 dentry 和 inode
$ echo 3 > /proc/sys/vm/drop_caches   # 释放所有缓存(dentry+inode+page cache)

# 永久解决方案:调整 vfs_cache_pressure
$ echo 50 > /proc/sys/vm/vfs_cache_pressure  # 默认 100,降低值减少缓存回收压力
$ echo 10000 > /proc/sys/vm/min_free_kbytes  # 设置最小空闲内存(根据实际内存调整)

六、内核演进趋势

Linux 内存管理子系统仍在快速演进中,未来的主要方向包括:

  • Folio(大叶页):从 5.16 引入,统一管理大页和小页,减少内核代码复杂度
  • MGLRU(Multi-Gen LRU):5.18 引入,通过多代 LRU 更精准地识别冷页面,减少误回收
  • Memory Bypass Architecture:配合 CXL、PMem 等新内存技术,构建异构内存管理
  • Per-Process THP 控制:细粒度控制每个进程是否启用透明大页

七、总结与排查速查表

问题现象排查入口常见原因
系统抖动、延迟抖动sar -B + vmstatDirect Reclaim 频繁,内存压力过大
进程突然被杀dmesg | grep oomOOM Killer 触发
free 显示内存足够但分配失败/proc/zoneinfo外部碎片 + 不可迁移页面过多
Slab 内存持续增长/proc/slabinfo + kmemleak模块内存泄漏或缓存膨胀

理解内存管理的底层机制,是成为Linux系统高手的关键一步。Buddy System 保证了物理内存的高效分配,Slab 分配器优化了高频小对象的复用,OOM Killer 系统运行的最后屏障。三者共同构成了 Linux 可靠的内存管理基石。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部