弄清楚伙伴系统算法的原理以后,我们就可以开开心心地处理页框了。
我们可以通过6个稍有差别的函数和宏请求页框。一般情况下,他们都返回第一个所分配页的线性地址,或者分配失败则返回NULL。
alloc_pages(gfp_mask, order):用这个函数请求2order 个连续的页框。他返回第一个所分配页框描述符的地址,或者如果失败,则返回NULL。
alloc_page(gfp_mask):用于获得一个单独页框的宏,它其实只是alloc_pages(gfp_mask, 0)。它返回所分配页框描述符的地址,或者如果分配失败,则返回NULL。
_ _get_free_pages(gfp_mask, order):该函数类似于alloc_pages( ),只不过它返回第一个所分配页对应的内存线性地址。
_ _get_free_page(gfp_mask):用于获得一个单独页框的宏,它也只是__get_free_pages(gfp_mask, 0)
get_zeroed_page(gfp_mask):函数用来获取满是0的页面,它调用alloc_pages(gfp_mask | __GFP_ZERO, 0),然后返回所获取页框的线性地址。
_ _get_dma_pages(gfp_mask, order):该宏获取用于DMA的页框,它扩展调用__get_free_pages(gfp_mask | _ _GFP_DMA, order)。
参数gfp_mask是一组标志,它指明了如何寻找空闲的页框:
标志 | 说明 |
_ _GFP_DMA | 所请求的页框必须处于ZONE_DMA 管理区。 |
_ _GFP_HIGHMEM | 所请求的页框处于ZONE_HIGHMEM 管理区 |
_ _GFP_WAIT | 允许内核对等待空闲页框的当前进程进行阻塞 |
_ _GFP_HIGH | 允许内核访问保留的页框池 |
_ _GFP_IO | 允许内核在低端内存页上执行I/O 传输以释放页框 |
_ _GFP_FS | 如果清0,则不允许内核执行依赖于文件系统的操作 |
_ _GFP_COLD | 所请求的页框可能为“冷的” |
_ _GFP_NOWARN | 一次内存分配失败将不会产生警告信息 |
_ _GFP_REPEAT | 内核重试内存分配直到成功 |
_ _GFP_NOFAIL | 与__GFP_REPEAT 相同 |
_ _GFP_NORETRY | 一次内存分配失败后不再重试 |
_ _GFP_NO_GROW | slab 分配器不允许增大slab 高速缓存 |
_ _GFP_COMP | 属于扩展页的页框 |
_ _GFP_ZERO | 任何返回的页框必须被填满0 |
下面4个函数和宏中的任意一个都可以释放页框:
_ _free_pages(page, order):该函数首先查找page指向的页描述符;如果该页框未被保留(PG_reserved标志为0),就把描述符count字段减1。如果count字段变为0,就假定从page对应页框开始的2order个连续页框不再被使用,在这种情况下,该函数释放页框。
free_pages(addr, order):这个函数类似于 _ _free_pages( ),只不过它接收的参数为要释放的第一个页框的线性地址addr。
_ _free_page(page):该宏释放page所指描述符对应的页框,它扩展为__free_pages(page, 0)
free_page(addr):该宏释放线性地址为addr的页框,它扩展为:free_pages(addr, 0)
分配一组页框
对一组连续页框的每次请求实质上是通过执行alloc_pages 宏来处理的。接着,这个宏又依次调用__alloc_pages()函数,该函数是分配页框的核心。它接收以下3个参数:
gfp_mask:在内存分配请求中指定的标志(参见上上篇博文)。
order:将要分配的一组连续页框数量的对数(即要分配2order 个连续的页框)。
zonelist:指向zonelist数据结构的指针,该数据结构按优先次序描述了适于内存分配的内存管理区。
__alloc_pages()函数概括起来执行以下代码:
for (i = 0; (z=zonelist->zones[i]) != NULL; i++) {
if (zone_watermark_ok(z, order, ...)) {
page = buffered_rmqueue(z, order, gfp_mask);
if (page)
return page;
}
}
__alloc_pages()函数首先扫描包含在zonelist数据结构中的每个内存管理区。
对于每个内存管理区,该函数将空闲页框的个数与一个阈值作比较,该阈值取决于内存分配标志、当前进程的类型以及管理区被函数检查过的次数。
实际上,如果空闲内存不足,那么每个内存管理区一般会被检查n遍,每一遍在所请求的空闲内存最低量的基础上使用更低的阈值扫描。因此前面一段代码在__alloc_pages()函数体内被复制了几次,每次变化很小。__alloc_pages()函数调用buffered_rmqueue()函数:它返回第一个被分配的页框的页描述符;如果内存管理区没有所请求大小的一组连续页框,则返回NULL。
buffered_rmqueue()函数在指定的内存管理区中分配页框。其参数为内存管理区描述符的地址,请求分配的内存大小的对数order,以及分配标志gfp_flags。该函数本质上执行如下操作:
1.如果order等于0,则使用每CPU页框高速缓存,这里咱不讨论;如果order不等于0,则表明请求跨越了几个连续页框,每CPU页框高速缓存就不能被使用,函数执行下面步骤:
2.调用__rmqueue()函数从伙伴系统中分配所请求的页框。
3.如果内存请求得到满足,函数就初始化(第一个)页框的页描述符:清除一些标志,将private 字段置0,并将页框引用计数器置1。此外,如果gfp_flags 中的__GPF_ZERO 标志被置位,则函数将被分配的内存区域填充0。
4.返回(第一个)页框的页描述符地址,如果内存分配请求失败则返回NULL。
这里要提一下zone_watermark_ok()辅助函数,他的目的就是来探测对应的内存管理区中有没有足够的空闲页框,该函数接收几个参数,它们决定对应内存管理区z中空闲页框个数的阈值min。阈值是个很高深的数学概念,解释起来很麻烦,所以干脆不去详细介绍他了,我只解释一下zone_watermark_ok同时满足下列两个条件则返回1的情况:
1.除了被分配的页框外,在内存管理区中至少还有min个空闲页框,不包括为内存不足保留的页框(管理区描述符的lowmem_reserve 字段)。
2.除了被分配的页框外,这里在order至少为k的块中起码还有min/2k 个空闲页框,其中,对于每个k,取值在1 和分配的order 之间。因此,
如果order大于0,那么在大小至少为2 的块中起码还有min/2 个空闲页框;如果order 大于1,那么在大小至少为4 的块中起码还有min/4 个空闲页框;依此类推。
在内核中,真正的__alloc_pages()函数是很复杂的,需要结合内核回收页框机制来做分析,他本质上执行如下步骤:
1.执行对内存管理区的第一次扫描(参见前面列出的代码)。在第一次扫描中,阈值min 被设为z->pages_low,其中的z指向正在被分析的管理区描述符(参数can_try_harder 和gfp_high 被设为0)。
2.如果函数在上一步没有终止,那么没有剩下多少空闲内存:函数唤醒kswapd内核线程来异步地开始回收页框。
3.执行对内存管理区的第二次扫描,将值z->pages_min作为阈值base传递。正如前面解释的,实际阈值由the_can_try_harder和gfp_high标志决定。这一步与第1步相似,但该函数使用了较低的阈值。
4.如果函数在上一步没有终止,那么系统内存肯定不足。如果产生内存分配请求的内核控制路径不是一个中断处理程序或一个可延迟函数,并且它试图回收页框(或者是current的PF_MEMALLOC标志被置位,或者是它的PF_MEMDIE标志被置位),那么函数随即执行对内存管理区的第三次扫描,试图分配页框并忽略内存不足的阈值, 也就是说,不调用zone_watermark_ok()。唯有在这种情况下才允许内核控制路径耗用为内存不足预留的页(由管理区描述符的lowmem_reserve字段指定)。其实,在这种情况下产生内存请求的内核控制路径最终将试图释放页框,因此只要有可能它就应当得到它所请求的。如果没有任何内存管理区包含足够的页框,函数就返回NULL 来提示调用者发生了错误。
5.在这里,正在调用的内核控制路径并没有试图回收内存。如果gfp_mask 的__GFP_WAIT标志没有被置位,函数就返回NULL 来提示该内核控制路径内存分配失败:在这种情况下,如果不阻塞当前进程就没有办法满足请求。
6.在这里当前进程能够被阻塞:调用cond_resched()检查是否有其它的进程需要CPU。
7.设置current 的PF_MEMALLOC 标志来表示进程已经准备好执行内存回收。
8.将一个指向reclaim_state 数据结构的指针存入current->reclaim_state。这个数据结构只包含一个字段reclaimed_slab,被初始化为0(我们将在后面的“slab 分配器与分区页框分配器的接口”博文中看到如何使用这个字段)。
9.调用try_to_free_pages()寻找一些页框来回收。后一个函数可能阻塞当前进程。一旦函数返回,__alloc_pages()就重设current的PF_MEMALLOC 标志并再次调用cond_resched()。
10.如果上一步已经释放了一些页框,那么该函数还要执行一次与第3步相同的内存管理区扫描。如果内存分配请求不能被满足,那么函数决定是否应当继续扫描内存管理区:如果_ _GFP_NORETRY标志被清除, 并且内存分配请求跨越了多达8 个页框或__GFP_REPEAT 和__GFP_NOFAIL 标志其中之一被置位,那么函数就调用blk_congestion_wait()使进程休眠一会儿,并且跳回到第6 步。否则,函数返回NULL来提示调用者内存分配失败了。
11.如果在第9步中没有释放任何页框,就意味着内核遇到很大的麻烦,因为空闲页框已经少到了危险的地步,并且不可能回收任何页框。也许到了该作出重要决定的时候了。如果允许内核控制路径执行依赖于文件系统的操作来杀死一个进程(gfp_mask中的__GFP_FS标志被置位)并且_ _GFP_NORETRY标志为0,那么执行如下子步骤:
a)使用等于z->pages_high 的阈值再一次扫描内存管理区。
b)调用out_of_memory()通过杀死一个进程开始释放一些内存(参见第十七章的“内存不足删除程序”一节)。
c)跳回第1 步。
因为第11a 步使用的界值远比前面扫描时使用的界值要高,所以这个步骤很容易失败。实际上,只有当另一个内核控制路径已经杀死一个进程来回收它的内存后,第11a 步才会成功执行。因此,第11a步避免了两个无辜的进程(而不是一个)被杀死。
释放一组页框
管理区分配器同样负责释放页框;令人欢欣鼓舞的是,释放内存比分配它要简单得多。
释放页框的所有内核宏和函数都依赖于__free_pages()函数。它接收的参数为将要释放的第一个页框的页描述符的地址(page)和将要释放的一组连续页框的数量的对数(order)。该函数执行如下步骤:
1.检查第一个页框是否真正属于动态内存(它的PG_reserved 标志被清0);如果不是,则终止。
2.减少page->_count 使用计数器的值;如果它仍然大于或等于0,则终止。
3.如果order 等于0,那么该函数调用free_hot_page()来释放页框给适当内存管理区的每CPU 热高速缓存。
4.如果order大于0,那么它将页框加入到本地链表中,并调用free_pages_bulk()函数把它们释放到适当内存管理区的伙伴系统中。