引言

Linux 内核的内存管理是整个操作系统中最核心、最复杂的子系统之一。它负责物理内存的分配与回收、虚拟地址空间的映射与管理、页面置换与回收、以及用户态与内核态之间的内存交互。本文将深入剖析 Linux 内核内存管理的完整机制,从物理页面的 Buddy System 到虚拟地址的 VMA 映射,从 Slab 分配器到反向映射机制,带你全面理解这一复杂而优雅的子系统。

一、物理内存管理:Buddy System

1.1 核心概念

物理内存以页面为单位进行管理,x86_64 架构下默认页面大小为 4KB。内核使用 Buddy System(伙伴系统)来管理物理页面,其核心思想是将内存划分为不同阶(order)的连续页面块,阶为 n 时包含 2^n 个连续页面。

1.2 alloc_pages 分配流程

/* 分配 2^order 个连续物理页面 */
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

/* 分配单个页面 */
struct page *alloc_page(gfp_t gfp_mask);

/* 将 page 转为虚拟地址进行访问 */
void *page_address(const struct page *page);

分配流程:首先尝试当前阶的空闲链表,如果为 NULL 则向更高阶分裂,分裂时将一半挂入低阶链表,另一半返回给调用者。释放时则检查伙伴页面是否空闲,若空闲则合并为更高阶块。

1.3 页面分配器层次结构

  • Zone 分配器:将物理内存划分为 DMA、Normal、HighMem 等区域
  • Per-CPU 缓存:使用 per_cpu_pageset 缓存避免锁竞争,批量补充/回收
  • 伙伴系统:管理实际的页面分配与释放

二、Slab 分配器:内核对象的内存分配

2.1 为什么需要 Slab?

Buddy System 以页面(4KB)为最小单位,而内核中大量对象远小于 4KB(如 task_struct ~1.7KB、inode ~600B、dentry ~200B)。若直接使用 Buddy System 会造成严重的内部碎片浪费。Slab 分配器在页面之上构建对象缓存池,实现小对象的高效分配与释放。

2.2 三代演进

  • Slab(Linux 2.2 引入):原始的 slab 分配器,缓存链表较复杂
  • Slub(Linux 2.6.23 成为默认):简化设计,性能更好,当前主流
  • Slob:面向嵌入式系统的极简分配器

2.3 Slub 核心数据结构

struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;    /* per-CPU 活跃 slab */
    struct kmem_cache_node *node[MAX_NUMNODES]; /* NUMA node 部分空 slab */
    unsigned long min_partial;        /* 最小 partial 数 */
    size_t size;                      /* 对象实际大小 */
    size_t object_size;              /* 用户请求大小 */
    unsigned int offset;            /* freepointer 偏移 */
    const char *name;               /* 缓存名称 */
    struct list_head list;            /* 全局链表 */
    int refcount;                    /* 引用计数 */
    void (*ctor)(void *);           /* 构造函数 */
};

2.4 kmem_cache_alloc 分配流程

分配路径检查顺序:

  1. cpu_slab->freelist:per-CPU 无锁快速路径,直接取 freelist 头部
  2. cpu_slab->partial:当前 slab 的 partial 页面中有空闲对象
  3. node->partial:NUMA node 层面的部分空闲 slab 链表
  4. Buddy System:分配全新 slab

三、虚拟内存管理:VMA 与页表

3.1 虚拟地址空间布局

x86_64 Linux 进程的虚拟地址空间从 0x0000000000000000 到 0x00007fffffffffff(用户态 128TB),内核空间从 0xffff800000000000 开始。用户空间包括代码段、数据段、堆、内存映射区、栈等。

3.2 vm_area_struct (VMA)

struct vm_area_struct {
    unsigned long vm_start;        /* 区域起始地址 */
    unsigned long vm_end;          /* 区域结束地址 */
    struct mm_struct *vm_mm;       /* 所属 mm_struct */
    pgprot_t vm_page_prot;         /* 访问权限 */
    unsigned long vm_flags;        /* 标志位 */
    struct rb_node vm_rb;          /* 红黑树节点 */
    const struct vm_operations_struct *vm_ops; /* 操作函数表 */
    struct file *vm_file;          /* 映射的文件 */
    unsigned long vm_pgoff;        /* 文件偏移 */
};

3.3 页表遍历与 Page Fault

