引言

Linux 内核的内存管理子系统是整个操作系统中最复杂、最核心的模块之一。它负责管理物理内存的分配与回收、虚拟地址空间的映射、页面缓存与交换、以及内存不足时的回收策略。理解这些机制不仅是内核开发者的必修课,也是每一位系统程序员、性能优化工程师和基础设施构建者的基本功。

本文将从物理页面管理的基石——伙伴系统(Buddy System)出发,深入剖析内核内存分配器 SLAB/SLUB/SLOB 的实现原理,解析缺页异常(Page Fault)的处理流程,介绍页面回收与交换机制,最后讨论 OOM Killer 的工作策略与调优方法。

一、物理内存管理:伙伴系统(Buddy System)

1.1 为什么需要伙伴系统

操作系统需要频繁地分配和释放不同大小的物理页面。如果简单地以单页为单位管理,会产生大量外部碎片,导致无法满足连续物理内存的请求。伙伴系统通过将内存划分为不同阶(order)的块来解决这个问题——第 n 阶的块包含 2^n 个连续页面。

1.2 核心数据结构

Linux 内核为每个内存节点(Node)维护一个 zone 结构,每个 zone 包含一个 free_area 数组,数组索引就是阶(order),每个元素是一个自由链表(free list):

struct zone {
    struct free_area    free_area[MAX_ORDER];
    ...
};

struct free_area {
    struct list_head    free_list[MIGRATE_TYPES];
    unsigned long       nr_free;
};

其中 MAX_ORDER 通常为 11,即最大连续分配 2^10 = 1024 个页面(4MB,假设页大小 4KB)。

1.3 分配与释放流程

分配过程:当请求 order-n 的块时,首先在 free_area[n] 的链表中查找,如果有空闲块则直接返回。如果 free_area[n] 为空,则向更高阶寻找——从 free_area[n+1] 中取出一个块,分裂为两个伙伴(buddy),一个返回请求者,另一个放入 free_area[n]。

释放过程:释放一个 order-n 的块时,检查其伙伴是否也在对应阶的 free_list 中。如果是,则合并为 order-(n+1) 的块,并递归向上检查是否可以继续合并。这就是'伙伴'得名原因——只有地址连续的、大小相同的两个块才能合并。

1.4 页面迁移类型(Page Migration Types)

为减少碎片,Linux 将页面按可迁移类型分组:

  • MIGRATE_UNMOVABLE:内核数据、页表等不可移动页面
  • MIGRATE_MOVABLE:用户态数据、文件缓存等可移动页面
  • MIGRATE_RECLAIMABLE:可以通过页面回收重新获得的缓存
  • MIGRATE_ISOLATE:用于内存热插拔的隔离区

这种分类使得不可移动页面不会碎片化可迁移页面,提升了内存紧凑机制的效率。

二、内核内存分配器:SLAB/SLUB/SLOB

2.1 SLAB 的设计思想

伙伴系统以页(4KB)为最小分配单位,但内核经常需要分配小对象(如 task_struct、inode、dentry 等)。频繁分配完整页面会造成严重的内部碎片。SLAB 分配器引入对象缓存(kmem_cache)的概念,预先分配完整页面,将其切分为固定大小的对象槽位(slot),实现小对象的高效分配。

2.2 SLAB 的核心结构

struct kmem_cache {
    struct kmem_cache_cpu *cpu_slab;    // 每CPU快速分配缓存
    struct kmem_cache_node *node[MAX_NUMNODES];  // 每个NUMA节点缓存
    unsigned int size;         // 对象实际大小
    unsigned int object_size;  // 含元数据的对象大小
    unsigned int offset;       // 下一个空闲对象的偏移
    struct list_head list;     // 全局缓存链表
    const char *name;
    ...
};

struct kmem_cache_cpu {
    void **freelist;          // 空闲对象链表
    unsigned long tid;        // 全局唯一事务ID
    struct page *page;        // 当前使用的slab页
    struct page *partial;     // 部分空闲slab链表
};

2.3 SLAB 的三级分配策略

SLAB 采用三级缓存体系,优先从最快速的层级分配:

  1. per-cpu 缓存 (cpu_slab):每CPU独占,无锁分配,最快路径
  2. per-node 部分空闲链表:当前NUMA节点上的 partial slabs
  3. 伙伴系统新分配:分配新的 slab 页面

