do_wp_page()分析与疑惑

凡是调用do_wp_page,一定满足的条件是,要写一个只读的页。这里的写,是一次用户进程的操作,从handle_pte_fault的最后一个参数write_access来的。而对于只读的页,则是因为页框受保护的,而进程所辖属的vm_area_struct里面的标志一定是可写的,如果不可写,而进程要求可写,那么早已经在do_page_fault里面进入bad_area代码块处理去了(在这个代码块的具体操作是Kill进程),看一下:

463 case 2: /* write, not present */


464 if (!(vma->vm_flags & VM_WRITE))


465 goto bad_area;


466 write++;//vma write


467 break;





那么从哪里判断是只读的页,然后出现缺页异常呢?

答曰:从pteflags字段的相关属性来判断。在fork进程的时候已经订好了什么样的页写了保护。真的吗?真的!请看:

fork一个进程的时候,会调用copy_one_pte(),在这个函数里面是这样处理进程间共享页面的:mm/memory.c

467 if (is_cow_mapping(vm_flags)) {


468 ptep_set_wrprotect(src_mm, addr, src_pte);


469 pte = *src_pte;


470 }





因此,对于按需调页的,一定会触发缺页异常,然后COW.

下面,让我们来看一下对于写一个只读的页,是Linux是怎样来处理的吧:

1、首先获得发生写异常时候的页描述符,通过vm_normal_page()函数来获得,这个函数的功能:就是如果有页描述符,就返回页描述符;如果没有页描述符,那么就返回NULL。(为什么会没有页描述符呢?我们不是说每一个4K的页面不都是与struct page数据结构相对应的么?让我们来看一下vm_normal_page()函数前面的注释吧:

370 * NOTE! Some mappings do not have "struct pages". A raw PFN mapping


371 * will have each page table entry just pointing to a raw page frame


372 * number, and as far as the VM layer is concerned, those do not have


373 * pages associated with them - even if the PFN might point to memory


374 * that otherwise is perfectly fine and has a "struct page".





从注释中,我们看到,如果虚拟层想这样干,就可以这样干,姑且认为VM layer就是虚拟机监视器所在的那一层吧。当然,我是推测,对于VM layer我也是望文生义啊,呵呵,欢迎拍砖!

2、在第一步中,①. 如果获得了页描述符,那么下一步就要判断,需不需要写复制了。②. 而如果没有获得页描述符,即vm_normal_page()返回了NULL,那么就直接进入分配新页的代码块(goto gotten)

3、不需要写复制的几种情况是:①,如果vma_area_struct所管辖的区域是公共可写的区域,即vma->vm_flags 不仅设置了 VM_SHARED,而且也设置了VM_WRITE。那么这种情况下,默认是不需要再分配一个块的需要做的仅仅是将PTE改为可写的就可以啦。我在这里存在的疑问是,既然VMA设定为共享可写了,难道页表项还不听VMA的话么?难道分配一个匿名的VMA,所映射的页框默认是写保护的么?

我觉得我的推测是站的住脚的,因为如果所映射的页框很听VMA的话,那么do_wp_page()
n style="font-size:9pt;font-family:宋体">还有存在的价值么?也许你会认为,有啊,因为
VMA的属性和PTE的属性是一致的,而PTE是写保护的,那么VMA也就是写保护的咯,这样子的话,VMA认为我所指定的页是写保护的,而进程从VMA那里已经知道我这个线性区域是写保护的,还有必要继续do_wp_page()么?而早在do_page_fault()那里已经进入bad_area代码块那里把进程给干掉了。

好了,继续我们上面的话题,如果该线性区制定了我映射的页是共享可写的,那么只需要改变PTE就可以了,说起来很简单,可是也要分分情况,看看有没有特例:如果我映射的是一个文件,而该文件是只读的,那么我要去写它,它当然不愿意啦,因此,对于这种特殊情况,我们是需要考虑的,于是就产生了如下的代码:



1485 if (unlikely((vma->vm_flags & (VM_SHARED|VM_WRITE)) ==


1486 (VM_SHARED|VM_WRITE))) {


1487 if (vma->vm_ops && vma->vm_ops->page_mkwrite) {


1496 page_cache_get(old_page);//call get_page(),inc the page->count


1497 pte_unmap_unlock(page_table, ptl);


1499 if (vma->vm_ops->page_mkwrite(vma, old_page) < 0)


1500 goto unwritable_page;


1502 page_cache_release(old_page);


1510 page_table = pte_offset_map_lock(mm, pmd, address,


1511 &ptl);


1512 if (!pte_same(*page_table, orig_pte))


1513 goto unlock;


1514 }


1516 reuse = 1;





看第1499-1500行,如果映射文件的写操作不允许作用于该页,那么,就goto unwritable_page。从上面的代码的最后一行我们看到一个局部变量是reuse,那么这个就是重用flag。如果其为1,那么不需要再分配新的页面了。

②.不需要再分配页面的第二种情况就是,产生异常的页面虽然是只读的,我现在需要写,如果,这个页框仅仅被我自己的这个进程所用,即,没有其他的进程来使用这个页框,那么我就很随意啦,让这个页框改为可写就可以了。而如果有多个进程共同使用该页的话,那么就不能那么自私,需要照顾其他进程的情绪,重新分配一页来用吧。

注意,这里的情况只可能是匿名区的情况,对于映射了文件的话,就不需要考虑我这个区域是不是仅仅我一个人使用。为什么呢?为什么文件区域不管是几个进程使用,如果vm_area_struct线性区没有定义为共享可写的话,就不考虑只改变页框的属性就可以了?我想这是因为,在操作系统中,对于文件,或者代码,都认为是可以共享的,只要映射了文件,没有显示的定义其为大家都可以随便写的,那么进程想进行写的时候,复制该页面的内容,然后想怎么写就怎么写吧。

4、满足了第三步的情况,那么就是把对应的PTE改改就可以啦。请看Linux的处理代码:

1524 if (reuse) {//


1525 flush_cache_page(vma, address, pte_pfn(orig_pte));


1526 entry = pte_mkyoung(orig_pte);


1527 entry = maybe_mkwrite(pte_mkdirty(entry), vma)
;


1528 ptep_set_access_flags(vma, address, page_table, entry, 1);


1529 update_mmu_cache(vma, address, entry);


1530 lazy_mmu_prot_update(entry);


1531 ret |= VM_FAULT_WRITE;


1532 goto unlock;


1533 }





1526行是把PTE里面设置一个ACCESSED标志位,设置这个标志位的目的就像函数名字那样,让自己年轻一点,为了延迟kswapd守护进程换出刚刚处理的这个页面。

1527行进行了两项操作,首先把PTE位设置了,然后,把PTE设置为可写的,以防止以后进行重复的这种异常处理工作。

1528行就是把新设置的这个entry更新到PTE上面。

1529-1530对于i386是不起作用的,为什么捏?因为i386MMU是设置在CPU里面的,让CPU看着办吧。

下面的几行,就不废话啦,往下继续。

5、如果第三步的情况不满足了,那么就定义reuse=0,重新分配一个复制原来内容的页吧。

6、现在我们来看一下,需要COW的具体处理过程:①.如果原来的页是指向的ZEOR_PAGE,那么就分配一个全是0的页面。②.如果原来的页不是ZEOR_PAGE,那么就把老的那个页面复制一下,然后把复制的这个页面纳入自己的财产范围中。

1544 if (old_page == ZERO_PAGE(address)) {


1545 new_page = alloc_zeroed_user_highpage(vma, address);


1546 if (!new_page)


1547 goto oom;


1548 } else {


1549 new_page = alloc_page_vma(GFP_HIGHUSER, vma, address);


1550 if (!new_page)


1551 goto oom;


1552 cow_user_page(new_page, old_page, address);


1553 }





7、好了,do_wp_page()的主要工作已经做完了,然而,对于一个操作系统来讲robust是至关重要的,因此要把一切可能的情况都要一一考虑了。

1560 page_table = pte_offset_map_lock(mm, pmd, address, &ptl);


1561 if (likely(pte_same(*page_table, orig_pte))) {


1562 if (old_page) {


1563 page_remove_rmap(old_page);//old_page 's count -1


1564 if (!PageAnon(old_page)) {


1565 dec_mm_counter(mm, file_rss);


1566 inc_mm_counter(mm, anon_rss);


1567 }


1568 } else


1569 inc_mm_counter(mm, anon_rss);


1572
flush_cache_page(vma, address, pte_pfn(orig_pte));//do nothing


1573 entry = mk_pte(new_page, vma->vm_page_prot);//new_page地址写入


1574 entry = maybe_mkwrite(pte_mkdirty(entry), vma);


1575 lazy_mmu_prot_update(entry);


1582 ptep_clear_flush(vma, address, page_table);


1583 set_pte_at(mm, address, page_table, entry);//set pte


1584 update_mmu_cache(vma, address, entry);


1585 lru_cache_add_active(new_page);


1586 page_add_new_anon_rmap(new_page, vma, address);


1589 new_page = old_page;


1590 ret |= VM_FAULT_WRITE;


1591 }


1592 if (new_page)


1593 page_cache_release(new_page);


1594 if (old_page)


1595 page_cache_release(old_page);





在这个代码块中,不明白的地方是

1、 为什么要增加匿名区文件的rss?(1566行和第1569)


2、 为什么要对于new_page进行page_cache_release(),对于old_page,能够理解,可是对于刚分配的new_page,为什么还要把count-1(page_cache_release()操作就是将map_count-1的操作)?因为之前,并没有page_cache_get(new_page)啊,难道在之前分配new_page的时候加了1


讨论贴:http://www.linuxforum.net/forum/showflat.php?Cat=&Board=linuxK&Number=747164&page=0&view=collapsed&sb=5&o=7&fpart=



评论

此博客中的热门博文

Linux/ARM Page Table Entry 属性设置分析

提交了30次才AC ---【附】POJ 2488解题报告

笔记