OS / Linux / 伙伴(buddy)算法

通常情况下,一个高级操作系统必须要给进程提供基本的、能够在任意时刻申请和释放任意大小内存的功能,就像 malloc 函数那样,然而,实现 malloc 函数并不简单,由于进程申请内存的大小是任意的,如果操作系统对 malloc 函数的实现方法不对,将直接导致一个不可避免的问题,那就是内存碎片。

内存碎片就是内存被分割成很小很小的一些块,这些块虽然是空闲的,但是却小到无法使用。随着申请和释放次数的增加,内存将变得越来越不连续。最后,整个内存将只剩下碎片,即使有足够的空闲页框可以满足请求,但要分配一个大块的连续页框就可能无法满足,所以减少内存浪费的核心就是尽量避免产生内存碎片。

针对这样的问题,有很多行之有效的解决方法,其中伙伴算法被证明是非常行之有效的一套内存管理方法,因此也被相当多的操作系统所采用。

伙伴算法,简而言之,就是将内存分成若干块,然后尽可能以最适合的方式满足程序内存需求的一种内存管理算法,伙伴算法的一大优势是它能够完全避免外部碎片的产生。什么是外部碎片以及内部碎片,前面博文 slab 分配器后面已有介绍。申请时,伙伴算法会给程序分配一个较大的内存空间,即保证所有大块内存都能得到满足。很明显分配比需求还大的内存空间,会产生内部碎片。所以伙伴算法虽然能够完全避免外部碎片的产生,但这恰恰是以产生内部碎片为代价的。

Linux 便是采用这著名的伙伴系统算法来解决外部碎片的问题。把所有的空闲页框分组为 11 块链表,每一块链表分别包含大小为 1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。对1024 个页框的最大请求对应着 4MB 大小的连续 RAM 块。每一块的第一个页框的物理地址是该块大小的整数倍。例如,大小为 16 个页框的块,其起始地址是 16 * 2 ^ 12 (2 ^ 12 = 4096,这是一个常规页的大小)的倍数。

下面通过一个简单的例子来说明该算法的工作原理:

假设要请求一个 256(129 ~ 256)个页框的块。算法先在 256 个页框的链表中检查是否有一个空闲块。如果没有这样的块,算法会查找下一个更大的页块,也就是,在 512 个页框的链表中找一个空闲块。如果存在这样的块,内核就把 512 的页框分成两等分,一般用作满足需求,另一半则插入到 256 个页框的链表中。如果在 512 个页框的块链表中也没找到空闲块,就继续找更大的块 —— 1024 个页框的块。如果这样的块存在,内核就把 1024 个页框块的 256 个页框用作请求,然后剩余的 768 个页框中拿 512 个插入到 512 个页框的链表中,再把最后的 256 个插入到 256 个页框的链表中。如果 1024 个页框的链表还是空的,算法就放弃并发出错误信号。

简而言之,就是在分配内存时,首先从空闲的内存中搜索比申请的内存大的最小的内存块。如果这样的内存块存在,则将这块内存标记为“已用”,同时将该内存分配给应用程序。如果这样的内存不存在,则操作系统将寻找更大块的空闲内存,然后将这块内存平分成两部分,一部分返回给程序使用,另一部分作为空闲的内存块等待下一次被分配。

以上过程的逆过程就是页框块的释放过程,也是该算法名字的由来。内核试图把大小为 b 的一对空闲伙伴块合并为一个大小为 2b 的单独块。满足以下条件的两个块称为伙伴:

  • 两个块具有相同的大小,记作 b 。
  • 它们的物理地址是连续的。
  • 第一块的第一个页框的物理地址是 2 * b * 2^12 的倍数。

该算法是迭代的,如果它成功合并所释放的块,它会试图合并 2b 的块,以再次试图形成更大的块。

假设要释放一个 256 个页框的块,算法就把其插入到 256 个页框的链表中,然后检查与该内存相邻的内存,如果存在同样大小为 256 个页框的并且空闲的内存,就将这两块内存合并成 512 个页框,然后插入到 512 个页框的链表中,如果不存在,就没有后面的合并操作。然后再进一步检查,如果合并后的 512 个页框的内存存在大小为 512 个页框的相邻且空闲的内存,则将两者合并,然后插入到 1024 个页框的链表中。

简而言之,就是当程序释放内存时,操作系统首先将该内存回收,然后检查与该内存相邻的内存是否是同样大小并且同样处于空闲的状态,如果是,则将这两块内存合并,然后程序递归进行同样的检查。

下面通过一个例子,来深入地理解一下伙伴算法的真正内涵(下面这个例子并不严格表示Linux 内核中的实现,是阐述伙伴算法的实现思想):

假设系统中有 1MB 大小的内存需要动态管理,按照伙伴算法的要求:需要将这 1 M 大小的内存进行划分。这里,我们将这 1 M 的内存分为 64K、64K、128K、256K、和 512K 共五个部分,如下图 a 所示

1、此时,如果有一个程序 A 想要申请一块 45 K 大小的内存,则系统会将第一块 64 K 的内存块分配给该程序(产生内部碎片为代价),如图 b 所示;

2、然后程序 B 向系统申请一块 68 K 大小的内存,系统会将 128 K 内存分配给该程序,如图 c 所示;

