伙伴系统关键实现原理和机制


伙伴系统用于处理物理内存的连续页框分配和释放,防止内存碎片:

  • 随着碎片的增多,系统中看似有足够的空闲内存,但是这些空闲内存由于碎片化,无法满足一些较大的内存申请请求。
  • 这就可能导致系统频繁地进行内存交换操作(如果有虚拟内存机制的话),将内存中的数据和磁盘交换空间中的数据进行交换,从而降低系统的性能

伙伴系统(Buddy System)

伙伴系统 是 Linux 内核中用于管理物理内存页的核心分配器,其核心目标是高效分配和释放 连续物理页,同时尽可能减少内存碎片。以下是其关键实现原理和机制:


一、伙伴系统的核心思想

  1. 内存块分割与合并

    • 将物理内存划分为 2^order 个连续页的块order 称为“阶”,范围通常为 0~MAX_ORDER-1)。
    • 分配:若请求大小的块不可用,则递归分割更大的块,生成两个“伙伴”块。
    • 释放:释放后的块会检查其伙伴是否空闲,若空闲则合并为更大的块,减少碎片。
  2. 防碎片优化

    • 通过合并机制减少 外部碎片(无法分配连续大块内存)。
    • 但可能引入 内部碎片(分配块大于实际需求)。
  3. 快速路径与慢速路径

    • 伙伴系统通过快速路径和慢速路径的分工,实现了高效的内存管理。
    • 快速路径处理理想情况下的内存操作,而慢速路径应对资源不足或碎片整理的复杂场景
    • 这种分层策略平衡了性能与功能完整性,是操作系统内存管理的核心机制之一:
  • 分配流程
    1. 快速路径尝试:检查目标阶数的空闲链表,若有块则直接分配。
    2. 慢速路径回退:若失败,向上查找更大阶的块,分割后分配,剩余块加入空闲链表。
  • 释放流程
    1. 快速标记:将块标记为空闲,插入对应链表。
    2. 慢速合并:递归检查伙伴是否空闲,若满足条件则合并并升级到更高阶链表。

