(本文基于2.6.1代码,参考2.6.17代码)在linux内核中,懒惰的方式已经成了它的一种性格,几乎所有的资源都是用引用计数来管理的,只有到没有实体使用资源的时候也就是其引用计数为0的时候,该资源就该释放了,实际上只要一个实体使用一个资源,它不必在意该资源当前有多少引用计数,只管递增一个就可以了,在它使用完了以后再递减掉它。既然整个内核都在这么干,page也不例外,page中就有一个引用计数,它可以起到锁定页面的作用,这种锁定方式不是那么优雅,为何这么说呢?因为这是一种站着茅坑不拉屎的方式,意思就是说内核路径中有一条路径在使用这个page,既然内核在使用,那么当然是至高无上的了,vmscan就不能释放这个page,但是一切的目的仅仅是为了不让vmscan释放这个page,并没有什么内核路径使用这个page,这种方式的实施很简单,就是在内核中简单的调用get_page即可。linux的懒惰使得一切变得简单无比,一个page的释放与否仅仅看它的引用计数,只要不为0,那么就不能释放这个page。
曾经我对linux内核内存管理一直有个认识误区,认为page的引用计数仅仅是一个类似于年龄的性质的字段,只要执行vmscan一次均会将引用计数递减,不管多大的引用计数,随着每次调用vmscan内核路径,它终究会递减到0,页面终究会被释放,可是虽然这么理解很合理但是我始终给不出一个更合理的这么做的理由,既然年龄的意义这么显然,那么为何使用引用计数这个有着另外意义的字段表示呢,第二就是如果一个内核路径正在读写这个页面,该路径显然递增了这个page的引用计数,但是一旦vmscan可以释放这个页面,那么内核中会导致缺页,而linux的内核页面不允许缺页,并且内核页面一般都映射到了临时区或者动态区,这么处理内核缺页很复杂,如果内核空间不允许缺页,那么一旦缺页将会导致很严重的后果,因此引用计数必须保证页面不被换出或者直接释放,当然这并不是说内核空间的执行路径不允许缺页,而是说内核空间的内存不允许缺页。当我了解了linux内核懒惰的性格之后,我再也不把page的引用计数当成年龄类似的字段了,我明白了只要一条内核路径递增了page的引用计数,那么vmscan就不能释放该page,这里所说的内核路径不包括缺页中断的路径,事实是在缺页中断中为用户分配一个page并且将这个页面的引用计数设置为1并且加入到lru链表,只要没有别的内核路径引用该page,那么该page的引用计数始终为1,在内存紧缺或者是定时执行lru逻辑时将会判断“最近最少使用”的页面的引用计数递减后是否为0,如果是就说明没有别的路径引用该page(只要引用必将page引用计数递增),否则就说明有别的路径在引用该page,也就是不能释放该page,这个逻辑如此简单以至于随便一个合格的程序员都能想到,linux就是靠这种简单的逻辑堆积的,简单就是美,简单就是稳定,linux内核很美很稳定!
事实上代码真的像理论这么清晰吗?不是这样的,2.6内核为了效率并没有直接将在缺页中断中分配的页面加入到lru链表,而是加入到了一个叫做pagevec的结构体,这么做的原因是为了更高效,因为将一个页面加入lru链表的时候必然要先锁住该链表,不管几个cpu,这个链表每个node只有一个,因此几个cpu都会受到影响,为了使锁的粒度最小化,linux引入了pagevec这个结构体:
struct pagevec {
unsigned long nr;
unsigned long cold;
struct page *pages[PAGEVEC_SIZE];
};
内核为每一个cpu维护了一个以上pagevec结构体,有一个负责存放要加入active链表的page,另一个存放加入inactive链表的page,这样在缺页中断中分配的页面将首先加入到这些pagevec中,事实上pagevec使用了这个page,也就是说pagevec是这些page的引用路径,那么显然的药将引用计数递增,以下代码体现:
void fastcall lru_cache_add_active(struct page *page)
{
struct pagevec *pvec = &get_cpu_var(lru_add_active_pvecs); //pvec是每cpu的,这样可以避免使用全局的锁,使用全局的锁会影响性能,2.6内核引入的pagevec将同步锁定限制在了一个cpu内,锁的粒度小了
page_cache_get(page); //增加一个引用计数,这个引用计数完全是为了让pvec用的
if (!pagevec_add(pvec, page)) //加入到pagevec
__pagevec_lru_add_active(pvec); //如果已经满了,就将pagevec中的页面全部加入到lru,它们不再在pagevec了,因此这个调用中将递减两行前递增的page引用计数
put_cpu_var(lru_add_active_pvecs);
}
由于pagevec的引入,代码更凌乱了,但是只要你明白page的引用计数的作用,那么一切都不难,你只要知道在加入到lru之前的page先加入到pagevec时要增加一个引用计数,然后在从pagevec转到lru的时候要递减这个引用计数就可以了,虽然代码看似很混乱,但是基本逻辑还是很清晰的。接下来看看一个重量级的函数get_user_pages的意义,这个函数可以锁定用户页面,将之锁定在内存中不被换出,其实这个函数就是靠增加页面的引用计数来实现的,另外一种锁定页面的方式是调用mlock系统调用,但是后者是主动的锁定,并且在页面的基本属性上保证了不被换出,这是用户可以控制的,而前者使用引用计数的方式是用户所不能控制的,它只是有的时候内核在使用该page,由于内核使用而不能被换出,比如内核在执行aio或者bio,通过引用计数来锁定页面的方式是通过get_user_pages来完成:
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, int len, int write, int force, struct page **pages, struct vm_area_struct **vmas)
{
int i;
unsigned int flags;
flags = write ? (VM_WRITE | VM_MAYWRITE) : (VM_READ | VM_MAYREAD);
flags &= force ? (VM_MAYREAD | VM_MAYWRITE) : (VM_READ | VM_WRITE);
i = 0;
do {
struct vm_area_struct * vma;
vma = find_extend_vma(mm, start);
...
spin_lock(&mm->page_table_lock);
do {
struct page *map;
while (!(map = follow_page(mm, start, write))) {
spin_unlock(&mm->page_table_lock);
switch (handle_mm_fault(mm,vma,start,write)) { //通过缺页中断来分配页面
...
}
spin_lock(&mm->page_table_lock);
}
if (pages) {
pages[i] = get_page_map(map);
if (!pages[i]) {
spin_unlock(&mm->page_table_lock);
while (i--) //一旦出错,废除以前已经成功分配的
page_cache_release(pages[i]);
i = -EFAULT;
goto out;
}
flush_dcache_page(pages[i]);
if (!PageReserved(pages[i]))
page_cache_get(pages[i]); //增加引用计数,为了不被换出
}
if (vmas)
vmas[i] = vma;
i++;
start += PAGE_SIZE;
len--;
} while(len && start < vma->vm_end);
spin_unlock(&mm->page_table_lock);
} while(len);
out:
return i;
}
以上就是get_user_pages的基本逻辑,在vmscan中将选择最近最不常使用的页面换出,当然有个前提就是没有一个内核路径在使用该页面才可以换出该页面,首先看看shrink_list逻辑:
static int shrink_list(struct list_head *page_list, unsigned int gfp_mask, int *max_scan, int *nr_mapped)
{
...
pagevec_init(&freed_pvec, 1);
while (!list_empty(page_list)) {
struct page *page;
int may_enter_fs;
int referenced;
page = list_entry(page_list->prev, struct page, lru);
list_del(&page->lru); //将页面从链表删除
if (TestSetPageLocked(page)) //如果是锁定的那么不释放
goto keep;
...
if (PageWriteback(page)) //正在写回的页面不释放
goto keep_locked;
pte_chain_lock(page);
referenced = page_referenced(page); //被引用的页面不被释放
if (referenced && page_mapping_inuse(page)) {
pte_chain_unlock(page);
goto activate_locked;
}
mapping = page->mapping;
...
if (PagePrivate(page)) {
if (!try_to_release_page(page, gfp_mask))
goto activate_locked;
if (!mapping && page_count(page) == 1) //如果引用计数已经是1了,那么只需要再递减一次就释放了,free_it做到了
goto free_it;
}
...
__remove_from_page_cache(page);
spin_unlock(&mapping->page_lock);
__put_page(page); //除了当前由于某种原因还不能释放的page以及page的引用计数为1的情况都会到达这里,这里首先递减了在shrink_cache中递增的引用计数,然后后面的free_it中再递减一个引用计数,page的引用计数为0,释放之
free_it:
unlock_page(page);
ret++;
if (!pagevec_add(&freed_pvec, page)) //如果pagevec内有空闲空间,那么先暂且不递减引用计数
__pagevec_release_nonlru(&freed_pvec);
continue;
activate_locked:
SetPageActive(page);
pgactivate++;
keep_locked:
unlock_page(page);
keep:
list_add(&page->lru, &ret_pages);
BUG_ON(PageLRU(page));
}
list_splice(&ret_pages, page_list); //将那些没有被释放的页面加入到page_list,待到返回shrink_cache时再递减引用计数,因为使用pagevec会更好管理
if (pagevec_count(&freed_pvec))
__pagevec_release_nonlru(&freed_pvec); //这里肯定要递减page的引用计数了,无论如何都要递减,就是为了递减掉在shrink_cache中递增的引用计数
mod_page_state(pgsteal, ret);
if (current_is_kswapd())
mod_page_state(kswapd_steal, ret);
mod_page_state(pgactivate, pgactivate);
return ret;
}
下面看一下调用上述shrink_list的函数shrink_cache:
static int shrink_cache(const int nr_pages, struct zone *zone, unsigned int gfp_mask, int max_scan, int *nr_mapped)
{
...
pagevec_init(&pvec, 1);
lru_add_drain(); //首先将每cpu的pagevec中的页面加入到正式的lru链表
spin_lock_irq(&zone->lru_lock);
while (max_scan > 0 && ret < nr_pages) {
struct page *page;
int nr_taken = 0;
int nr_scan = 0;
int nr_freed;
while (nr_scan++ < nr_to_process &&!list_empty(&zone->inactive_list)) {
page = list_entry(zone->inactive_list.prev,struct page, lru);
...
list_del(&page->lru);
...
list_add(&page->lru, &page_list);
page_cache_get(page); //增加一个引用计数,因为vmscan实体正在使用这个page
nr_taken++;
}
...
nr_freed = shrink_list(&page_list, gfp_mask, &max_scan, nr_mapped); //释放一切可以释放的
ret += nr_freed;
if (nr_freed <= 0 && list_empty(&page_list))
goto done;
spin_lock_irq(&zone->lru_lock);
while (!list_empty(&page_list)) { //将无法释放的页面重新放到lru链表,当然不再首先尝试放入pagevec
page = list_entry(page_list.prev, struct page, lru);
if (TestSetPageLRU(page))
BUG();
list_del(&page->lru); //由于page已经在一个临时的page_list链表中了,这里把它删除,最终将之加入到真正的lru链表
if (PageActive(page))
add_page_to_active_list(zone, page); //直接放入lru链表
else
add_page_to_inactive_list(zone, page);
if (!pagevec_add(&pvec, page)) { //由于上面通过page_cache_get增加了引用计数,那么这里要递减之
spin_unlock_irq(&zone->lru_lock);
__pagevec_release(&pvec); //如果pvec已经满了,那么就此了断
spin_lock_irq(&zone->lru_lock);
}
}
}
spin_unlock_irq(&zone->lru_lock);
done:
pagevec_release(&pvec); //最终一定要了断,最终必然要递减一个引用计数
return ret;
}
void __pagevec_release(struct pagevec *pvec)
{
lru_add_drain(); //将pagevec的页面加入到lru,将释放一个pagevec的引用计数,注意这个函数中涉及的页面和当前shrink路径的页面没有关系,可以说当前shrink路径的页面都是lru的页面,而本lru_add_drain函数的页面时已经加入到pagevec但是还没有加入到lru的页面,之所以调用这个函数是因为要将临时存放于pagevec的页面在释放期间全部加入到lru,因此这里面递减的引用计数是在lru_cache_add之类的函数中递增的
release_pages(pvec->pages, pagevec_count(pvec), pvec->cold); //将再释放一个引用计数,在shrink环境中这个引用计数是在shrink_cache中的page_cache_get(page)中被递增的。
pagevec_reinit(pvec);
}
void lru_add_drain(void)
{
struct pagevec *pvec = &get_cpu_var(lru_add_pvecs);
if (pagevec_count(pvec))
__pagevec_lru_add(pvec); // 如果pagevec上有页面,那么就将它们全部加入到lru链表,因为lru才是最终需要权衡的,pagevec只是一个临时的存放点,为了缩小锁的粒度提高效率罢了
pvec = &__get_cpu_var(lru_add_active_pvecs);
if (pagevec_count(pvec))
__pagevec_lru_add_active(pvec);
put_cpu_var(lru_add_pvecs);
}
void __pagevec_lru_add(struct pagevec *pvec)
{
int i;
struct zone *zone = NULL;
for (i = 0; i < pagevec_count(pvec); i++) { //循环将所有的pagevec中的页面加入到lru链表同时最终递减引用计数
struct page *page = pvec->pages[i];
struct zone *pagezone = page_zone(page);
if (pagezone != zone) {
if (zone)
spin_unlock_irq(&zone->lru_lock);
zone = pagezone;
spin_lock_irq(&zone->lru_lock);
}
if (TestSetPageLRU(page))
BUG();
add_page_to_inactive_list(zone, page);
}
if (zone)
spin_unlock_irq(&zone->lru_lock);
release_pages(pvec->pages, pvec->nr, pvec->cold); //这个调用放到这里其实就是递减引用计数,顺便判断计数是否为0,如果是那么释放
pagevec_reinit(pvec);
}
void release_pages(struct page **pages, int nr, int cold)
{
int i;
struct pagevec pages_to_free;
struct zone *zone = NULL;
pagevec_init(&pages_to_free, cold);
for (i = 0; i < nr; i++) {
struct page *page = pages[i];
struct zone *pagezone;
if (PageReserved(page) || !put_page_testzero(page)) //递减了引用计数
continue;
pagezone = page_zone(page); //如果递减引用计数以后引用计数为0,那么顺便释放这个页面
if (pagezone != zone) {
if (zone)
spin_unlock_irq(&zone->lru_lock);
zone = pagezone;
spin_lock_irq(&zone->lru_lock);
}
if (TestClearPageLRU(page))
del_page_from_lru(zone, page);
if (page_count(page) == 0) {
if (!pagevec_add(&pages_to_free, page)) {
spin_unlock_irq(&zone->lru_lock);
__pagevec_free(&pages_to_free);
pagevec_reinit(&pages_to_free);
zone = NULL;
}
}
}
if (zone)
spin_unlock_irq(&zone->lru_lock);
pagevec_free(&pages_to_free); //释放之
}
以上详细论述了页面的引用计数的典型应用--页面的锁定以及释放过程,可以清晰的看出,vmscan逻辑没有随意递减页面的引用计数,而是只递减了它自己增加的那一个引用计数,内核中的任何使用一个page的地方都要递增它的引用计数,这就完全符合了linux内核的约定,开发起来十分简单。起初不明白linux引用计数的时候看linux代码的时候总是一头雾水,但是一旦明白了一个简单的道理之后再看代码,不是清晰了很多而是一下子完全明白了,这就是linux,linux代码一定要一读再读才能有收获!本文的宗旨在于阐明,只有在内核引用page的时候才会递增page的引用计数,并且必须这么做,包括vmscan逻辑也要递增之,事后一定释放了该引用计数,起初我以为get_user_pages过后得到的用户页面可能会被释放,事实证明我错了,在get_user_pages中已经递增了页面的引用计数,那么直到内核路径自己释放这些页面,vmscan是不会将其释放的。mlock和get_page两种方式可以锁定内存页面不被换出,前者主动受用户控制,后者只受内核控制。在内核中使用用户页面的时候经常使用get_user_pages来得到用户的页面然后直接操作之,那么为何不直接使用呢?内核在访问用户空间内存的时候是通过该进程地址空间虚拟地址访问的,而内核自己使用的时候可能是通过页面来访问的,而该页面除了映射到了当事进程的地址空间外,一般的还要被映射到内核空间,比如临时映射区段的位置,这样就要严格要求两个映射完全一致,增加引用计数可以简化映射同步开销;再一个要知道在内核空间是允许用户内存缺页的,事实证明通过缺页来得到用户内存不好,因为内核的执行上下文不一定是其引用内存的进程上下文,比如AIO就是这样,AIO虽然使用use_mm来切换了地址空间,但是本质上这很勉强,另外在中断上下文中是任意进程的上下文,在中断线程化的环境中的上下文是中断线程的上下文;另外在中断种缺页的话会导致系统死锁或者崩溃,因为缺页中断处理会睡眠,而中断不允许睡眠。