最新的linux2.6.28内核的一个重大改进就是对内存回收算法进行了修改,以往的内核中保持了active和inactive两个链表用来实现 lru算法,这个方式一直工作的很好,但是粒度过于粗糙,另外,每次回收内存的kswapd守护进程运行的时候,为了找出哪些内存该回收,不得不扫描整个 active链表和inactive链表,实际上就是重新填充这两个链表,这种方式的弊端显而易见,就是在大内存的情况下,扫描一遍内存需要消耗大量的时间,虽然在理论上,双指针时钟回收算法可以使得扫描的内存不再那么大,但是理论和实践的距离非常之遥远,不信的话你可以看看,理论上不到十行就可以阐述清 楚地lru算法,在任何操作系统上要想完美的实现,没有万行以上的代码是下不来的,而且,理论上一种回收算法是自成体系的,是孤立的,但是实际应用的时候就要不得不和别的子系统交互,正是因为这样,我们看代码的时候会觉得和理论上的描述大相径庭,也正是因为这个原因,linux直到现在才朝着完美迈了一步,这一步迈得不容易。不要觉得新代码比原来的更难,实际上比原来的更简单,更清晰。
总的来说,新的代码不再将匿名页面和磁盘缓存页面等同,在以往的内核中,内核不区分匿名页面和磁盘缓存页面,这种方式使得代码实现更容易以一种统一的方式描述,可是仔细想想,为了这种视觉上的统一的和谐,我们失去的是事情原来的本质,匿名页面和磁盘缓存真的平等吗?我们可以想象一下,匿名页面是什么,它们 其实是我们的程序逻辑在内存中的表示,完全cpu消耗型的程序几乎全部都是匿名页面,可是说,所有程序都要有匿名页面,匿名页面实际上就是程序执行的时候 在内存中临时的驻留地,是每个程序都必须拥有的,但是磁盘缓存页面仅仅是为了效率而提供的一种策略,在这个意义上,匿名页面更像是一种机制,它们的本质不同,因此地位当然不会相同,所以,最新的2.6.28内核完美的重现了这种不同,匿名页面更容易被改变,毕竟程序的执行在内存里面的体现就是页面的读写,反过来,磁盘缓存页面的改变却不是那么频繁,它们存在的意义更多的是作为磁盘的备份而不是被修改的实体。一不做二不休,既然匿名页面和磁盘缓存区分了开 来,那么为何不使用双指针时钟算法使得效率进一步提高呢?2.6.28确实这么做了。
新内核不再每次回收页面的时候都扫描整个链表,而是将inactive链表维持在一个范围内并在一定条件下给与填充,每次只扫描一个可控范围链表的页面,这样可大大减少扫面时间,新算法的本质就是不再一个一个页面检查以清除或者设置标志位然后搜集可回收的页面,最后回收这些可回收的页面,取而代之的是直接 察看当前扫到的页面能否被回收,双指针时钟算法能确保扫到这个页面,为了进一步缩减需要扫描的页面,将不可回收的页面置入一个不可回收链表,这样在扫描的时候就不用扫描这些页面了。
当被文字折磨得头昏脑胀的时候,最好的方式就是看代码,首先看一下数据结构,既然内核区分了这么多的链表,因此链表就不再是原来的active和inactive了,而是扩展成了一个枚举,定义如下:
enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE, //初始的链表,匿名页面的不活动链表
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE, //匿名页面的活动链表
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE, //磁盘缓存的不活动链表
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE, //磁盘缓存的活动链表
#ifdef CONFIG_UNEVICTABLE_LRU
LRU_UNEVICTABLE, //不可回收的页面链表
NR_LRU_LISTS
};
我们来看一下重要的宏和帮助函数:
#define for_each_lru(l) for (l = 0; l < NR_LRU_LISTS; l++) //遍历所有的链表
#define for_each_evictable_lru(l) for (l = 0; l <= LRU_ACTIVE_FILE; l++) //遍历所有的可回收链表
static inline int is_file_lru(enum lru_list l) //是否为文件缓存
{
return (l == LRU_INACTIVE_FILE || l == LRU_ACTIVE_FILE);
}
static inline int is_active_lru(enum lru_list l) //是否为活动链表
{
return (l == LRU_ACTIVE_ANON || l == LRU_ACTIVE_FILE);
}
static inline int is_unevictable_lru(enum lru_list l) //是否为不可回收的链表
{
return (l == LRU_UNEVICTABLE);
}
kswapd是内存回收的守护内核线程,我们不讨论try_to_free_pages,因为手工释放内存的操作是完全自主的,只有内核自动的回收才可以体现回收算法的精髓,在kswapd中调用的主题函数是balance_pgdat,这个函数我们来分析一下:
static unsigned long balance_pgdat(pg_data_t *pgdat, int order)
{
...
loop_again:
total_scanned = 0;
nr_reclaimed = 0;
sc.may_writepage = !laptop_mode;
count_vm_event(PAGEOUTRUN);
for (i = 0; i < pgdat->nr_zones; i++)
temp_priority[i] = DEF_PRIORITY;
for (priority = DEF_PRIORITY; priority >= 0; priority--) { //优先级循环,和以往的版本无异。
int end_zone = 0;
unsigned long lru_pages = 0;
...
all_zones_ok = 1;
for (i = pgdat->nr_zones - 1; i >= 0; i--) { //zone循环
struct zone *zone = pgdat->node_zones + i;
if (!populated_zone(zone))
continue;
if (zone_is_all_unreclaimable(zone) &&priority != DEF_PRIORITY)
continue;
if (inactive_anon_is_low(zone)) //为了让匿名的inactive链表保持在一个可控的范围
shrink_active_list(SWAP_CLUSTER_MAX, zone,&sc, priority, 0);
if (!zone_watermark_ok(zone, order, zone->pages_high, 0, 0)) { // 当发现一个zone的空闲页面数量不足的时候,记录此zone并且跳出循环,zone的方向是从低的dma到高的highmem,结止于end_zone 会使得内核先回收end_zone之前的内存区域。zone_watermark_ok函数的本质就是确保区域中拥有一定数量的内存页面。
end_zone = i;
break;
}
}
if (i < 0)
goto out;
for (i = 0; i <= end_zone; i++) {
struct zone *zone = pgdat->node_zones + i;
lru_pages += zone_lru_pages(zone);
}
for (i = 0; i <= end_zone; i++) { // 扫描到end_zone的原因就是为了迎合内存分配,在内存分配的时候会从highmen到dma的方向扫描,如果前一个zone中没有可用的内存,那么 就会从更低的zone中分配,这种在end_zone结束的策略可以使得系统更少的扫描更低的zone,实际上内存分配系统和内存回收系统在扫描时在 end_zone汇合。
struct zone *zone = pgdat->node_zones + i;
int nr_slab;
...
if (!zone_watermark_ok(zone, order, zone->pages_high, end_zone, 0))
all_zones_ok = 0;
temp_priority[i] = priority;
sc.nr_scanned = 0;
note_zone_scanning_priority(zone, priority);
if (!zone_watermark_ok(zone, order, 8*zone->pages_high, end_zone, 0))
nr_reclaimed += shrink_zone(priority, zone, &sc); //实际的回收该
zone
...
}
static unsigned long shrink_zone(int priority, struct zone *zone, struct scan_control *sc)
{
unsigned long nr[NR_LRU_LISTS];
unsigned long nr_to_scan;
unsigned long nr_reclaimed = 0;
unsigned long percent[2];
enum lru_list l;
get_scan_ratio(zone, sc, percent); //得到各个链表的回收率。
for_each_evictable_lru(l) {
if (scan_global_lru(sc)) {
int file = is_file_lru(l);
int scan;
scan = zone_page_state(zone, NR_LRU_BASE + l);
if (priority) {
scan >>= priority;
scan = (scan * percent[file]) / 100;
}
zone->lru[l].nr_scan += scan;
nr[l] = zone->lru[l].nr_scan;
if (nr[l] >= sc->swap_cluster_max)
zone->lru[l].nr_scan = 0;
else
nr[l] = 0;
}
...
}
while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] || nr[LRU_INACTIVE_FILE]) {
for_each_evictable_lru(l) {
if (nr[l]) { //前提是nr[l]不为0,这样才会回收内存。
nr_to_scan = min(nr[l],(unsigned long)sc->swap_cluster_max);
nr[l] -= nr_to_scan;
nr_reclaimed += shrink_list(l, nr_to_scan, zone, sc, priority);//实际回收
}
}
}
if (inactive_anon_is_low(zone)) //平衡匿名的内存页面链表,之所以不动file缓存的页面链表是因为缓存情况是不确定的,而匿名页面却是一直存在的。
shrink_active_list(SWAP_CLUSTER_MAX, zone, sc, priority, 0);
...
}
static void get_scan_ratio(struct zone *zone, struct scan_control *sc, unsigned long *percent)
{
unsigned long anon, file, free;
unsigned long anon_prio, file_prio;
unsigned long ap, fp;
anon = zone_page_state(zone, NR_ACTIVE_ANON) +
zone_page_state(zone, NR_INACTIVE_ANON);
file = zone_page_state(zone, NR_ACTIVE_FILE) +
zone_page_state(zone, NR_INACTIVE_FILE);
free = zone_page_state(zone, NR_FREE_PAGES);
if (nr_swap_pages <= 0) { //没有交换空间的情况下,回收磁盘缓存,因为匿名页面无处可以交换
percent[0] = 0;
percent[1] = 100;
return;
}
if (unlikely(file + free <= zone->pages_high)) { //相反,只有很少的磁盘缓存的情况下,回收匿名页面,这样可针对cpu消耗型或者io消耗型程序采取不同的策略
percent[0] = 100;
percent[1] = 0;
return;
}
//以下计算出匿名页面和磁盘缓存页面的回收比例
if (unlikely(zone->recent_scanned[0] > anon / 4)) {
spin_lock_irq(&zone->lru_lock);
zone->recent_scanned[0] /= 2;
zone->recent_rotated[0] /= 2;
spin_unlock_irq(&zone->lru_lock);
}
if (unlikely(zone->recent_scanned[1] > file / 4)) {
spin_lock_irq(&zone->lru_lock);
zone->recent_scanned[1] /= 2;
zone->recent_rotated[1] /= 2;
spin_unlock_irq(&zone->lru_lock);
}
anon_prio = sc->swappiness;
file_prio = 200 - sc->swappiness;
ap = (anon_prio + 1) * (zone->recent_scanned[0] + 1);
ap /= zone->recent_rotated[0] + 1;
fp = (file_prio + 1) * (zone->recent_scanned[1] + 1);
fp /= zone->recent_rotated[1] + 1;
percent[0] = 100 * ap / (ap + fp + 1);
percent[1] = 100 - percent[0];
}
整 个回收过程,我们再也见不到了填充inactive链表中的遍历整个active链表的操作,确实显得高效了很多,不光在这个vmscan.c文件中更改 了内核代码,只要在获得内存页面的时候都会将页面放入属于它的链表,这样内存页面们各就各位,等待接受内存回收管理系统的管理。