引言

内存管理是Linux内核中最复杂、最核心的子系统之一。它负责在有限的物理内存中为整个系统高效地分配、回收和管理内存资源。从早期的伙伴系统(Buddy System)到后来的Slab分配器,再到现代Linux中的SLUB和SLOB,内存管理经历了多次重大的架构演进。本文将深入剖析Linux内核内存管理的核心机制,带你理解操作系统如何在微观世界中驾驭内存。

1. 物理内存的组织:节点与区域

在NUMA(非统一内存访问)架构出现之前,内核将物理内存视为一个统一的平坦空间。但随着多处理器系统的普及,CPU访问不同物理内存位置的速度不再相同。Linux内核引入了节点(node)的概念来描述这种架构。

每个节点中包含若干个区域(zone),用于将物理内存按用途分类:

  • ZONE_DMA:用于DMA操作的低端内存区域(通常为0-16MB)
  • ZONE_DMA32:32位DMA可寻址的内存区域
  • ZONE_NORMAL:常规可映射内核地址空间的内存
  • ZONE_HIGHMEM:高端内存(仅32位系统需要)
  • ZONE_MOVABLE:可移动内存区域,支持内存热插拔

这种分层的组织结构使得内核能够根据不同场景选择合适的内存区域进行分配。

2. 伙伴系统(Buddy System):物理页框的分配与回收

伙伴系统是Linux内核管理物理内存的基础算法,由Knowlton在1965年提出。其核心思想是将空闲页面按大小组织成不同阶(order)的链表,从而实现高效的内存分配。

2.1 核心数据结构

每个内存区域维护一个free_area数组,包含从0到MAX_ORDER-1(通常为11)共11个链表,每个链表管理大小为2^n个连续页面块的链表:

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

2.2 分配流程

当内核申请2^n个连续物理页面时:

  1. 在相同阶的空闲链表中查找,如果存在则直接分配
  2. 如果当前阶没有空闲块,向高一阶查找
  3. 找到较大块后,递归二分拆分直到获得所需大小的块
  4. 将拆分产生的伙伴块放入对应阶的链表中

2.3 伙伴块的合并

当内存页被释放时,系统检查其"伙伴"(地址相邻且大小相同的块)是否也在空闲链表中。如果是,将它们合并成更大的块,并递归尝试向上合并。这就是"伙伴系统"名称的由来。

2.4 伙伴算法的优缺点

优点:

  • 实现简单,分配和释放操作都是O(1)或O(log n)
  • 支持连续物理页分配,适合DMA等需要连续内存的场景
  • 高效的合并机制,减少碎片

缺点:

  • 内部碎片:请求大小不是2的幂时会浪费内存(如申请3页得到4页)
  • 外部碎片:长时间运行后难以分配大块连续内存
  • 仅以页(通常4KB)为最小粒度,无法满足小对象分配需求

3. Slab分配器:小对象的高效管理

Jeff Bonwick在1994年为Solaris操作系统设计了Slab分配器,Linux在2.1.23版本中引入了这一机制。Slab分配器建立在伙伴系统之上,专门用于内核中小对象的分配。

3.1 设计动机

内核中大量使用的对象(如task_struct、inode、dentry等)通常只有几十到几百个字节。如果直接使用伙伴系统,每个对象至少占用一个页面(4KB),浪费严重。此外,频繁创建和销毁对象时,初始化和析构的开销也很可观。

3.2 Slab的核心概念

Slab分配器引入了三个关键结构:

  • Cache(缓存):每种对象类型对应一个kmem_cache,包含多个Slab
  • Slab(板):由1个或多个连续物理页组成的内存块,被切分成等大小的槽位
  • Object(对象):Slab中的槽位,存放实际的内核对象

3.3 Slab的状态

每个Slab有三种状态:

  • Full:所有槽位都已分配
  • Partial:部分槽位已分配,部分空闲
  • Empty:所有槽位都空闲,可释放给伙伴系统

3.4 Slab分配器的优势

  • 消除内部碎片:精确匹配对象大小,不浪费内存
  • 对象缓存:对象初始化后保留初始化状态,下次分配可直接使用
  • 硬件缓存友好:对象紧密排列,提高CPU缓存命中率
  • 着色机制(Slab Coloring):在Slab中偏移对象位置,减少缓存行冲突

