请求调页和写时复制

请求调页

请求调页和写时复制

在创建进程的时候,一是进程结构体的创建以及插入到内核进程管理链表中,二是进程的空间是怎么括增的,因为创建进程只是创建了结构体,并没有执行代码在里面,所以怎么把执行代码加入是很重要的。

这里就介绍一下内核的做法:请求调页。

“请求调页”指的是一种动态内存分配技术,它把页框的分配推迟到不能再推迟的时候。也就是说,一直推迟到进程要访问的页不在 RAM 中时为止,由此引起一个缺页异常。

为什么使用请求调页?因为在进程开始运行的时候并不访问其地址空间中的全部地址;事实上,有一部分地址也许永远不会被进程使用。此外,进程的局部性原理保证了在程序执行的每个阶段,真正引用的进程页只有一小部分,因此临时用不着的页所在的页框可以由其他进程来使用。因此,对于全局分配(一开始就给进程分配所需要的全部页框,直到程序结束才释放这些页框)来说,请求调页是首选的,因为它增加了系统中的空闲页框的平均数,从而更好地利用空闲内存。从另一个观点来看,在 RAM 总数保持不变的情况下,请求调页从总体上能使系统有更大的吞吐量。

但是请求调页也是存在缺点的,那就是系统的额外开销。因为请求调页这一操作必须由内核完成,而这样会造成 CPU 时钟周期的浪费。

下来分析内核源码

当内核响应请求调页时,首先会检测一下缺少的这一页在哪?它有可能不在主存中,原因是进程从来都没有访问过该页,或者是该页长时间没有被访问,被内核回收了。

源码中根据两个宏定义的结果来区分这两种情况:

entry = *pte;

if (!pte_present(entry)) {

if (pte_none(entry))

return do_no_page(mm, vma, address, write_access, pte, pmd);

if (pte_file(entry)) {

return do_file_page(mm, vma, address, write_access, pte, pmd);

}

return do_swap_page(mm, vma, address,pte, pmd, entry, write_access);

}

该代码运行在 handle_pte_fault() 函数中。

其中的 pte_none() 宏定义是用来该页有没有被进程访问过,如果没有则返回 1 。

pte_file() 宏定义页是否属于非线性磁盘文件的映射,若是非线性的,则返回 1 。

当上述两种判断均 不成立时,则说明该页被进程访问过,只是该页当前被内核回收了。

下来,讨论的重点放在,进程从未访问过该页。如果是这样,内核接下来将会执行 do_no_page() 函数。该函数通过检查 vma 线性区描述符的 nopage 字段来确定这一点,如果页被映射到一个文件, nopage 字段就指向一个函数, 该函数把所缺的页从磁盘装入到 RAM 。但是,内核在这里对 nopage 字段做了判断。

因为 vma->vm_ops->nopage 字段不为 NULL ,则线性区映射了一个磁盘文件, nopage 指向装入页的函数。

如果 vma->vm_ops 或 vma->vm_ops->nopage 字段为 NULL ,则线性区没有映射磁盘文件,也就是说,它是一个匿名映射。因此,do_no_page() 调用 do_anonymous_page() 函数获得一个新的页框,执行代码如下:

if (!vma->vm_ops || !vma->vm_ops->nopage)

return do_anonymous_page(mm, vma, page_table, pmd, write_access, address);

do_anonymous_page() 函数分别处理写请求和读请求,执行代码如下:

if (write_access) {

pte_unmap(page_table);

spin_unlock(&mm->page_table_lock);

page = alloc_page(GFP_HIGHUSER | __GFP_ZERO);

spin_lock(&mm->page_table_lock);

page_table = pte_offset_map(pwd, addr);

mm->res++;

entry = maybe_mkwriter(pte_mkdirty(mk_pte(page, vma->vm_page_prot)), vma);

lru_cache_add_active(page);

SetPageReference(page);

set_pte(page_table, entry);

pte_unmap(page_table);

spin_unlock(&mm->page_table_lock);

return VM_FAULT_MINOR;

}

pte_unmap() 宏的第一次执行是释放一种临时的内核映射,它映射了在调用 handle_pte_fault() 函数之前由 pte_offset_map 所建立的页表项的高端内存物理地址。 pte_offset_map 和 pte_unmap 这对宏获取和释放同一个临时内核映射。临时内核映射必须在调用 alloc_page() 之前释放,因为这个函数可能阻塞当前进程。

函数递增内存描述的 rss 字段以记录分配给进程的页框总数,相应的页表项设置为页框的物理地址,页表框被标记为即脏又可写。

lru_cache_add_active() 函数把新页框插入与交换相关的数据结构中。

处理读访问时,页的内容是无关紧要的,因为进程第一次对它访问。给进程一个填充为0的页要比给它一个由其他进程填充了信息的旧页更为安全。Linux在请求调页方面做得更深入一些。没有必要立即给进程分配一个填充为0的新页框,由于我们也可以给它一个现有的称为零页的页,这样就可以进一步推迟页框的分配。零页在内核初始化期间被静态分配,并存放在empty_zero_page变量中(长为4096字节的数组,并用0填充)。

