思考题
4.1
内核在保存现场的时候是如何避免破坏通用寄存器的?
系统陷入内核调用后可以直接从当时的\(a0-\)a3参数寄存器中得到用户调用msyscall留下的信息吗?
我们是怎么做到让sys开头的函数“认为”我们提供了和用户调用msyscall时同样的参数的?
内核处理系统调用的过程对Trapframe做了哪些更改?这种修改对应的用户态的变化是?
- 保存现场过程中只改写了$k0, $k1, \(v0寄存器的值,\)k0暂存了\(sp的值,\)k1则用于帮助更新\(sp的值,\)v0用于帮助存储非通用寄存器的值。$k0, \(k1在MIPS规则中为暂时的、随便使用的寄存器,\)v0则用于存放函数返回值,这三个寄存器的改写不会造成影响。
- 可以直接获取,系统调用核心部分执行前(用到\(a0-\)a3前)的过程中\(a0-\)a3寄存器的值没有被改变过,因此可以直接使用。
- 在调用sys开头函数前我们先人工把参数加载到了sys开头函数认为的位置。
- 处理中,Trapframe中cp0_epc增加了4,并将执行系统调用后的返回值存入了$v0寄存器,使得系统调用结束后用户态可以获得正确的系统调用返回值并从发生系统调用的下一条指令开始执行。
4.2
子进程完全按照fork()之后父进程的代码执行,说明了什么?
但是子进程却没有执行fork()之前父进程的代码,又说明了什么?
- 说明子进程和父进程共享代码段且具有相同的状态和数据。
- 说明子进程在创建时保存了父进程的上下文、PC值等,状态与父进程一致。
4.3
关于fork 函数的两个返回值,下面说法正确的是:
A. fork 在父进程中被调用两次,产生两个返回值
B. fork 在两个进程中分别被调用一次,产生两个不同的返回值
C. fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D. fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
- C正确
4.4
如果仔细阅读上述这一段话, 你应该可以发现, 我们并不是对所有的用户空间页都使用duppage进行了保护。那么究竟哪些用户空间页可以保护,哪些不可以呢,请结include/mmu.h里的内存布局图谈谈你的看法。
- UTOP以上的用户空间对于用户进程来说不可改变;系统空间对于每个进程来说是相同且不可改变的,因此UTOP以上空间不用进行保护;
- UXSTACKTOP - BY2PG到UXSTACKTOP的空间是用户进程的异常栈,若进行写时复制保护可能陷入死循环,因此不能被保护。
- USTACKTOP到USTACKTOP + BY2PG的空间在空间分布图上是Invalid memory,用不到所以无需保护。
- 去除上述,需要保护的是在UTEXT与USTACKTOP之间且被使用的页面。
4.5
在遍历地址空间存取页表项时你需要使用到vpd和vpt这两个“指针的指针”,请思考回答这几个问题:
vpt和vpd的作用是什么?怎样使用它们?
从实现的角度谈一下为什么能够通过这种方式来存取进程自身页表?
它们是如何体现自映射设计的?
进程能够通过这种存取的方式来修改自己的页表项吗?
- vpt存放着二级页表,vpd存放着一级页表,使用时把它们当作数组头。
对于虚拟地址va,(*vpd)[va >> 22]
为二级页表的物理地址,(*vpt)[va >> 12]
为va对应的物理页面。 - 观察下列代码:
//mmu.h
extern volatile Pte* vpt[];
extern volatile Pde* vpd[];
//entry.S
.globl vpt
vpt:
.word UVPT
.globl vpd
vpd:
.word (UVPT+(UVPT>>12)*4)
可以发现vpt指向UVTP区域,也就是当前进程页表项所在的位置,变量类型为Pte数组。
vpd指向(UVPT+(UVPT>>12)4)区域,是自映射机制下的一级页表地址,变量类型是Pde*数组。
这样便很清楚如何使用了。
- (UVPT+(UVPT>>12)*4)是vpd指向的地址,很好地体现了自映射。
- 用户进程无权限修改页表项。
4.6
page_fault_handler函数中,你可能注意到了一个向异常处理栈复制Trapframe运行现场的过程,请思考并回答这几个问题:
这里实现了一个支持类似于“中断重入”的机制,而在什么时候会出现这种“中断重入”?
内核为什么需要将异常的现场Trapframe复制到用户空间?
- 在用户发生写时复制引发的缺页中断并进行处理时,可能会再次发生缺页中断,从而“中断重入”。
- 我们在用户进程处理此缺页中断,因此用户进程需要读取Trapframe中的值;同时用户进程在中断结束恢复现场时也需要用到Trapframe中数据,因此存到用户空寂。
4.7
到这里我们大概知道了这是一个由用户程序处理并由用户程序自身来恢复运行现场的过程,请思考并回答以下几个问题:
用户处理相比于在内核处理写时复制的缺页中断有什么优势?
从通用寄存器的用途角度讨论用户空间下进行现场的恢复是如何做到不破坏通用寄存器的?
- 符合微内核设计理念,精简系统。
- 首先使用存放函数调用返回值的$v0, \(v1恢复非通用寄存器,之后通过\)sp恢复通用寄存器,最后恢复$sp,保证了恢复后个寄存器值正确。
4.8
请思考并回答以下几个问题:
为什么需要将set_pgfault_handler的调用放置在syscall_env_alloc之前?
如果放置在写时复制保护机制完成之后会有怎样的效果?
子进程需不需要对在entry.S定义的字__pgfault_handler赋值?
- syscall_env_alloc过程中亦可能需要进行异常处理。
- 此时进程给__pgfault_handler变量赋值时就会触发缺页中断,但中断处理没有设置好,故无法进行正常处理。
- 不需要,子进程复制了父进程中__pgfault_handler变量值。
实验难点
个人来说,本次实验的难点在于C语言与汇编语言的混合使用。
对我来说最大的困难有三:
对于汇编语言的不熟练
- 命令遗忘
只能多用才会记住 - 寄存器
对于寄存器的规则理解不够深入。在上学期计组的学习中,只使用了纯汇编编程,寄存器的维护和使用标准都是由自己控制的。这次存在C语言自动控制寄存器,因此要符合C语言对寄存器的使用规则。
我们需要理解:- V0和V1会存储函数返回值;
- A0 - A3会存储函数传入值(剩余传值在栈里)
- S0 - S7在调用函数前后不变
- SP存放栈值
- RA存放返回地址
从而在直接书写汇编时对寄存器进行合理使用。(extra部分更为凸显)
对C语言函数调用时对栈的操作不熟悉
主要在于我们要想明白自己开始编写一个函数时,其寄存器内已有值是什么与其需要的值在什么地方。
图为C语言进行函数调用时典型的栈空间。
按照图示,我们很容易找到我们想要的值在哪里。
用户态和内核态
我们要想明白用户态和内核态使用的栈空间不同。这是我们书写handle_syscall
时必须想明白的。
体会和感想
- 该来的还会来的
计组时候偷的懒,现在全都补回来了。lab3偷的懒,lab4就补回来了…… - 有时候你会想少了,有时候你会想多了
fork创建子进程,我忘记了设置异常处理栈,同时还多此一举的把子进程env_cr3赋值了…… - C语言帮忙我们干了很多事情
函数调用时对寄存器的保存以及传值、恢复等,写C时候完全不可见。但一和汇编对接起来,就会感受到高级语言的美好。
- 总体用时
所用时间相比前几个lab突飞猛进。主要是在恶补之前没弄懂的地方。lab4直接串联起来了前面所有lab的内容,bug真的是无处不在。
用时的话课下部分在3天左右,但到了extra就用了一天半,主要是该补的补完了。
难度在于对规范的熟悉程度,本身逻辑很清晰。