Linux内核空间内存管理(二):buddy system 伙伴系统算法

版权声明:本文为博主整理文章,未经博主允许不得转载。 https://blog.csdn.net/ZYZMZM_/article/details/90670455


外碎片问题

每当页面被分配和回收时,系统都要遇到名为外部碎片的内存碎片问题。这是由于可用页面散布于整个内存空间中,即使系统可用页面总数足够多,但也无法分配大块连续页面

内核应该为分配一组连续的页框而建立一种健壮、高效的分配策略。频繁的请求和释放不同大小的一组连续页框,必然导致在已分配页框的块内分散了许多小块的空闲页框。 这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足

从本质上来说,避免外碎片的方法有两种:

  • 利用分页单元把一组非连续的空闲页框映射到连续的线性地址区间。
  • 开发一种适当的技术来记录现存的空闲连续页框块的情况,以尽量避免为满足对小块的请求而把大的空闲块进行分割。

基于以下三种原因,内核首选第二种方法:

  • 在某些情况下,连续的页框确实是必要的,因为连续的线性地址不足以满足请求。比如给DMA处理器分配缓冲区的时候,DMA会忽略分页单元而直接访问地址总线,因此,所请求的缓冲区就必须位于连续的页框中。
  • 频繁的修改页表势必导致平均访问内存次数的增加,因为会频繁的刷新TLB的内容。
  • 内核通过4MB的页可以访问大块连续的物理内存

伙伴系统概述

Linux使用著名的 伙伴系统(buddy system) 这样的内存管理算法来解决外部碎片问题。

伙伴系统把内存中的空闲块组成链表。每个链表都指向不同大小的内存块,虽然大小不同,但都是 2 n 2^n 。至于系统中有多少个链表,则取决于具体实现。从最小的空闲块链表中分配页面,这样保证了较大的内存块可留给更大的内存请求

当分配的块被释放时,伙伴系统搜索与所释放块大小相等的可用空闲内存块,如果找到相邻的空闲块,那么就将它们合并成两倍于自身大小的一个新内存块。这些可合并的内存块(所释放块和与其相邻的空闲块)就称为伙伴,所以也就有了伙伴系统一说。之所以要这么做,是因为内核需要确保只要页面被释放,就能获得更大的可用内存块。

Linux内核把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存

每个页框块的第一个页框的物理地址是该块大小的整数倍。 例如:大小为16个页框的块,其起始地址是 16 × 2 12 16 × 2^{12} 的倍数。其中, 2 12 = 4096 2^{12}=4096 ,是一个常规页的大小。


我们接下来通过一个简单的例子来说明该算法的工作原理。

假设要请求一个 128 个页框的块(0.5M)。

  • 算法首先会在128个页框的链表中检查是否有一个空闲块。如果没有找到,那么算法会查找下一个更大的页块。
  • 继续在256个页框的链表中找一个空闲块。如果存在这样的块,那么内核就把256的页框分成两等分,一半用做满足请求,另一半插入到 128 个页框的链表中。
  • 如果在256个页框中没有找到空闲块,那么就继续找更大的块——512个页框的块。如果在其中找到空闲块,那么内核就把512个页框的块的128个页框用做请求,然后从剩余的384个页框中拿出256个插入到256个页框的链表中,再把最后的128个插入到128个页框的链表中。
  • 如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找,如果仍然没有,则返回错误

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

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

该算法是迭代的,如果它成功合并所释放的块,那么它会试图继续合并 2 b 2b 的块来形成更大的块


主要数据结构

Linux2.6为每个管理区使用不同的伙伴系统内核空间分为三种区:DMA、NORMAL、HIGHMEM,对于每一种区,都有对应的伙伴算法

每个伙伴系统使用的主要数据结构如下:

  • 每个管理区都关系到mem_map元素的子集,子集中的第一个元素和元素的个数分别由管理区描述符 zone_mem_map 和 size 字段指定。
  • 包含有11个元素、元素类型为 free_area 的一个数组,每个元素对于一种块大小,该数组存放在管理区描述符的 free_area 字段中。
