前言:为什么理解Linux仍然重要

在云计算、容器化和边缘计算的今天,Linux 内核几乎无处不在。从你的手机到全球排名前 500 的超级计算机,无一不在 Linux 上运行。而内存管理作为 Linux 内核的核心子系统之一,直接决定了系统的性能、稳定性和响应速度。无论你是系统管理员、后端开发,还是 DevOps 工程师,深入理解 Linux 的内存管理机制都是通往高级技术能力的必经之路。

这篇文章将从内核源码级别剖析 Linux 的内存管理子系统,涵盖物理内存管理、虚拟内存机制、页面分配器演进(伙伴系统 → SLUB)、交换与回收机制、以及当今热门技术在内存子系统中的实现。

一、物理内存管理:从 NUMA 到 memblock

1.1 NUMA 架构下的内存拓扑

现代服务器通常采用 NUMA(Non-Uniform Memory Access)架构,每个 CPU 节点拥有本地内存,同时可以访问远程节点内存,但访问延迟更高。Linux 内核通过 pg_data_t 结构体来描述每个 NUMA 节点的内存信息:

typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];    // 内存区域
    struct zoneref node_zonelists[MAX_ZONETYPES]; // 分配区域列表
    int nr_zones;                             // 区域数量
    struct page *node_mem_map;                // 页面描述符数组
    unsigned long node_start_pfn;             // 起始页帧号
    unsigned long node_present_pages;         // 可用页面总数
    unsigned long node_spanned_pages;         // 跨度页面总数
} pg_data_t;

在 x86_64 架构下,每个物理页帧都由一个 struct page 描述(存储在 mem_map 数组中),每个 struct page 大约 64 字节。这意味着每 1GB 物理内存需要约 16MB 的 struct page 元数据空间和 4MB 的页表开销。

1.2 memblock → buddy 的演进

内核启动早期阶段使用 memblock 作为物理内存分配器,它简单但低效。当核心内存子系统初始化完成后,memblock 会将所有物理内存交给伙伴系统(Buddy System)管理,随后自身被标记为"dead":

// 内核启动流程
start_kernel()
  └── setup_arch()
  └── mm_init()
  └── memblock_alloc()
  └── buddy_system_init()  // 接管物理内存管理
  └── memblock_free_all()  // 释放 memblock 保留的内存

二、伙伴系统:物理页分配的核心

2.1 算法原理

伙伴系统的核心思想是将物理内存划分为不同阶(order)的连续块,每个阶 k 管理 2^k 个连续物理页:

  • Order-0: 1 页 (4KB)
  • Order-1: 2 页 (8KB)
  • Order-2: 4 页 (16KB)
  • ...
  • Order-10: 1024 页 (4MB)

分配时,如果目标阶没有空闲块,就从更高阶拆分出一半;释放时,如果相邻的"伙伴块"也是空闲的,就合并成更大的块。

2.2 源码级剖析

// mm/page_alloc.c
static inline struct page *__alloc_pages_nodemask(
    gfp_t gfp_mask, unsigned int order,
    int preferred_nid, nodemask_t *nodemask)
{
    struct page *page;
    unsigned int alloc_flags = ALLOC_WMARK_LOW;
    
    // 1. 快速路径:从 PCU 缓存分配
    page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac);
    if (likely(page))
        goto out;
    
    // 2. 慢速路径:页面回收与直接压缩
    page = __alloc_pages_slowpath(alloc_gfp, order, &ac);
    
out:
    return page;
}

__alloc_pages_nodemask 是所有物理页分配的入口函数,几乎所有内核分配路径最终都会到达这里。它包含快速路径和慢速路径两部分,快速路径优先尝试从 Per-CPU 缓存(PCPU lists)分配,失败则进入涉及水标检查、内存回收的慢速路径。

2.3 GFP 标志与分配语义

gfp_t 标志决定了内核内存分配的行为方式:

