伙伴系统用于处理物理内存的连续页框分配和释放,防止内存碎片:
- 随着碎片的增多,系统中看似有足够的空闲内存,但是这些空闲内存由于碎片化,无法满足一些较大的内存申请请求。
- 这就可能导致系统频繁地进行内存交换操作(如果有虚拟内存机制的话),将内存中的数据和磁盘交换空间中的数据进行交换,从而降低系统的性能
伙伴系统(Buddy System)
伙伴系统 是 Linux 内核中用于管理物理内存页的核心分配器,其核心目标是高效分配和释放 连续物理页,同时尽可能减少内存碎片。以下是其关键实现原理和机制:
一、伙伴系统的核心思想
-
内存块分割与合并
- 将物理内存划分为 2^order 个连续页的块(
order
称为“阶”,范围通常为 0~MAX_ORDER-1)。 - 分配:若请求大小的块不可用,则递归分割更大的块,生成两个“伙伴”块。
- 释放:释放后的块会检查其伙伴是否空闲,若空闲则合并为更大的块,减少碎片。
- 将物理内存划分为 2^order 个连续页的块(
-
防碎片优化
- 通过合并机制减少 外部碎片(无法分配连续大块内存)。
- 但可能引入 内部碎片(分配块大于实际需求)。
-
快速路径与慢速路径
- 伙伴系统通过快速路径和慢速路径的分工,实现了高效的内存管理。
- 快速路径处理理想情况下的内存操作,而慢速路径应对资源不足或碎片整理的复杂场景。
- 这种分层策略平衡了性能与功能完整性,是操作系统内存管理的核心机制之一:
- 分配流程:
- 快速路径尝试:检查目标阶数的空闲链表,若有块则直接分配。
- 慢速路径回退:若失败,向上查找更大阶的块,分割后分配,剩余块加入空闲链表。
- 释放流程:
- 快速标记:将块标记为空闲,插入对应链表。
- 慢速合并:递归检查伙伴是否空闲,若满足条件则合并并升级到更高阶链表。
二、伙伴系统的核心数据结构
-
内存区域(
struct zone
)
每个内存区域(如ZONE_DMA
、ZONE_NORMAL
)独立管理伙伴系统。 -
空闲链表(
free_area
)struct free_area { struct list_head free_list[MIGRATE_TYPES]; // 按迁移类型分类的空闲链表 unsigned long nr_free; // 空闲块总数 };
- 每个
free_area
对应一个阶(order
),管理该阶的空闲内存块。 - 通过
MIGRATE_TYPES
分类(如不可移动、可回收、可移动),减少内存碎片对伙伴合并的影响。
- 每个
-
页框(
struct page
)
每个物理页的struct page
记录其所属的伙伴块状态:struct page { unsigned long flags; // 页状态标志(如是否属于伙伴系统) int _mapcount; // 引用计数 unsigned int order; // 所属伙伴块的阶 struct list_head lru; // 链接到空闲链表 ... };
三、伙伴系统操作流程
1. 内存分配(alloc_pages
)
- 输入参数:
gfp_mask
:分配标志(如GFP_KERNEL
)。order
:请求的阶(分配2^order
个连续页)。
- 流程:
- 从当前阶的
free_area[order]
查找空闲块。 - 若当前阶无空闲块,向高阶(
order+1
)查找,找到后分割为两个低阶块:- 一块用于分配,另一块加入低阶空闲链表。
- 递归分割直至满足请求的阶。
- 更新
free_area
计数和struct page
元数据。
- 从当前阶的
2. 内存释放(free_pages
)
- 输入参数:
- 起始页的
struct page
指针。 order
:待释放块的阶。
- 起始页的
- 流程:
- 检查待释放块的伙伴块是否空闲且同阶。
- 若伙伴块空闲,合并两者形成一个高阶块。
- 递归合并直至无法继续或达到最大阶。
- 将最终合并后的块加入对应阶的
free_area
链表。
3. 其他与伙伴系统相关的函数
以下是伙伴系统相关的关键函数及其作用:
函数名 | 作用描述 |
---|---|
alloc_pages | 上层封装函数,调用 __alloc_pages_nodemask ,返回指定 order 的物理页。 |
alloc_page | 分配单页的快捷宏(alloc_pages(0, ...) )。 |
__free_pages | 释放内存页,触发伙伴块的合并操作。 |
free_pages | 用户态封装,确保释放正确的页数。 |
split_page | 将大块内存显式分割为小块(如分配大块后拆分为多个单页)。 |
rmqueue | 伙伴系统内部函数,从 zone 的空闲链表中取出指定 order 的内存块。 |
expand | 处理内存块分裂后的剩余部分,插入低阶链表。 |
__find_buddy_index | 计算伙伴块的索引,用于合并操作。 |
zone_watermark_ok | 检查内存区域的“水位”(空闲页是否足够),决定是否允许分配。 |
补充:
__alloc_pages_nodemask
是 Linux 内核中伙伴系统(Buddy System)的核心入口函数,负责根据请求的页面数量(order
)和内存策略(nodemask
)分配物理内存页。其与伙伴系统的关系如下:
- 接口层:它是伙伴系统对外的直接接口,上层函数(如
alloc_pages
)通过调用它触发内存分配流程。 - 策略实现:通过
nodemask
参数控制内存分配的 NUMA 节点选择(多核服务器场景),结合gfp_mask
(如GFP_KERNEL
)处理内存分配的限制条件(如是否允许睡眠、是否使用高端内存等)。 - 算法调用:内部调用伙伴系统的核心逻辑(如遍历空闲列表、分割内存块、合并伙伴块等),最终返回
struct page
表示的物理页。
四、分配过程步骤
1. 数据结构准备
伙伴系统为每个可能的页框块大小(大小为 2 的幂次方,如 1 页、2 页、4 页等)维护一个空闲链表。这些链表存储了当前可用的对应大小的页框块。在 Linux 内核中,通常用 struct zone
结构体来管理内存区域,其中包含了各个大小页框块的空闲链表数组。以下是简化示意:
struct zone {
// 其他成员...
struct free_area free_area[MAX_ORDER];
};
struct free_area {
struct list_head free_list; // 空闲链表
unsigned int nr_free; // 该链表中可用页框块的数量
};
这里 MAX_ORDER
表示最大的分配阶数,分配的页框数量为 2^order
个。
2. 确定分配阶数
当有内存分配请求时,首先需要根据请求的页框数量确定对应的分配阶数 order
。例如,如果请求 3 个页框,由于伙伴系统分配的页框数量必须是 2 的幂次方,所以会向上取整为 4 个页框,对应的分配阶数 order
为 2(因为 2^2 = 4
)。
3. 查找合适的空闲链表
从对应分配阶数的空闲链表开始查找。如果该链表不为空,说明有合适大小的页框块可用,直接从链表中取出一个页框块进行分配。以下是简化的查找代码示意:
struct page *find_suitable_page(struct zone *zone, unsigned int order) {
struct free_area *area = &zone->free_area[order];
if (!list_empty(&area->free_list)) {
struct list_head *entry = area->free_list.next;
struct page *page = list_entry(entry, struct page, lru);
list_del(entry); // 从链表中移除该页框块
area->nr_free--;
return page;
}
return NULL;
}
4. 拆分页框块
如果对应阶数的空闲链表为空,则需要向上查找更大阶数的空闲链表。找到后,将该大的页框块拆分成两个较小的、互为伙伴的页框块,其中一个用于分配,另一个放入对应较小阶数的空闲链表中。重复这个拆分过程,直到得到所需阶数的页框块。以下是拆分页框块的简化代码示意:
struct page *split_page(struct zone *zone, struct page *page, unsigned int current_order, unsigned int target_order) {
while (current_order > target_order) {
current_order--;
struct page *buddy = page + (1 << current_order); // 计算伙伴页框的地址
// 将伙伴页框加入对应阶数的空闲链表
struct free_area *area = &zone->free_area[current_order];
list_add(&buddy->lru, &area->free_list);
area->nr_free++;
}
return page;
}
struct page *allocate_pages(struct zone *zone, unsigned int order) {
struct page *page = find_suitable_page(zone, order);
if (page) {
return page;
}
// 向上查找更大阶数的空闲链表
for (unsigned int higher_order = order + 1; higher_order < MAX_ORDER; higher_order++) {
struct page *higher_page = find_suitable_page(zone, higher_order);
if (higher_page) {
return split_page(zone, higher_page, higher_order, order);
}
}
return NULL; // 分配失败
}
5. 分配失败处理
如果遍历完所有可能的阶数,仍然没有找到合适的页框块,说明当前系统内存不足,分配失败。此时可以根据具体情况进行相应的处理,如触发内存回收机制、返回错误信息等。
五、伙伴系统的关键优化
机制 | 作用 |
---|---|
迁移类型(MIGRATE_TYPES) | 将内存块按迁移能力(如不可移动、可回收)分类,减少因内存碎片导致的大块分配失败。 |
水位线(Watermark) | 根据内存压力动态调整分配策略(如 min 、low 、high 水位)。 |
每 CPU 页缓存(pcp) | 缓存单页(order=0 )分配请求,减少对伙伴系统链表的频繁操作。 |
六、伙伴系统与 Slab 分配器的关系
-
层级分工
- 伙伴系统:分配 连续物理页(通常为
order >= 0
的块)。 - Slab 分配器:基于伙伴系统分配的连续页,进一步拆分为 小对象缓存(如几十字节的结构体)。
- 伙伴系统:分配 连续物理页(通常为
-
协作流程
- Slab 通过
kmem_getpages()
从伙伴系统申请物理页。 - 当 Slab 中所有对象均释放时,通过
kmem_freepages()
将物理页返还给伙伴系统。
- Slab 通过
七、代码示例:伙伴系统分配与释放
// 分配 2^3 = 8 个连续页
struct page *page = alloc_pages(GFP_KERNEL, 3);
if (page) {
void *vaddr = page_address(page); // 转换为虚拟地址
// 使用内存...
__free_pages(page, 3); // 释放
}
八、伙伴系统的局限性
-
内部碎片
- 若请求大小不是 2^order 的整数倍,可能浪费内存(例如请求 3 页,实际分配 4 页)。
-
外部碎片缓解有限
- 尽管合并机制减少外部碎片,但长期运行后仍可能因内存布局问题无法分配大块连续内存。
九、伙伴系统(Buddy System)的算法逻辑
伙伴系统(Buddy System)是一种高效的内存分配算法,通过将内存页帧按大小分组,快速找到合适大小的内存块,从而减少内存碎片。
1. 分配逻辑
当用户请求分配 n
阶页块时,伙伴系统会按照以下步骤进行:
-
查找
n
阶页块链表:- 如果
n
阶页块链表不为空,直接从链表中取出一个n
阶页块并分配给用户。 - 如果
n
阶页块链表为空,进入下一步。
- 如果
-
查找更高阶的页块链表:
- 从
n+1
阶页块链表开始查找,如果n+1
阶页块链表不为空,取出一个n+1
阶页块,将其分成两个n
阶页块。 - 将其中一个
n
阶页块加入n
阶页块链表,另一个n
阶页块分配给用户。 - 如果
n+1
阶页块链表也为空,继续查找n+2
阶页块链表,重复上述步骤,直到找到一个不为空的更高阶页块链表。
- 从
-
查找最高阶页块链表:
- 如果所有更高阶页块链表都为空,查找最高阶(10 阶)页块链表。
- 如果 10 阶页块链表也为空,进入后备迁移类型的页块链表查找。
-
后备迁移类型查找:
- 从最高阶(10 阶)页块链表开始,按照后备迁移类型顺序查找,直到找到一个不为空的页块链表。
- 如果后备页块也分配不到内存,进行内存回收。
2. 释放逻辑
当用户释放内存时,伙伴系统会按照以下步骤进行:
-
合并页块:
- 假设释放的是
n
阶页块,页帧号为x
。 - 检查
x-1
和x+1
页帧是否也是n
阶页块且为free
。 - 如果
x-1
页帧是free
且符合对齐要求(即(x-1) % (2^n) == 0
),则合并x-1
和x
页帧,形成一个n+1
阶页块。 - 如果
x+1
页帧是free
且符合对齐要求(即x % (2^n) == 0
),则合并x
和x+1
页帧,形成一个n+1
阶页块。 - 如果
x-1
和x+1
页帧都不满足条件,直接将x
页帧加入n
阶页块链表。
- 假设释放的是
-
继续合并:
- 合并后的
n+1
阶页块继续检查其前后页块是否为free
且符合对齐要求。 - 例如,如果合并后的
n+1
阶页块的前一个页块也是free
且符合对齐要求,继续合并形成n+2
阶页块。 - 重复此过程,直到无法合并或已经到达最大阶(10 阶)页块。
- 合并后的
-
插入页块链表:
- 最终形成的页块插入到对应的页块链表中。
示例
假设用户申请了一个 0 阶页块,页帧号为5 。用完后,归还给伙伴系统:
-
检查 4 号页帧:
- 如果 4 号页帧是
free
,则 4 和 5 合并成一个 1 阶页块,首页帧号为 4。 - 如果 4 号页帧不是
free
,则 5 号页帧直接还给 0 阶页块链表。
- 如果 4 号页帧是
-
检查 6 号页帧:
- 6 号页帧即使
free
,也不能与 5 号页帧合并,因为 5 不符合对齐要求(5 除以 2 不能整除)。 - 4 和 5 合并成 1 阶页块后,继续检查 6 和 7 号页帧是否
free
且符合对齐要求。 - 如果 6 和 7 号页帧是
free
且符合对齐要求(4 除以 4 能整除),则 4、5、6、7 合并成一个 2 阶页块。
- 6 号页帧即使
-
继续合并:
- 如果 2 和 3 号页帧是
free
但不符合对齐要求(2 除以 4 不能整除),则不能与 4、5、6、7 合并。 - 继续检查更高阶页块,直到无法合并或到达最大阶(10 阶)页块。
- 如果 2 和 3 号页帧是
-
插入页块链表:
- 最终形成的页块插入到对应的页块链表中。
总结
伙伴系统通过 分割-合并策略 和 阶分类管理,在内核中高效分配物理连续内存,是 Slab 等上层分配器的基石。其设计平衡了性能与碎片问题,但在极端场景仍需结合其他机制(如内存压缩、CMA)优化。