Lab4 Report

Lab4 Report

思考题

Thinking 4.1

  • 内核在保存现场的时候是如何避免破坏通用寄存器的?
    • 内核使用 SAVE_ALL 将用户进程上下文环境中通用寄存器的值存在结构体 TrapFrame 中,TrapFrame 结构体在内核栈中,系统调用结束后又会重新将这些值放回通用寄存器。
  • 系统陷入内核调用后可以直接从当时的 \(a0-\)a3 参数寄存器中得到用户调用 msyscall 留下的信息吗?
    • 能,因为调用函数时默认前四个参数传入 \(a0-\)a3 寄存器。但在内核态中可能使用这些寄存器进行一些操作计算,此时寄存器原有值被改变,因此再次以这些参数调用其他函数时需要重新以 sp 为基地址,按相应偏移从用户栈中取用这四个寄存器值。
  • 我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样的参数的?
    • 在调用 syscall 前都将前四个参数按顺序放入 \(a0-\)a3 寄存器,后两个参数按顺序存入内核栈中的相同位置,内核处理这些参数时也按照这个顺序读取。
  • 内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?
    • 对于 EPC 加 4,即返回时从进入内核态的陷入指令的下一条指令开始执行,即 syscall 后面的 jr ra 指令,避免返回时又再次执行刚才执行过的陷入指令。

Thinking 4.2

  • 思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
    • 因为这个 envid 对应的进程控制块可能并不存在,没有这个判断可能会导致最终返回的进程控制块是错误的

Thinking 4.3

  • 思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件 中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与 envid2env() 函数的行为进行解释。

    •   u_int mkenvid(struct Env *e) {
            static u_int i = 0;
            return ((++i) << (1 + LOG2NENV)) | (e - envs);
        }
    • 因为 i 大于等于 1,envs 为 env 数组首地址,e 的地址始终大于 envs 的地址,所以向左平移量也为正数,所以返回值不会为 0

Thinking 4.4

  • 关于 fork 函数的两个返回值,下面说法正确的是:
    • C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值

Thinking 4.5

  • 我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考。
    • 我们需要将 0 到 USTACKTOP 之间的用户空间进行映射,具体在 fork 函数中进行体现。在这部分地址空间,其中一部分是内核,另一部分是所有用户进程共享的空间,模板页表的部分不需要映射,因为在 env_init 中会进行对于模板页表页表项的映射,以及进程控制块的映射。

Thinking 4.6

  • 在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考 user/include/lib.h 中的相关定义,思考并回答这几个问题:
    • vpt 和 vpd 的作用是什么?怎样使用它们?
      • vpt 和 vpd 分别指向的是映射在用户空间中的所有页表和页目录的指针,以下是它们的定义,通过索引获取某一页表或某一页目录项的内容。 >#define vpt ((volatile Pte *)UVPT)
        >#define vpd ((volatile Pde*)(UVPT + (PDX(UVPT) << PGSHIFT)))
    • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
      • 在 env_vm_setup 函数中,Step 3: Map its own page table at 'UVPT' with readonly permission.As a result, user programs can read its page table through 'UVPT'。即页目录自映射保证了可以通过访问 UVPT 访问页目录即 vpd,同时访问 UVPT 可以访问所有页表。
    • 它们是如何体现自映射设计的?
      • 在 #define vpd ((volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT))) 就体现了页目录自映射的设计。
    • 进程能够通过这种方式来修改自己的页表项吗?
      • 不能修改,因为这里设置页面权限的是只读的。

Thinking 4.7

  • 在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe 运行现场的过程,请思考并回答这几个问题:
    • 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重入”?
      • 中断重入可能是因为在处理缺页中断时又发生了中断。
    • 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
      • 因为 tlb_mod 的异常处理函数在用户态执行,需要知道异常发生时的状态,最后需要通过 tf 保存好的现场,使用系统调用 syscall_set_trapframe 恢复事先保存好的现场,其中也包括 sp 和 PC 寄存器的值,使得用户程序恢复执行。

Thinking 4.8

  • 在用户态处理页写入异常,相比于在内核态处理有什么优势?
    • 内核态处理失误产生的影响较大,可能会使得操作系统崩溃。此外,用户状态下不能得到一些在内核状态才有的权限,避免改变不必要的内存空间。同时微内核的模式下,用户态进行新页面的分配映射也更加灵活方便。