释放时按优先级回收到对应层级:per-cpu free 超量时返还给 node partial,node full slab 全空闲时返还给伙伴系统。

2.4 SLUB 分配器

SLUB(Unqueued SLAB)是 Linux 2.6.23 后的默认分配器,简化了 SLAB 的设计:

  • 去除了 per-cpu 队列的复杂管理(如 cold cache、hot cache)
  • 每个 slab 页面通过 page 结构体的 freelist 指针维护空闲链表
  • 释放时不合并 slab 碎片,仅当页面全空闲时返回伙伴系统
  • 更少的元数据开销,更好的 NUMA 扩展性

SLUB 的关键创新是将 slab 元数据嵌入到 struct page 中,而不是使用独立的 slab 管理结构,减少了内存开销和缓存行竞争。

2.5 SLOB 分配器

SLOB(Simple List Of Blocks)是专为嵌入式等内存受限环境设计的极简分配器。它不使用 slab 概念,而是维护一个全局空闲块链表,使用 first-fit 策略分配。SLOB 代码简单、内存开销极小,但分配效率低、碎片严重,仅适用于内存小于数MB的嵌入式场景。

2.6 kmalloc 家族

内核通过 kmalloc() 分配连续物理内存,其内部通过预定义大小缓存实现:

// SLUB 预定义的 kmalloc 缓存大小
static char *cache_names[] = {
    'kmalloc-8', 'kmalloc-16', 'kmalloc-32', 'kmalloc-64',
    'kmalloc-96', 'kmalloc-128', 'kmalloc-192', 'kmalloc-256',
    'kmalloc-512', 'kmalloc-1k', 'kmalloc-2k', 'kmalloc-4k',
    ...
};

当请求超过 2 * PAGE_SIZE 的内存时,kmalloc 直接回退到伙伴系统(alloc_pages)进行分配,以避免 SLUB 内部碎片的浪费。

三、缺页异常(Page Fault)处理

3.1 缺页异常的产生