二、伙伴系统的核心数据结构

  1. 内存区域(struct zone
    每个内存区域(如 ZONE_DMAZONE_NORMAL)独立管理伙伴系统。

  2. 空闲链表(free_area

    struct free_area {
        struct list_head    free_list[MIGRATE_TYPES]; // 按迁移类型分类的空闲链表
        unsigned long       nr_free;                 // 空闲块总数
    };
    
    • 每个 free_area 对应一个阶(order),管理该阶的空闲内存块。
    • 通过 MIGRATE_TYPES 分类(如不可移动、可回收、可移动),减少内存碎片对伙伴合并的影响
  3. 页框(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 个连续页)。
  • 流程
    1. 从当前阶的 free_area[order] 查找空闲块。
    2. 若当前阶无空闲块,向高阶(order+1)查找,找到后分割为两个低阶块:
      • 一块用于分配,另一块加入低阶空闲链表。
    3. 递归分割直至满足请求的阶。
    4. 更新 free_area 计数和 struct page 元数据。
2. 内存释放(free_pages
  • 输入参数
    • 起始页的 struct page 指针。
    • order:待释放块的阶。
  • 流程
    1. 检查待释放块的伙伴块是否空闲且同阶。
    2. 若伙伴块空闲,合并两者形成一个高阶块。
    3. 递归合并直至无法继续或达到最大阶。
    4. 将最终合并后的块加入对应阶的 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)根据内存压力动态调整分配策略(如 minlowhigh 水位)。
每 CPU 页缓存(pcp)缓存单页(order=0)分配请求,减少对伙伴系统链表的频繁操作。

六、伙伴系统与 Slab 分配器的关系

  1. 层级分工

    • 伙伴系统:分配 连续物理页(通常为 order >= 0 的块)。
    • Slab 分配器:基于伙伴系统分配的连续页,进一步拆分为 小对象缓存(如几十字节的结构体)。
  2. 协作流程

    • Slab 通过 kmem_getpages() 从伙伴系统申请物理页。
    • 当 Slab 中所有对象均释放时,通过 kmem_freepages() 将物理页返还给伙伴系统。

七、代码示例:伙伴系统分配与释放

// 分配 2^3 = 8 个连续页
struct page *page = alloc_pages(GFP_KERNEL, 3);
if (page) {
    void *vaddr = page_address(page); // 转换为虚拟地址
    // 使用内存...
    __free_pages(page, 3); // 释放
}

八、伙伴系统的局限性

  1. 内部碎片

    • 若请求大小不是 2^order 的整数倍,可能浪费内存(例如请求 3 页,实际分配 4 页)。
  2. 外部碎片缓解有限

    • 尽管合并机制减少外部碎片,但长期运行后仍可能因内存布局问题无法分配大块连续内存。

九、伙伴系统(Buddy System)的算法逻辑

伙伴系统(Buddy System)是一种高效的内存分配算法,通过将内存页帧按大小分组,快速找到合适大小的内存块,从而减少内存碎片。

1. 分配逻辑

当用户请求分配 n 阶页块时,伙伴系统会按照以下步骤进行:

  1. 查找 n 阶页块链表

    • 如果 n 阶页块链表不为空,直接从链表中取出一个 n 阶页块并分配给用户。
    • 如果 n 阶页块链表为空,进入下一步。
  2. 查找更高阶的页块链表

    • n+1 阶页块链表开始查找,如果 n+1 阶页块链表不为空,取出一个 n+1 阶页块,将其分成两个 n 阶页块。
    • 将其中一个 n 阶页块加入 n 阶页块链表,另一个 n 阶页块分配给用户。
    • 如果 n+1 阶页块链表也为空,继续查找 n+2 阶页块链表,重复上述步骤,直到找到一个不为空的更高阶页块链表。
  3. 查找最高阶页块链表

    • 如果所有更高阶页块链表都为空,查找最高阶(10 阶)页块链表。
    • 如果 10 阶页块链表也为空,进入后备迁移类型的页块链表查找。
  4. 后备迁移类型查找

    • 从最高阶(10 阶)页块链表开始,按照后备迁移类型顺序查找,直到找到一个不为空的页块链表。
    • 如果后备页块也分配不到内存,进行内存回收。

2. 释放逻辑

当用户释放内存时,伙伴系统会按照以下步骤进行:

  1. 合并页块

    • 假设释放的是 n 阶页块,页帧号为 x
    • 检查 x-1x+1 页帧是否也是 n 阶页块且为 free
    • 如果 x-1 页帧是 free 且符合对齐要求(即 (x-1) % (2^n) == 0),则合并 x-1x 页帧,形成一个 n+1 阶页块。
    • 如果 x+1 页帧是 free 且符合对齐要求(即 x % (2^n) == 0),则合并 xx+1 页帧,形成一个 n+1 阶页块。
    • 如果 x-1x+1 页帧都不满足条件,直接将 x 页帧加入 n 阶页块链表。
  2. 继续合并

    • 合并后的 n+1 阶页块继续检查其前后页块是否为 free 且符合对齐要求。
    • 例如,如果合并后的 n+1 阶页块的前一个页块也是 free 且符合对齐要求,继续合并形成 n+2 阶页块。
    • 重复此过程,直到无法合并或已经到达最大阶(10 阶)页块。
  3. 插入页块链表

    • 最终形成的页块插入到对应的页块链表中。

示例

假设用户申请了一个 0 阶页块,页帧号为5 。用完后,归还给伙伴系统:

  1. 检查 4 号页帧

    • 如果 4 号页帧是 free,则 4 和 5 合并成一个 1 阶页块,首页帧号为 4。
    • 如果 4 号页帧不是 free,则 5 号页帧直接还给 0 阶页块链表。
  2. 检查 6 号页帧

    • 6 号页帧即使 free,也不能与 5 号页帧合并,因为 5 不符合对齐要求(5 除以 2 不能整除)。
    • 4 和 5 合并成 1 阶页块后,继续检查 6 和 7 号页帧是否 free 且符合对齐要求。
    • 如果 6 和 7 号页帧是 free 且符合对齐要求(4 除以 4 能整除),则 4、5、6、7 合并成一个 2 阶页块。
  3. 继续合并

    • 如果 2 和 3 号页帧是 free 但不符合对齐要求(2 除以 4 不能整除),则不能与 4、5、6、7 合并。
    • 继续检查更高阶页块,直到无法合并或到达最大阶(10 阶)页块。
  4. 插入页块链表

    • 最终形成的页块插入到对应的页块链表中。

总结

伙伴系统通过 分割-合并策略阶分类管理,在内核中高效分配物理连续内存,是 Slab 等上层分配器的基石。其设计平衡了性能与碎片问题,但在极端场景仍需结合其他机制(如内存压缩、CMA)优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值