文章目录
伙伴的算法的引入
在系统运行过程中,经常需要分配一组连续的页,而频繁的申请和释放内存页会导致内存中散布着许多不连续的页,这样,当某一时刻要申请一块较大的连续内存时,虽然系统内存余量足够,即很多页是空闲的,但找不到一大块连续的内存供使用。这就是外部碎片问题。
这是一个真实的物理内存,前面1-5表示5个页面,如果分配了3个4KB,占用了1、3、5,这时候我们想分配一个连续的8KB物理内存,2和4是8KB,但是不连续,不能分配使用。这时候相对于8KB连续物理内存来说,2和4就成了内存碎片了。
伙伴
-
两个块具有相同的大小,记为b
-
它们的物理地址是连续的
-
第一块的第一个页框的物理地址是 2 × b × 2 12 2×b×2^{12} 2×b×212的倍数
第0块和第1块是伙伴,第2块和第3块是伙伴,但第1块和第2块不是伙伴
伙伴位图
用一位描述伙伴块的状态位码,称之为伙伴位码。
比如,bit0为第0块和第1块的伙伴位码,如果bit0为1,表示这两块至少有一块已经分配出去,如果bit0为0,说明两块都空闲,还没分配。
伙伴算法的思想
Linux内核中使用伙伴系统(buddy system)算法来管理内存页,以达到减少外部碎片的目的。
-
它把所有的空闲页放到链表中。每个链表都指向不同大小的内存块,虽然大小不同,但都是2的幂次方(如1,2,4,8,16,32,64,128,256等)。当系统需要分配内存时,就可以从buddy系统中获取。
例如,要申请一块包含4个页的连续内存,就直接从buddy系统中管理4个页连续内存的链表中获取。 -
当系统释放内存时,则将释放的内存放回buddy系统对应的链表中,如果释放内存后发现有两块相邻的内存又可以合并为一个更高阶的内存块。
例如释放4个页,而恰好相邻的内存也为4个页的空闲内存,则合并这两块内存并放到buddy系统管理8个连续页的链表中。同 -
buddy分配器分配的最小单位是一个页。要分配小于一页的内存需要用到slab分配器,而slab是基于buddy分配器的。
伙伴算法的数据结构
Linux2.6为每个管理区使用不同的伙伴系统,内核空间分为三种区,DMA,NORMAL,HIGHMEM,对于每一种区,都有对应的伙伴算法。
struct zone {
...
/*
* 标识出管理区中的空闲页框块。
* 包含11个元素,被伙伴系统使用。分别对应大小的1,2,4,8,16,32,128,256,512,1024连续空闲块的链表。
* 第k个元素标识所有大小为2^k的空闲块。free_list字段指向双向循环链表的头。
*/
struct free_area free_area[MAX_ORDER];
//#define MAX_ORDER 11
free_area数组
struct free_area {
struct list_head free_list;//空闲块的双向链表
unsigned long nr_free;//空闲块的数目
};
伙伴算法内存分配的过程
伙伴算法分配的思想
当内核收到alloc_pages系列函数的分配请求时,首先需要确定是从哪一个节点上分配,然后再确定需要从节点的哪一个zone上分配,最后再根据伙伴算法,确定从zone的哪一个free_area数组成员上分配。在内存不足的情况下,还要回收内存,如果内存还是不够,还要调度kswapd把必要的内存存储到交换分区中。内存分配模块总是试图从代价最小的节点上分配,而对zone的确定则根据alloc_pages()系列函数的gfp_mask用以下规则确定:
-
如果gfp_mask参数设置了__GFP_DMA位,则只能从ZONE_DMA中分配。
-
如果gfp_mask参数设置了__GFP_HIGHMEM位,则能够从ZONE_DMA、ZONE_NORMAL、ZONE__HIGHMEM中分配。
-
如果__GFP_DMA和__GFP_HIGHMEM都没有设置,则能够从ZONE_DMA和ZONE_NORMAL中分配。
如果没有指定__GFP_DMA标志,则会尽量避免使用ZONE_DMA区的内存,只有当指定的区内存不足,而ZONE_DMA区又有充足的内存时,才会从ZONE_DMA中分配。
伙伴算法的具体内存分配过程
比如,要分配4(2^2)页(16k)的内存空间,算法会先从free_area[2]中查看nr_free是否为空,如果有空闲块,就直接从中摘下并分配出去,如果没有空闲块,就顺着数组向上查找,从它的上一级free_area[3](每块32K)中分配,如果free_area[3]中有空闲块,则将其从链表中摘下,分成等大小的两部分,前四个页面作为一个块插入free_area[2],后4个页面分配出去,如果free_area[3]也没有空闲,则从更上一级申请空间,如果free_area[4]中有,就将这16(222*2)个页面等分成两份,前一半挂在free_area[3]的链表头部,后一半的8个页等分成两等分,前一半挂free_area[2]的链表中,后一半分配出去。依次递推,直到free_area[max_order],如果顶级都没有空间,那么就报告分配失败。
__alloc_pages()函数的实现
/*
* 请求分配一组连续页框,它是管理区分配器的核心
* gfp_mask:在内存分配请求中指定的标志
* order: 连续分配的页框数量的对数(实际分配的是2^order个连续的页框)
* zonelist: zonelist数据结构的指针。该结构按优先次序描述了适于内存分配的内存管理区
*/
struct page * fastcall
__alloc_pages(unsigned int gfp_mask, unsigned int order,
struct zonelist *zonelist)
{
const int wait = gfp_mask & __GFP_WAIT;
struct zone **zones, *z;
struct page *page;
struct reclaim_state reclaim_state;
struct task_struct *p = current;
int i;
int classzone_idx;
int do_retry;
int can_try_harder;
int did_some_progress;
might_sleep_if(wait);//该函数允许内核将当前进程阻塞指定的时间
can_try_harder = (unlikely(rt_task(p)) && !