当 CPU 访问虚拟地址时,MMU 在页表中找不到对应的物理页表项(PTE),或权限检查失败时触发缺页异常(#PF)。#PF 会压入错误码(error code)并跳转到内核的异常处理例程。

3.2 处理流程总览

page_fault(vector) -> do_page_fault(regs, error_code)
  -> user_mode -> do_user_addr_fault()
      -> VMA查找 (find_vma)
      -> 权限检查 -> SIGSEGV (段错误)
      -> 写时复制 (COW) -> do_wp_page()
      -> 文件映射 -> filemap_fault()
      -> 匿名映射 -> do_anonymous_page()

3.3 写时复制(Copy-On-Write)

fork() 子进程时不立即复制物理页面,而是共享父进程的页面并标记为只读。当任一进程尝试写入页面时触发 COW 缺页,内核才复制页面内容并更新页表。这大大提升了 fork() 的执行速度,同样被用于文件映射的私有映射场景。

3.4 文件映射缺页

当访问 mmap 映射的文件区域时触发 do_fault() -> filemap_fault()。内核首先在 page cache 中查找页面块(radix tree / XArray),如果存在则直接映射(minor fault);如果不存在则从磁盘读入(major fault),通常以预读窗口(readahead)方式加载邻接页面。

3.5 匿名映射缺页

对匿名映射(如 malloc 大内存、MAP_ANONYMOUS)的首次访问触发 do_anonymous_page()。内核调用 alloc_pages() 从伙伴系统分配一个零页(zero page),然后写入进程页表。首次匿名访问分配的是特殊的'零页'(全局共享的零初始化页),仅在实际写入时才真正分配物理页面(demand paging)。

四、内存回收与交换机制

4.1 LRU 算法与双链策略

Linux 使用增强型 LRU(Least Recently Used)算法管理页面回收。每个 zone 维护两个 LRU 链表:

  • active 链表:近期被访问过的页面被认为是'热'的
  • inactive 链表:近期未被访问的页面,候选回收

页面在两个链表之间移动:active 链表尾的页面经过一段时间未被访问会被移到 inactive 链表;inactive 链表的页面被重新访问两回会被提升到 active 链表(双链策略 / double-clock algorithm)。

4.2 页面类型与回收优先级

内核管理的页面分为四种类型:

enum lru_list {
    LRU_INACTIVE_ANON,  // 匿名页面(堆、栈)
    LRU_ACTIVE_ANON,
    LRU_INACTIVE_FILE,  // 文件缓存
    LRU_ACTIVE_FILE,
};

回收优先级:swappiness 参数控制匿名页面与文件缓存的回收比例。默认值 60 表示两种类型按 LRU 时间公平回收;swappiness=0 表示尽可能不交换匿名页面;swappiness=200 表示积极交换匿名页面。

4.3 直接回收与后台回收

  • kswapd 后台回收:当空闲页面低于 zone->low 阈值时唤醒 kswapd 内核线程,按 LRU 顺序回收页面直到达到 high 阈值
  • 直接回收(Direct Reclaim):当空闲页面低于 min 阈值时,分配进程同步执行回收,会造成明显的延迟抖动
  • 内存碎片整理(Memory Compaction):kcompactd 线程定期整理碎片,合并可移动页面以释放连续大块

4.4 Zswap 与 Zram

Zram:在RAM中创建压缩的块设备用于交换。数据写入 swap 时被实时压缩,比传统磁盘 swap 快得多,适用于低内存设备。

Zswap:作为交换的压缩缓存层。数据先写入 zswap 的内存池(LRU管理),仅在池满时写入真正的磁盘 swap。优点是减少磁盘 I/O,缺点是占用额外 CPU 用于压缩。

五、OOM Killer 与内存不足处理

5.1 OOM Kill 触发条件

当系统所有 zone 的空闲页面均低于 min 阈值,直接回收失败,kswapd 无法及时补充内存,且已完全耗尽 swap 空间时,触发 out_of_memory()。OOM Killer 是内核最后的防崩溃手段。

5.2 OOM 评分机制

OOM Killer 通过 oom_badness() 为每个进程计算分数(oom_score),选择分数最高的进程杀死:

points = total_vm_pages * 1000 / totalram_pages
adj = oom_score_adj  // -1000 ~ +1000
oom_badness = points * (1 + adj / 1000)
  • adj=-1000 表示 OOM 免疫(如 init 进程、systemd)
  • adj=0 表示正常评分
  • adj=+1000 表示优先被杀死(如大型内存消耗进程)

5.3 OOM 调优实践

  • memory.limit_in_bytes:通过 cgroup v1 memory 子系统限制进程组内存
  • Cgroup v2 memory.min:保护关键服务不被回收
  • vm.overcommit=2:严格模式限制内存超量分配
  • vm.oom_kill_allocating_task=1:直接杀死触发OOM的进程

六、性能调优与监控

6.1 Slab 信息监控

$ slabtop -o          # 实时slab分配器内存统计
$ cat /proc/slabinfo  # 各kmem_cache详细信息
$ slabinfo -T         # 超过指定KB的大缓存

通过 /proc/sys/vm/ 下的可调参数动态调整内存行为:

vm.swappiness = 10            # 降低交换倾向
vm.vfs_cache_pressure = 50    # 控制dentry/inode回收速度
vm.dirty_ratio = 40           # 脏页占总内存40%时写回
vm.min_free_kbytes = 262144   # 保留256MB为紧急使用

6.2 内存 NUMA 优化

NUMA 系统中内存访问存在远近差异:

  • numactl --membind=node:将进程内存绑定到特定节点
  • numactl --interleave=all:交替分配所有节点的内存
  • zone_reclaim_mode:节点内存不足时优先本地回收

6.3 透明大页(THP)

透明大页将连续 4KB 页面合并为 2MB 大页,减少 TLB 不命中。适合数据库、HPC 场景:

echo always > /sys/kernel/mm/transparent_hugepage/enabled
echo defer+madvise > /sys/kernel/mm/transparent_hugepage/defrag

七、总结

Linux 内存管理子系统是一个精密的分层体系:伙伴系统管理物理页面,满足大块连续分配需求;SLUB 分配器在上面构建对象缓存,满足小对象高频分配;缺页异常机制实现了按需分配和写时复制;LRU 回收算法平衡了内存利用与性能;OOM Killer 则是最后的防线。

理解这些机制,可以帮助我们更好地诊断内存性能瓶颈——是直接回收造成的延迟抖动?是 slab 缓存持续增长?还是 THP 碎片化导致的大页分配失败?掌握 /proc/meminfo、slabtop、vmstat、perf 等工具也成为了系统工程师日常调优的利器。

随着 Cgroup v2、BPF、持久内存(PMEM)等新技术的引入,Linux 内存管理仍在持续演进。对技术人而言,底层原理始终是应对上层变化的坚实底气。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部