#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 2^{10} 页面

上面两个结构体向我们揭示了伙伴算法管理结构。

  • zone结构中的free_area数组,大小为11,分别存放着这11个组
  • free_area结构体里面又标注了该组别空闲内存块的情况

我们考虑管理区描述符中 free_area 数组的第 k k 个元素,它标识所有大小为 2 k 2^k 的空闲块这个元素的 free_list 字段是双向循环链表的头,这个双向循环链表集中了大小为 2 k 2^k 页的空闲块对应的页描述符

更精确的说,该链表包含每个空闲页块(大小为 2 k 2^k )的起始页框的页描述符;指向链表中相邻元素的指针存放在页描述符的lru字段中。

除了链表头外,free_area 数组的第k个元素同样包含字段nr_free,它指定了大小为 2 k 2^k 页的空闲块的个数。当然,如果没有大小为 2 k 2^k 的空闲页框块,则nr_free等于 0 且 free_list 为空( free_list 的两个指针都指向它自己的 free_list 字段)。

最后,一个 2 k 2^k 的空闲页块的第一个页的描述符的private字段存放了块的 order,也就是数字 k k 正是由于这个字段,当页块被释放时,内核可以确定这个块的伙伴是否也空闲,如果是的话,它可以把两个块结合成大小为 2 k + 1 2^{k+1} 页的单一块。

整个分配图示,大概如下:


分配块

使用 __rmqueue() 函数用来在管理区中找到一个空闲块。该函数需要两个参数:管理区描述符的地址zone和order,order表示请求的空闲页块大小的对数值(0 表示一个单页块,1 表示一个两页块,2表示四个页块,以此类推)。

如果页框被成功分配,__rmqueue()函数就返回第一个被分配页框的页描述符。否则,函数返回NULL。

在__rmqueue()函数中,从所请求order的链表开始,它扫描每个可用块链表进行循环搜索,如果需要搜索更大的order,就继续搜索:

struct free_area *area;
unsigned int current_order;

for (current_order=order; current_order<11; ++current_order) {
    area = zone->free_area + current_order;
    if (!list_empty(&area->free_list))
        goto block_found;
}
return NULL;

如果直到循环结束还没有找到合适的空闲块,那么__rmqueue()就返回NULL

否则,找到了一个合适的空闲块,在这种情况下,从链表中删除它的第一个页框描述符,并减少管理区描述符中的 free_pages 的值:

block_found:
    page = list_entry(area->free_list.next, struct page, lru);
    list_del(&page->lru);
    ClearPagePrivate(page);
    page->private = 0;
    area->nr_free--;
    zone->free_pages -= 1UL << order;

如果从 curr_order 链表中找到的块大于请求的order,就执行一个while循环。这几行代码蕴含的原理如下:当为了满足 2 h 2h 个页框的请求而有必要使用 2 k 2k 个页框的块时 h &lt; k (h &lt; k) ,程序就分配前面的 2 h 2h 个页框,而把后面 2 k 2 h 2k-2h 个页框循环再分配给 free_area 链表中下标在 h h k k 之间的元素:

size = 1 << curr_order;
while (curr_order > order) {
    area--;
    curr_order--;
    size >>= 1;
    buddy = page + size;
    /* insert buddy as first element in the list */
    list_add(&buddy->lru, &area->free_list);
    area->nr_free++;
    buddy->private = curr_order;
    SetPagePrivate(buddy);
}
return page;

因为__rmqueue()函数已经找到了合适的空闲块,所以它返回所分配的第一个页框对应的页描述符的地址page。


释放块

__free_pages_bulk()函数按照伙伴系统的策略释放页框。它使用3个基本输入参数:

  • page:被释放块中所包含的第一个页框描述符的地址。
  • zone:管理区描述符的地址。
  • order:块大小的对数。

