思考题
Thinking 3.1
为什么我们在构造空闲进程链表时必须使用特定的插入的顺序?(顺序或者逆序)
为了保证链表中Env块的顺序和在envs中的顺序相同
Thinking 3.2
思考env.c/mkenvid 函数和envid2env 函数:
• 请你谈谈对mkenvid 函数中生成id 的运算的理解,为什么这么做?
• 为什么envid2env 中需要判断e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
习\操作系统课设\
低10位用于表示当前Env在envs中的位置,高位则表示调用分配函数的次数。
如果只有低位的话,当Env被重复调用的时候id就会重复,而只有高位的话则没法根据envid查找对应Env在envs中的位置。
如果没有判断e->env_id!=envid
的话,可能实际上这一进程并没有分配id,但是因为未分配时低10位是0而被误判为envs中第一个进程。
Thinking 3.3
结合include/mmu.h 中的地址空间布局,思考env_setup_vm 函数:
• 我们在初始化新进程的地址空间时为什么不把整个地址空间的pgdir 都清零,而是复制内核的boot_pgdir作为一部分模板?(提示:mips 虚拟空间布局)
• UTOP 和ULIM 的含义分别是什么,在UTOP 到ULIM 的区域与其他用户区相比有什么最大的区别?
• 在env_setup_vm 函数的最后,我们为什么要让pgdir[PDX(UVPT)]=env_cr3?(提示: 结合系统自映射机制)
• 谈谈自己对进程中物理地址和虚拟地址的理解
因为我们采用的是2G/2G的布局,没有真正的内核进程,每个用户进程都可能临时变为内核态从而获得内核空间的管理权限。所以有可能会需要用boot_pgdir
访问相应的内核区域
UTOP
含义是用户可以使用的空间中的最高地址,ULIM
含义是用户空间的最高地址(再往上就是内核空间了)
它们之间的区域应该是用户没有权限修改的
因为env_cr3
储存的是进程页目录的物理地址,通过这样的赋值完成了页目录的自映射。
进程中的虚拟地址就是对于每个用户来说独立的”假地址”,物理地址是共同使用的真实地址。
Thinking 3.4 思考user_data 这个参数的作用。没有这个参数可不可以?为什么?(如果你能说明哪些应用场景中可能会应用这种设计就更好了。可以举一个实际的库中的例子)
不可以。因为需要这个参数传递的信息才可以完成加载二进制镜像的作用,如果没有的话得不到页目录的信息。
Thinking 3.5 结合load_icode_mapper 的参数以及二进制镜像的大小,考虑该函数可能会面临哪几种复制的情况?你是否都考虑到了? (提示:1、页面大小是多少;2、回顾lab1中的ELF文件解析,什么时候需要自动填充.bss段)
-
bin_size<BY2PG-offset (填入的文件内容不能充满一页中的剩余位置)
-
bin_size>=BY2PG-offset && bin_size-BY2PG+offset %BY2PG !=0 (可以充满剩余位置,但是文件内容末尾不是BY2PG的整数倍)
-
bin_size>=BY2PG-offset && bin_size-BY2PG+offset %BY2PG =0 (可以充满且是整数倍)
sg_size>bin_size的时候需要自动填充
这里的e->env_tf.pc是什么呢?就是在我们计组中反复强调的甚为重要的PC。它指示着进程当前指令所处的位置。你应该知道,冯诺依曼体系结构的一大特点就是:程序预存储,计算机自动执行。我们要运行的进程的代码段预先被载入到了entry_ point为起点的内存中,当我们运行进程时,CPU 将自动从pc 所指的位置开始执行二进制码。
Thinking 3.6 思考上面这一段话,并根据自己在lab2 中的理解,回答:
• 我们这里出现的” 指令位置” 的概念,你认为该概念是针对虚拟空间,还是物理内存所定义的呢?
• 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?
是针对虚拟空间定义的,指令所在的物理页面可能是分散的,但是虚拟地址是连续的,所有只有按照虚拟地址才能顺序执行指令。
entry_point对于每个进程来说是相同的,都是从elf文件中读取的,不过他们储存的物理地址是不同的。
Thinking 3.7 思考一下,要保存的进程上下文中的env_tf.pc的值应该设置为多少?为什么要这样设置?
应该设为epc的值,因为这样可以在处理完中断之后返回到之前的位置继续执行
Thinking 3.8 思考TIMESTACK 的含义,并找出相关语句与证明来回答以下关于TIMESTACK 的问题:
• 请给出一个你认为合适的TIMESTACK 的定义
• 请为你的定义在实验中找出合适的代码段作为证据(请对代码段进行分析)
• 思考TIMESTACK 和第18 行的KERNEL_SP 的含义有何不同
TIMESTACK是内核中保存现场所用的栈指针,以下sizeof(struct Trapframe)保存了当前进程的上下文信息
SAVE_ALL
1:
move k0, sp
get_sp
move k1, sp
subu sp, k1, TF_SIZE
get_sp
mfc0 k1, CP0_CAUSE
andi k1, 0x107C
xori k1, 0x1000
bnez k1, 1f
nop
li sp, 0x82000000
j 2f
nop
1:
bltz sp, 2f
nop
lw sp, KERNEL_SP
nop
2:
nop
.endm
get_sp先判断是否发生的是4号中断引起的异常,然后决定该用哪个栈指针。如果是的话把sp设为TIMESTACK用于保存上下文
TIMESTACK是产生时钟中断异常时用的栈指针,KERNEL_SP是非时钟中断异常用的栈指针
Thinking 3.9 阅读 kclock_asm.S 文件并说出每行汇编代码的作用
将1存入0xb5000100的位置开启实时钟,sp的值存入内核栈,调用宏来设置CP0的状态,返回上一级函数
Thinking 3.10 阅读相关代码,思考操作系统是怎么根据时钟周期切换进程的。
当时钟中断产生时,MIPS将PC指向异常处理代码段,调用handle_int函数处理
handle_int读取CPU_CASUE和CPU_SATUS到t0t2中,把t0 and t2存入t0得到具体的中断号,如果判断是4号中段位则执行中断服务函数time_irq,time_irq跳转到sched_yield时间片轮转算法从而切换进程
实验难点
本次实验感觉最难的两个函数是mapper和sched_yield,分别实现加载二进制文件镜像和进程切换的调度算法。
可以看到这两个函数中都残留了需要混乱的注释,这都是debug时留下的痕迹。
mapper的难点主要在于有很多种情况需要考虑到,但是又最好能够用一种统一的写法将其表示出来。
sched_yiled则主要是理解调度算法的机制是什么。
不过比较搞笑的是,我现在也不能确定自己最早写的这两个函数是不是错误的。因为debug的过程中对于bug出现的准确位置一直没能实现精准定位,所以走投无路的时候想到的往往就是这两个最难的函数是不是错了。结果就是这两个函数被我重写了很多个版本,但是最后发现的bug却是在env_run中,而不是这两个里面。真是让人哭笑不得。
体会与感想
本次实验所花费的时间再创新高,在延长了一周时间的情况下,网站统计的在线时间达到了惊人的60小时,debug的过程充满了折磨。
究其原因还是没办法准确定位bug。即使采用了printf和breakpoint相结合的方式,运行到某一条汇编指令出错的时候仍然不知道究竟在执行哪个函数。
可能是我错误理解了助教的意思,“`除调度算法之外其他地方没有问题,则结果如下:”。两种测试方法我都得到了正确的结果,我以为错误一定就在调度算法中,所以调度算法重写了很多遍,也和同学讨论了很久,始终不知道自己的问题出在哪。
此时根据报错信息和prinf的定位,我又发现是调度算法执行到env_run出现的问题。
这两者的矛盾成为了debug时间超长的罪魁祸首,因为不确定哪里是对的哪里是不对的,只能凭感觉瞎改一通,效果往往不好,甚至把正确的改成了错误的。
结果最后发现果然是env_run写错了。事实证明自己的bug还是要根据具体情况自己判断,不能僵死地照搬助教的建议,毕竟助教给的建议是针对大部分人的,而不是针对具体一个人的bug来说。