物理内存管理:请求PFN快速分配实现(3)
物理内存管理:请求PFN快速分配实现(3)
前面主体函数和慢速分配函数最后都会调用到get_page_from_freelist()函数,区别在于主体函数是直接调用,而慢速分配是通过回收page,或者交换到swap分区的方式从已经满了的zone里面回收空闲的page。
这个函数可以看做buddy system 的前置函数,通过传入order与flag判断当前的内存是否可以分配一块内核,如果可以转入buffered_rmqueue()进行,如果实在没有的话,就只能返回NULL了。
static struct page *
get_page_from_freelist(gfp_t gfp_mask, nodemask_t *nodemask, unsigned int order,
struct zonelist *zonelist, int high_zoneidx, int alloc_flags,
struct zone *preferred_zone, int classzone_idx, int migratetype)
{
struct zoneref *z;
struct page *page = NULL;
struct zone *zone;
nodemask_t *allowednodes = NULL;/* zonelist_cache approximation */
int zlc_active = 0; /* set if using zonelist_cache */
int did_zlc_setup = 0; /* just call zlc_setup() one time */
bool consider_zone_dirty = (alloc_flags & ALLOC_WMARK_LOW) &&
(gfp_mask & __GFP_WRITE);
int nr_fair_skipped = 0;
bool zonelist_rescan;
上面这个声明头,安装了zlc,也就是zonelist_cache还有判断zone_dirty的位,包括是否进行fairness分配等。
zonelist_scan:
zonelist_rescan = false;
for_each_zone_zonelist_nodemask(zone, z, zonelist,
high_zoneidx, nodemask) {
unsigned long mark;
if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
!zlc_zone_worth_trying(zonelist, z, allowednodes))
continue;
if (cpusets_enabled() &&
(alloc_flags & ALLOC_CPUSET) &&
!cpuset_zone_allowed(zone, gfp_mask))
continue;
if (alloc_flags & ALLOC_FAIR) {
if (!zone_local(preferred_zone, zone))
break;
if (test_bit(ZONE_FAIR_DEPLETED, &zone->flags)) {
nr_fair_skipped++;
continue;
}
}
if (consider_zone_dirty && !zone_dirty_ok(zone))
continue;
上面的这个段有一个zonelist_scan段,因为扫描空闲的zonelist可能会出现找不到page,所以kernel会使用goto重新扫描遍历zonelist寻找合适page。
for_each_zone_zonelist_nodemask()是一个宏函数,用来进行zonelist的遍历,我们之前结合内存结构分析过NUMA架构中存在多个pg_list,而high_zoneidx则是遍历的边界,也就是说如果high_zoneidx为ZONE_NORMAL,那么遍历的zone区域就是ZONE_DMA、ZONE_NORMAL。而zone的区域都是通过enmu来声明的。
mark = zone->watermark[alloc_flags & ALLOC_WMARK_MASK];
if (!zone_watermark_ok(zone, order, mark,
classzone_idx, alloc_flags)) {
int ret;
BUILD_BUG_ON(ALLOC_NO_WATERMARKS < NR_WMARK);
if (alloc_flags & ALLOC_NO_WATERMARKS)
goto try_this_zone;
if (IS_ENABLED(CONFIG_NUMA) &&
!did_zlc_setup && nr_online_nodes > 1) {
allowednodes = zlc_setup(zonelist, alloc_flags);
zlc_active = 1;
did_zlc_setup = 1;
}
if (zone_reclaim_mode == 0 ||
!zone_allows_reclaim(preferred_zone, zone))
goto this_zone_full;
if (IS_ENABLED(CONFIG_NUMA) && zlc_active &&
!zlc_zone_worth_trying(zonelist, z, allowednodes))
continue;
上面这些代码都是存在于遍历zonelist的循环中,如果当前的watermark使能,而且zone_watermark_ok()判断在当前的watermark下是否可以分配内存。如果可以分配内存则跳入最后的try_this_zone段。
在下面会提到。如果这里分配page失败,也就意味内存不足,需要进行page回收:zone_reclaim(),上面这段代码还包括一些其他判断,如果在nodemask中指定了在当前节点分配page,而同时当前node又不满足分配的需求,这里只能返回this_zone_full段。
ret = zone_reclaim(zone, gfp_mask, order);
switch (ret) {
case ZONE_RECLAIM_NOSCAN:
continue;
case ZONE_RECLAIM_FULL:
continue;
default:
if (zone_watermark_ok(zone, order, mark,
classzone_idx, alloc_flags))
goto try_this_zone;
if (((alloc_flags & ALLOC_WMARK_MASK) == ALLOC_WMARK_MIN) ||
ret == ZONE_RECLAIM_SOME)
goto this_zone_full;
continue;
}
}
如果运行到这里,说明此刻空闲内存不足,需要使用zone_reclaim()进行内存回收,回收的情况通过ret来确定。如果结果是ZONE_RECLAIM_NOSCAN,说明并没有进行回收,那么直接尝试下一个zone,如果结果是ZONE_RECLAIM_FULL,说明虽然进行了回收但是并没有回收到,默认的情况则是没有回收到足够多的内存。后两种情况均跳入default处。
这里需要再次判断是否分配的内存在watermark以下,如果可以的话,跳入try_this_zone。第二个if是为了判断分配的mask是否是最小值,这里涉及到ZONE_RECLAIM_SOME,不太清楚,需要继续看。
这里需要声明的是ALLOC_WMARK_MIN指的是当前的内存空闲量已经很低,我们可以将内存看成一桶水,用掉内存的相当于用掉的水,所以越用越少。我们查看代码,发现使用watermark的话,会主要的存在以下几个标志位:
#define ALLOC_WMARK_MIN WMARK_MIN
#define ALLOC_WMARK_LOW WMARK_LOW
#define ALLOC_WMARK_HIGH WMARK_HIGH
#define ALLOC_NO_WATERMARKS 0x04 /* don't check watermarks at all */
大致分为MIN、LOW、HIGH三个watermark标志。当系统当前node节点内存非常少的时候,就会启用ALLOC_NO_WATERMARKS标志。
try_this_zone:
page = buffered_rmqueue(preferred_zone, zone, order,
gfp_mask, migratetype);
if (page)
break;
上面这个段就是如果系统找到空闲的page,会跳到这里,这个函数就是buddy system的前置函数。
this_zone_full:
if (IS_ENABLED(CONFIG_NUMA) && zlc_active)
zlc_mark_zone_full(zonelist, z);
}
上面这个段说明当前zone的空闲内存不足,那么标记它。这样下次分配时可以直接将其忽略。
if (page) {
page->pfmemalloc = !!(alloc_flags & ALLOC_NO_WATERMARKS);
return page;
}
上面这个已经跳出了遍历zonelist的范围,如果分配到了page,走到这里,系统已经分配了page,设置pfmemalloc 也就意味着目前系统在内存方面有压力,应该保持这个page不被swap出内存,并使得系统换出其他的页。
if (alloc_flags & ALLOC_FAIR) {
alloc_flags &= ~ALLOC_FAIR;
if (nr_fair_skipped) {
zonelist_rescan = true;
reset_alloc_batches(preferred_zone);
}
if (nr_online_nodes > 1)
zonelist_rescan = true;
}
if (unlikely(IS_ENABLED(CONFIG_NUMA) && zlc_active)) {
/* Disable zlc cache for second zonelist scan */
zlc_active = 0;
zonelist_rescan = true;
}
if (zonelist_rescan)
goto zonelist_scan;
return NULL;
走到这里,一般意味着page为NULL,这里我们要考虑到一种情况本地的node,内存分配已经达到饱和,而远端的node还没有被考虑,所以这个时候系统放弃fairness,将远端的node节点纳入到考虑范围。
###当然这是在进入慢速分配和唤醒kswapd的前提下,总的来说系统page分配的mechanism顺序是:local node -> remote node -> slowpath -> kswapd ->NULL
这里设置了zonelist_rescan 位,如果这一位为真,则重新扫描所有的内存node节点,包括远端node!
[1] http://lxr.free-electrons.com/source/mm/page_alloc.c#L2036