x86_64 使用 4 级页表(PML4 → PDPT → PD → PT),当 CPU 访问未映射的虚拟地址时触发 #PF(Page Fault)。内核处理函数 do_page_fault 的路径:

  • 用户态缺页:查找 VMA → 按需分配页面 → 建立映射
  • 用户态写时复制:复制页面 → 标记可写
  • 内核态缺页:vmalloc区直接映射,线性区 panic

四、页面回收与交换

4.1 LRU 算法与双链表

Linux 使用 LRU(Least Recently Used)算法决定哪些页面应被回收。自 2.6 内核起采用 Active/Inactive 双链表设计:

  • Active List:最近被访问过的页面,不易回收
  • Inactive List:长时间未访问的页面,优先回收

4.2 kswapd 守护进程

kswapd 是内核内存回收的后台守护进程。当空闲页面低于 High Watermark 时主动唤醒,通过直接回收(direct reclaim)和后台回收(background reclaim)两种模式维持内存水位。

4.3 swap 交换机制

  • 匿名页面:通过 swap 写入磁盘交换分区
  • 文件页面:直接丢弃(干净页)或回写(脏页)
  • swappiness:内核参数(/proc/sys/vm/swappiness),默认值60,控制匿名页与文件页的回收倾向

五、NUMA 内存管理

5.1 NUMA 感知分配

NUMA 系统中,CPU 访问本地节点的内存远快于跨节点访问。Linux 的 NUMA 策略:

  • 本地优先:优先从请求 CPU 所在的 node 分配内存
  • Fallback:本地不足时可按 fall back 顺序从其他节点分配
  • NUMA Balancing:内核自动将页面迁移到访问者的本地节点

5.2 自动 NUMA 均衡

Linux 4.7+ 引入 Auto NUMA Balancing,内核通过采样页面访问情况,将频繁跨节点访问的页面迁移到发起访问的 CPU 所在节点。触发参数 /proc/sys/kernel/numa_balancing。

六、实战调优案例分析

6.1 透明大页(Transparent Huge Pages)

THP 允许内核自动将连续小页面(4KB)合并为大页(2MB),减少 TLB miss 次数。但在数据库等高并发场景下可能反而导致延迟抖动:

# 查看 THP 状态
cat /sys/kernel/mm/transparent_hugepage/enabled

# 禁用 THP(推荐数据库场景)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

6.2 OOM Killer

当系统内存极度不足时,OOM Killer 根据 oom_score 选择进程终止:

# 查看进程 OOM 评分
cat /proc/[pid]/oom_score

# 调整 OOM 保护值(-1000 禁止 kill)
echo -17 > /proc/[pid]/oom_score_adj

6.3 内存碎片化问题

长时间运行后物理内存可能碎片化,导致无法分配连续大块内存。Linux 提供 compaction 机制整理碎片:

# 触发内存碎片整理
echo 1 > /proc/sys/vm/compact_memory

七、前沿发展方向

  • Multi-Gen LRU(Linux 6.1 引入):基于多代 LRU 的页面回收,性能显著优于传统 active/inactive 双链表,减少扫描开销,提升回收精度
  • Unmap Delay:延迟非映射页面的释放以提升吞吐量,减少频繁 free/alloc 的锁开销
  • 用户态内存管理(SPDK/DPDK):面向高性能 I/O 场景的用户态内存映射与大页分配,绕过内核带来数量级的延迟降低
  • CXL(Compute Express Link)内存:扩展 NUMA 拓扑为三层结构(本地/远程/CXL-attached),为内存池化提供硬件基础
  • DAMON(Data Access MONitor):基于硬件采样的高效数据访问监控,辅助自适应页面回收和大页合并决策

总结

Linux 内核内存管理是一个精密的层次化系统:底层的 Buddy System 管理物理页面分配,Slab 分配器在其之上实现内核对象的高效缓存,VMA 与多级页表构建虚拟地址空间,LRU 算法与 kswapd 守护进程负责页面回收,NUMA 感知策略优化多路服务器性能。理解这些机制不仅是内核开发的基础,也是系统性能调优、故障诊断的核心技能。随着 Multi-Gen LRU、CXL 内存、DAMON 等新特性的不断演进,Linux 的内存管理将继续在大规模、高性能场景下实现新的突破。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部