Linux 内核内存管理机制深度解析:从伙伴系统到 Slab 分配器

内存管理是 Linux 内核最核心的子系统之一,它直接关系到系统的性能、稳定性和安全性。本文将深入剖析 Linux 内核内存管理的核心机制,包括伙伴系统(Buddy System)、Slab/Slob/Slub 分配器、虚拟内存管理以及内存回收策略。

一、物理内存的管理模型

1.1 节点(Node)与区域(Zone)

现代 NUMA 架构系统中,Linux 将物理内存组织为节点(pg_data_t)和区域(zone)的层级结构:

  • 节点:每个 NUMA 节点对应一个 pg_data_t 结构,包含该节点下的所有内存区域
  • 区域:每个节点内按用途划分为 ZONE_DMA、ZONE_DMA32、ZONE_NORMAL、ZONE_HIGHMEM 等
// 简化的节点结构
typedef struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];      // 内存区域数组
    struct zonelist node_zonelists[MAX_ZONELISTS]; // 分配 fallback 列表
    int nr_zones;                               // 区域数量
    struct page *node_mem_map;                  // 页描述符数组
    unsigned long node_start_pfn;               // 起始页帧号
} pg_data_t;

1.2 页(Page)— 物理内存的最小管理单位

物理内存以页(通常 4KB)为基本单位管理。每个物理页对应一个 struct page 描述符,保存在全局 mem_map 数组中。

struct page {
    unsigned long flags;        // 页状态标志(PG_locked, PG_dirty, PG_lru等)
    atomic_t _refcount;        // 引用计数
    atomic_t _mapcount;        // 映射计数(被多少个页表引用)
    struct { 
        union {
            struct list_head lru;     // LRU 链表节点
            struct slab_head slab;    // Slab 链表
        };
    };
    struct address_space *mapping; // 关联的地址空间
    pgoff_t index;             // 在映射中的偏移
};

二、伙伴系统(Buddy System)

伙伴系统是内核管理连续物理页框的核心算法,由 Knowlton 于 1965 年提出,Linux 在此基础上进行了优化实现。

2.1 核心思想

伙伴系统将空闲页面按"阶"(order)组织,每个阶 n 对应 2^n 个连续物理页。系统维护 11 个 free_area 链表(order 0~10),分别管理 1、2、4、8、...、1024 个连续页块。

分配过程:

  1. 从满足大小的最小 order 的 free_list 中取出一个块
  2. 如果该 order 无空闲,向更大 order 递归查找,将大块分裂为两个"伙伴"
  3. 一半分配出去,另一半放入低一阶的 free_list

释放过程:

  1. 释放块时检查其"伙伴"是否也在 free_list 中
  2. 若伙伴空闲则合并为高一阶的大块
  3. 递归合并直到最高阶或伙伴不在空闲列表

2.2 伙伴的判定

两个块互为"伙伴"的条件:

  • 大小相同(同一 order)
  • 物理地址连续
  • 合并后的块起始地址是 2^(order+1) 的整数倍
块A起始地址 = 2^order × K        (K为偶数)
块B起始地址 = 2^order × (K+1)    (K+1为奇数)
合并后起始 = 2^order × K        (低 order+1 位为零)

2.3 避免碎片:页面迁移与反碎片

伙伴系统面临的最大挑战是外部碎片(空闲内存不连续)。Linux 引入了基于页面迁移类型的反碎片机制:

迁移类型含义可回收性
MIGRATE_UNMOVABLE无法迁移(如 slab、内核代码)不可迁移
MIGRATE_MOVABLE可迁移(如用户页、缓存)可迁移
MIGRATE_RECLAIMABLE可回收但不能迁移仅回收
MIGRATE_PCP每-CPU 页面缓存快速分配

分配时优先从对应类型的 free_list 取页,从而使同类页面聚集,便于后续整理。

三、Slab 分配器 — 内核对象的内存管理

