主动释放本来就是不需要的,以往的限制自动扫描到end只是阻止了高端zone被过度扫描,然而阻止的不彻底,因为虽然kswap自动扫描不和主动扫描重合扫描高端zone了,但是如果有多个主动扫描内存的进程同时存在就构成了竞争,比如该node当前系统物理内存吃紧,然后在多cpu上,多个进程同时分不到内存而调用了try_to_free_pages,然后就有了多个扫描进程,它们会带来过多的内存被释放,从而引起抖动,因为很多大量被频繁使用的页面都被换出后释放了,置换算法怎么会让频繁被引用的页面换出呢?其实这就是linux中置换算法的一个小小的漏洞,试想本来应该的置换过程,内核的置换进程每隔一段相同的时间就扫描一下内存,老化一下页面,这个过程中,lru算法执行的很不错,因为频繁被使用的页面会被再引用,此时会在ptep_clear_flush_young_notify中返回非0,如此就说明该页面最近被访问过,一般的处理器硬件都有一定的机制,在MMU访问了一个页面时,其对应的页表项的访问位会被置1,操作系统内核可以应用此机制很简单的实现lru算法。
如果一切按照上面的方式扫描,根本不会出现任何的不平衡现象,但是不光kswap一个地方扫描了内存的lru链表,在应用程序分配内存的时候,如果当前的内存状况不能满足此次分配,那么try_to_free_pages也会同样的进行内存lru链表的扫描,并且扫描的方式和kswap的lru算法一模一样,也是扫描一遍页面老化一点,最终将最老化的inactive链表的页面置换出去,虽然有kswap内核线程在执行,但是在内存压力比较大的情况下,内存子系统还是会自己置换内存的,为这种置换而进行的扫描老化就是主动扫描,就是它扰乱了标准的lru算法,想象一下,在多处理器系统上,一共有4个cpu,恰恰4个cpu上都在执行try_to_free_pages,如果第1个cpu刚刚扫描过lru链表,链表的页面老化了一个单位,紧接着第二个cpu又执行了一遍,又老化了一个单位,...短短的时间内,很多的活跃页面经过频繁的老化之后变成了不活跃页面,然而它们确实是活跃的,因此最终置换了大量的页面从而不远的将来引发大量的内存抖动。
因此就需要作出一些限制,在主动扫描的情况下不要任凭代码逻辑去毫无限制的按照kswapd的方式扫描内存的lru链表,这个限制在2.6.29内核版本中体现了出来:
static void shrink_zone(int priority, struct zone *zone, struct scan_control *sc)
{
unsigned long nr[NR_LRU_LISTS];
unsigned long nr_to_scan;
unsigned long percent[2];
enum lru_list l;
unsigned long nr_reclaimed = sc->nr_reclaimed;
unsigned long swap_cluster_max = sc->swap_cluster_max;
get_scan_ratio(zone, sc, percent);
for_each_evictable_lru(l) {
int file = is_file_lru(l);
int scan;
scan = zone_nr_pages(zone, sc, l);
if (priority) {
scan >>= priority;
scan = (scan * percent[file]) / 100;
}
if (scanning_global_lru(sc)) {
zone->lru[l].nr_scan += scan;
nr[l] = zone->lru[l].nr_scan;
if (nr[l] >= swap_cluster_max)
zone->lru[l].nr_scan = 0;
else
nr[l] = 0;
} else
nr[l] = scan;
}//如果priority很小或者系统内存比较大,nr[l]很容易不为0导致下面的连锁扫描(释放了inactive页面会引发inactive页面不足从而扫描active页面,下一轮扫描中同样会重复这种本不该发生的事情,这在多重主动扫描中比较容易发生)
while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] || nr[LRU_INACTIVE_FILE]) {
for_each_evictable_lru(l) {
if (nr[l]) {
nr_to_scan = min(nr[l], swap_cluster_max);
nr[l] -= nr_to_scan;
nr_reclaimed += shrink_list(l, nr_to_scan, zone, sc, priority);
}
}
/* 注释很不错,保留
* On large memory systems, scan >> priority can become
* really large. This is fine for the starting priority;
* we want to put equal scanning pressure on each zone.
* However, if the VM has a harder time of freeing pages,
* with multiple processes reclaiming pages, the total
* freeing target can get unreasonably large.
*/
//因此加入了判断,可以及时退出疯狂的扫描代码,目标就是只要够了就退出,不多也不少
if (nr_reclaimed > swap_cluster_max &&
priority < DEF_PRIORITY && !current_is_kswapd())
break;
}
sc->nr_reclaimed = nr_reclaimed;
/*
* Even if we did not try to evict anon pages at all, we want to
* rebalance the anon lru active/inactive ratio.
*/
if (inactive_anon_is_low(zone, sc))
shrink_active_list(SWAP_CLUSTER_MAX, zone, sc, priority, 0);
throttle_vm_writeout(sc->gfp_mask);
}
只要有进程调用try_to_free_pages,就说明需要释放sc.swap_cluster_max个页面,也就是上面的swap_cluster_max,正确的情况就是只要释放掉swap_cluster_max个页面就返回了,但是老的内核版本的vmscan实现的效果却不是这样,上面的shrink_zone是新的2.6.30版本的,在2.6.28以及之前的版本是没有if (nr_reclaimed > swap_cluster_max &&...判断的,我们看看会发生什么?一开始priority较高,收缩内存的压力较小,但是在大内存系统上也会相当大,如果有好几个主动扫描进程,那么再恰巧它们几乎同时检测到内存不足,那么当它们置换inactive之后便开始了轮番老化active链表,结果就是在最初的几个priority中,主要就是置换少量的inactive链表的页面然后老化并inactive比这多的active链表,也就是一个inactive链表积攒的过程,同时主动扫描的进程越多,最后积攒到inactive链表的页面就越多,因为它们检测到了inactive不足后便开始老化active链表的页面并用active填充inactive链表,这个过程一旦开始就要做完。随着priority的减少,需要扫描的页面不断增加,最终很容易也很显然的,实际释放的页面要比swap_cluster_max多很多,为何?就是别的主动扫描进程助力的结果,所有的扫描进程操作的是同一个lru系列链表,很多进程一起老化它们当然比一个来得快得多了。一个形象的例子就是起初的时候,4个进程几乎同时开始扫描所有zone,因为每个zone的inactive页面并不是很多,那么它们几乎又一次同时进入了shrink_active_list,结果就是在inactive链表上积攒了比预想的要多的页面,如果priority减少了之后还是检测到inactive页面不够,就会依照原样再来一次,结果到了priority很低的时候,inactive上已经积累了大量的页面,释放它们需要很长的时间,并且这些页面中有很多是频繁使用的页面,由于多个进程轮番扫描才进入inactive链表的。2.6.29内核改进了这一状况,在这之前,vmscan的代码只有在一个priority扫描完所有的zone之后才判读是否该结束了,2.6.29的代码在每一个zone的扫描中都加上了判断,其实在第一次进入扫描代码的时候,priority为最高,此时一般都是清理一下inactive链表,然后再从active链表补充页面到inactive,到了后面的比较小的priority才是真正的为了本次申请内存而释放内存的操作,这里比较拗口,其实不必考虑太复杂,比如一个扫描进程在释放一个脏页面的时候延迟了导致了很多页面不能及时置换并释放(其实它们马上就可以被置换而释放),随后的扫描进程还是会认为inactive页面不足而扫描active页面,
类似的场景还有很多,不必考虑得很周全,只要明白会有问题就可以了,在每个扫描的时候都加入判断有利于避免这种误判。如果多个进程扫描了大量的内存,本不该被置换的却被置换了引发的内存抖动不说,仅仅内核释放这么多inactive页面就会消耗太多的时间。
附:关于机制和策略的边界的一个实例
今天看关于vmscan的lkml,发现有个哥们提出一个补丁,叫做:make mapped executable pages the first class citizen,大致意思就是在扫描页面的时候,放过带有PROT_EXEC属性的页面,不多说,这个想法太错误了,它基本上是mlock的重复,是mlock的一个策略罢了,因为在linux中,永远不要将能用n个机制实现的一个东西作为一个机制单独实现,在linux的哲学里,那个东西叫做策略。可是有人就要反驳了,同样是vmscan的补丁,为何将lru链表分类就被采纳了呢?为何在lru分类的机制中作者说优先释放文件缓存而不是匿名页面,因为后者为脏的可能性比较大,lru分了扫描的优先级别,这难道不是内核中的策略吗? 内核中实现策略合适吗?确切的说,不合适,但是注意,lru分类是机制而不是策略,文件缓存优先释放才是策略,然而内核保证文件缓存一定优先释放吗?没有,这个策略留给了用户空间。如此看来实现lru分类机制就是为了更好的让用户的策略影响vmscan的行为。这只不过是将内核的行为划分得更加可控制了,更加细腻了,注意,内核本身只是留了一个默认的策略,并没有别的策略实现,所有的策略实现都要用户空间通过sysctl或者别的机制进行。机制在两种情况下会变成策略,第一种情况就是机制太细了会成为策略,第二种情况就是机制确定了,写死了,不可变了就成了策略,机制必然要有类似微调钮一样的可设计方案。