4. 现代Linux中的SLUB分配器

虽然Slab分配器性能优秀,但其复杂的元数据结构和多队列管理在系统规模扩大时成为瓶颈。Nick Pigge在2006年提出了SLUB(Unqueued Slab Allocator),并在2.6.23版本中成为默认的Slab分配器。

4.1 SLUB的设计改进

  • 简化元数据:每个CPU只有一个活动Slab指针,去除了复杂的full/partial/empty队列
  • 每CPU缓存:每个CPU维护自己的页列表,减少锁竞争
  • 简化合并规则:相同大小对象共用一个cache,减少碎片
  • 更好的可扩展性:Multi-CPU场景下性能显著优于传统Slab

4.2 SLUB的核心结构

SLUB对SLAB做了大刀阔斧的简化。每个CPU只维护一个活动Slab指针,每个NUMA节点维护partial链表。分配对象时直接从CPU活动Slab获取下一个空闲指针(类似LIFO栈),速度极快;释放时仅需把对象放回原Slab,大部分操作无需加锁。这种设计使得SLUB在NUMA大规模并行场景下扩展性远优于传统Slab。

5. 页表与虚拟内存映射

伙伴系统和SLUB管理的是物理内存,而进程看到的是虚拟地址空间。Linux内核通过四级(或五级)页表来完成虚拟地址到物理地址的转换。

5.1 四级页表结构

在x86-64架构中,48位虚拟地址分为:

  • PML4(Page Map Level 4):39-47位,512条目
  • PDPT(Page Directory Pointer Table):30-38位
  • PD(Page Directory):21-29位
  • PT(Page Table):12-20位
  • 页内偏移:0-11位(对应4KB页)

5.2 大页(Huge Page)支持

标准4KB页面在现代工作负载中会导致大量的TLB(Translation Lookaside Buffer)Miss。Linux内核支持两种大页机制:

  • Transparent Huge Pages(THP):内核自动将连续的小页合并为大页
  • Hugetlbfs:用户显式申请的大页文件系统

使用2MB大页时,TLB中一个条目覆盖的内存从4KB增加到256倍,显著提升内存密集型应用的性能。

6. OOM Killer:内存不足的最后一搏

当系统内存耗尽且无法通过回收、压缩等方式获得足够内存时,OOM Killer会被触发,选择一个进程终止以释放内存。

6.1 OOM评分机制

内核为每个进程计算一个oom_score:

  • 占用内存越多,分数越高
  • 运行时间越长,分数越低(倾向于保留老进程)
  • 关键系统进程(如sync)分数设为-1000,不会被kill
  • root进程获得3%的减免

6.2 OOM Killer的选择策略

OOM Killer选择oom_score最高的进程终止。从Linux 4.16开始引入了oom_score_adj,允许用户空间通过/proc/[pid]/oom_score_adj来调整进程的OOM分数。

7. 内存碎片整理与CMA

7.1 内存碎片整理(Memory Compaction)

长期运行后,伙伴系统会产生大量外部碎片。Linux内核的页面迁移机制可以将分散的页面移动到一起,形成大块连续内存。kcompactd内核线程负责周期性扫描和压缩内存区域。

7.2 CMA(Contiguous Memory Allocator)

对于需要大块连续物理内存的设备(如摄像头、GPU),Linux引入了CMA机制。CMA在系统启动时预留一块内存区域,平时可作为普通内存使用,需要连续大块内存时通过页面迁移释放。

8. 总结

Linux内核内存管理经过三十多年的演进,形成了一套层次分明、高效运作的体系:

  • 伙伴系统管理物理页面,提供2^n粒度的连续内存分配
  • SLUB分配器建立在伙伴系统之上,高效管理小对象
  • 四级页表实现虚拟地址到物理地址的映射
  • 大页机制优化TLB性能
  • 页面压缩与CMA应对碎片化和特殊设备需求

理解这些机制不仅有助于编写更高效的系统级代码,也是深入理解操作系统、性能调优和嵌入式开发的重要基础。当你的服务器OOM时,当DMA缓冲区分配失败时,当你需要优化内存密集型应用性能时——这些知识将成为你最有力的工具。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部