翼度科技»论坛 云主机 LINUX 查看内容

mit6.828笔记 - lab4 Part B:写时复制Fork

8

主题

8

帖子

24

积分

新手上路

Rank: 1

积分
24
Part B  Copy-on-Write Fork

Unix 提供 fork() 系统调用作为主要的进程创建基元。fork()系统调用复制调用进程(父进程)的地址空间,创建一个新进程(子进程)。
不过,在调用 fork() 之后,子进程往往会立即调用 exec(),用新程序替换子进程的内存。例如,shell 通常就是这么做的。在这种情况下,拷贝父进程地址空间所花费的时间基本上是白费的,因为子进程在调用 exec() 之前几乎不会使用它的内存。
因此,后来的 Unix 版本利用虚拟内存硬件,允许父进程和子进程共享映射到各自地址空间的内存,直到其中一个进程实际修改了内存。这种技术被称为 "写时拷贝"(copy-on-write)。 为此,内核会在 fork() 时将父进程的地址空间映射复制到子进程,而不是映射页的内容,同时将现在共享的页标记为只读。 当两个进程中的一个试图写入其中一个共享页面时,该进程就会发生页面错误。此时,Unix 内核会意识到该页面实际上是一个 "虚拟 "或 "写时拷贝 "副本,因此会为发生故障的进程创建一个新的、私有的、可写的页面副本。 这样,单个页面的内容在实际写入之前不会被复制。这种优化使得在子进程中执行 fork() 之后执行 exec() 的成本大大降低:在调用 exec() 之前,子进程可能只需要复制一个页面(堆栈的当前页面)。
我们接下来的目标就是实现写时复制的fork
用户级页面故障处理 User-level page fault handling

为了实现用户级的写时复制 fork(),exercise7做的syscall外,我们还需要实现一些基础设施,即用户级页面故障处理。
注意啊,是用户级的页面故障处理,在 lab3 中,缺页故障的处理函数使用的是自带的简易实现,它是由 trap() 调用的,这个过程显然是在内核态完成的。手册中描述如下:
内核需要跟踪的信息太多了。与传统的 Unix 方法不同,你将在用户空间中决定如何处理每个页面故障,因为在用户空间中,错误的破坏性较小。 这种设计的另一个好处是,允许程序非常灵活地定义其内存区域;稍后在映射和访问基于磁盘的文件系统上的文件时,我们将使用用户级页面故障处理方法。
做到这里一定有一堆疑问,所以可以看一下 part B后面的小标题,实际上 JOS 实现用户级页面故障的思路是:

  • 增加一个系统调用,sys_env_set_pgfault_upcall ,允许用户进程指定自己的页面故障程序
  • 在 lab3 的 page_fault_handler 的基础上修改,检查异常来源是否是用户态,如是,则调用上一步部指定的页面故障程序
设置页面故障处理程序

为了处理自己的页面故障,用户环境需要向 JOS 内核注册一个页面故障处理程序入口点。用户环境通过新的 sys_env_set_pgfault_upcall 系统调用来注册其页面故障入口点。我们在 Env 结构中添加了一个新成员 env_pgfault_upcall,以记录这一信息。
练习8

练习 8. 执行 sys_env_set_pgfault_upcall 系统调用。由于这是一个 "危险 "的系统调用,因此在查找目标环境的环境 ID 时一定要启用权限检查。
实现 sys_env_set_upcall 系统调用。
  1. // 通过修改相应结构体 Env 的 “env_pgfault_upcall ”字段,
  2. // 为 “envid ”设置页面故障上调。
  3. // 当 “envid ”导致页面故障时,
  4. // 内核会将故障记录推送到异常堆栈,然后分支到 “func”。
  5. //
  6. // 成功时返回 0,错误时返回 <0。 错误包括
  7. // -E_BAD_ENV 如果环境 envid 当前不存在,或者调用者没有权限更改 envid。
  8. static int
  9. sys_env_set_pgfault_upcall(envid_t envid, void *func)
  10. {
  11.         // LAB 4: Your code here.
  12.         // panic("sys_env_set_pgfault_upcall not implemented");
  13.         struct Env * e;
  14.         if(envid2env(envid, &e, 1)<0)        // 检查envid是否有误
  15.                 return -E_BAD_ENV ;
  16.         e->env_pgfault_upcall = func;        //        设置该环境page fault的handler
  17.         return 0;
  18. }
复制代码
用户模式页面故障入口点

用户级页面故障管理还有一个问题,那就是谁负责初始化、维护用户异常栈。我们知道,内核会帮用户将trap-time时的状态保存到用户异常栈上。
但实际上,这个用户异常栈,从始至终都没有被初始化过。
对于内核而言,每个用户都有一个默认的页面故障处理程序,那就是打印错误地址。然后退出。
用户页面故障是个自选的功能,JOS让需要自定义处理的进程,自己初始化、维护用户异常栈。内核至负责必要的传值工作,即 page_fault_handler。而page_fault_handler 最后直接使用 env_run 将控制权归还用户了,这意味着,用户需要自己销毁内核传到用户异常栈上的数据。并且自己恢复到 trap-time 状态。
实际上,这一步还挺不容易的,这里存在的困难在于,我们要让所有寄存器保持trap-time state,并跳转回去。

  • 我们不能调用 "jmp xxx",因为这要求我们将地址加载到某个寄存器中,而这会使得该寄存器无法保持trap-time state
  • 我们也不能从异常堆栈调用 "ret",因为如果这样做,%esp 就不是trap-time 的值。
因此,手册给出的答题思路是:

  • 从用户异常栈上读取 trap-time 的 sp
  • 将 trap-time 的 eip 推送到 trap-time 的stack (即保存到 trap-time 的 sp 所指位置)
  • 从 用户异常栈上的utrapframe,恢复寄存器状态(跳过 eip)
  • 恢复 esp (切换回 trap-time 的sp),由于第二步的操作,此时esp所指位置是 trap-time的eip
  • ret,将 esp 所指的值弹给 PC。
接下来练习10 完成恢复 trap-time state,在练习11 完成用户异常栈的初始化
Exercise 10

练习 10. 实现 lib/pfentry.S 中的 _pgfault_upcall 例程。有趣的部分是返回到用户代码中引起页面故障的原始点。你将直接返回到那里,而无需返回内核。困难的部分是同时切换堆栈和重新加载 EIP。
_pgfault_upcall
  1.                     <-- UXSTACKTOP
  2. trap-time esp
  3. trap-time eflags
  4. trap-time eip
  5. trap-time eax       start of struct PushRegs
  6. trap-time ecx
  7. trap-time edx
  8. trap-time ebx
  9. trap-time esp
  10. trap-time ebp
  11. trap-time esi
  12. trap-time edi       end of struct PushRegs
  13. tf_err (error code)
  14. fault_va            <-- %esp when handler is run
复制代码
结果是没有出发 handler ,反而是 user_mem_assert 输出了。

faultallocbad 和 faultalloc 的 handler 是一样的,区别在于使用 sys_cputs 打印。sys_cputs 第一件工作就是用 user_mem_assert 确认 0xDEADBEEF 是否使用,还没有机会触发页错误。

by the way :user_mem_assert的检查方式是查页表,看PTE是否合规。这个过程是不会发生页错误的。
实现写时复制的fork

现在,我们终于完全拥有了完全在用户空间实现写时复制 fork() 的内核设施。
在 lib/fork.c 中 fork() 已经提供了一个骨架。与 dumbfork() 一样,fork() 也会创建一个新环境,然后扫描父环境的整个地址空间,并在子环境中设置相应的页面映射。
不同之处在于,dumbfork() 将每个页面逐个字节的复制。而 fork() 最初只会复制页面映射。
只有当其中一个环境试图写入页面时,fork() 才会复制每个页面。
fork的基本框架如下:

  • fork函数:负责复制自身,并调用duppage复制页映射,设置页面故障handler
  • duppage:负责复制页映射的具体工作
  • pgfault:页故障handler,当发生对写时复制页进行写操作时,将页面进行实际复制。
fork的具体流程:

  • 父级程序会使用上面实现的 set_pgfault_handler() 函数安装 pgfault() 作为用户级页面故障处理程序。
  • 父环境调用 sys_exofork(),创建子环境。
  • 父环境将[0~UTOP]的地址空间中所有“可写PTE_W”、“写时复制PTE_COW”页面的映射,通过 duppage 复制到子环境中,然后将写时复制页面重新映射到自己的地址空间(为何?不太清楚)。
  • 对于 [UXSTACKTOP-PGSIZE, UXSTACKTOP] 的部分则是申请新的页面。
  • 对于只读页面直接保持原权限复制即可。
发生页错误时,就会触发 pgfault() 然后将PTE_COW的页面用新页替换。

练习12 完成fork, duppage, pgfault
  1. void
  2. page_fault_handler(struct Trapframe *tf)
  3. {
  4.         uint32_t fault_va;
  5.         // Read processor's CR2 register to find the faulting address
  6.         fault_va = rcr2();        //获取发生页错误的地址
  7.         // Handle kernel-mode page faults.
  8.         // LAB 3: Your code here.
  9.         if ((tf->tf_cs & 3) == 0)
  10.                 panic("page_fault_handler():page fault in kernel mode!\n");
  11.         // 我们已经处理过内核模式异常,所以如果我们到达这里,页面故障就发生在用户模式下。
  12.         // 调用环境的页面故障上调(如果有的话)。
  13.         // 在用户异常堆栈(低于 UXSTACKTOP)上建立一个页面故障堆栈框架,
  14.         // 然后分支到 curenv->env_pgfault_upcall。
  15.         //
  16.         // 页面故障向上调用可能会导致另一个页面故障,
  17.         // 在这种情况下,我们会递归分支到页面故障向上调用,
  18.         // 在用户异常堆栈顶部推送另一个页面故障堆栈框架。
  19.         //
  20.         // 从页面故障返回的代码(lib/pfentry.S)在陷阱时间栈的顶部有一个字的抓取空间,
  21.         // 这对我们来说很方便,可以更容易地恢复 eip/esp。
  22.         // 在非递归情况下,我们不必担心这个问题,因为常规用户栈的顶部是空闲的。
  23.         // 在递归情况下,这意味着我们必须在当前的异常栈顶和新的栈帧之间多留一个字,
  24.         // 因为异常栈 _ 就是陷阱时间栈。
  25.         //
  26.         // 如果没有向上调用页面故障,环境没有为其异常堆栈分配页面或无法写入页面,
  27.         // 或者异常堆栈溢出,则销毁导致故障的环境。
  28.         // 请注意,本级脚本假定您将首先检查页面故障上调,
  29.         // 如果没有,则打印下面的 “用户故障 va ”信息。
  30.         // 其余三个检查可以合并为一个测试。
  31.         //
  32.         // 提示:
  33.         // user_mem_assert() 和 env_run() 在这里很有用。
  34.         // 要改变用户环境的运行方式,请修改'curenv->env_tf' // ('tf'变量的值为 0)。
  35.         // tf'变量指向'curenv->env_tf')。
  36.         // LAB 4: Your code here.
  37.         //检查是否有处理页错误的handler
  38.         if(curenv->env_pgfault_upcall)
  39.         {
  40.                 uintptr_t stacktop = UXSTACKTOP;
  41.                 //检查是否在递归调用handler
  42.                 if(tf->tf_esp > UXSTACKTOP-PGSIZE && tf->tf_esp < UXSTACKTOP)
  43.                         stacktop = tf->tf_esp;
  44.                
  45.                 //预留32位字的scratch space
  46.                 uint32_t size = sizeof(struct UTrapframe) + sizeof(uint32_t);
  47.                 //检查是否有权限读写exception stack
  48.                 user_mem_assert(curenv, (void *)(stacktop-size), size, PTE_U|PTE_W);
  49.                 //填充UTrapframe
  50.                 struct UTrapframe *utf = (struct UTrapframe *)(stacktop-size);
  51.                 utf->utf_fault_va = fault_va;
  52.                 utf->utf_err = tf->tf_err;
  53.                 utf->utf_regs = tf->tf_regs;
  54.                 utf->utf_eip = tf->tf_eip;
  55.                 utf->utf_eflags = tf->tf_eflags;
  56.                 utf->utf_esp = tf->tf_esp;
  57.                 //设置eip和esp,运行handler
  58.                 curenv->env_tf.tf_eip = (uintptr_t)curenv->env_pgfault_upcall;
  59.                 curenv->env_tf.tf_esp = (uintptr_t)utf;
  60.                 env_run(curenv);
  61.         }
  62.        
  63.         // Destroy the environment that caused the fault.
  64.         cprintf("[%08x] user fault va %08x ip %08x\n",
  65.                 curenv->env_id, fault_va, tf->tf_eip);
  66.         print_trapframe(tf);
  67.         env_destroy(curenv);
  68. }
复制代码
fork

注意理解 uvpt 和 uvpd
uvpt 就是 UVPT,是虚拟地址,范围是PTSIZE,4mb,使用PGNUM宏搜索,取出pte的虚拟地址。
uvpd 是 pgdir 所在的虚拟地址,范围是一个内存页,4kb,使用PDE宏搜索,取出pde的虚拟地址。。
然后就是关于 PFTEMP,前文中有测试过用户的dupapge,我们需要这个区域实现进程间的页面复制。
  1. // 每当我们在用户空间引发页面故障时,
  2. // 我们都会要求内核将我们重定向到这里
  3. //(参见 pgfault.c 中对 sys_set_pgfault_handler 的调用)。
  4. //
  5. // 当页面故障实际发生时,如果我们尚未进入用户异常堆栈,
  6. // 内核会将我们的 ESP 切换到用户异常堆栈,
  7. // 然后将一个 UTrapframe 推入用户异常堆栈:
  8. //
  9. // 陷阱时 esp
  10. // 陷阱时 eflags
  11. // 陷阱时 eip
  12. // utf_regs.reg_eax
  13. // ...
  14. // utf_regs.reg_esi
  15. // utf_regs.reg_edi
  16. // utf_err(错误代码)
  17. // utf_fault_va <-- %esp
  18. //
  19. // 如果这是一个递归故障,
  20. // 内核将在陷阱时 esp 的上方为我们保留一个空白字,
  21. // 以便在我们解除递归调用时进行从头处理。
  22. //
  23. // 然后,我们在 C 代码中调用相应的页面故障处理程序,
  24. // 该处理程序由全局变量“_pgfault_handler ”指向。
  25. .text
  26. .globl _pgfault_upcall
  27. _pgfault_upcall:
  28.         // Call the C page fault handler.
  29.         pushl %esp                        // function argument: pointer to UTF
  30.         movl _pgfault_handler, %eax
  31.         call *%eax
  32.         addl $4, %esp                        // pop function argument
  33.        
  34.         // Now the C page fault handler has returned and you must return
  35.         // to the trap time state.
  36.         // Push trap-time %eip onto the trap-time stack.
  37.         //
  38.         // Explanation:
  39.         //   We must prepare the trap-time stack for our eventual return to
  40.         //   re-execute the instruction that faulted.
  41.         //   Unfortunately, we can't return directly from the exception stack:
  42.         //   We can't call 'jmp', since that requires that we load the address
  43.         //   into a register, and all registers must have their trap-time
  44.         //   values after the return.
  45.         //   We can't call 'ret' from the exception stack either, since if we
  46.         //   did, %esp would have the wrong value.
  47.         //   So instead, we push the trap-time %eip onto the *trap-time* stack!
  48.         //   Below we'll switch to that stack and call 'ret', which will
  49.         //   restore %eip to its pre-fault value.
  50.         //
  51.         //   In the case of a recursive fault on the exception stack,
  52.         //   note that the word we're pushing now will fit in the
  53.         //   blank word that the kernel reserved for us.
  54.         //
  55.         // Throughout the remaining code, think carefully about what
  56.         // registers are available for intermediate calculations.  You
  57.         // may find that you have to rearrange your code in non-obvious
  58.         // ways as registers become unavailable as scratch space.
  59.         //
  60.         // LAB 4: Your code here.
  61.         addl $8, %esp                                        // 清除 fault_va 和 error code
  62.         movl 32(%esp), %eax                                // 取 trap-time-eip 到 eax
  63.         movl 40(%esp), %edx                                // 取 trap-time-esp 到 edx
  64.         subl $4, %edx                                        // 在 trap-time的栈上开辟4字节用于存储 trap-time-eip
  65.         movl %eax, (%edx)                                 // 将 trap-time-eip 保存到 trap-time-esp
  66.         movl %edx, 40(%esp)                                // 将修改后的trap-time esp保存回栈上
  67.        
  68.         // Restore the trap-time registers.  After you do this, you
  69.         // can no longer modify any general-purpose registers.
  70.         // LAB 4: Your code here.
  71.         popal                                                        // 恢复寄存器
  72.         // Restore eflags from the stack.  After you do this, you can
  73.         // no longer use arithmetic operations or anything else that
  74.         // modifies eflags.
  75.         // LAB 4: Your code here.
  76.         addl $4, %esp                                        // 跳过 eip
  77.         popfl                                                        // 恢复 eflags
  78.         // Switch back to the adjusted trap-time stack.
  79.         // LAB 4: Your code here.
  80.         popl %esp                                                // 切换会 trap-time栈
  81.         // Return to re-execute the instruction that faulted.
  82.         // LAB 4: Your code here.
  83.         ret                                                                // 回到 trap-time的指令
复制代码
Part B 结束

来源:https://www.cnblogs.com/toso/p/18202341
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
来自手机

举报 回复 使用道具