因此用零页的物理地址设置页表项:

entry = pte_wrprotect(mk_pte(virt_to_page(empty_zero_page), vma->vm_page_prot));

set_pte(page_table, entry);

spin_unlock(&mm->page_table_lock);

return VM_FAULT_MINOR;

由于这个页被标记为不可写的,因此如果进程试图写这个页,则写时复制机制被激活。当且仅当在这个时候,进程才获得一个属于自己的页并对它进行写操作。

写时复制

请求调页和写时复制

写时复制(copy on write, cow)。这种思想相当简单:父进程和子进程共享页框而不是复制页框。然而,只要页框被共享,它们就不能被修改。无论父进程还是子进程何时试图写一个共享的页框,就产生一个异常,这是内核就把这个页复制到一个新的页框并标记为可写。原来的页框仍然是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页框的唯一属主,如果是,就把这个页框标记为对这个进程是可写的。

页描述符的 _count 字段用于跟踪共享相应页框的进程数目。只要进程释放一个页框或者在它上面执行写时复制,它的 __count 字段就减小;只有当 __count 变为 -1 时,这个页框才被释放。

现在我们讲述 Linux 怎样实现写时复制。当 handle_pte_fault() 确定缺页异常是由访问内存中现有的一个页而引起时,它执行以下指令:

if (pte_present(entry)) {

if (write_access) {

if (!pte_write(entry))

return do_wp_page(mm, vma, address, pte, pmd, entry);

entry = pte_mkdirty(entry);

}

entry = pte_mkyoung(entry);

set_pte(pte, entry);

flush_tlb_page(vma, address);

pte_unmap(pte);

spin_unlock(&mm->page_table_lock);

return VM_FAULT_MINOR;

}

handle_pte_fault() 函数是与体系结构无关的:它考虑任何违背页访问权限的可能。然而,在 80x86 体系结构上,如果页是存在的,那么,范文权限是写允许的而页框是写保护的。因此,总是要调用 do_wp_page() 函数。

do_wp_page() 函数首先获取与缺页异常相关的页框描述符(缺页表项对应的页框)。接下来,函数确定页的复制是否真正必要。如果仅有一个进程拥有这个页,那么,写时复制就不必应用,且该进程应当自由的写该页。具体来说,函数读取页描述符的 _count 字段:如果它等于 0 (只有一个所有者),写时复制就不必进行。实际上,检查要稍微复杂些,因为当页插入到交换高速缓存,并且当设置了页描述符的 PG_private 标志时,_count 字段也增加。不过,当写时复制不进行时,就把该页框标记为可写的,一面试图写时引起进一步的缺页异常:

set_pte(page_table, maybe_mkwrite(pte_mkyoung(pte_mkdirty(pte)), vma));

flush_tlb_page(vma, address);

pte_unmap(page_table);

spin_unlock(&mm->page_table_lock);

return VM_FAULT_MINOR;

如果两个或多个进程通过写时复制共享页框,那么函数就把旧页框(old_page)的内容复制到新分配的页框(new_page)中。为了避免竞争条件,在开始复制操作前调用 get_page() 把 old_page 的使用计数加1:

old_page = pte_page(pte);

pte_unmap(page_table);

get_page(old_page);

spin_unlock(&mm->page_table_lock);

if (old_page == virt_to_page(empty_zero_page))

new_page = alloc_page(GFP_HIGHUSER | __GFP_ZERO);

} else {

new_page = alloc_page(GFP_HIGHUSER);

vfrom = kmap_atomic(old_page, KM_USER0);

vto = kmap_atomic(new_page, KM_USER1);

copy_page(vto, vfrom);

kunmap_atomic(vfrom, KM_USER0);

kunmap_atomic(vto, KM_USER1);

}

如果旧页框是零页,就在分配新的页框时把 __GFP_ZERO 标志置为 0 。否则,使用 copy_page() 宏复制页框的内容。不要求一定要对零页做特殊处理,但是特殊处理确实能够提高系统的性能,因为他减少地址引用而保护了微处理器的硬件告诉缓存。

因为页框的分配可能阻塞进程,因此,函数检查自从函数开始执行以来是否已经修改了页表项(pte 和 *page_table 具有不同的值)。如果已经修改,那么新的页框被释放, old_page 的使用计数器被减少(取消以前的增加),函数结束。

如果所有的事情看起来进展顺利,那么,新页框的物理地址最终被写进页表项,且使相应的 TLB 寄存器无效:

spin_lock(&mm->pagfe_table_lock);

entry = maybe_mkwrite(pte_mkdirty(mk_pte(new_page, vma->vm_pae_prot)), vma);

set_pte(page_table, entry);

flush_tlb_page(vma, address);

lru_cache_add_active(new_page);

pte_unmap(page_table);

spin_unlock(&mm->page_table_lock);

lru_cache_add_active() 函数把新页框插入到与交换相关的数据结构中。

最后, do_wp_page() 把 old_page 的使用计数器减少两次。第一次的减少是取消复制页框内容之前进行的安全性增加;第二次的减少是反映当前进程不再拥有该页框这一事实。


分享到:


相關文章: