do_wp_page()分析与疑惑
463 case 2: /* write, not present */
464 if (!(vma->vm_flags & VM_WRITE))
465 goto bad_area;
466 write++;//vma write
467 break;
那么从哪里判断是只读的页,然后出现缺页异常呢?
答曰:从pte的flags字段的相关属性来判断。在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是不起作用的,为什么捏?因为i386的MMU是设置在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=
评论
发表评论