Flags含义典型场景
GFP_KERNEL标准内核分配,允许睡眠通用分配
GFP_ATOMIC不允许睡眠,可访问紧急保留池中断上下文、自旋锁内
GFP_DMA分配 ISA DMA 可用内存(<16MB)老旧 ISA 设备驱动
GFP_DMA32分配 32 位 DMA 可用内存(<4GB)32 位 PCI 设备
GFP_NOIO不允许 I/O 操作块层内部分配
GFP_NOFS不允许文件系统调用文件系统内部分配
__GFP_ZERO分配后清零内存安全敏感场景

三、SLUB 分配器:小对象分配的艺术

3.1 slab 分配器的设计动机

伙伴系统以页面(4KB)为最小分配单位,而内核需要大量小对象(task_struct、inode、dentry 等)。直接使用伙伴系统会造成严重的内部碎片。为此,Linux 实现了 slab 分配器作为二级分配机制。

3.2 slab → slub → slob 三兄弟

  • slab:经典的 slab 分配器,每个 CPU 维护完整的 slab 页面队列,缓存友好但管理开销大
  • slub(当前默认):简化版,使用通用链表管理 slab,内存占用更低,调试更友好
  • slob:极简版,适合嵌入式系统(内存 < 64MB 场景)

SLUB 分配器的核心数据结构:

struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;      // Per-CPU slab
    unsigned long flags;
    unsigned int size;                    // 对象实际大小
    unsigned int object_size;             // 用户请求大小
    unsigned int offset;                  // 下一个空闲对象偏移
    
    struct kmem_cache_node *node[MAX_NUMNODES]; // NUMA 节点
    struct page *partial;                 // 部分空闲 slab 链表
    struct page *full;                    // 已满 slab 链表
};

3.3 slub 分配流程

快速路径(fast path):直接从 kmem_cache_cpu→freelist 取出第一个空闲对象。这是 slub 最高效的路径,单条指针操作即可完成。

中速路径:当前 CPU 的 freelist 为空时,检查 CPU 是否有 partial slab。如果有,就从 partial slab 中获取对象,并迁移到 freelist。

慢速路径(slow path):向伙伴系统申请新的 slab 页面。这涉及 alloc_pages() 调用,可能触发回收。

3.4 slab 调试功能

SLUB 内建了强大的调试能力:

  • Red zoning:在对象末尾放置特殊标记,检测越界写入
  • Poisoning:释放后填充特定字节模式(0x6b = "free"),检测 use-after-free
  • Tracking:记录每个对象的分配/释放调用栈

启用方式:slub_debug=UFP 内核参数(U=用户跟踪,F=失败保护,P=中毒)。

四、虚拟内存:地址转换与内存保护

4.1 多级页表结构

x86_64 使用四级页表(PGD→PUD→PMD→PTE),每个表项 8 字节,每级 512 项(9 位索引),总共可以映射 256TB 虚拟地址空间:


Virtual Address (48-bit used):
┌────────┬────────┬────────┬────────┬──────────────┐
│ PGD[9] │ PUD[9] │ PMD[9] │ PTE[9] │  Offset[12]  │
└────────┴────────┴────────┴────────┴──────────────┘
     │          │         │         │
     ▼          ▼         ▼         ▼
  512 entry  512 entry  512 entry  512 entry

CR3 寄存器存储 PGD 基地址,每次上下文切换时更新。用户态/内核态切换时 CR3 不变,但内核页表包含所有物理内存的映射。

4.2 大页(Huge Pages)机制

标准 4KB 页面对现代应用(数据库、虚拟机)会造成巨大的 TLB 压力和页表内存开销。Linux 提供了两种大页方案:

传统 HugeTLB:启动时预先预留,vm.nr_hugepages 参数控制。仅匿名映射可用,缺失则直接段错误。