3、接下来,程序 C 要申请一块大小为 35 K 的内存。系统将空闲的 64 K 内存分配给该程序,如图 d 所示;

4、之后程序 D 需要一块大小为 90 K 的内存。当程序提出申请时,系统本该分配给程序 D 一块 128 K 大小的内存,但此时内存中已经没有空闲的 128 K 内存块了,于是根据伙伴算法的原理,系统会将 256 K 大小的内存块平分,将其中一块分配给程序 D,另一块作为空闲内存块保留,等待以后使用,如图 e 所示;

5、紧接着,程序 C 释放了它申请的 64 K 内存。在内存释放的同时,系统还负责检查与之相邻并且同样大小的内存是否也空闲,由于此时程序A并没有释放它的内存,所以系统只会将程序 C 的 64 K 内存回收,如图 f 所示;

6、然后程序 A 也释放掉由它申请的 64 K 内存,系统随机发现与之相邻且大小相同的一段内存块恰好也处于空闲状态。于是,将两者合并成 128 K 内存,如图 g 所示;

7、之后程序 B 释放掉它的 128 k,系统也将这块内存与相邻的 128 K 内存合并成 256 K 的空闲内存,如图 h 所示;

8、最后程序 D 也释放掉它的内存,经过三次合并后,系统得到了一块 1024 K 的完整内存,如图 i 所示。

 

有了前面的了解,我们通过Linux 内核源码(mmzone.h)来看看伙伴算法是如何实现的:

伙伴算法管理结构

#define MAX_ORDER 11

struct zone {

……

struct free_area free_area[MAX_ORDER];

……

}

struct free_area {

struct list_head free_list[MIGRATE_TYPES];

unsigned long nr_free; //该组类别块空闲的个数

};

前面说到伙伴算法把所有的空闲页框分组为 11 块链表,内存分配的最大长度便是 2^10 页面。

上面两个结构体向我们揭示了伙伴算法管理结构。zone 结构中的 free_area 数组,大小为 11,分别存放着这 11 个组,free_area 结构体里面又标注了该组别空闲内存块的情况。

                                          

将所有空闲页框分为 11 个组,然后同等大小的串成一个链表对应到 free_area 数组中。这样能很好的管理这些不同大小页面的块。

(啊哦,有时间再补充吧...)

 

 

(SAW:Game Over!)

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux内核中采用了伙伴算法来管理内存。伙伴算法是一种二叉树算法,它将可用内存块组织成一个二叉树,每个节点表示一块内存区域。当一个内存块被分配时,它会被划分成两个等大小的块,其中一个块被分配给请求者,另一个块成为伙伴块。如果伙伴块也是空闲的,那么这两个伙伴块会被合并成一个更大的块,这个过程会一直持续到不能再合并为止。 伙伴算法的实现主要包括以下几个步骤: 1. 初始化内存池,将整个可用内存块作为一棵二叉树。 2. 当有内存请求时,从根节点开始遍历二叉树,找到第一个大小合适的空闲块。 3. 如果找到的空闲块比请求的内存大,就将它划分成两个等大小的块,并将其中一个块分配给请求者,另一个块成为伙伴块。 4. 如果伙伴块也是空闲的,那么这两个伙伴块会被合并成一个更大的块,这个过程会一直持续到不能再合并为止。 5. 当内存释放时,将该内存块标记为空闲状态,并检查它的伙伴块是否也为空闲状态,如果是,则将这两个伙伴块合并成一个更大的块。 下面是一个简单的伙伴算法的实现示例: ```c struct buddy_node { int size; // 内存块大小 int used; // 是否已被使用 struct buddy_node *next; // 指向下一个空闲块 }; struct buddy_node *buddy_pool; // 内存池 int pool_size; // 内存池大小 void buddy_init(int size) { pool_size = size; buddy_pool = (struct buddy_node *)malloc(sizeof(struct buddy_node) * pool_size); buddy_pool[0].size = pool_size; buddy_pool[0].used = 0; buddy_pool[0].next = NULL; } int buddy_alloc(int size) { int node_size = 1; while (node_size < size) { node_size <<= 1; } for (int i = 0; i < pool_size; i++) { if (buddy_pool[i].size == node_size && !buddy_pool[i].used) { buddy_pool[i].used = 1; return i; } } for (int i = 0; i < pool_size; i++) { if (buddy_pool[i].size > node_size && !buddy_pool[i].used) { int left = i; int right = i + node_size; buddy_pool[left].used = 1; buddy_pool[right].size = buddy_pool[left].size - node_size; buddy_pool[right].used = 0; buddy_pool[right].next = buddy_pool[left].next; buddy_pool[left].next = &buddy_pool[right]; buddy_pool[left].size = node_size; return left; } } return -1; } void buddy_free(int index) { buddy_pool[index].used = 0; struct buddy_node *buddy = &buddy_pool[index ^ buddy_pool[index].size]; while (buddy->next != &buddy_pool[index]) { buddy = buddy->next; } buddy->next = buddy_pool[index].next; buddy_pool[index ^ buddy_pool[index].size].size += buddy_pool[index].size; buddy_pool[index ^ buddy_pool[index].size].used = 0; buddy_pool[index].next = NULL; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值