date: 2014-10-07 19:09node
内核能够经过alloc_pages函数来进行内存页面的分配,函数的定义在mm.h文件中:linux
<include/linux/mm.h>
#ifndef CONFIG_DISCONTIGMEM
static inline struct page * alloc_pages(int gfp_mask, unsigned long order)
{
/*
* Gets optimized away by the compiler.
*/
if (order >= MAX_ORDER)
return NULL;
return __alloc_pages(contig_page_data.node_zonelists+(gfp_mask), order);
}
#else /* !CONFIG_DISCONTIGMEM */
extern struct page * alloc_pages(int gfp_mask, unsigned long order);
#endif /* !CONFIG_DISCONTIGMEM */
看来有两个版本的alloc_pages,另外一个版本的实如今numa.c,编译时根据编译选项CONFIG_DISCONTIGMEM进行取舍。为何要有这个取舍?这与以前讲到的NUMA相关。但这里的宏开关却不是CONFIG_NUMA而是CONFIG_DISCONTIGMEM,表示“非连续存储空间”。其实非连续存储空间是一种广义的NUMA,由于那表示在最低物理地址到最高物理地址之间存在空洞,既然存在空洞,质地固然是非均匀的了,从而也就要划分出若干质地均匀(地址连续)的“节点”了,从而也就有一个pg_data_t结构队列。数组
本文主要分析“连续存储空间”中的内存页面分配,这种状况下只有一个节点,意即只有一个pg_data_t结构contig_page_data。pg_data_t结构中的node_zonelists是一个数组,表示全部的分配策略,这里参数gfp_mask被用做数组下标;参数order表示要分配的连续的页面个数为2^order个。alloc_pages函数检查order以后调用__alloc_pages(内核中以“__”开头的函数表示仅供模块内部使用),下面主要梳理下__alloc_pages的流程。函数

按流程顺序说明以下:操作系统
- 若是申请分配一个页面(由于不活跃干净页面队列中的页面是零散的不连续的,因此只有在申请一个页面时容许从该队列中分配),而分配策略要求等待分配完成,且当前进程不是“内存分配工做者”进程(即当前进程没有设置PF_MEMALLOC标志),那么万不得已时,能够从内存管理区中的不活跃干净队列中分配。
- 进程的PF_MEMALLOC标志。若是进程的task_struct结构中的flag字段设置了PF_MEMALLOC,那么表示该进程为“内存分配工做者”,通常是内核进程kswapd和kreclaimd。若是这两个进程要分配内存页面,那是为了执行公务,是为了使得其余进程更好地分配内存页面,因此它们比通常的进程要有更高的待遇。图中的step7就是在内存极端紧缺的状况下,试图知足这种进程的内存分配需求。
- rmqueue用来在管理区的空闲队列中分配内存页面。前面讲过,为了减小内存碎片,管理区的空闲列表是按连续页面个数分开存放的。rmqueue在分配页面时,先根据申请的页面大小去相应的队列中找空闲的页面。若是相应的队列中没有可用的页面,则去更高一档的队列中去找。找到之后,将一半分配出去,将另外一半链到低档的队列中。
- 要是分配策略中全部的管理区都分配失败,则要加大力度再试了。一方面将管理区中的不活跃干净页面考虑进去,一方面下降水位的要求。这就是__alloc_pages_limit函数所作的事情。先以参数“PAGES_HIGH”调用__alloc_pages_limit,若是还不行就再狠一点,以参数“PAGES_LOW”调用__alloc_pages_limit再试一次。
- 在__alloc_pages_limit 函数中会调用reclaim_page函数,后者直接从管理区的inactive_clean_list 队列中回收页面。
- 若是仍然不成功,则唤醒守护kswapd,让它设法换出一些页面。若是分配策略设置了__GFP_WAIT标志,表示本次分配志在必得,不成功毋宁等。当前进程主动让出CPU,并执行一次调度。这样,一来,kswapd有可能当即调度执行;二来即便其余的进程被调度运行,其余进程也可能释放出一些页面。当当前进程再次被调度运行后,或者分配策略代表不容许等待时,以参数“PAGES_MIN”在调一次__alloc_pages_limit,内核已经有了“破釜沉舟”的决心了。
- 若是仍是分配不成功,那就要看当前进程是普通进程仍是“内存分配工做者”。对于普通进程,失败的缘由可能有两个。
- 缘由之一,可分配页面总数还不少,可是比较零散,不知足申请连续页面的要求。这种状况下,能够将不活跃脏页面洗净(经过page_launder函数),使得这些页面转入内存管理区的不活跃干净页面队列。对于每一个管理区,调用reclaim_page从不活跃干净队列中回收一个页面,再调用__free_page释放页面并试图将空闲页面拼凑成尽量大的页面块,以后再调用rmqueue再试图从管理区的空闲队列中分配。值得注意的是,在page_launder执行期间把当前进程的PF_MEMALLOC标志置1,使其有了“执行公务”的特权,执行完成以后,再将其恢复成0。为何这样作呢?由于在page_launder中也会要求分配一些临时性的工做页面,不提升它的权限的话,就有可能递归执行到这里。
- 缘由之二,系统中的内存真的不足了。这种状况下,有两种处理:若是分配策略中同时设置了__GFP_WAIT和__GFP_IO,则唤醒kswapd,当前进程进入睡眠并等待kswapd唤醒咱们,当前进程被唤醒后,可能kswapd已经设法回收了页面,再回到开头处(标号try_again处)从头再试;若是分配策略中没有指定__GFP_IO,那么咱们就不能唤醒kswapd并等待被唤醒。由于守护进程kswapd在进行页面回收操做时须要IO操做,须要申请某些IO锁,而若是这些锁恰好被当前进程所hold,这样就形成了死锁。这种状况下,直接调用try_to_free_pages(本来该有kswapd进程调用)来试图释放些页面。
- 一次次加大力度调用__alloc_pages_limit,实际上仍是有所保留的(至少可分配内存数量>最小水位值),管理区中还有一些压箱底的内存,以备紧急状况下使用。对于执行公务的进程,咱们只能不惜下血本了。
- 流程图展现了内核为了分配内存页面所进行的坚苦卓绝的努力。当正常状况来说,在step1便可分配成功。剩下的流程都是内核“屡战屡败”“屡败屡战”的结果,毕竟做为一个操做系统的内核,遇到困难它可不能简单的撂挑子,要绞尽脑汁去解决困难。
- 另,流程图中涉及到分配策略中的几个标志,简要解释以下:
- __GFP_WAIT:等待页面分配完成,即申请页面的进程能够被阻塞,意味着调度器能够在分配期间调度另一个进程运行。
- __GFP_IO:内核在查找空闲页的过程当中能够进行I/O操做,如此内核能够将换出的页面写到磁盘中,即将不活跃脏页面洗白。