重读balance_pgdat函数

如今内核版本已经到了2.6.29,离2.6.28已经有过一段时间了,可是我还是重新读了2.6.28的代码,别的特性就不说了,最让我感到不错的就是它对内存回收算法的改进了,这个算法的改进我认为直接适应了现代大内存的普及,如果说内存很小,那么扫描一遍的时间可能不会很长,而且那种情况下,回收算法根本就不会算计时间耗费了多少,其本身会迷恋于算法的合理性以及别的特性,可是在大型内存的情形下,需要考虑的就多了,内存回收毕竟是操作系统的内务,不能占用太久的时间,因此新的算法在以下两个方面进行了改进:1.不再每次都扫描整个active链表,而是将inactive维持在一个恒量,active只是这个inactive的后备链表;2.细化了lru,将文件缓存和匿名页面区分在不同的lru中,并且为不可换出的页面单独提供了一个iru。

这两个改进就从根本上改进了页面回收的性能,大大减少了回收时的延迟,举个宏观的例子,有两种方式可以填充inactive链表最终进行回收,第一种方式就是每次都扫描active整个链表,然后递减引用计数,并且在可能的情况下将之放入inactive链表中,然后回收inactive链表,这样的话,延迟时间就会受制于系统的内存数量,这其实就是单时钟指针算法,优点就是每次都可以最大限度的回收内存,另外一种方式就是将inactive的数量保持在一个恒定的值,每次扫描的时候查看系统可用内存是否过于少或者inactive是否过于小,如果是的情况下填充inactive链表,从active链表将页面移动到inactive链表,移动多少呢?根据那个恒定的值权衡,然后回收inactive链表,这其实就是双指针算法,那个恒定的值就是两个指针臂之间的间隔,这个算法的优点是粒度更小,回收占用的时间不大,但是缺点也显而易见,每次不一定能释放足够的内存。

以上仅仅是理论上的描述,没有哪个操作系统完全重现了时钟指针算法的,一切都是要靠软件来完成,而且不能耽误太久的时间,那么我们就可以完全克服以上第二种方式的缺点,不是每次不能回收足够的内存吗?那么我多来几次不就可以了吗?最起码我逃过了扫描整个active链表,这就是成功,我可能有些前后矛盾,为什么呢?马上就可以看到,因为如果你看2.6.28之前的几个版本内核,就会发现有这么一个判断:while (pgscanned active_list)),这个判断表明早期的版本也不需要扫描整个链表,呵呵,是的,果真如此,所以说这个patch刚刚被提出的时候并没有说其对扫描链表的改进,可是如果你从2.6.28的代码实现上来看,哪一个更加合理呢?前期版本用硬编码的形式强行判断循环条件,而2.6.28则是合情合理的用双指针算法进行扫描,起码说用更加接近双指针算法的方式来进行扫描,更加符合理论的描述,虽然我不太喜欢没有实践的理论描述,可是当你躺在床上或者没有编译环境的时候,难道你会选择拿起锤子钉木箱子吗?

linux的内存页面回收有两个调用方式,一种是主动的调用,另一种是自动的调用,怎么理解呢?kswap内核线程每隔一个时间就会自动的平衡空闲页面,还有一个主动调用就是在__alloc_page中,当无法分配页面的时候就会想办法换出一些页面,这个时候就要扫描内存,或者在进程主动调用释放页面的时候也要调用,这些调用方式调用的是内核函数try_to_free_pages。不过这二者有个不同,就是扫描方向不同,它们都是一个接一个的扫描zone,对于每一个zone进行页面回收,不同就不同在kswap是按照从dma到highmem的方向也就是从低到高的方向扫描的,而try_to_free_pages却是按照从高到低的相反的方向扫描的,而且在__alloc_pages函数中,内存分配器也是按照zone从高到低的方向尝试分配内存的,并且这有在内存不足时才会调用try_to_free_pages进行相同方向的从高到低地释放内存。这样的话,两种扫描就会有交叉,导致highmem区被过度的扫描,其实就是导致两种扫描方向的扫描重合的zone被过度扫描,这样很不好,破坏了页面老化度的判断,因为按照道理就不应该有主动的try_to_free_pages这一说,就是因为这一个主动的释放破坏了平衡。2.6.5内核往后矫正了这种不平衡。

首先看一下老的balance_pgdat逻辑:

for (i = 0; i nr_zones; i++) {

struct zone *zone = pgdat->node_zones + i;

int nr_mapped = 0;

int max_scan;

int to_reclaim;

if (zone->all_unreclaimable && priority != DEF_PRIORITY)

continue;

if (nr_pages && to_free > 0) { /* Software suspend */

to_reclaim = min(to_free, SWAP_CLUSTER_MAX*8);

} else {

to_reclaim = zone->pages_high-zone->free_pages; //如果这个zone已经平衡了,那么就跳过了,直接继续下一个zone的扫描

if (to_reclaim

continue;

}

这个算法理论上很合理,是的,几乎完全照搬了理论,在每一个zone维持空闲页面在一个范围内,可是内存分配器和主动的内存页面平衡操作破坏了这一切,因为内存分配器是从高zone到低zone扫描的,如果balance_pgdat扫描一个zone,然后发现它平衡了,然后继续更高的zone,这时一个内存分配恰恰到了这个刚刚查过的证明已经平衡的zone,怎么到这里的呢?可能由于更高的zone没有空闲页面而且balance_pgdat还没有扫到那个更高的zone,或者更一般的,zonelist就是从这个zone开始的,这么一分配就可能会造成这个已经平衡的zone不再平衡,因此更好的做法就是无论是否有水平值之上的空闲页面,都要扫描。另外,尽量不要和主动的try_to_free_pages扫描的区域相重合,因为这样的话,重合区域将面临两次扫描,记住,扫描一次不单单是将空闲页面维持到了一定范围,而相应的也造成了所有的页面老化,如此一来,重合区域就会比别的区域老化的快,主动的try_to_free_pages目的是为了释放一些页面来满足内存分配,一旦空闲页面足够,那么马上停止,可以肯定,高zone比低zone被主动扫描的更多,可是kswap的平衡扫描却不是为了得到确定数量的空闲页面,而是从低到高为了将每个zone的空闲页面维持在一定的水平,如果每个zone相同强度扫描,那么加上主动的try-to扫描,高zone总体比低zone被更多扫描,如此结果就是高zone比低zone老化的更快,要知道,页面老化度也是判断其是否被置换的重要依据啊。那么就需要一种措施来加强低zone的扫描,这完全是为了迎合主动的内存分配器的行为。于是新的策略在2.6.5内核中出来了。

2.6.5内核的balance_pgdat逻辑是:

for (i = pgdat->nr_zones - 1; i >= 0; i--) {

struct zone *zone = pgdat->node_zones + i;

if (zone->all_unreclaimable && priority != DEF_PRIORITY)

continue;

if (zone->free_pages pages_high) {

end_zone = i;

goto scan;

}

...

scan:

for (i = 0; i

struct zone *zone = pgdat->node_zones + i;

...

reclaimed = shrink_zone(zone, max_scan, GFP_KERNEL,&total_scanned, ps);

...

可以看出出现了一个end_zone,以后简称end,这个end表示kswap只扫描到这个zone,因为比这个end高的zone在try中可能被释放并且所有的页面将老化,因此这个end就最小化了两种扫描的重合,毕竟分配内存的时候从高zone开始扫描,如果可以分配,那么就分配,如果失败,那么往下,最多到达dma-zone,不过最可能的就是最多在kswap的end这个zone分配到页面,如果实在还是没有分配到,那么try-to-free-pages就会从zoneist的最高zone开始释放页面,一般情况这个释放过程不可能扫描到dma,也就是说在中间的一个zone就停止了,释放了足够的页面,那么以后的分配也可以从中受益,但是由于try-to-free-pages方式是懒惰的,不到万不得已不会被调用,另外一层懒惰的意义就是一旦空闲页面足够,那么就不再进行,那么这个zone之下的怎么办呢?不要紧,balance_pgdat在for (i = pgdat->nr_zones - 1; i >= 0; i--)这个循环中会找到这个end的,如果没有找到这个end,说明系统内存平衡,不再扫描,如果找到了这个end,就说明要扫描之下的内存zone了,因为很可能发生了上面的懒惰try-to-free-pages,或者更高的zone拥有足够的空闲内存并且这些足够的空闲内存是在不久前的try-to-free-pages中被释放的,这样所有的更低zone都要经历一次扫描,以平衡页面的老化。

奇妙吧,很不错。很早以前初涉这个函数的时候,我一直不知道zonelist为何是从高向低排列zone的,后来看了代码才明白,这个设计的也不错,最后还是看一下吧:

#define __GFP_DMA 0x01 //如果有这个标志,那么zonelist就是dma

#define __GFP_HIGHMEM 0x02 //如果有这个标志,那么zoneist就是high,normal,dma

以下的逻辑初始化了zonelist

for (i = 0; i

zonelist = pgdat->node_zonelists + i; //每个pgdat拥有GFP_ZONETYPES个zonelist

for (j = 0; zonelist->zones[j] != NULL; j++);

k = ZONE_NORMAL;

if (i & __GFP_HIGHMEM)

k = ZONE_HIGHMEM;

if (i & __GFP_DMA)

k = ZONE_DMA;

j = build_zonelists_node(NODE_DATA(node), zonelist, j, k); //这个函数很简单,就在下面

zonelist->zones[j] = NULL;

}

static int __init build_zonelists_node(pg_data_t *pgdat, struct zonelist *zonelist, int j, int k)

{

switch (k) { //注意这个switch的每一个case没有break,也就是说一旦进入此函数,那么就初始化了一个zonelist

struct zone *zone;

default:

BUG();

case ZONE_HIGHMEM:

zone = pgdat->node_zones + ZONE_HIGHMEM;

if (zone->present_pages) {

zonelist->zones[j++] = zone;

}

case ZONE_NORMAL:

zone = pgdat->node_zones + ZONE_NORMAL;

if (zone->present_pages)

zonelist->zones[j++] = zone;

case ZONE_DMA:

zone = pgdat->node_zones + ZONE_DMA;

if (zone->present_pages)

zonelist->zones[j++] = zone;

}

return j;

}

zonelist的初始化主要就是利用了一个位运算,呵呵。

你可能感兴趣的:(函数)