Buddy System,伙伴系统,linux中用来分配、释放内存页块的经典算法。数据结构structzone中有个free_area数组。存放着本zone的空闲页块链表,注意这是一组链表,而不是一个链表。我们常常需要按“块”分配连续多个内存页面,因此需要用链表来保存长度为1、2、4、8、…、直至2MAX_ORDER-1的页块。通常MAX_ORDER为11,则free_area中一共保存了11个链表。最大可分配的内存块是1024个页面,共4MB的内存空间。
那么什么是伙伴呢?如果同一个链表中的两个页块满足下面这三个条件,我们就称这两个页块为伙伴:
1) 两个块的大小相同,假设为b。
2) 两个块的物理地址连续。
3) 伙伴中第一个块的起始物理地址是2*b*PAGE_SIZE的整数倍。即第0块和第1块是伙伴,第2块和第3块是伙伴,但是第1块和第2块不是伙伴。这样规定的目的是确保一对伙伴中的两个块可以合并成更高一级的大块。
伙伴在这里的意义包含几种情况,一对伙伴表示两个 块,按照从0开始的索引,一对伙伴就是从偶数开始到其相邻奇数的两个相邻的块,如果order为0,那么k/k+1(k为偶数)就是一对伙伴,如果 order为2,那么k/k+2(k为偶数)就是一对伙伴,伙伴就是两个相邻的块,它们的关系有三种,第一就是一个被分配时,那么另一个就等着这个分配出去的块被释放后合并,然后递归的进行更大order的合并;第二就是如果两个都被分配,那么肯定有一个先被释放,那么化为情况一,注意在等待伙伴被释放的 同时,该块可以被分配,从而情况一化为情况二,但是最终它们结果总是趋向于情况三,也就是都被释放从而被合并然后插入到更大一层的链表中。
分配
ULK中通过一个例子介绍了伙伴系统的分配流程。假设我们需要申请28(256)个连续的页,算法首先查找28链表,如果没有找到,继续查找29(512)链表,如果查找成功,分配其中的256个页面,将剩余的256个页面作为一块插入到28链表。如果在29链表中查找仍然失败,内核继续查找下一个更大块的链表210(1024),如果查找成功,分配其中的256个页面,将剩余768个页面中的高地址区的512个页面作为一块插入到29链表,低地址区的256个页面作为一块插入到28链表。如果在210链表中查找仍然失败,则放弃查找并报错。
释放
页块释放是分配的逆过程。释放一个页块时,先在其对应的free_area链表中查找是否有伙伴存在,如果没有伙伴,直接将页块插入链表头。如果有,则将其从链表上摘下,合并成一个更大的块,然后继续查找合并后的块在更大一级链表中是否有伙伴存在,直至不能合并或者已经合并至最大的块为止。
从上面的释放过程可知,伙伴中两个页块的状态只可能有2种情况:都被分配出去,或是伙伴中的一个被分配出去。如果都没有被分配出去,那么必然会合并成更大的页块。
算法
如果是老版本的内核,数据结构free_area中还有一个map成员。每个order的链表都对应一个位图,位图中的每一个比特位对应一对伙伴的状态。位图的设计比较复杂,追求简洁的linux内核已经抛弃了位图,取而代之的是巧妙的位运算。
下面介绍新版本kernel中伙伴系统的算法:
假设物理内存一组页面的PFN为0、1、2、3、4、5、……。
n 如果要把这组页面插入order为0的链表,那么0和1是伙伴,2和3是伙伴,……。
n 如果要把这组页面插入order为1的链表,那么[0-1]与[2-3]是伙伴,[4-5]与[6-7]是伙伴,……。
n 如果要把这组页面插入order为2的链表,那么[0-3]与[4-7]是伙伴,[8-11]与[12-15]是伙伴,……。
n 如果要把这组页面插入order为3的链表,那么[0-7]与[8-15]是伙伴,[16-23]与[24-31]是伙伴,……。
内核的伙伴系统很简单的将整个物 理内存按照单一页面进行索引,也即是0,1,2,3,...但是怎么区分不同的order呢?很简单,如果是order为0,那么就是简单的自然数索引,如果order为1,那么就是隔一个一个索引,就是说0和2会是一对伙伴,如果order为2,那么就是隔4个,就是0和4会是一对伙伴,如果用十进制比较不好理解,那么用二进制就很简单了
如果在order级别为O的链表中,给出页块B1,那么其伙伴B2的计算公式为:
公式一:B2 = B1 ^ (1 << O)
公式二:P = B & ~(1 << O)
其 中1中B1和B2代表两个伙伴,公式2表示的是如果能合并的话B代表合并前的索引,而PB代表的是合并之后的索引,其实就是屏蔽了后面的几个二进制位。公 式1主要就是找朋友的方式,其实就是将某一位按位取反了,得到的就是不同的一个相邻的块,比如:000,001,010,011,100,101, 110,111中分别代表十进制0,1,2,3,4,5,6,7,按照伙伴算法公式,如果order为0,那么0/1,2/3,4/5,6/7就是伙伴, 因为它们的每一对的最低位不同,别的位相同,符合公式1,如果order为2,那么0,1,2,3/4,5,6,7就是一对伙伴,因为它们的第3位也就是 1<<2位不同,而更低的位在合并过程中已经由公式2屏蔽了,更高的位都为0,因此也符合公式,于是它们也是伙伴,看来这两个公式代替了原来 的位图的大部分原理,很是奇妙!linux采用的统一编址,低位屏蔽的方式处理不同的order的索引甚是不错,因为不同的伙伴链表中连续页面的数量都是 2的k次幂,这样通过位运算就可以处理不同order的索引问题。对于order比较大的,比如order为3,意思是该order对应的链表中连续页面的数量是8,那么对于该链表元素的索引,我们知道0到7是第一个元素,8到15是第二个,如果我们目前释放的索引是8,order为3,那么很显然当前块的伙伴就是0到7页面,8到15的二进制是1000,1001,1010,1011,1100,1101,1110,1111,看得出来低三位和0到7的 低三位在更大的尺度上是一致的,虽然每一个都不一样,但是正如order的意义所在,如果我们把0到7和8到15这两组数的低三位捆成捆儿的话,我们会发 现它们是一致的,order表现的正是这个尺度中的绳索。0到7和8到15这两组数的低三位不再考虑的话,我们发现它们的第四位不同,这也正是上面公式1 的体现。
释放页面的另一半就是分配页面了,只有两半互相配合,一张一弛才可以做到和谐,在分配的时候如果当前请求的
order 对应的链表还有空闲块,那么直接分配即可,它的伙伴肯定已经分配了,否则它不可能自己留下,早就和它的伙伴合并成更大的块了,接下来它们哥儿俩开始互相等待,徘徊于上述的两个情况,最终向情况三收敛,如果当前的请求order没有空闲内存块了,那么就在order+1的链表中找,依次类推,一旦找到的话就 会分解这个更大的块,然后递归的将分解后的小块插入更小的order的链表中,值得注意的是,这里体现了和谐,分配内存的机制并没有故意将内存碎片化而释 放内存的机制却有意将内存整体化,这样最终的结果就是内存向整体化收敛,在分配的时候进行的expand中就是分解操作,虽然是分解,但是是迫不得已的分 解,最小化碎片的分解,其的反向操作就是释放中的__free_one_page,其本质是有意的合并,正好抵消了expand中的迫不得已的分解,由于迫不得已的分解并没有引入除了需求以外额外的小内存(碎片),因此结果总是会向内存块整体化合并跑偏,最终最小化碎片,下面看一眼expand函数:
static inline void expand(struct zone *zone, struct page *page, int low, int high, structfree_area *area, int migratetype) { unsigned long size = 1<< high; while (high > low) { area--; high--; size >>= 1; VM_BUG_ON(bad_range(zone, &page[size])); list_add(&page[size].lru, &area->free_list[migratetype]); area->nr_free++; set_page_order(&page[size], high); } }