Linux内核物理内存管理深度实战:从伙伴系统到OOM Killer的完整链路

一、为什么需要了解内存管理?

在高性能系统调优中,内存分配的速度与碎片率直接影响整个系统的吞吐量。许多工程师知道 kmalloc、vmalloc,却不理解它们背后的伙伴系统(Buddy System)与 SLUB 分配器如何协同工作,也不知道 OOM Killer 的评分机制如何决定进程生死。本文将打通从物理页分配到底层硬件的完整链路,并结合 /proc/buddyinfo、slabtop、oom_score 等工具给出实战调优策略。

二、物理内存的组织架构

2.1 Node → Zone → Page 三级模型

Linux 将物理内存建模为 pg_data_t (Node) → zone → page 三级结构:

  • NUMA Node:每个 CPU _socket_ 对应一个 Node,/sys/devices/system/node/ 可查看拓扑
  • Memory Zone:ZONE_DMA(0-16MB)、ZONE_DMA32(4GB以下)、ZONE_NORMAL(线性映射区)、ZONE_MOVABLE(可迁移页)
  • struct page:每个物理页帧对应一个 struct page(约 64 字节),全局数组 mem_map[] 记录所有页的元信息
// 简化的三级关系
pg_data_t  numa_node_0
 ├── ZONE_DMA     (0x00000000 - 0x00FFFFFF)
 ├── ZONE_NORMAL  (0x01000000 - 0x3FFFFFFF)  -- 线性映射到 PAGE_OFFSET
 └── ZONE_HIGHMEM (仅32位系统需要)

2.2 页表映射与线性映射区

内核空间通过线性映射(direct mapping)直接映射所有物理内存。在 x86_64 上,PAGE_OFFSET 为 0xFFFF888000000000,内核可用简单的 __va(phys) = phys + PAGE_OFFSET 实现 O(1) 物理↔虚拟地址转换。这种设计让伙伴系统分配的物理页可以快速被内核访问。

三、伙伴系统(Buddy System)

3.1 核心思想

伙伴系统将物理内存划分为 11 个空闲链表(order 0~10),分别管理 2⁰、2¹... 2¹⁰(即 1、2、4...1024)个连续物理页块。分配时向上递归分裂,释放时向伙伴合并——这是经典的二分策略。

// mm/page_alloc.c 中的核心数据结构
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
};

3.2 分配与释放的完整流程

alloc_pages(gfp_mask, order) 的执行步骤:

  1. 快速路径:get_page_from_freelist() 直接从当前 order 的空闲链表取页,O(1) 操作
  2. 慢速路径:触发直接回收(direct reclaim)、压缩(compact),甚至唤醒 kswapd
  3. Watermark 检查:min/low/high 三条水位线决定是否需要回收,/proc/sys/vm/min_free_kbytes 控制最小空闲水量
  4. Fallback:使用 __GFP_RECLAIM 标志允许阻塞等待回收

释放时的伙伴合并:

// 伙伴计算:page_idx XOR (1 - page block_order)
// 例: order=3 的块 (0x40~0x47),其伙伴为 (0x48~0x4F)
// 因为 0x40 XOR 0x08 = 0x48
void __free_pages(struct page *page, unsigned int order) {
    int buddy_idx = page_idx ^ (1 - page order);
    // 检查伙伴页是否空闲,若空闲则合并到 order+1 链表
}

3.3 迁移类型:内存碎片化的杀手锏

External fragmentation(外部碎片)是伙伴系统的经典问题。Linux 引入 MIGRATE_TYPES 将页按迁移能力分类:

  • MIGRATE_UNMOVABLE:内核数据结构(页表、slab),不可移动
  • MIGRATE_MOVABLE:用户空间页、文件缓存,可迁移
  • MIGRATE_RECLAIMABLE:可回收的内核对象(dentries、inodes)
  • MIGRATE_ISOLATE:热插拔或离线隔离

通过将可迁移页聚合,memory compaction 可以将零散页整合为大页块,这是透明大页(THP)成功的关键基础。

3.4 实战:监控伙伴系统健康度

$ cat /proc/buddyinfo
Node 0, zone   Normal   28   15   8   4   2   1   2   1   0   0   1
                                  order→  0   1   2   3   4   5   6   7   8   9  10
各order空闲块数:order=6(2块=128页=512KB) order=10(1块=1024页=4MB连续)

碎片化判断:高阶连续块过少 + 低阶块充足 = 严重碎片。可通过 /proc/sys/vm/compact_memory 手动触发压缩。

四、SLAB / SLUB / SLOB 分配器

4.1 SLAB 设计哲学

伙伴系统以页(4KB~4MB)为粒度分配,但内核大量对象只有几十到几百字节(task_struct ~6KB、inode ~600B、dentry ~200B)。为此引入 SLAB 分配器,在页内预分配固定大小的对象池:

  • kmem_cache:管理特定大小的对象池(如 kmem_cache_task_struct)
  • kmem_cache_create():创建 cache,传入 size/align/flags/constructor
  • kmem_cache_alloc() / kmem_cache_free():O(1) 分配/释放

4.2 SLUB:现代内核的选择

Linux 2.6.23 起,SLUB 取代 SLAB 成为默认分配器,解决了 SLAB 的以下问题:p>

  • 取消 metadata inline:SLAB 在每个页的 struct page 旁维护链表,SLUB 仅在kmem_cache结构中跟踪
  • NUMA 感知:每个 Node 维护单独的 partial/full/empty 队列
  • Debug 支持:slab_debug、red_zone、poison 可检测越界写入和 UAF

4.3 Per-CPU 缓存:走向 O(1)

