一、保留内存的用武之地
要了解内存区的保留内存,我们首先要知道这个保留内存用在何处。实际上它在内核水线计算时会用到,那内核是如何计算水线的呢?保留内存到底在水线计算起到什么作用呢?我们先来跟踪代码一探究竟。
为了方便说明,这里只针对UMA架构。UMA架构物理内存的分配最终由__alloc_pages_nodemask()函数实现。
structpage* __alloc_pages_nodemask(gfp_tgfp_mask,unsignedintorder,
structzonelist*zonelist,nodemask_t*nodemask)
{
structzoneref*preferred_zoneref;
......
structalloc_contextac= {
/* ac.high_zoneidx的值与入参gfp_mask有关系,即主要查看gftp_mask是否有特别指定__GFP_DMA|DMA32|HIGH|MOVEABLE*/
.high_zoneidx = gfp_zone(gfp_mask),/* 如果gfp_mask & __GFP_DMA , high_zoneidx == ZONE_DMA ,
gfp_mask & __GFP_DMA32 , high_zoneidx == ZONE_DMA32,
if ((flags & (__GFP_HIGHMEM | __GFP_MOVABLE)) == (__GFP_HIGHMEM | __GFP_MOVABLE)) high_zoneidx == ZONE_MOVABLE
if (flags & __GFP_HIGHMEM) return ZONE_HIGHMEM;
return ZONE_NORMAL */
.nodemask = nodemask,
.migratetype = gfpflags_to_migratetype(gfp_mask),
};
......
ac.zonelist = zonelist;
/* Dirty zone balancing only done in the fast path */
ac.spread_dirty_pages = (gfp_mask & __GFP_WRITE);
......
/* 从HIGH往DMA方向选择第一个满足(不大于)ac.high_zoneidx的zoneref */
preferred_zoneref = first_zones_zonelist(ac.zonelist, ac.high_zoneidx,
ac.nodemask ? : &cpuset_current_mems_allowed,
&ac.preferred_zone);
......
ac.classzone_idx = zonelist_zone_idx(preferred_zoneref);/* zoneref->zone_idx */
......
/* ac.classzone_idx 和 ac.zonelist 可以基本确定我们优先从哪个zone分配内存 */
page = __alloc_pages_slowpath(alloc_mask, order, &ac);
}
__alloc_pages_slowpath()-->get_page_from_freelist() 这个函数中最终会进行水线检查。
zone_watermark_ok()--> __zone_watermark_ok(struct zone *z, unsigned int order,unsigned long mark, int classzone_idx, int alloc_flags,long free_pages)
这个是水线检查的主要实现函数,在检查前会对内存区的空闲内存free_pages和水线mark根据内存分配标志进行调整。
[1] 根据alloc_flags调整水线和free_pages;
free_pages -= (1<< order) -1
/* 降低水线,使其可更容易分配到内存 */
if(alloc_flags & ALLOC_HIGH)
min -= min /2;
/* 若未设置ALLOC_HARDER,则需要保留z->nr_reserved_highatomic内存以不时之需 */
if(likely(!alloc_harder))
free_pages -= z->nr_reserved_highatomic;
else
min -= min /4;/* 否则的话降低水线使其更容器分配到内存 */
#ifdefCONFIG_CMA
/* 如果配置了CMA,则没有明确指明从CMA分配时(即没有设置ALLOC_CMA)需要保留出CMA内存 */
/* If allocation can't use CMA areas don't use free CMA pages */
if(!(alloc_flags & ALLOC_CMA))
free_pages -= zone_page_state(z, NR_FREE_CMA_PAGES);
#endif
[2] 调整好水线后开始计算是否到达水线
if(free_pages <= min + z->lowmem_reserve[classzone_idx])
returnfalse;
在这里内存区的保留内存终于浮出水面。这里的水线实际等于 min + z->lowmem_reserve[classzone_idx]。min我们实际上了解了就是zone->watermark[NR_WMARK]中的一个值,它的取值跟实际情况关;而z->lowmem_reserve[classzone_idx]就是我们寻找已久的保留内存。
也就是说内核在进行水线检查时,不仅仅要检查内存区的真正的水线"zone->watermark[x]",还要考虑保留内存。
有时候我们会在某个zone还有比较客观的内存数量时(远超min水线)时,仍然会发生内存分配失败,这时可以考虑一下是否是保留区内存太大导致。
二、认识保留内存
好了,是时候了解一下什么是lowmem_reserve了。
kernel在分配内存时,可能会涉及到多个zone,分配会尝试从zonelist第一个zone分配,如果失败就会尝试下一个低级的zone(这里的低级仅仅指zone内存的位置,实际上低地址zone是更稀缺的资源)。考虑这样一种场景应用进程通过内存映射申请Highmem 并且加mlock分配,如果此时HIGH zone无法满足分配,则会尝试从Normal进行分配。问题来了,应用进程在从HIHG“降”到Normal区的分配请求有可能会耗尽Normal区的内存,而且由于mlock又无法回收,最终的结果就是Normal区无内存--在i386这样的架构上内核能够正常访问的线性区正是Normal区,这就导致kernel可能无法正常工作,然而HIGH zone却可能有足量的可回收内存。
针对这个情形,当Normal zone在碰到来自HIGH的分配请求时,可以通过lowmem_reserve声明:可以使用我的内存,但是必须要保留lowmem_reserve[NORMAL]给我自己使用。
同样当从Normal失败后,会尝试从zonelist中的DMA申请分配,通过lowmem_reserve[DMA],限制来自HIGHMEM和Normal的分配请求。
2.2 保留内存的初始化
有了上面的铺垫我们看一下各个区的lowmem_reseve[]是如何配置的,各个区究竟保留了多少内存。
1】内核定义了一个long lowmem_reserve[MAX_NR_ZONES]数组来表示各个区的保留内存。这个数组的大小MAX_NR_ZONES与内核配置有关系,例如内核使能了CONFIG_ZONE_DMA、CONFIG_ZONE_DMA32、则MAX_NR_ZONES值为4,他们是:
enumzone_type {
ZONE_DMA,/* 0 */
ZONE_DMA32,
ZONE_NORMAL,
ZONE_MOVABLE,
__MAX_NR_ZONES/* 3 */
};
【2】那各个lowmem_reserve[MAX_NR_ZONES]是如何计算的呢?
我们来看看这个数组的初始化流程,它是由setup_per_zone_lowmem_reserve(void)函数来完成的。
四、总结
这篇文章还是从实际遇到的问题引出的。之前在某个环境出现过Normal区内存量远高于水线时出现OOM的情况,最后查明原因就是因为保留内存太多导致。最终通过调整sysctl_lowmem_reserve_ratio参数进行规避。
网友评论