OS_Lab3

实验思考题

3.1

为什么我们在构造空闲进程链表时必须使用特定的插入的顺序?(顺序或者逆序)

根据注释的意思,我们在完成插入后,链表内的顺序和envs数组本身的顺序一致,这样在加载进程时会首先使用数组前部的空闲块;而在调度过程中,最近使用过的空闲进程也会因此存在表头的位置,会被优先调度。

3.2

思考env.c/mkenvid 函数和envid2env 函数:
-请你谈谈对mkenvid 函数中生成id 的运算的理解,为什么这么做?
-为什么envid2env 中需要判断e->env_id != envid 的情况?如果没有这步判断会发生什么情况?

  1. 如此生成ID可以给每个进程一个独一无二的进程号,同时还可以方便的通过进程号找到进程块进而获得进程的全部信息。
  2. 一个进程块可能在被调用前发生了替换,而进程号只会对应一个进程块不会改变,因此需要进行此判断。否则可能执行错误进程导致问题。

3.3

结合include/mmu.h 中的地址空间布局,思考env_setup_vm 函数:
• 我们在初始化新进程的地址空间时为什么不把整个地址空间的pgdir 都清零,而是复制内核的boot_pgdir作为一部分模板?(提示:mips 虚拟空间布局)
• UTOP 和ULIM 的含义分别是什么,在UTOP 到ULIM 的区域与其他用户区相比有什么最大的区别?
• 在env_setup_vm 函数的最后,我们为什么要让pgdir[PDX(UVPT)]=env_cr3?(提示: 结合系统自映射机制)
• 谈谈自己对进程中物理地址和虚拟地址的理解

  1. 因为每个进程都需要拥有内核的页表信息从而能够在陷入内核时正确运行程序,因此需要把内核拥有的虚拟地址部分对应的页表进行拷贝。
  2. ULIM是系统分配给用户进程的最高地址;UTOP之下是用户进程可以自由读写的部分,其上到ULIM则是相对固定的用户用的进程信息、页表信息,只能读取。
  3. env_cr3是页目录本身所在的物理地址,pgdir[PDX(UVPT)]=env_cr3旨在帮助程序找到正确的页目录物理地址。
  4. 每个进程看到的都是虚拟地址空间,进程需要的物理地址空间存储在进程的页表当中。物理地址空间是硬件有多大地方就会编址到多大,虚拟空间则是约定好的,也就是人为设置的。对于不同的进程来说。每个进程都有着各自独立的虚拟地址空间,这样进程切换时不同的进程对相同的虚拟地址空间进行访问时互不影响。

3.4

思考user_data 这个参数的作用。没有这个参数可不可以?为什么?(如果你能说明哪些应用场景中可能会应用这种设计就更好了。可以举一个实际的库中的例子)
不能没有这个参数。这个参数的存在方便了向更内层函数传值。C语言stdlib中有

void qsort(void*base, size_t num, size_t width, int(__cdecl*compare)(const void*,const void*));

就是一个例子,第三个参数告诉了程序应当如何分割base起始的内存数据,方便调用传入的比较函数进行排序操作。

3.5

结合load_icode_mapper 的参数以及二进制镜像的大小,考虑该函数可能会面临哪几种复制的情况?你是否都考虑到了? (提示:1、页面大小是多少;2、回顾lab1中的ELF文件解析,什么时候需要自动填充.bss段)

  • offset + sgsize <= pagesize
    先把页清零,再从offset位置开始装载;
  • offset + bin_size <= pagesize && offset + sgsize > pagesize
    先把页清零,再从offset位置开始装载,之后清零下一页;
  • n * pagesize <= offset + bin_size <= offset + sgsize <= (n + 1) * pagesize
    首、末页清零后装载,其他页直接装载;
  • n * pagesize <= offset + bin_size <= (n + 1) * pagesize <= offset + sgsize
    首页、bin结束页清零后装载,中间页直接装载,后续页清零。

3.6

思考上面这一段话,并根据自己在lab2中的理解,回答:
• 我们这里出现的“指令位置”的概念,你认为该概念是针对虚拟空间,还是物理内存所定义的呢?
• 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?

  1. 其指的是虚拟空间。顺序执行时PC永远都是进行+4操作,但一个进程程序段可能不在一段连续的物理空间上,因此显然PC应当存放虚拟地址。
  2. 应当是一样的。存放虚拟地址的entry_point没有必要不一样,因为本身就可以从页表映射到不同的物理地址。如果设计成不一样会给操作系统带来更多麻烦事,一致则可以减少复杂度。

3.7

思考一下,要保存的进程上下文中的env_tf.pc的值应该设置为多少?为什么要这样设置?

其应当设置为cp0_epc中的值,这个寄存器存储了中断或异常发生时的PC值,在程序恢复进行时应当从这条语句开始执行。

3.8

