写时复制技术最初产生于Unix系统,用于实现一种傻瓜式的进程创建:当发出fork( )系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种行为是非常耗时的,因为它需要:
· 为子进程的页表分配页面
· 为子进程的页分配页面
· 初始化子进程的页表
· 把父进程的页复制到子进程相应的页中
创建一个地址空间的这种方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中的内容。在大多数情况下,这样做常常是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。
现在的Unix内核(包括Linux),采用一种更为有效的方法称之为写时复制(或COW)。这种思想相当简单:父进程和子进程共享页面而不是复制页面。然而,只要页面被共享,它们就不能被修改。无论父进程和子进程何时试图写一个共享的页面,就产生一个错误,这时内核就把这个页复制到一个新的页面中并标记为可写。原来的页面仍然是写保护的:当其它进程试图写入时,内核检查写进程是否是这个页面的唯一属主;如果是,它把这个页面标记为对这个进程是可写的。
Page结构的count域用于跟踪共享相应页面的进程数目。只要进程释放一个页面或者在它上面执行写时复制,它的count域就递减;只有当count变为NULL时,这个页面才被释放。
现在我们讲述Linux怎样实现写时复制(COW)。当handle_pte_fault( )确定“缺页”错误是由请求写一个页面所引起的时(这个页面存在于内存中且是写保护的),它执行以下语句:
if (pte_present(pte)) {
entry = pte_mkyoung(entry);
set_pte(pte, entry);
flush_tlb_page(vma, address);
if (write_access) {
if (!pte_write(entry))
return do_wp_page(tsk, vma, address, pte);
entry = pte_mkdirty(entry);
set_pte(pte, entry);
flush_tlb_page(vma, address);
}
return 1;
}
首先,调用pte_mkyoung( ) 和
set_pte( )函数来设置引起错误的页所对应页表项的访问位。这个设置使页“年轻”并减少它被交换到磁盘上的机会。如果错误由违背写保护而引起的,handle_pte_fault( ) 返回由do_wp_page( )函数产生的值;否则,则已检测到某一错误情况(例如,用户态地址空间中的页,其User/Supervisor标志为0),且函数返回1。
do_wp_page(
)函数首先把page_table参数所引用的页表表项装入局部变量pte,然后再获得一个新页面:
pte = *page_table;
new_page = __get_free_page(GFP_USER);
由于页面的分配可能阻塞进程,因此,一旦获得页面,这个函数就在页表表项上执行下面的一致性检查:
· 当进程等待一个空闲的页面时,这个页是否已经被交换出去(pte 和
*page_table的值不相同)
· 这个页是否已不在物理内存中(页表表项中页的Present标志为0)
· 页现在是否可写(页项中页的Read/Write标志为1)
如果这些情况中的任意一个发生,do_wp_page( )释放以前所获得的页面,并返回1。
现在,函数更新次级缺页的数目,并把引起错误的页的页描述符指针保存到page_map局部变量中。
tsk->min_flt++;
page_map = mem_map + MAP_NR(old_page);
接下来,函数必须确定是否必须真的把这个页复制一份。如果仅有一个进程使用这个页,就无须应用写时复制技术,而且进程应该能够自由地写这个页。因此,这个页面被标记为可写,这样当试图写入的时候就不会再次引起“缺页”错误,以前分配的新的页面也被释放,函数结束并返回1。这种检查是通过读取page结构的count域而进行的:
if (page_map->count == 1) {
set_pte(page_table, pte_mkdirty(pte_mkwrite(pte)));
flush_tlb_page(vma, address);
if (new_page)
free_page(new_page);
return 1;
}
相反,如果这个页面由两个或多个进程所共享,函数把旧页面(old_page)的内容复制到新分配的页面(new_page)中:
if (old_page == ZERO_PAGE)
memset((void *) new_page, 0, PAGE_SIZE);
else
memcpy((void *) new_page, (void *) old_page, PAGE_SIZE);
set_pte(page_table,
pte_mkwrite(pte_mkdirty(
mk_pte(new_page, vma->vm_page_prot))));
flush_tlb_page(vma, address);
__free_page(page_map);
return 1;
如果旧页面是零页面,就使用memset宏把新的页面填充为0。否则,使用memcpy宏复制页面的内容。不要求一定要对零页作特殊的处理,但是特殊处理确实能够提高系统的性能,因为它使用很少的地址而保护了微处理器的硬件高速缓存。
然后,用新页面的物理地址更新页表的表项,并把新页面标记为可写和脏。最后,函数调用__free_pages( )减小对旧页面的引用计数。
1. 通过fork()建立进程,开始时只有一个页目录和一页左右的可执行页 ,于是缺页异常会频繁发生。
2. 虚拟地址映射到物理地址,只有在请页时才完成,这时要建立页表和更新页表(页表是动态建立的)。页表不可被换出,不记年龄,它们被内核中保留,只有在exit时清除。
3. 在处理页故障的过程中,因为要涉及到磁盘访问等耗时操作,因此操作系统会选择另外一个进程进入执行状态,即进行新一轮调度。