操作系统 Lab4:系统调用与FORK
一、思考题
Thinking 4.1
内核在保存现场的时候是如何避免破坏通用寄存器的?
move k0,sp
:先把通用寄存器的sp复制到$k0sw k0,TF_REG29(sp)、sw $2,TF_REG2(sp)
:保存现场需要使用$v0作为协寄存器到内存的中转寄存器,写到内存时需要$sp,所以在正式保存协寄存器和通用寄存器前先保存这两个寄存器
系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用msyscall 留下的信息吗?
可以。从用户函数syscall_*()
到内核函数sys_*()
时,$a1-$a3未改变,$a0在handle_sys()
的时候被修改为内核函数的地址,但在内核函数sys_*()
仅为占位符,不会被用到。
我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时
同样的参数的?
- 用户调用时的参数:用户进程的寄存器现场(保存在了内核栈的TF_4-TF_7)的$a0-$a3;用户栈(栈指针为用户现场的sp)的参数a4a5
- 把上面两部分参数分别拷贝至内核现场寄存器$a0-$a3和内核栈。
内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是?
- 返回值存入$v0
- PC+4,跳转到下一条指令。如果是延迟槽指令异常,PC不变。
Thinking 4.2
请回顾 lib/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与 envid2env() 函数的行为进行解释。
mkenvid()
有有效位,所以不会返回0envid2env()
的envid为0时返回curenv。- 由于curenv为内核态的变量,用户态不能获取curenv的envid,所以用0代表curenv->envid。
- 目的是方便用户进程调用syscall_*()时把当前进程的envid作为参数传给内核函数,即方便用户态在内核变量不可见的情况下调用内核接口。
Thinking 4.3
子进程完全按照 fork() 之后父进程的代码执行,说明了什么?
fork()后子进程的data段和text段与父进程相同
但是子进程却没有执行 fork() 之前父进程的代码,又说明了什么?
PC从fork后的指令开始执行,子进程恢复到的上下文位置是fork函数。
Thinking 4.4
关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
C
Thinking 4.5
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合本章的后续描述、mm/pmap.c中 mips_vm_init 函数进行的页面映射以及 include/mmu.h 里的内存布局图进行思考。
- USTACKTOP以下的应该映射。
- USTACKTOP到UTOP之间的 user exception stack 是用来进行页写入异常的,不会在处理COW异常时调用fork(),所以 user exception stack 这一页不需要共享。
- USTACKTOP到UTOP之间的 invalid memory 是为处理页写入异常时做缓冲区用的,所以同理也不需要共享。
- UTOP以上页面的内存与页表是所有进程共享的,且用户进程无权限访问,不需要做父子进程间的duppage。
Thinking 4.6
在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个“指针的指针”,请参考 user/entry.S 和 include/mmu.h 中的相关实现,思考并回答这几个问题:
• vpt 和 vpd 的作用是什么?怎样使用它们?
• 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
• 它们是如何体现自映射设计的?
• 进程能够通过这种方式来修改自己的页表项吗?
- 作用:在用户态下通过访问进程自己的物理内存获取用户页的页目录项页表项的perm,用于duppage根据不同的perm类型在父子进程间执行不同的物理页映射
- mmu.h 中,声明vpt和vpd的类型:
extern volatile Pte* vpt[];
extern volatile Pde* vpd[];
- 使用:
- vpd是类型为页表项指针Pde *数组,即int *的数组;vpt同理。
- 以vpt举例,vpt是Pte *数组指针,(*vpt)代表第一个pte*,也就是第一个页表项的地址。
- 第i个页表项:*((*vpt)+i),也就是(*vpt)[i]
- 由于用户进程下的系统调用的虚拟内存管理的函数传入的pgdir均为env结构体的kuseg的进程页目录,并且
env_setup_vm()时
把页目录进行自映射e->env_pgdir[PDX(UVPT)] = e->env_cr3
,所以实现了用户进程的虚拟内存管理,可以通过两级页表机制访问 - user/entry.S文件中,初始化了vpt和vpd的地址,在kuseg范围:
vpt:
.word UVPT
.globl vpd
vpd:
.word (UVPT+(UVPT>>12)*4)
此处也体现了自映射设计。kuseg范围内只给VPT留出了1024*BY2PG的大小,没有给VPD设置额外BY2PG。
- 可以,因为在pmap.c中实现的虚拟内存机制,给页表项和页目录项的perm均为可写。
Thinking 4.7
page_fault_handler 函数中,你可能注意到了一个向异常处理栈复制 Trapframe 运行现场的过程,请思考并回答这几个问题:
• 这里实现了一个支持类似于“中断重入”的机制,而在什么时候会出现这种“中断重入”?
• 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
- 当出现COW异常时,需要使用用户态的系统调用发生中断,即中断重入
- 由于处理COW异常时调用的
handle_mod()
函数把epc改为用户态的异常处理函数__asm_pgfault_handler
(env结构体的env_pgfault_handler域),退出内核中断后跳转到epc所在的用户态的异常处理函数。由于用户态把异常处理完毕后仍然在用户态恢复现场,所以此时要把内核保存的现场保存在用户空间的用户异常栈。
Thinking 4.8
到这里我们大概知道了这是一个由用户程序处理并由用户程序自身来恢复运行现场的过程,请思考并回答以下几个问题:
• 在用户态处理页写入异常,相比于在内核态处理有什么优势?
• 从通用寄存器的用途角度讨论,在可能被中断的用户态下进行现场的恢复,要如何做到不破坏现场中的通用寄存器?
- 解放内核,不用内核执行大量的页面拷贝工作。
- 使用存放函数调用返回值的$v0, $v1恢复非通用寄存器,之后通过sp恢复通用寄存器,最后恢复$sp
Thinking 4.9
• 为什么需要将 set_pgfault_handler 的调用放置在 syscall_env_alloc 之前?
• 如果放置在写时复制保护机制完成之后会有怎样的效果?
• 子进程是否需要对在 entry.S 定义的字 __pgfault_handler 赋值?
syscall_env_alloc()
返回后父子进程各自执行自己的进程,子进程需要修改entry.S中定义的env指针,涉及到对COW页面的修改,会触发COW写入异常,COW中断的处理机制依赖于set_pgfault_handler。所以set_pgfault_handler
要放在syscall_env_alloc()
之前。- 写时复制保护机制会把entry.S中定义的__pgfault_handler变量所在的页面被COW保护,而
set_pgfault_handler()
需要写__pgfault_handler变量。此时会涉及到对COW页面的修改,会触发COW写入异常,COW中断的处理机制依赖于set_pgfault_handler。所以set_pgfault_handler
要放在写时复制保护机制duppage()
之前。 - 不需要,父进程已经在syscall_env_alloc子进程前对在 entry.S 定义的字 __pgfault_handler 赋值,syscall_env_alloc后父子进程共享读即可。
二、实验难点
如果是延迟槽指令,epc为当前pc,不需要再加4
sys_yield(void)时可以使用sched_yield()调度,但是sched_yield()从TIMESTACK中读取现场,所以需要先把现场从KERNEL_SP复制到TIMESTACK中。
权限位:
- sys_mem_alloc的页面初始权限为不能设置为COW;只能给当前进程或子进程sys_mem_alloc,所以此处checkperm为1
- sys_mem_map
sys_env_alloc要修改子进程的pc为父进程的e->env_tf.pc = e->env_tf.cp0_epc;返回值 e->env_tf.regs[2]=0;子进程状态env_status=ENV_NOT_RUNNABLE;
内核栈、用户栈和用户异常栈:内核栈保存中断时的现场,用户栈保存系统调用的参数,用户异常栈保存缺页中断时的现场。
duppage时父进程需要设置COW权限位时也要使用syscall_mem_map(0, addr, 0, addr, perm | PTE_COW)系统调用进行相同页面的映射从而修改权限位。
用户处理COW异常和页面复制时要引入用户异常栈下面的Invalid memory临时区域的原因:用户态处理COW异常,所以使用用户态的bcopy,由于不在内核处理,不能使用dst物理页面的地址进行映射,所以需要使用dst复制页面的虚拟地址。
流程图
I/O中断
系统调用
fork
页缺失处理
三、体会与感想
- 对计组P7的知识要求比较高,关于中断的机制了解的更加细致
- 用户进程的二进制映像elf执行流程的很多细节可以类比lab1,内存管理的理念和lab2是几乎相同的,lab4有关栈和中断的部分又与lab3联系比较紧密,所以整体上综合了前边的知识,难度也很高,断断续续看了将近一周
- 这次的指导书加了很多流程图,对于理解很有帮助
- 做完lab4之后参加的Lab3-2的上机,但是extra还是没有顺利做出来,自我反思是除了需要熟悉汇编语法以外,对于中断的机制需要更加深入的理解掌握,比如关于延迟槽出现异常;中断现场从寄存器还是栈取、从哪个栈取;以及需要把汇编和c函数灵活掌握应用,比如中断处理函数可以写c代替写法比较繁琐的汇编。
四、指导书反馈
sys_mem_alloc
- post-condition:
va must be < UTOP
应该是pre-condition env may modify its own address space or the address space of its children
很难联想到指的是envid2env的checkperm,可以把注释再细化一些。
- post-condition:
sys_mem_map
Perm has the same restrictions as in sys_mem_alloc.
这句注释容易带来很大误解,sys_mem_alloc对perm的限制是不允许出现COW,而map允许COW。
五、残留问题
- sys_*()函数中,envid2env的checkperm什么时候是1什么时候是0
- sys_mem_map中为什么va需要对齐?sys_mem_alloc需要吗?
- 处理COW缺页中断时还会再发生中断吗?观察缺页中断用户态的代码
entry.S的__asm_pgfaut_handler()
和fork.c的pgfault()
,均没有发生修改用户态页面的行为。如果不会再发生中断,那为什么在handle_mod()
跳转到trap.c的__pgfault-handler()
时要判断tf->regs[29]是用户栈指针还是异常栈指针呢? - set_pgfault_handler函数调用sys_set_pgfault_handler系统调用时,对env的赋值的两个域都是常量,为什么不能初始化时候就赋值,要特地系统调用给它赋值呢?