linux fork一个新的进程时,并不是将父进程的所有数据都复制一遍,一些页可以共享的,关于如何共享这里有分析
假如现在有A进程和B进程共享一个页帧frame,那么这个frame对应的页表项的RW标志为0,现在B进程修改frame,引发中断page_fault,可以根据出错码和cr2寄存器上的地址信息来判断是因为页不存在还是由于没有权限导致的,显然这里是没有写权限,这样就是调用do_wp_page,这个函数就是重新申请有一个新页,然后把B进程那个frame的页表项重新指定为这个新页,把之前的frame数据复制到这个新申请的页上,修改A进程fame对应的页表项的RW位为1,重新刷新TLB缓存。
void do_wp_page(unsigned long error_code,unsigned long address)
{
//去除线性地址中的偏移地址,这样算出来的结果就是一个页
un_wp_page((unsigned long *)
(((address>>10) & 0xffc)
+ (0xfffff000 & *((unsigned long *) ((address>>20) &0xffc)))));
}
((address>>10) & 0xffc)
就是((address>>12) <<2 & 0xffc)
,address>>12就是拿到在页表中的索引,因为一个页表项(PTE)占据4(<<2)个字节,所以只要索引乘以4就知道PTE在页表中的偏移位置,与上0xffc表示的是4字节对齐。
address>>20
就是address>>22 << 2
,address>>22就是获取页目录中的索引,乘以4即左移2位,就是页目录项(PDE)在页目录中的偏移位置。
// table_entry是页表项的地址
void un_wp_page(unsigned long * table_entry)
{
unsigned long old_page,new_page;
//取出线性地址中的中的内容,以页为单位对齐
old_page = 0xfffff000 & *table_entry;
//低端内存是内核使用的,如果页引用数是1,直接修改页表项内容,修改RW=1
if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {
*table_entry |= 2;
invalidate(); //刷新TLB
return;
}
// 否则就需要在主内存区申请一页空闲页面给执行写操作的进程单独使用,取消页面
// 共享。如果原页面大于内存低端(则意味着mem_map[]>1,页面是共享的),则将原页
// 面的页面映射字节数组递减1。然后将指定页表项内容更新为新页面地址,并置可读
// 写等标志(U/S、R/W、P)。在刷新页变换高速缓冲之后,最后将原页面内容复制
// 到新页面上。
if (!(new_page=get_free_page())) //申请一个新的页
oom();
if (old_page >= LOW_MEM)
mem_map[MAP_NR(old_page)]--; //old_page引用数减去1,如果减到0表示没有进程持有该页
*table_entry = new_page | 7; // 新申请的页表项P=1,RW=1,US=1
invalidate(); //刷新TLB
copy_page(old_page,new_page); //复制4KB字节
}
// 从from处复制1页内存到to处(4K字节)。
#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024))
// 刷新页变换高速缓冲
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))
get_free_page
函数的分析可以看这里