SLUB 的核心优化是 per-CPU cpu_slab:每个 CPU 维护一个本地 freelist,分配/释放时无需加锁。__slab_alloc() 直接从 kmem_cache_cpu->freelist 摘节点。本地队列用完时再从 Node 的 partial 队列补充(这时才需要锁):

// 分配路径(简化)
static void *___slab_alloc(...) {
    // fast path:per-CPU freelist 直接获取
    object = c-next; c-next = get_freepointer(s, object);
    if (likely(object)) return object;

    // slow path:从 node->partial 补充,若仍空则调 new_slab
    object = get_partial(s, node, c);
    if (!object)
        object = new_slab(s, gfpflags, node);
}

4.4 kmalloc 家族

kmalloc() 不是独立的分配器,而是建立在 SLUB cache 之上的薄封装。内核预先创建了一组 kmem_cache,按 2 的幂次作为桶大小:

kmalloc_caches[8] 覆盖 32B~8B → kmalloc-32/kmalloc-64/.../kmalloc-8192
kmalloc(1500, GFP_KERNEL) → 命中 kmalloc-2048
kmalloc(9000, GFP_KERNEL) → 超出桶范围,降级走 alloc_pages(由 MAX_SIZE 决定)

五、vmalloc vs kmalloc vs kmem_cache_alloc

三种分配方式的关键差异:

  • kmalloc(size, flags):返回线性映射的虚拟地址,物理上连续,上限通常 4MB~8MB
  • vmalloc(size):返回 vmalloc 区的虚拟地址,物理上可非连续,适合大块分配(如加载内核模块),但有 TLB 效率开销
  • kmem_cache_alloc(cache, flags):专用 cache,O(1) 速度 + SLUB 保护,适合高频分配特定对象

六、OOM Killer:最后的防线

6.1 触发条件

当快速分配→慢速分配→direct reclaim→compact 全部失败,且 gfp_mask 允许触发 OOM 时,调用 out_of_memory()。触发路径:alloc_pages → __alloc_pages_may_oom → out_of_memory。

6.2 oom_badness 评分机制

OOM Killer 通过 oom_badness() 为每个进程打分,评分公式:

// mm/oom_kill.c
points = total_vm (RSS + swap + page_table * scale)
adjust  = (1 - oom_score_adj)
oom_points = points + (points * oom_score_adj / 1000)

关键规则:

  • 占用内存越多,分数越高(越可能被杀)
  • oom_score_adj 范围 -1000~+1000,-1000 表示 OOM_DISABLE(永不被杀)
  • root 进程额外加 3% 加分
  • 运行不足 1 秒的进程有保护阈值
  • /proc/pid/oom_score 可实时查看当前分数

6.3 实战保护与调优

# 保护关键服务
echo -1000 - /var/run/sshd/oom_score_adj
echo -17   - /usr/bin/redis-server

# 全局策略:panic on OOM(适合控制节点)
sysctl vm.panic_on_oom=1
sysctl kernel.panic=10

# cgroup 级别限制:
# memory.max 触发 cgroup OOM(不触发全局 killer)
echo "1G" - /sys/fs/cgroup/myapp/memory.max

# 提前触发回收(减少硬阻塞)
sysctl vm.min_ratio=5 # 该 NUMA node 空闲低于 5% 时直接 reclaim
sysctl vm.vfs_cache_pressure=100 # 缓存回收积极度(默认 100,降低可保留更多 dentry cache)

七、高级话题

7.1 透明大页(THP)

THP(Transparent Huge Pages)利用 2MB(x86)或 1GB 大页减少 TLB miss。启用路径:khugepaged 内核线程在后台扫描可合并的连续小页区域。
注意:数据库(如 PostgreSQL、Redis)通常建议关闭 THP(echo never /sys/kernel/mm/transparent_hugepage/enabled),因为大页合并延迟可能导致尾延迟飙升。

7.2 CMA(Contiguous Memory Allocator)

CMA 预留一块可迁移大内存区,平时供可移动页使用,关键时刻归还给需要连续内存的设备(如 Camera、GPU 显存)。核心原理是基于可迁移类型的伙伴系统隔离。

7.3 Memory Cgroup(v2)

cgroup v2 的 memory 控制器提供 memory.min(硬保护)、memory.low(尽力保护)、memory.high(限速触发器)、memory.max(硬上限)四级隔离,是容器化场景的内存 QoS 基石。

八、性能调优清单

# 1. 监控碎片化
watch -n1 "cat /proc/buddyinfo"
cat /proc/pagetypeinfo | grep -E "free|Movable"

# 2. 监控 slab 使用
slabtop -o  # 按大小排序
cat /proc/slabinfo | grep -E "dentry|inode|task_struct"

# 3. 查看 OOM 候选
for pid in /proc/[0-9]*/oom_score; do echo "$(cat $pid) $(awk '{print $2}' $(dirname $pid)/comm)"; done | sort -rn | head

# 4. watermark 调优
sysctl -w vm.min_free_kbytes=524288  # 256MB 系统留 512MB 是神话,默认按内存比例
sysctl -w vm.swappiness=10           # 关闭 swap 倾向(默认60)

# 5. 触发压缩
echo 1 - /proc/sys/vm/compact_memory

# 6. NUMA 感知
numactl --cpunodebind=0 --membind=0 ./my_app
numastat -cm | head -5

九、总结

Linux 内核物理内存管理是一个精妙的层级架构:伙伴系统以页为单位管理连续物理块,SLUB 在页内为高频小对象提供 O(1) 池化分配,OOM 则在资源枯竭时通过评分算法做出艰难但合理的牺牲。理解这套机制,是从 "会用 kmalloc" 走向 "能调优系统内存行为" 的关键一步。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部