Transparent Huge Pages(THP):内核自动将普通页合并成 2MB 大页(只支持匿名映射)。虽然易于启用,但存在三个问题:

  • 合并/拆分操作引发延迟抖动
  • 内核 khugepaged 后台线程消耗 CPU
  • 某些场景(内存数据库)下导致性能下降

推荐配置:transparent_hugepage=madvise(需程序显式申请)或针对不同场景关闭。

五、页面回收:OOM 之前的最后防线

5.1 页面回收触发路径

当系统内存紧张时,内核通过三个水标触发回收:

  • high:有充足空闲,不需要回收
  • low:开始唤醒 kswapd 背景回收
  • min:分配器进入直接回收(direct reclaim)

直接回收会阻塞调用者性能影响很大。

5.2 LRU 算法与两类页面

Linux 使用双 LRU 链表(active/inactive)管理页面回收:

LRU List页面类型典型内容
Active Anonymous活跃匿名页进程堆、栈
Inactive Anonymous不活跃匿名页待交换的堆内存
Active File活跃文件缓存频繁读取的文件页
Inactive File不活跃文件缓存可丢弃的文件页
Unevictable不可驱逐mlock、ramfs

回收优先级:Inactive File > Inactive Anonymous > Active File > Active Anonymous(大页)。

5.3 Swap 机制

交换空间可以是swap 分区(文件系统的 10-20%)或swap 文件。新内核支持 zswap ——通过压缩匿名页减少 I/O,zram —— 将 RAM 的一部分模拟为压缩交换设备,在嵌入式和服务器场景都非常受欢迎。

5.4 OOM Killer:末日的审判

当所有回收手段耗尽仍无法释放足够内存时,OOM Killer 会根据 oom_score 选择进程终止。评分公式大致为:

oom_score = (物理内存占用比例 × 1000) + 用户调整值 (oom_score_adj)

保护关键进程的方法:设置 oom_score_adj=-1000 使其永远不会被杀死(如 systemd、sshd)。

六、高级话题与性能调优

6.1 CMA:连续内存分配器

视频播放、摄像头、GPU 等需要大段连续物理内存。CMA(Contiguous Memory Allocator)在系统启动时预留一块连续区域,平时可被当作普通内存使用,有需求时通过页面迁移腾出连续区域。

6.2 内存控制组(memcg v2)

cgroup v2 的 memory 控制器提供精细化的内存控制:

  • memory.min:硬性保证的最小内存限制(不会被回收)
  • memory.low:软性保护(优先不回收)
  • memory.high:软性限制(超过触发 throttle)
  • memory.max:硬性上限(超过触发 OOM)

Kubernetes 中 Pod 的 memory request 对应 memory.min,limit 对应 memory.max。

6.3 perf 与 ftrace 实战

内存相关问题的诊断工具:

# 查看系统内存概况
$ free -h
$ cat /proc/meminfo

# 查看大页配置
$ cat /proc/sys/vm/nr_hugepages

# slab 使用情况(使用 slabtop 或以下命令)
$ slabtop -o
$ cat /proc/slabinfo

# 查看进程内存映射
$ pmap -x <pid>
$ cat /proc/<pid>/smaps | grep -i huge

# 追踪页面回收事件
$ trace-cmd record -e vmscan:mm_vmscan_kswapd_wake

# 使用 BPF 分析分配热点
$ bpftrace -e 'kprobe:__alloc_pages_nodemask { @[comm] = count(); }'

总结

Linux 内存管理是一个层层递进的复杂系统:最底层是 memblock/伙伴系统管理物理页,中间层是 SLUB/slab 负责小对象分配,最上层是虚拟内存(多级页表 + swap + 页面回收)为用户提供连续的地址空间。

理解这些机制,不仅能帮助你在遇到性能问题时快速定位,还能在设计分布式系统、容器编排、高性能应用时做出更好的决策。下次当你看到一个 OOM 告警或一次毛刺延迟时,希望你能从内核级的视角理解发生了什么。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部