13.分配页之选择页(zone_watermark_ok和get_page_from_freelist)

所有API函数都追溯到alloc_pages_node,从某种意义上说,该函数是伙伴系统主要实现的“发射台”。
<gfp.h> static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask,unsigned int order) { if (unlikely(order >= MAX_ORDER)) return NULL; /* 未知结点即当前结点 */ if(nid< 0) nid = numa_node_id(); return __alloc_pages(gfp_mask, order,NODE_DATA(nid)->node_zonelists + gfp_zone(gfp_mask)); }
只执行了一个简单的检查,避免分配过大的内存块。如果指定负的结点ID(不存在),内核自动地使用当前执行CPU对应的结点ID。接下来的工作委托给__alloc_pages,只需传递一组适当的参数。请注意,gfp_zone用于选择分配内存的内存域。
1. 选择页
我们先把注意力转向页面选择是如何工作的。
辅助函数
首先我们需要定义一些函数使用的标志,用于控制到达各个水印指定的临界状态时的行为。
mm/page_alloc.c
#define ALLOC_NO_WATERMARKS 0x01 /* 完全不检查水印 */ #define ALLOC_WMARK_MIN 0x02 /* 使用pages_min水印 */ #define ALLOC_WMARK_LOW 0x04 /* 使用pages_low水印 */ #define ALLOC_WMARK_HIGH 0x08 /* 使用pages_high水印 */ #define ALLOC_HARDER 0x10 /* 试图更努力地分配,即放宽限制 */ #define ALLOC_HIGH 0x20 /* 设置了__GFP_HIGH */ #define ALLOC_CPUSET 0x40 /* 检查内存结点是否对应着指定的CPU集合 */
前几个标志表示在判断页是否可分配时,需要考虑哪些水印。默认情况下(即没有因其他因素带来的压力而需要更多的内存),只有内存域包含页的数目至少为zone->pages_high时,才能分配页。这对应于ALLOC_WMARK_HIGH标志。如果要使用较低(zone->pages_low)或最低(zone->pages_min)设置,则必须相应地设置ALLOC_WMARK_MIN或ALLOC_WMARK_LOW。ALLOC_HARDER通知伙伴系统在急需内存时放宽分配规则。在分配高端内存域的内存时,ALLOC_HIGH进一步放宽限制。最后,ALLOC_CPUSET告知内核,内存只能从当前进程允许运行的CPU相关联的内存结点分配,当然该选项只对NUMA系统有意义。
设置的标志在zone_watermark_ok函数中检查,该函数根据设置的标志判断是否能从给定的内存域分配内存。
mm/page_alloc.c int zone_watermark_ok(struct zone *z, int order, unsigned long mark,int classzone_idx, int alloc_flags) { /* free_pages可能变为负值,没有关系 */ long min = mark; long free_pages = zone_page_state(z, NR_FREE_PAGES) -(1 << order) + 1; int o; if (alloc_flags & ALLOC_HIGH) min -= min / 2; if (alloc_flags & ALLOC_HARDER) min -= min / 4; if (free_pages <= min + z->lowmem_reserve[classzone_idx]) return 0; for(o= 0;o <order;o++){ /* 在下一阶,当前阶的页是不可用的 */ free_pages -= z->free_area[o].nr_free << o; /* 所需高阶空闲页的数目相对较少 */ min >>= 1; if (free_pages <= min) return 0; } return 1; }
zone_per_state用来访问每个内存域的统计量。在上述代码中,得到的是空闲页的数目。
在解释了ALLOC_HIGH和ALLOC_HARDER标志之后(将最小值标记降低到当前值的一半或四分之一,使得分配过程努力或更加努力),该函数会检查空闲页的数目是否小于最小值与lowmem_reserve中指定的紧急分配值之和。如果不小于,则代码遍历所有小于当前阶的分配阶,从free_pages减去当前分配阶的所有空闲页(左移o位是必要的,因为nr_free记载的是当前分配阶的空闲页块数目)。同时,每升高一阶,所需空闲页的最小值折半。如果内核遍历所有的低端内存域之后,发现内存不足,则不进行内存分配。
理解:注意,nr_free记录的是当前分配阶的空闲页块数目,这里突出了一个块字,根据阶的不同,块的大小也就不同,而free_pages得到的是整个内存域空闲页的数目,故需要左移,如果当前阶内存块为空则向更高阶申请内存,所以减去的是比当前阶小的空闲内存页,得到的便是整个内存域大于等于该阶的内存页数目。
get_page_from_freelist是伙伴系统使用的另一个重要的辅助函数。它通过标志集和分配阶来判断是否能进行分配。如果可以,则发起实际的分配操作。
mm/page_alloc.c static struct page *get_page_from_freelist(gfp_t gfp_mask, unsigned int order,struct zonelist *zonelist, int alloc_flags) { struct zone **z; struct page *page = NULL; int classzone_idx = zone_idx(zonelist->zones[0]); struct zone *zone; ... /* * 扫描zonelist,寻找具有足够空闲空间的内存域。 * 请参阅kernel/cpuset.c中cpuset_zone_allowed()的注释。 */ z = zonelist->zones; do { ... zone = *z; if ((alloc_flags & ALLOC_CPUSET) &&!cpuset_zone_allowed_softwall(zone, gfp_mask)) continue; if (!(alloc_flags & ALLOC_NO_WATERMARKS)) { unsigned long mark; if (alloc_flags & ALLOC_WMARK_MIN) mark = zone->pages_min; else if (alloc_flags & ALLOC_WMARK_LOW) mark = zone->pages_low; else mark = zone->pages_high; if (!zone_watermark_ok(zone, order, mark,classzone_idx, alloc_flags)) continue; } ...
该函数的一个参数是指向备用列表的指针。在预期内存域没有空闲空间的情况下,该列表确定了扫描系统其他内存域(和结点)的顺序。该数据结构的布局和语义详见3.内存初始化整体大概流程(建立结点及内存域备用列表)
遍历备用列表的所有内存域,用最简单的方式查找一个适当的空闲内存块。首先,解释ALLOC_*标志(cpuset_zone_allowed_softwall是另一个辅助函数,用于检查给定内存域是否属于该进程允许运行的CPU)。zone_watermark_ok接下来检查所遍历到的内存域是否有足够的空闲页,并试图分配一个连续内存块。如果两个条件之一不能满足,即或者没有足够的空闲页,或者没有连续内存块可满足分配请求,则循环进行到备用列表中的下一个内存域,作同样的检查。
如果内存域适用于当前的分配请求,那么buffered_rmqueue试图从中分配所需数目的页:
mm/page_alloc.c ... page = buffered_rmqueue(*z, order, gfp_mask); if (page) { zone_statistics(zonelist, *z); break; } } while (*(++z) != NULL); return page; }
buffered_rmqueue详见未知,如果分配成功,则将页返回给调用者。否则,进入下一个循环,选择备用列表中的下一个内存域。
浙公网安备 33010602011771号