linux内核内存管理中的pagevec结构体

linux内核的内存管理中有一个2.6内核才加入的并不很张扬的结构体,那就是pagevec:
struct pagevec {
unsigned long nr;
unsigned long cold;
struct page *pages[14];
};
以往要加入到lru链表的page都要加入到这个pagevec了,并不再直接往lru中加入了。可是不加入lru的page就不会被内存管理机制所管理,因此仅仅这样是不行的,除非给pagevec结构加上lru的功能,然而这又势必会使这个结构体复杂化,再一个,这个结构体使用的最大范围就是“每cpu”,更多的它都是局部使用的,这样就使得锁的粒度细化了很多,而lru则是全局的,设计pagevec的目的之一正是因为这个。
我们知道,虽然lru机制理论上很完美,并且对于内存管理还有很多理论上完美的机制,然而linux内核在运行时却是有实际上的扫描时机的,与其说将页面一个一个的单独加入lru链表,不如使用懒惰的思想,在内核实际扫描lru的时候再加入。然而pagevec作为一个批量暂存的容器又不能过于庞大,应该将它调整为刚刚好的大小,关于此的解释我想没有比下面的解释更好并且通俗的了:
instead of carrying water from the well to the house in a spoon, it is more efficient to carry it in a bucket. this is because of the constant time for walking and opening doors and getting exclusive access to the well. but the full bucket (i.e. the contents _and_ the bucket together) should have a good size to handle; together with nr and cold you have 16 pointer-sized members in the structure which fits nicely into the cache lines and is easy to calculate with (if you don't count thumbs and big toes (which may have a special use as carry flags) most humans have 16 fingers and toes altogether).
是这样的,如果pagevec暂存的page数目过多,在vmscan的时候就会导致负担过重,并且pagevec本身的维护费用也会随之增加的,因此就刚好将之定为了2^4这么大。
我们可以从shrink_active_list和shrink_inactive_list的最后看到,都会将扫描到的page加入一个局部变量pvec(struct pagevec pvec)中,这个举动其实有两个层次的含义,第一个含义请看这两个函数的开始都有隔离链表的动作(isolate_lru_pages),这个动作的目的也是为了使锁的粒度更小一些,否则在操作链表的时候就要锁住全局的链表了。在隔离函数中我们看到递增了page的引用计数:
对于active或者inactive链表的每一个page:
list_del(&page->lru);
get_page_unless_zero(page);
list_add(&page->lru, target);
这个get_page的操作意思是说,现在vmscan正在扫描这个页面,每一个引用计数都代表了一条内核使用路径。因此我们必须在另一个地方能进行一个UNisolate_lru_pages的操作,在这个函数中递减page的引用计数,然而没有这样的函数,也不需要这样的函数,pagevec相关的接口函数就能做到这一点,在这两个shrink函数的最后,都有:
if (!pagevec_add(&pvec, page)) {
spin_unlock_irq(&zone->lru_lock);
__pagevec_release(&pvec);
spin_lock_irq(&zone->lru_lock);
}
其中加入到pvec的page一般是可以full的,因此__pagevec_release总能被调到
void __pagevec_release(struct pagevec *pvec)
{
lru_add_drain();
release_pages(pvec->pages, pagevec_count(pvec), pvec->cold);
pagevec_reinit(pvec);
}
即使它没有被调用到,也就是page数量不足14,那么函数结尾也要调用pagevec_release的。release函数中的第一行lru_add_drain就是将原先加入到“每cpu”各个pagevec的page们转移到它们真正应该处于的相应的lru链表上,同时将当时它们加入pagevec时递增的计数减下去(加入pagevec就说明有一条pagevec这个路径在引用这个page),__pagevec_release的第二行就是shrink函数中pvec相应举动的第二层意义,那就是批量地递减在isolate中被递增的引用计数。
shrink函数调用pagevec_add的真实目的其实就是为了最终调用__pagevec_release,这种代码如果不从全局理解是很难弄懂的。还有一个很值得注意的地方就是shrink_inactive_list和shrink_active_list的返回值的不同,扫描active返回空,而扫描inactive返回实际释放的页面数量,这也是合理的,因为扫描active链表的目的是为了补充inactive链表,也就是说参与内存管理的page们必然存在一个“被分配->active->inactive->free”的序列,只有最终地在inactive链表中的页面会被free,然而在shrink_active_list的最后由于pvec这个局部变量的举动,可能真的会有一个page被释放,为何不将它们的数量累加到释放的数量呢,也就是返回它们的数量呢?这是因为凡是这种方式被释放的页面都是在别的内核路径被put_page的,由于它们在加入“每cpu”的pagevec的时候递增了一个引用计数才导致当时被别的路经put_page的时候没有真的被free掉,因此它早就应该被free了,根本不是这次scan的功劳,它们由于一直暂时安全的呆在pagevec上,因此才活到了现在,它们被free的时刻应该是以前的某个时间(如果它们当初直接加在全局的lru而不是每cpu的pagevec上的话),所以它们当然是被直接的释放而不发出任何通知。
总之,shrink_active的时候,其目的是将“这次”牵扯的page“释放”到inactive链表,而只有在shrink_inactive的时候才会将page通过shrink_page_list“释放”到伙伴系统(广义上的“释放”)。pagevec其实本质上就是一个暂存容器,和上述的那段英文说明一样,它就是一个桶,代替了汤勺

你可能感兴趣的:(linux)