伙伴系统以页为单位分配,但内核经常需要分配远小于 4KB 的小对象(如 task_struct、dentry、inode)。直接通过伙伴系统分配会造成严重的内部碎片。Slab 分配器应运而生。

3.1 核心思想

由 Jeff Bonwick 在 Solaris 中首创,SunOS 5.4(1994)。Linux 中经历了三代演进:

  • Slab:Linux 2.2 引入,原始实现
  • Slob:面向嵌入式系统的极简分配器
  • Slub:Linux 2.6.23 起成为默认分配器,优化了元数据开销和碎片

Slab 本质上是一个对象缓存(Object Cache):

  1. 预先从伙伴系统申请若干页面组成一个 slab
  2. 将这些页面切分为大小固定的槽位(slot)
  3. 分配时从已 partial 的 slab 中取空闲槽位
  4. 释放时标记槽位空闲,归还到原 slab

3.2 三层结构

┌──────────────────────────────────────────────────┐
│                kmalloc_cache (Size Cache)         │
│  如 kmalloc-32, kmalloc-64, kmalloc-128, ...     │
├──────────────────────────────────────────────────┤
│                kmem_cache (Object Cache)          │
│  如 task_struct_cache, dentry_cache, inode_cache  │
├──────────────────────────────────────────────────┤
│                  slab (物理内存块)                │
│  ┌──────┬──────┬──────┬──────┬──────┐            │
│  │ slot │ slot │ slot │ slot │ ... │            │
│  └──────┴──────┴──────┴──────┴──────┘            │
└──────────────────────────────────────────────────┘
  • size cache:kmalloc 使用,按 2 的幂次组织通用大小的缓存
  • kmem_cache:特定类型对象缓存,通过 kmem_cache_create() 创建
  • slab:实际的物理内存块,可能有多个 slab 组成链表

3.3 Slab 着色(Cache Coloring)

Slab 着色通过为每个 slab 预留不同大小的偏移量(colour offset),使得不同 slab 中的对象映射到不同的 cache line。这样可以减少缓存行冲突(Cache Line Thrashing),提升 CPU cache 利用率。

struct kmem_cache {
    unsigned int colour;          // 可用颜色数量
    unsigned int colour_off;      // 颜色偏移步长
    void *freelist;               // 空闲对象链表
    struct list_head slabs_partial;  // 部分空闲 slab
    struct list_head slabs_full;     // 全部占用 slab
    struct list_head slabs_free;     // 完全空闲 slab
};

四、虚拟内存管理

4.1 地址空间布局

Linux 采用分页机制实现虚拟内存。x86_64 架构下虚拟地址空间划分:

0x0000 0000 0000 0000  ┌──────────────────┐
                       │  User Space      │ 128TB
0x0000 7FFF FFFFFFFF  ├──────────────────┤
                       │  Non-canonical   │
FFFF 8000 0000 0000  ├──────────────────┤
                       │  Kernel Space    │ 128TB
FFFF FFFF FFFFFFFF  └──────────────────┘

用户空间各段:

  • text:代码段,只读可执行
  • data:数据段,已初始化的全局变量
  • BSS:未初始化的全局变量
  • heap:动态内存分配区,向高地址增长
  • memory mapping:mmap 区域(共享库、匿名映射等)
  • stack:栈区,向低地址增长

4.2 页表与地址转换

四级页表(x86_64 默认):

CR3 → PGD → PUD → PMD → PTE → Physical Page
 47:39  38:30  29:21  20:12   物理页内偏移(12bit)
  • 每级页表占用 4KB 页,512 项(9bit 索引)
  • 实际可寻址 2^48 = 256TB 虚拟地址空间
  • 五级页表(Linux 4.14+)扩展到 2^57 = 128PB

4.3 缺页异常处理

当访问未映射的虚拟地址时触发 page fault,处理路径:

  1. do_page_fault() 检查触发原因(读/写/执行、用户态/内核态)
  2. 查找 VMA:确认地址是否在合法区域内
  3. 不同场景处理:
    • Demand Paging:首次访问匿名映射,分配零页
    • Page Cache 回写:文件映射从未加载,从磁盘读入
    • Copy-on-Write:fork 后的写操作触发,分配新页复制
    • SIGSEGV:访问非法地址(如 NULL 解引用)

