MIT_6.s081_Lab6_Copy-on-Write Fork for xv6
早期的fork()实现较为“简单粗暴”,会将父进程的物理内存完整拷贝一份,并映射到子进程的内存空间中,这种方式在很多情况下是不必要的,一部分虚拟内存是只读的,对他们进行拷贝是一种浪费,其次,由于进程有时会在调用fork函数之后立即调用exec以载入新的可执行文件,重置地址空间,之前的内存拷贝就完全失去了意义。因此可以用写时拷贝技术来对fork的实现进行优化。对于本来就是只读的虚拟页,父进程和子进程可以共享这些页,减少了拷贝的开销。但对于容易发生变化的虚拟页,如果出现了写操作,就会触发写时拷贝。
——陈海波《现代操作系统:原理与实现》
传统的fork
函数会复制数据段、堆、栈,而代码段则不会复制,而是采用映射的方式映射到父进程的代码段,这也很好解释,因为我们不需要去更改代码段的内容,也就没必要复制。
当两个进程拥有很多相同的内存数据,如果把这些数据相同的内存页在物理内存中仅存储一份,然后以只读的方式映射给两个应用程序,那么就能显著地节约物理内存资源。如下图所示:
PTE除了记录物理页号,还记录了别的信息,我们回忆实验3里面讲的,PTE中有一些标志位,用来表示虚拟页的权限的权限位。写时拷贝正是利用表示“是否可写”的权限位来实现的。
COW fork()
只为孩子创建一个页表,用户内存的 PTE 指向父级的物理页面。COW fork()
将 parent 和 child 中的所有用户 PTE 标记为不可写。当任一进程尝试写入这些 COW 页之一时,CPU 将强制发生页错误。内核页面错误处理程序检测到这种情况,为出错进程分配物理内存页面,将原始页面复制到新页面中,并修改出错进程中的相关 PTE 以引用新页面,这次使用PTE 标记为可写。当页面错误处理程序返回时,用户进程将能够写入它的页面副本。
值得注意的是:当多个用户进程的虚拟内存都指向相同的物理内存page,父进程退出的时候我们要更加小心,因为我们要判断是否能立即释放相应的物理page,因为子进程可能同样会用到这个物理内存所存储的数据。这个时候我们要引入引用计数器count
,当物理内存的引用数为0的时候,我们就能释放这个物理页表了。
值得注意的是,在linux中有一个vfork()
,可以用来解决创建子进程后又马上调用exec()
函数的情况,与fork
函数一样,vfork
函数也是用来创建子进程的。且该函数创建出的子进程与父进程共用一个地址空间。通过vfork
创建的子进程会执行完后,才到父进程执行。
我们需要修改uvmcopy()
,在复制副进程的内存到子进程的时候,不立即进行复制数据,而是建立指向原物理页的映射,并将父子两端的页表项都设置为不可写。
我们首先看到sys_fork
函数:
1 |
|
这个fork
函数如下所示,我将自己的理解写在了注释中:
1 |
|
我们先来说说进程的state
字段:
A question about the fork
function
1 |
|
英文术语zombie process源自丧尸——不死之人,隐喻子进程已死但仍然没有被回收。与正常进程不同,
kill
命令对僵尸进程无效。
在类Unix系统中,僵死进程是指完成执行,但在操作系统进程表中仍然存在其进程控制块,处于”终止状态“的进程。
下面是一个进程僵死的例子,解释在注释中:
1 |
|
在kernel/proc.c
的fork
函数中,我们会将子进程设置为RUNNABLE
状态,也就是拿到当前的就绪队列中,等待调度了,值得注意的是,在对np
的状态进行状态赋值的时候我们会上锁,为什么呢?
我们来看看kernel/proc.c
中的proc
函数,调用了uvmcopy
函数,将父进程的页表以及内存拷贝到子进程,如下所示:
1 |
|
uvmcopy
函数如下所示:
1 |
|
当我们在面试的时候很多面试官也会问malloc
的底层实现,那么我们就根据xv版本的代码来具体看看:
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!