引言

Linux 内核的内存管理子系统是整个系统中最复杂、最核心的组件之一。它不仅负责管理物理内存的分配与回收,还要为上层应用提供高效、透明的虚拟内存抽象。本文将从内核源码层面,深入剖析 Linux 内存管理的核心机制——伙伴系统(Buddy System)、Slab 分配器、虚拟内存管理以及 OOM Killer,并结合实际案例讲解性能调优与问题排查方法。

1. 物理内存模型与 Zone 架构

Linux 内核将物理内存组织为 Node → Zone → Page 的三层结构。在 NUMA 架构中,每个 CPU 节点拥有一个 pg_data_t 结构;每个节点内又划分为多个 Zone,用于解决历史遗留的 DMA 地址限制问题。

// include/linux/mmzone.h
struct zone {
    unsigned long _watermark[NR_WMARK];  // 水位线: min/low/high
    unsigned long nr_free_pages;          // 当前空闲页面数
    struct free_area    free_area[MAX_ORDER]; // 伙伴系统的空闲区域表
    // ...
};

// 水位线含义
enum zone_watermarks {
    WMARK_MIN,    // 紧急保留内存,分配会触发直接回收
    WMARK_LOW,    // 启动kswapd回收的阈值
    WMARK_HIGH,   // 空闲充足,无需回收
};

在 64 位系统中,Zones 的重要性已大幅降低——ZONE_DMA 和 ZONE_DMA32 仅用于兼容老设备,ZONE_NORMAL 才是内存分配的主要来源。

2. 伙伴系统(Buddy System)

伙伴系统用于管理连续物理页面的分配与释放,核心思想是将页面按阶(Order,以 2 的幂次分组)组织为链表,分配时向上分裂,释放时向下合并。

// mm/page_alloc.c: __alloc_pages_nodemask()
// 这是页面分配的入口函数

page = get_page_from_freelist(alloc_mask, order, ...);
if (unlikely(!page))
    page = __alloc_pages_slowpath(...);

// fast path: 从 free_area 链表中直接取
// slow path: 触发回收、压缩、甚至调用 OOM Killer

分配过程:

  1. 从目标 Zone 的 free_area[order] 链表取一个空闲块
  2. 若目标 Order 无空闲,从更高 Order 分裂(expand())为两个伙伴块
  3. 空闲块伙伴放回低 Order 链表
  4. 触发 __zone_watermark_ok() 检查水位线,决定是否走 slow path

碎片整理(Memory Compaction):当系统运行较长时间后,伙伴系统可能因碎片化而无法分配高阶连续页面。compaction.c 通过迁移页面来聚合空闲页面:

// mm/compact.c: compact_zone()
// 两个扫描器:
// - migration scanner: 找到可移动的页面
// - free scanner: 找到目标空闲区域
// 两者相向移动,将已分配页面移出形成大块连续空闲空间

3. Slab 分配器:小对象高效复用

伙伴系统以页(通常 4KB)为粒度分配,对于内核中频繁创建的小对象(如 task_struct、inode、dentry),直接分配整页会造成严重的内部碎片。Slab 分配器通过在页面内部预先切分和缓存对象,实现近乎 O(1) 的分配速度。

三层架构:

用户调用 kmalloc()
       ↓
   Slab 层(对象缓存管理)
       ↓
   伙伴系统(底层页面获取)
// mm/slab.h
struct kmem_cache {
    struct kmem_cache_cpu __percpu *cpu_slab;  // per-CPU 无锁快速分配
    struct kmem_cache_node *node[MAX_NUMNODES]; // per-Node 后备存储
    unsigned int size;       // 对象(含对齐)大小
    unsigned int object_size; // 用户请求大小
    unsigned long flags;     // SLAB_ACCOUNT 等
    const char *name;
};

// 快速分配路径:cpu_slab→freelist 直接取
// 慢速路径:Node partial 页补充 或向 Buddy 申请新页

三种变体:

  • Slab(Jeff Bonham 设计):原始版本,使用 SLAB/SLAB_FULL 等状态位
  • Slob:面向嵌入式,极简实现,仅百余行代码
  • Slub(当前默认):简化元数据,per-CPU 无锁设计,碎片更小

kmalloc 与 vmalloc 的核心差异:

特性kmallocvmalloc
物理地址连续(直接映射)不一定连续
虚拟地址线性映射区单独的 vmalloc 区间
性能高(无 TLB 刷新)低(需修改页表)
大小通常 ≤ 8KB可达数百 MB
适用场景DMA、硬件交互大内存模块加载

4. 虚拟内存与页表管理

每个进程拥有独立的虚拟地址空间,由 mm_struct 结构描述。32 位系统用户空间约 3GB,64 位系统可达 128TB(x86-64 使用 48 位虚拟地址)。

// include/linux/mm_types.h
struct mm_struct {
    pgd_t *pgd;                           // 全局页目录(PGD)
    struct vm_area_struct *mmap;          // VMA 链表(虚拟内存区域)
    unsigned long total_vm;               // 总映射页面数
    unsigned long locked_vm;              // mlock 锁定的页面
    struct rw_semaphore mmap_sem;         // 保护 VMA 操作的信号量
    // ...
};

多级页表(以 x86-64 为例):

x86-64 采用 4 级页表:PGD → P4D → PUD → PMD → PTE,每级 9 位(512 项),覆盖 48 位(256TB)虚拟地址空间。