五、内存回收与 OOM 处理

5.1 kswapd 后台回收

当空闲内存低于阈值时唤醒 kswapd 内核线程,执行页面回收:

  • 活跃/不活跃 LRU 链表:每个 zone 维护 active_list 和 inactive_list
  • 页面老化:通过 PG_referenced 标志位判断页面是否被频繁访问
  • 回收策略:优先回收 inactive 链表中的干净页(可直接丢弃),其次是脏页(需写回磁盘)

5.2 Direct Reclaim

当 kswapd 回收不及时,分配者进入直接回收(同步回收),会阻塞当前分配请求,严重影响性能。

5.3 OOM Killer

当系统内存极度紧张且回收无效时触发:

// OOM 评分计算
points = total_vm_pages + swapents + oom_score_adj
  • 遍历所有进程计算 oom_score
  • 选择得分最高的进程终止
  • /proc/[pid]/oom_score_adj 可调整(-1000~1000,-1000 表示不杀)

六、内核态动态分配:vmalloc 与 kmalloc

6.1 kmalloc

void *kmalloc(size_t size, gfp_t flags);
  • 基于 Slab 分配器
  • 物理连续——适合 DMA 等需要连续物理内存的场景
  • 大小限制通常 4MB~8MB(早期 128KB,现代内核有所增加)
  • GFP flags 控制分配行为:GFP_KERNEL、GFP_ATOMIC、GFP_DMA 等

6.2 vmalloc

void *vmalloc(unsigned long size);
  • 基于伙伴系统 + 页表映射
  • 虚拟连续,物理可不连续
  • 适合只需要虚拟连续大内存的场景(如模块加载、大数组)
  • 访问有 TLB miss 开销,不适用于频繁访问的小内存

七、性能调优与实战

7.1 /proc/buddyinfo 诊断碎片

$ cat /proc/buddyinfo
Node 0, zone   Normal   10  21  45  30  3  22  7  2  1  0   0
                      order: 0  1  2  3  4   5  6 7 8 9  10

如果高阶连续页数量很少,说明存在严重的外部碎片。

7.2 /proc/slabinfo 查看 Slab 使用

$ cat /proc/slabinfo | head -20
<name>  <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab>
dentry   123456     200000       192       20             1
inode_cache 45678   50000       648        8             2

7.3 调整内存参数

参数路径含义
swappiness/proc/sys/vm/swappiness交换倾向 (0-200)
min_free_kbytes/proc/sys/vm/min_free_kbytes最小空闲内存
vfs_cache_pressure/proc/sys/vm/vfs_cache_pressure缓存回收倾向
overcommit_memory/proc/sys/vm/overcommit_memory内存超分配策略

总结

Linux 内核内存管理子系统通过伙伴系统管理物理页,通过 Slab 分配器高效处理小对象,通过虚拟内存机制隔离和保护进程空间,再配合 LRU 页面回收和 OOM Killer 保障系统整体稳定性。理解这些机制对于系统调优、驱动开发、性能分析都有着重要意义。

随着硬件发展(持久内存、CXL、HBM),Linux 内存管理系统也在持续演进。例如:

  • Memory Tiering(Linux 5.15+):自动分层管理不同速度的内存
  • MGLRU(Multi-Gen LRU):新型页面回收算法,取代传统双链表
  • Landlock:文件系统沙箱安全模块

深入理解内核内存管理,是每一位系统开发者进阶的必经之路。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿
网站二维码

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
/* 跳过导航链接 (无障碍) */ position: absolute; top: -100px; left: 15px; z-index: 99999; padding: 8px 16px; background: #007bff; color: #fff; font-size: 14px; border-radius: 0 0 4px 4px; text-decoration: none; transition: top 0.2s; } top: 0; outline: 3px solid #0056b3; }