Thinking 4.9

  • 为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
    • 以处理在 alloc 过程中发生的缺页中断。
  • 如果放置在写时复制保护机制完成之后会有怎样的效果?
    • 由于无法处理缺页中断错误,写时复制保护机制不能执行。

难点分析

Exercise 4.2

  • do_syscall 中提前设置 tf->cp0_epc += 4; 使 syscall 处理完毕后返回到原程序的下一条。
  • u_int arg4 = *(int*)(tf->regs[29] + 16);
    u_int arg5 = *(int*)(tf->regs[29] + 20); 注意 arg4,arg5 需要从用户栈中 sp+16,sp+20 处获取。注意地址与内容转换

  • tf->regs[2] = func(arg1,arg2,arg3,arg4,arg5); 注意要使系统调用的返回值存储在寄存器 v0 中。

Exercise 4.3

  • envid == 0 表示找当前的 curenv,取到之后直接返回即可
  • mkenvid 时,其低 10 位为 (e - envs) 的 e 在 envs 中的偏移量,所以用 ENVX(envid) 即 ((envid) & (NENV - 1)) 可取得 envid 的低 10 位,即偏移量,从 envs 数组中即可找到目标进程的 env 结构体。

envid2env 中 checkperm

  • 在需要修改 envid 对应 env 时,需要检查在 curenv 下有无权限修改,try(envid2env(envid, &env, 1))

Exercise 4.8

  • sys_ipc_recv 最后设置 ((struct Trapframe *)KSTACKTOP - 1)->regs[2] = 0; 表示将用户现场的 v0 寄存器设置为 0。这个设置的意义是:当发送进程唤醒本进程时,进程将从 syscall_ipc_recv 中返回,函数的返回值设置为 0,表示进程成功接收到了数据

Exercise 4.9

  • sys_exofork 创建子进程中需要复制当前父进程栈帧,用 memcpy 不可用 e->env_tf = *((struct Trapframe*)KSTACKTOP - 1); 复制栈帧,不是指向那个值

Exercise 4.10

  • duppage 中先通过页号 vpn 乘页面大小 BY2PG 得到地址
  • 再用 perm = vpt[vpn] & 0xfff; //vpt:页表中页的数组,取第 vpn 页,取低 12 位,即页的权限位
  • 如果已经不可写或共享、有写时复制要求,则直接分享给子进程,否则需要更改自身权限为 PTE_COW 并取消 PTE_D
  • 要注意在修改页面控制权限时要先给子进程映射再给自己映射。以防止父进程的一页在复制之前已被写入,复制时会触发 TLB_MOD,而使父进程虚拟地址对应页面触发写时复制,映射到了一个新的物理页,然后才映射到子进程,页面错误。

Exercise 4.11

  • do_tlb_mod 处理 TLB 异常时,需要将当前栈指针指向用户异常处理栈并将 Trapframe 压入异常处理栈,并将 sp 栈指针设置为当前 Trapframe 的地址,使得可能出现的异常重入完毕后返回能够恢复发生时的状态
  • 如果用户已经注册了 TLB Mod 异常处理函数,则将指向当前 Trapframe 的指针放入 a0,并在栈上为该参数分配空间,并且将 tf->cp0_epc 设置为 curenv->env_user_tlb_mod_entry,在返回后,用户态的异常处理函数将会从该地址开始执行。

Exercise 4.13

  • cow_entry COW 写入异常中,首先需要检查权限 perm = vpt[VPN(va)] & 0xfff; 是否为 PTE_COW
  • 然后进行写时复制后,两个页面已独立,权限即可改为 PTE_D 可写,取消 PTE_COW
  • 然后在物理内存中分配出特意为 COW 保留的页面 UCOW,映射给对其到 BY2PG 的 va,然后再释放 UCOW 为下次 COW 使用

实验体会

  • 内核态与用户态的函数多层调用需要理清顺序以及数据传递依赖
  • fork 时需要注意一些细节的逻辑顺序
  • 写时复制的权限判断与调整需要全面分析
  • 系统调用的流程图 图 1
  • fork 的流程图: 图 2