具体系统调用过程在实验难点中体现,具体的保存逻辑在SAVE_ALL(inlcude/stackframe.h)中
先用k0寄存器保存sp寄存器的值再改变sp寄存器的值,并在下次改变k0寄存器之前首先将k0(sp的值)存进TF_REG29,此后整个现场保存过程只涉及对sp, k0的修改,而k0寄存器本来就是约定好内核用的,不需要保存
$a0不行,$a1-$a3可以,$a0会在handle_sys
函数中被修改为(KSTACKTOP-TF_SIZE)用来给do_syscall
传tf参数
将$a0-$\a3以及$sp在进入内核时存入trap frame中,并在do_syscall
调用sys_
时将对应的参数取出来传参给它
将cp0_epc的值加了4,这样在系统调用结束回到用户态时会进入系统调用的下一条指令,不会同一地方反复进行系统调用
同一个Env结构体在被占用释放又被占用的过程中,每一次的envid是不一样的,但是e的确定是根据Env结构体确定的,也就是说同一个Env结构体在多次被占用的时候会有不同的envid
思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与envid2env() 函数的行为进行解释
当envid的值是0时,函数会通过形参penv返回指向当前进程控制块的指针。当某些系统调用(如IPC功能)函数需要访问当前进程的进程控制块时,可以直接通过向envid2env传0来会获得指向当前进程控制块的指针,然后通过指针对进程控制块进行访问。
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考
ULIM到UVPT存放的是页表和页目录,不是共享页面。
UVPT到UTOP存放的是pages和envs,这些在env_init中已经映射好了,不是共享页面。
UTOP和USTACKTOP之间是异常处理栈(user exception stack),进行异常处理时的数据存放点,不需要共享这部分的内存。
所以需要被映射的页面只有USTACKTOP之下有效(有PTE_V权限)的页面。
vpt 和 vpd 分别是指向用户页表和用户页目录的指针,可以用来对用户页表和页目录进行访问。 vpd 使用时用虚拟地址的一级页号作为偏移量即可访问对应页目录项 e.g.vpd[PDX(va)] vpt 使用时需要将一二级页号一起作为偏移量进行页表项访问 e.g.vpt[VPN(va)]
vpt 和 vpd 分别指向了两个值 UVPT 和(UVPT+(UVPT>>12)*4),这两个值分别是用户地址空间中页表的首地址和页目录的首地址。所以我们可以直接通过vpt和vpd访问到用户进程页表和页目录。
vpd 的值是(UVPT+(UVPT>>12)*4),而这个地址正好在UVPT和ULIM之间,说明页目录被映射到了某一个页表的位置。而每一个页表都被页目录中的一个页表项所映射。因此页目录中一定有一个页表项映射到了页目录本身。
不能,用户态不具备写权限,页表是内核态程序维护的,用户进程只能对页表项进行访问
用户发生写时复制导致异常并处理的过程中,有可能还会发生写时复制,所以要 “中断重入”,类似于函数嵌套递归调用的方式重复处理,直到不再缺页异常
因为最终实际是用户态cow_entry
函数在处理写时复制,所以需要将 tf 复制到用户空间
减少内核代码的工作量,即使出错也不至于系统崩溃,但是如果内核出现错误,就会导致系统崩溃
我认为还可以可以放在 syscall_exofork 之后,duppage 之前
在duppage中也会发生页写时复制,所以要先放置好页面写时复制的异常处理函数地址
可能出现在 duppage 的时候发生写时复制,但是没有异常处理地址的情况
本次实验我遇到的难点在于
- C语言和mips之间交替出现,对参数和寄存器的存取处理不清晰
- 调度队列加入方式唯一
- vpt使用(用VPN宏)
- 对用户态各个数据段用途的掌握不清
skinparam ComponentStyle rectangle
package user{
[user syscall function\n in "user/lib/c_file"] as user_func
[ function: "syscall_*"\nin "user/lib/syscall_lib"] as syscall
[ function: "msyscall"\nin "user/lib/syscall_wrap"] as msyscall
}
package kern{
package exception_part{
[function: "exc_gen_entry"\n in "kern/entry"] as exc_gen_entry
}
package syscall_part{
[function: "handle_sys"\n in "kern/genex"] as handle_sys
[function: "do_syscall"\n in "kern/syscall_all"] as do_syscall
[ function: "sys_*"\n in "kern/syscall_all"] as sys
}
note right of exc_gen_entry
save
all the 32 regs
cp0_status
hi
lo
cp0_badvaddr
cp0_cause
cp0_epc
to struct trapframe
end note
}
user_func -right-> syscall
syscall --> msyscall
msyscall --> exc_gen_entry : syscall
exc_gen_entry --> handle_sys : through handler_exception array
handle_sys --> do_syscall
do_syscall -right-> sys
skinparam ComponentStyle rectangle
package fork{
[syscall_set_tlb_mod_entry(curenv)] as set_entry_self
[syscall_exofork] as exofork
[duppage] as duppage
[syscall_set_env_status] as set_status
[syscall_set_tlb_mod_entry(child)] as set_entry_child
set_entry_self --> exofork
exofork --> duppage
duppage --> set_entry_child
set_entry_child --> set_status
note right of exofork
env_alloc
set child env
end note
note right of duppage
use "syscall_mem_map" to
set copy-on-write pages
end note
note right of set_status
set child status to runnable
and add it to schedule list
end note
}
本次实验用了一天时间debug,甚至倒逼着掌握了基本的gxe调试。此外还花了半天各种跳转看代码,摸清几个过程和数据段用途 结论是:多看代码有好处