引言
在上一篇中,我们概述了伙伴系统作为内核"页分配器"的角色。它从底层负责分配连续的物理页框。但物理内存并非一个 homogeneous(均匀)的整体。不同的硬件体系结构对内存的使用有特殊限制,而伙伴系统必须巧妙地处理这些限制,同时还要应对内存碎片化的永恒挑战。今天,我们将深入伙伴系统的内部,探索其如何通过内存区域(Zones) 的分类管理、精巧的分配算法 以及应对碎片化的策略,来高效地管理物理内存。
一、 内存区域(Zones):因地制宜的内存管理
为什么物理内存需要分区?原因在于硬件的异构性。并非所有物理内存对内核的所有部分都是平等的。伙伴系统将物理内存划分为不同的区域(Zones),主要是为了应对以下两种硬件限制:
- 硬件设备DMA的限制:古老的ISA总线设备在进行DMA(直接内存访问)时,只能访问物理内存的前16MB空间。
- CPU物理地址寻址限制:在32位系统上,CPU无法通过线性映射直接访问所有物理内存(参见上一篇关于HIGHMEM的讨论)。
Linux内核主要定义了以下三种区域(具体架构可能略有不同):
| 区域 | 目的 | 物理范围 (典型32位x86) | 说明 |
| :--- | :--- | :--- | :--- |
| ZONE_DMA
| 专供DMA使用 | 0 ~ 16MB | 满足那些无法访问高地址内存的老式设备的DMA操作需求。 |
| ZONE_DMA32
| 供32位设备DMA使用 | 16MB ~ 4GB | 在64位系统上,为只能访问32位地址的设备提供DMA内存。 |
| ZONE_NORMAL
| 正常内核映射 | 16MB ~ 896MB | 最重要的区域 。内核直接线性映射的区域,大部分内核分配发生于此,访问速度最快。 |
| ZONE_HIGHMEM
| 高端内存 | 896MB ~ 结束 | 仅存在于32位系统 。用于动态映射超过ZONE_NORMAL
的物理内存。64位系统无此区域。 |
工作方式 :
伙伴系统不是 只有一个全局的空闲链表。而是为每个内存区域(Zone)都维护一套独立的伙伴系统数据结构 (即一组free_area
链表)。当内核发起分配请求时,它会指定一个** GFP(Get Free Pages)标志**(如GFP_KERNEL
、GFP_DMA
),分配器会根据这个标志决定从哪个Zone中分配内存。
- 例如:一个设备驱动程序需要为DMA操作分配内存,它会使用
GFP_DMA
标志。伙伴系统就会只在ZONE_DMA
中为其寻找空闲页框,确保设备能够访问这块内存。
(这是一个简化的示意图,实际物理布局可能因架构和配置而异)
二、 页框分配与回收算法
伙伴系统的核心是它的分配和释放(回收)算法,其精髓在于分裂 与合并。
分配过程(以分配4个连续页为例,order=2
)
- 指定区域 :分配请求携带
gfp_mask
,确定备选Zone的优先级(如优先从ZONE_NORMAL
分配)。 - 查找空闲链表 :伙伴系统从当前Zone的
free_area[2]
链表中查找是否有大小为4页的空闲块。 - 递归分裂(如果未找到) :
- 如果
order=2
的链表为空,则向上查找order=3
的链表(8页的块)。 - 如果找到一个8页的块,将其分裂 成两个伙伴(Buddy) 块(各4页)。
- 将其中一个4页块插入
free_area[2]
链表,另一个用于满足分配请求。 - 如果
order=3
也没有,则继续向上查找和分裂,直到最大order。
- 如果
- 水位检查与回收 :如果所有Zone的所需order都无法满足分配,分配器会触发内存回收(kswapd),尝试释放一些内存,然后再重试。如果回收后仍无法满足,可能触发OOM Killer。
释放与合并过程
- 释放 :当释放4个页(
order=2
)时,系统将其放回对应Zone的free_area[2]
链表。 - 查找伙伴 :系统会计算被释放块的伙伴块 的地址。伙伴块的特点是:大小相同,且物理地址连续,其起始地址在二进制上只有第
(order+1)
位不同。 - 检查并合并 :
- 检查计算出的伙伴块是否存在 、空闲 且位于同一个Zone。
- 如果所有条件满足,则将这两个
order=2
的伙伴块从链表中移除,合并 成一个order=3
的更大块。 - 这个合并过程会递归进行 :系统会尝试继续合并新生成的
order=3
块和它的伙伴,直到无法合并为止。
- 挂入新链表:将最终合并成的大块插入到对应order的空闲链表中。
合并机制是伙伴系统对抗外部碎片的最强大武器,它尽可能地让零散的小块内存组合成连续的大块。
三、 碎片化问题与解决方案
尽管伙伴系统通过合并有效减少了外部碎片( scattered free memory),但内存碎片化仍然是系统长期运行后性能下降的主要原因之一。Linux内核引入了多种技术来缓解此问题。
1. 反碎片(Anti-Fragmentation)技术
内核在启动时就对页框进行了分类,根据其可移动性(Mobility) 组织到不同的伙伴系统链表中:
- 不可移动页(Unmovable):内核核心代码、大部分内核数据结构、 DMA缓冲区等。它们物理位置必须固定。
- 可回收页(Reclaimable):用户数据页、文件缓存等。它们可以被写入磁盘后再回收。
- 可移动页(Movable):用户态进程的堆栈空间、页缓存等。它们可以被移动到新的物理位置。
工作原理 :
伙伴系统为每种可移动类型都建立了独立的free_area
链表 。当需要分配一个不可移动的页时,它只会从不可移动页的链表中分配,而不会去占用一个本可用于移动页的块。这样,相同类型的页倾向于聚集在一起。
好处 :
即使系统运行很久,不可移动的页也不会分散在各个角落,从而"污染"大块的连续空闲区域。可移动页所占用的巨大连续空间依然保持完整,可以被大块分配请求使用,或者为内存规整提供条件。
2. 内存规整(Memory Compaction)
当系统确实因为碎片无法分配大块连续内存时,内核会触发一个名为内存规整的后台进程。
- 过程 :它的工作原理类似于磁盘碎片整理。它将可移动的页 从一块物理内存区域迁移到别处,从而在原位置创造出更大的连续空闲块。
- 触发条件:通常由伙伴系统分配高阶(high-order)内存失败时触发。
- 依赖 :内存规整的有效性高度依赖于反碎片技术。只有页被正确分类,规整才知道哪些页可以安全地移动。
3. 虚拟可移动内存块(CMA:Contiguous Memory Allocator)
这是应对碎片问题的"终极大招",主要用于嵌入式系统为大容量DMA设备(如GPU、摄像头)预留内存。
- 原理 :在系统启动时,预先保留一大块连续的物理内存区域。
- 巧妙之处 :在系统未使用CMA时,这块保留内存可以被内核分配给可移动页 使用(例如普通应用程序)。当设备驱动程序需要分配大块连续DMA内存时,CMA通过迁移这些可移动页,腾出这块保留区,从而快速满足分配请求。
- 优点:避免了在启动时就永久占用大量内存,提高了内存利用率,同时又保证了在需要时能提供可靠的连续内存。
总结
伙伴系统远不止是一个简单的"分裂与合并"算法。它是一个复杂的内存管理框架,通过:
- 分区管理(Zones):应对硬件限制。
- 精巧的分配/回收算法:高效处理各种大小的请求。
- 反碎片与内存规整:动态对抗外部碎片,保持内存的可用性。
- CMA:为特殊需求提供保证。
这些机制共同工作,使得Linux内核能够在各种硬件配置和负载下,高效、可靠地管理物理内存这一宝贵资源。理解这些底层机制,有助于我们更好地分析系统内存状态(如查看/proc/buddyinfo
)和调试复杂的内存问题。