思考TIMESTACK的含义,并找出相关语句与证明来回答以下关于TIMESTACK的问题:
• 请给出一个你认为合适的TIMESTACK的定义
• 请为你的定义在实验中找出合适的代码段作为证据(请对代码段进行分析)
• 思考TIMESTACK和第18行的KERNEL_SP的含义有何不同

  1. 定义:发生时钟中断时存放寄存器状态的栈顶地址。
  2. 在stackframe.h中有如下代码:
.macro 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

而之前的SAVE_ALL则把当前寄存器的值都写入了相对于sp偏移一定量的内存空间中。
阅读上述代码可见是分析中断类型并更改sp寄存器值的操作。TIMESTACK的值正是0x82000000.可见函数在分析出中断为时钟中断后会把寄存器当前值存放到TIMESTACK所在区域。
3. TIMESTACK是发生时钟中断异常时用到固定的的栈指针。KERNEL_SP则是发生其他中断时栈指针的值。

3.9

阅读 kclock_asm.S 文件并说出每行汇编代码的作用

.macro  setup_c0_status set clr
        .set    push
        mfc0    t0, CP0_STATUS
        or      t0, \set|\clr
        xor     t0, \clr
        mtc0    t0, CP0_STATUS
        .set    pop
.endm

.text
LEAF(set_timer)

        li t0, 0x01
        sb t0, 0xb5000100               //把0x5000100写入1
        sw sp, KERNEL_SP                //把sp值存入KERNEL_SP
setup_c0_status STATUS_CU0|0x1001 0     //把CP0——STATUS第4位和第1位置1
        jr ra                           //返回

        nop
END(set_timer)

3.10

阅读相关代码,思考操作系统是怎么根据时钟周期切换进程的。

系统有两个就绪队列,程序初次装载后链接至队列1末尾;
当一个进程时间片用完时,把这个程序放到另一个队列的末尾,系统取出当前队列的第一个进程开始执行(如果可以);
当一个队列空时,系统切换到另一对列继续上述操作。
当两队列都为空时,执行结束。


实验难点图示

对于我来说,经历时间最长的有两部分。由于个人认为本次难点没有图示的必要,故不单独进行作图。

  1. load_icode_mapper()
    这里主要是要考虑清楚各种情况,而考虑各种情况主要是保证两件事情:拷贝时候不越界、分配足够的空间。
    具体分类情况前面的思考题里面已经解答了。
    考虑过各种情况后,写几个循环不是问题。
  2. sched_yield()
    这是调度用的算法。调度的方法在思考题里面有所叙述。
    它把我难住的主要原因是我没有考虑它和env_run()的关系。
    env_run()中,程序会对上一个执行后的并被转换状态的进程进行状态转存,要用到curenv,而调度算法自然也要用到这个变量,并且这个算法最终目的其实就是改变这个变量的值——因为这个变量代表当前进行的进程。
    问题在于整体流程下其实先调用了sched_yield()然后才调用了env_run(),也就是说在执行调度算法时不能对curenv进行更改。
    我花了大量时间才意识到了上述问题。

实验感想

综述

总体时间花费和Lab2相比基本持平,但debug时间明显多于填充代码时间,前者至少占到了70%。扩展测试extra则相对简单,3个小时不到就搞定了。本地运行——特别是按助教给的方法检查bug时跑出来的结果总是跟助教的不太一致又好像一致,给定位bug造成了极大困难,进而变成了重新审视本lab的所有代码,造成了debug的时间很长。

心得

  1. printf()真的是个忠实可靠的伙伴。
    轮到它就一定有输出,一行后面加一个输出成功实现精准定位……
  2. 代码里面定义了一堆全局变量,一定要搞清楚这些变量的前后变化关系,不然会出乱子。

指导书问题

R3000 的SR 寄存器的低六位是一个二重栈的结构。KUo 和IEo 是一组,每当中断发生的时候,硬件自动会将KUp 和IEp 的数值拷贝到这里;KUp 和IEp 是一组,当中断发生的时候,硬件会把KUc 和IEc 的数值拷贝到这里。其中KU 表示是否位于内核模式下,为1 表示位于内核模式下;IE 表示中断是否开启,为1 表示开启,否则不开启2。

显然,这部分叙述有问题。

建议

  1. 对每个预设好的局部变量进行注释
    很多未补全的代码预设了一些变量给大家使用,想来一定是用作提示用。
    但很多时候我真的看着那些定义好的变量不知所措。每次补充完成代码,很多局部变量都没有使用,保存时候会被标黄提醒——这对强迫症不友好的同时,也让很多时候都是懵懵懂懂的我感到很慌
  2. 注释更加清晰的同时减少预设的代码
    本次作业中load_icode_mapper()中给好的循环体头部让我倍感迷惑,最终选择把循环体头部改变后进行补充,在班群里面很多人都对这部分进行了更改。
    个人认为像循环头这种可能有很多种写法的地方不要使用给好的代码对大家进行引导,如果真想给提示不如在注释里面多加一些细节。
posted @ 2020-05-26 22:34  \+_+/  阅读(911)  评论(0)    收藏  举报