__free_pages_bulk()首先声明和初始化一些局部变量:

struct page * base = zone->zone_mem_map;
unsigned long buddy_idx, page_idx = page - base;
struct page * buddy, * coalesced;
int order_size = 1 << order;

page_idx 局部变量包含块中第一个页框的下标,这是相对于管理区中的第一个页框而言的。order_size 局部变量用于增加管理区中空闲页框的计数器

zone->free_pages += order_size;

现在函数开始执行循环,最多循环 (10 - order) 次,每次都尽量把一个块和它的伙伴进行合并。函数以最小的块开始,然后向上移动到顶部:

while (order < 10) {
    buddy_idx = page_idx ^ (1 << order);
    buddy = base + buddy_idx;
    if (!page_is_buddy(buddy, order))
        break;
    list_del(&buddy->lru);
    zone->free_area[order].nr_free--;
    ClearPagePrivate(buddy);
    buddy->private = 0;
    page_idx &= buddy_idx;   /* 合并 */
    order++;
}

比如,我们这里order是 4,那么 order_size 的值为 2 4 2^4 ,也就是16,表明要释放16个连续的page。page_idx为这个连续16个page的老大的mem_map数组的下标。进入循环后,函数首先寻找该块的伙伴,即mem_map数组中page_idx-16或page_idx+16的下标buddy_idx,进一步说明一下,就是为了在下标为16的free_area中找到一个空闲的块,并且这个块与page所带的那个拥有16个page的块相邻。

在循环体内,函数寻找块的下标 buddy_idx,它是拥有 page_idx 页描述符下标块的伙伴,结果这个下标可以被简单地如下计算:

buddy_idx = page_idx ^ (1 << order)

这行代码很巧妙,短小精干。因为order一来就等于4,所以循环从4开始的,即第一个循环为buddy_idx = page_idx ^ (1<<4),即buddy_idx = page_idx ^ 10000。如果page_idx第5位为1,比如是20号页框(10100),那么在异或以后,buddy_idx为4号页框(00100)。如果page_idx第5位为0,比如是第40号页框(101000),那么在异或以后,buddy_idx为56号页框(111000)。

抽象一下上述计算:使用( 1 &lt; o r d e r 1&lt;order )掩码的异或(XOR)转换page_idx第order位的值。因此,如果这个位原先是0,buddy_idx 就等于 page_idx + order_size。相反,如果这个位原先是1,buddy_idx 就等于 page_idx - order_size

一旦知道了伙伴块下标,那么就可以通过下式很容易的获得伙伴块的页描述符

buddy = base + buddy_idx;

现在函数调用 page_is_buddy() 来检查 buddy 是否是真正的值得信赖的伙伴,也就是 buddy 是否描述了大小为order_size的空闲页框块的第一个页

int page_is_buddy(struct page *page, int order)
{
    if (PagePrivate(buddy) && page->private == order &&
          !PageReserved(buddy) && page_count(page) ==0)
        return 1;
    return 0;
}

正如所见,要想成为伙伴,必须满足以下四个条件:

  • buddy 的第一个页必须为空闲( _count 字段等于-1);
  • 它必须属于动态内存( PG_reserved 位清零);
  • 它的 private 字段必须有意义( PG_private 位置位);
  • 它的 private 字段必须存放将要被释放的块的 order。

如果所有这些条件都符合,伙伴块就被释放,并且函数将它从以order排序的空闲链表上删除,并再执行一次循环以寻找两倍大小的伙伴块

如果 page_is_buddy() 中至少有一个条件没有被满足,则该函数跳出循环,因为获得的空闲块不能再和其他空闲块合并。函数将它插入适当的链表并以块大小的 order 更新第一个页框的 private 字段。

coalesced = base + page_idx;
coalesced->private = order;
SetPagePrivate(coalesced);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;

猜你喜欢

转载自blog.csdn.net/ZYZMZM_/article/details/90670455