// 虚拟地址分解(4KB 粒度)
// | PGD(9) | P4D(9) | PUD(9) | PMD(9) | PTE(9) | Offset(12) |
// 内核通过硬件遍历(Page Table Walk)逐级查找到物理页面
// TLB(Translation Lookaside Buffer)缓存近期映射以加速遍历

VMA(虚拟内存区域):内核不逐页跟踪映射属性,而是将连续且属性相同的区域合并为 VMA。mmap()、brk()、缺页中断都会创建 VMA。/proc/[pid]/maps 就是 VMA 的导出。

5. 页面回收与 OOM Killer

当系统内存不足时,内核通过以下路径回收内存:

  • kswapd:后台守护线程,当 Zone 空闲低于 low 水位线时唤醒,异步回收
  • Direct Reclaim:分配路径同步回收,分配者会阻塞等待
  • OOM Killer:极端情况下直接终止进程以释放大量内存

页面回收的 LRU 算法:

内核使用两个链表(Active/Inactive)实现近似 LRU。页面首次访问加入 Inactive,再次访问提升到 Active。回收时从 Inactive 尾部取页面。Linux 5.0+ 引入了 Multi-Gen LRU(MGLRU),通过世代而非链表实现更高效的页面回收。

// mm/vmscan.c: shrink_lruvec()
// 扫描比例控制:swappiness 参数(0-200)
// swappiness=0: 优先回收 page cache(文件页)
// swappiness=100: 匿名页和文件页相同优先级
// swappiness=200: 激进地回收匿名页(换出)
// 默认值: swappiness=60

OOM Killer 评分机制:

// mm/oom_kill.c: oom_badness()
// 评分公式(简化):
// points = (total_vm * 1000 / total_ram_pages) + oom_score_adj
// 
// - total_vm: 进程占用的页数(越大越容易被杀)
// - oom_score_adj: -1000 到 +1000,-1000 表示禁用 OOM
// - 守护进程、systemd 等关键服务通常设为 -1000
//
// 实际还会考虑:子进程内存、运行时间、特权级等修正

OOM 实战分析:通过 dmesg | grep -i "out of memory" 可以看到 OOM Killer 的决策日志,包括被杀进程的评分、内存占用等信息。/proc/[pid]/oom_score 和 /proc/[pid]/oom_score_adj 可用于监控和调整进程的 OOM 优先级。

6. NUMA 架构下的内存分配

在 NUMA(非统一内存访问)系统中,CPU 访问本地节点的内存速度远快于跨节点访问。内核通过以下策略优化 NUMA 性能:

  • AutoNUMA Balancing:内核自动检测页面访问模式,将频繁被某 CPU 访问的页面迁移到该 CPU 的本地节点
  • zone_reclaim_mode:控制 Zone 回收策略,=1 时优先本地回收而非从远端分配
  • numactl:用户态工具,绑定进程内存策略(interleave/strict/preferred)

7. 性能调优与实战案例

7.1 内存碎片化排查

# 查看伙伴系统碎片情况
cat /proc/buddyinfo
# Node 0, zone   Normal    100   50   20   10    5    2    1    0    0    0    0
# 各 order 对应的空闲块数,order=10 (4MB) 为 0 表示大块连续内存已耗尽

# 查看 compaction 统计
grep -i compact /proc/vmstat
# compact_stall: 因compaction而阻塞的次数
# compact_fail: compaction 失败的次数
# compact_success: compaction 成功的次数

7.2 OOM 事件调查

# 查看最近的 OOM 事件
dmesg | grep -A 30 "Out of memory"

# 查看进程 OOM 评分
cat /proc/<pid>/oom_score
cat /proc/<pid>/oom_score_adj

# 免疫关键进程(如数据库)
echo -1000 > /proc/<pid>/oom_score_adj

7.3 Huge Pages 配置

2MB/1GB 大页可以减少 TLB Miss,显著提升数据库等内存密集型应用的性能:

# 查看大页配置
cat /proc/meminfo | grep Huge

# 预留 512 个大页(每个2MB = 1GB)
echo 512 > /proc/sys/vm/nr_hugepages

7.4 Per-CPU 内存分配陷阱

使用 alloc_percpu() 时要特别注意四舍五入问题。不同架构的 alloc_percpu() 可能会对齐到缓存行,导致实际内存消耗远超预期。

8. 总结与选型指南

场景推荐方案关键字
频繁创建的小对象Slab/kmallockmem_cache_create, kmalloc
大块连续物理内存alloc_pages + orderGFP_KERNEL, __get_free_pages
大块无需物理连续vmallocvmalloc, vfree
DMA 操作dma_alloc_coherentGFP_DMA, dma_pool
实时/中断上下文GFP_ATOMIC不允许睡眠的分配
用户态内存分配malloc/brk/mmapmmap_threshold 调优

Linux 内存管理的核心矛盾是:分配效率与碎片化的平衡。伙伴系统解决了大粒度连续分配问题,Slab 解决了小对象高频分配问题,LRU + compaction 解决了碎片回收问题。理解这三层机制,才能在内核开发和系统调优中做出正确的决策。

对于应用开发者,除了合理设置 oom_score_adj 和 Huge Pages 外,更应关注 cgroup 内存限制、memcg 统计以及 PSI(Pressure Stall Information)等新机制,它们比传统的 free 命令提供更精准的内存压力洞察。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.465683s