内存管理——伙伴系统算法

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

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

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

伙伴算法原理

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个页框的链表还是空的,算法就放弃并发出错误信号。

相关数据结构

#define MAX_ORDER 11
 
struct zone {
  ……
	struct free_area	free_area[MAX_ORDER];
	……
} 
struct free_area {
	struct list_head	free_list;
	unsigned long		nr_free;//该组类别块空闲的个数
};

Zone结构体中的free_area数组的第k个元素,它保存了所有连续大小为2^k的空闲块,具体是通过将连续页的第一个页插入到free_list中实现的,连续页的第一个页的页描述符的private字段表明改部分连续页属于哪一个order链表。

伙伴算法系统初始化

Linux内核启动时,伙伴算法还不可用,linux是通过bootmem来管理内存,在mem_init中
会把bootmem位图中空闲的内存块插入到伙伴算法系统的free_list中。
调用流程如下
mem_init----->__free_all_bootmem()—>free_all_bootmem()—>free_all_bootmem_core(NODE_DATA(0))–>
free_all_bootmem_core(pgdat)

//利用free_page 将页面分给伙伴管理器
free_all_bootmem
    return(free_all_bootmem_core(NODE_DATA(0)));  //#define NODE_DATA(nid)		(&contig_page_data)
        bootmem_data_t *bdata = pgdat->bdata;
        page = virt_to_page(phys_to_virt(bdata->node_boot_start));
		idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
		map = bdata->node_bootmem_map;
		
		for (i = 0; i < idx; ) 
			unsigned long v = ~map[i / BITS_PER_LONG];
			//如果32个页都是空闲的
			if (gofast && v == ~0UL)
				count += BITS_PER_LONG;
				__ClearPageReserved(page);
				order = ffs(BITS_PER_LONG) - 1;
				//设置32个页的引用计数为1
				set_page_refs(page, order)
				
				//一次性释放32个页到空闲链表
				__free_pages(page, order);
				i += BITS_PER_LONG;
				page += BITS_PER_LONG;
				
			//这32个页中,只有部分是空闲的	
			else if (v)
				for (m = 1; m && i < idx; m<<=1, page++, i++)
					if (v & m)
						count++;
						__ClearPageReserved(page);
						set_page_refs(page, 0);
						//释放单个页
						__free_page(page);
			else
				i+=BITS_PER_LONG;
				page += BITS_PER_LONG;
				
		//释放内存分配位图本身		
		page = virt_to_page(bdata->node_bootmem_map);
		for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++)
			__ClearPageReserved(page);
			set_page_count(page, 1);
			__free_page(page);
        

伙伴算法系统分配空间

page = __rmqueue(zone, order);
	//从所请求的order开始,扫描每个可用块链表进行循环搜索。
    for (current_order = order; current_order < MAX_ORDER; ++current_order)
        area = zone->free_area + current_order;
        if (list_empty(&area->free_list))
            continue; 
        page = list_entry(area->free_list.next, struct page, lru);  
		//首先在空闲块链表中删除第一个页框描述符。
        list_del(&page->lru);
		//清楚第一个页框描述符的private字段,该字段表示连续页框属于哪一个大小的链表
        rmv_page_order(page);
        area->nr_free--;
        zone->free_pages -= 1UL << order;
		//如果是从更大的order链表中申请的,则剩下的要重新插入到链表中
        return expand(zone, page, order, current_order, area);
            unsigned long size = 1 << high;
            while (high > low)
                area--;
                high--;
                size >>= 1; 
				//该部分连续页面插入到对应的free_list中
                list_add(&page[size].lru, &area->free_list); 
                area->nr_free++;
				//设置该部分连续页面的order
                set_page_order(&page[size], high);
                    page->private = order;
                    __SetPagePrivate(page);
                         __set_bit(PG_private, &(page)->flags)
            return page;

伙伴算法系统回收空间

free_pages_bulk
	//linux内核将空间分为三个区,分别是ZONE_DMA、ZONE_NORMAL、ZONR_HIGH,zone_mem_map字段就是指向该区域第一个页描述符
    struct page *base = zone->zone_mem_map;
    while (!list_empty(list) && count--)  
        page = list_entry(list->prev, struct page, lru);
        list_del(&page->lru);
        __free_pages_bulk
            int order_size = 1 << order;
			//该段空间的第一个页的下标
            page_idx = page - base; 
            zone->free_pages += order_size;
			//最多循环10 - order次。每次都将一个块和它的伙伴进行合并。
            while (order < MAX_ORDER-1)
				//寻找伙伴,如果page_idx=128,order=4,则buddy_idx=144
                buddy_idx = (page_idx ^ (1 << order)); 
                buddy = base + buddy_idx;
                /**
                 * 判断伙伴块是否是大小为order的空闲页框的第一个页。
                 * 首先,伙伴的第一个页必须是空闲的(_count == -1)
                 * 同时,必须属于动态内存(PG_reserved被清0,PG_reserved为1表示留给内核或者没有使用)
                 * 最后,其private字段必须是order
                 */
                if (!page_is_buddy(buddy, order))
                    break;
                list_del(&buddy->lru);
                area = zone->free_area + order;
				//原先所在的区域空闲页减少
                area->nr_free--;   
                rmv_page_order(buddy);
                    __ClearPagePrivate(page);
                    page->private = 0;
                page_idx &= buddy_idx;
                order++;

			/**
			 * 伙伴不能与当前块合并。
			 * 将块插入适当的链表,并以块大小的order更新第一个页框的private字段。
			 */
            coalesced = base + page_idx;
            set_page_order(coalesced, order);
            list_add(&coalesced->lru, &zone->free_area[order].free_list);
            zone->free_area[order].nr_free++;
发布了56 篇原创文章 · 获赞 8 · 访问量 13万+

猜你喜欢

转载自blog.csdn.net/u014104588/article/details/104185163