lab3实验报告

一、实验思考题

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

以这次以逆序插入为例,插入结束后,连续申请为多个进程申请进程控制块,能够使得从链表中取出的进程控制块在envs数组中的下标是连续的:为0、1、2...

即保证链表中的元素顺序与全局数组中的顺序相同

OS-lab3实验报告_BUAA_OS

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

  • 结合代码中的注释来理解,env_id低位的作用是表示env在envs中的位置,高位表示调用这个函数的次数。

    如果只有低位,当env被多次重复调用时,id就会重复

    而如果只有高位,就无法根据env_id得到当前env在envs中的位置

OS-lab3实验报告_宏函数_02
  • 如果不进行这一步判断,通过&envs[ENVX(envid)]得到的进程可能实际上并没有被分配id,而未被分配id时,低位是0,可能会在后续操作中被判断是envs[0]导致出错

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储存的是进程页目录的物理地址,通过pgdir[PDX(UVPT)]=env_cr3实现了页目录的自映射

OS-lab3实验报告_物理地址_03
  • 进程中的物理地址是所有进程公共所有的真实地址,而虚拟地址则是每个进程间都相互独立的“加密”地址,需要进行转换才能得到真实地址

Thinking 3.4
思考user_data 这个参数的作用。没有这个参数可不可以?为什么?(如果你能说明哪些应用场景中可能会应用这种设计就更好了。可以举一个实际的库中的例子)

不可以没有这个参数,因为load_icode-mapper函数需要使用user_data得到目录,从而才能完成二进制镜像的加载,如果没有的话就无法达到我们的目的

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

见实验难点部分

Thinking 3.6
这里的e->env_tf.pc是什么呢?就是在计算机组成原理中反复强调的甚为重要的PC。它指示着进程当前指令所处的位置。冯诺依曼体系结构的一大特点就是:程序预存储,计算机自动执行。我们要运行的进程的代码段预先被载入到了 entry_point为起点的内存中,当运行进程时,CPU 将自动从 pc 所指的位置开始执行二进制码。
思考上面这一段话,并根据自己在lab2 中的理解,回答:
• 我们这里出现的” 指令位置” 的概念,你认为该概念是针对虚拟空间,还是物理内存所定义的呢?
• 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?

  • 这里出现的“指令位置”概念是针对虚拟空间定义的,指令在真实物理内存中的分布可能是离散的,而对应的虚拟地址则是连续的
  • entry_point对于每个进程是相同的,都是从ELF文件中读取的,但是存储的物理地址是不同的

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

应该设置为epc的值,因为一般在异常中断处理完毕后会重新执行发生异常的指令,这样设置就可以在处理完异常中断之后返回到之前的位置继续执行

OS-lab3实验报告_ico_04

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

  • TIMESTACK是产生4号中断(结合本次实验的介绍,4号中断应该为时钟中断)时用来存储进程寄存器状态的栈地址
  • 首先,在函数env_pop_tf函数中有j k1这样一条语句,然后顺着这个找下去查找到了stackframe.h中的宏函数SAVE_ALL,而宏函数SAVE_ALL又调用了一个函数get_sp。
OS-lab3实验报告_宏函数_05

​ get_sp的前三条语句是把Cause寄存器中的值取出保存在k1中并且进行一定的处理,再结合《see mips run linux》中对Cause寄存器的说明。

OS-lab3实验报告_BUAA_OS_06
OS-lab3实验报告_物理地址_07
OS-lab3实验报告_ico_08

​ 我们可以知道其实andi k1, 0x107C、xori k1, 0x1000经过这两条语句处理后的k1寄存器的值如果为0,也就代表ExcCode为0(异常类型为中断)并且IP7-2的值为4(中断类型为4号),即产生4号中断时不跳转,并且把sp寄存器的值设置为0x82000000(即TIMESTACK),因此TIMESTACK为产生4号中断(时钟中断)时用来存储进程寄存器状态的栈地址

  • TIMESTACK为产生时钟中断时用来存储进程寄存器状态的栈地址;而KERNEL_SP是用来存储内核栈的地址的,在set_timer函数中有sw sp, KERNEL_SP这样一条语句,即把sp寄存器的值存入KERNEL_SP中,而set_timer函数是由内核调用的,因此KERNEL_SP是用来存储内核栈的地址的

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

见代码中注释

#include <asm/regdef.h>
#include <asm/cp0regdef.h>
#include <asm/asm.h>
#include <kclock.h>

.macro	setup_c0_status set clr
	.set	push
	mfc0	t0, CP0_STATUS			//取出CP0_STATUS寄存器的值到t0中
	or	t0, \set|\clr			//重新设置CP0_STATUS寄存器的值
	xor	t0, \clr
	mtc0	t0, CP0_STATUS			//将新设置的值写回CP0_STATUS寄存器
	.set	pop
.endm

	.text
LEAF(set_timer)

	li t0, 0x01				//将t0寄存器赋值为1
	sb t0, 0xb5000100			//将1存入0xb5000100表示开启实时钟
	sw	sp, KERNEL_SP			//将sp的值存入KERNEL_SP
setup_c0_status STATUS_CU0|0x1001 0 		//调用宏函数设置cp0寄存器
	jr ra					//返回

	nop
END(set_timer)

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

产生实时钟中断时,MIPS将PC指向0x80000080,从而跳转到.text.exc_vec3即异常分发代码,分发后调用handle_int函数处理实时钟中断,在handle_ int判断CP0_CAUSE寄存器是不是对应的4号中断位引发的中断,如果是,则执行中断服务函数timer_ irq,在timer_ irq里直接跳转到sched_ yield中执行时间片轮转算法从而切换进程

二、实验难点图示

1、加载二进制镜像

这一部分总共有三个函数:load_icode,load_elf,load_icode_mapper,前两个结合注释都能够比较顺利地理解和填写,比较麻烦的是load_icode_mapper这个函数,注释有很大的问题,写的时候把注释几乎都删完了自己写的,因为要考虑的情况实在太多,导致写的时间很长总是担心考虑不完全

OS-lab3实验报告_物理地址_09

并且附上部分实现代码(具体实现办法已略去)

static int load_icode_mapper(u_long va, u_int32_t sgsize,
                             u_char *bin, u_int32_t bin_size, void *user_data)
{
    struct Env *env = (struct Env *)user_data;
    struct Page *p = NULL;
    u_long i;
    int r;
    u_long offset = va - ROUNDDOWN(va, BY2PG);

    /*Step 1: load all content of bin into memory. */
    int size = 0;
    //如果offset > 0,va不是BY2PG对齐
    if (offset > 0) {
        p = page_lookup(env -> env_pgdir, va, NULL);
        if (p == NULL) {
            ...
        }
        size = ...;
        bcopy((void *)bin, (void *)(page2kva(p) + offset), size);
    }
    for (i = size; i < bin_size; i += BY2PG) {
        /* Hint: You should alloc a new page. */
        ...
    }
    /*Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`.
    * hint: variable `i` has the value of `bin_size` now! */
    //这部分与前一部分思路相同
    i = ...;
    offset = ...;
    if (offset > 0) {
        ...
    }
    while (i < sgsize) {
        ...
    }
    return 0;
}

2、sched_yield函数的编写

这个函数的编写难点主要在于理解这个调度算法,结合注释能够比较好的理解

  • 判断当前进程分配的时间片是否已经用完,如果已经用完了,就把当前进程从所在的就绪队列删除,加入到另一个就绪队列的末尾
  • 如果当前就绪队列为空,切换到另一个就绪队列,然后在当前的就绪队列中找到一个就绪的进程'e',并且把时间片重新设置为'e'的优先级
  • 时间片自减并且运行进程'e'
OS-lab3实验报告_物理地址_10

三、体会与感想

这次实验应该总共花了15个小时左右,难度比起lab2低了一点,重点仍然在于理解而不在于写代码,需要读的代码很多很分散,需要结合指导书理解程序的整体流程

不要太相信代码中的注释!要真正理解之后再去写代码,注释只是一个参考,并不是完全靠谱的(比如本次lab中的load_icode_mapper函数的注释)

bug的定位更难了!仅仅本次lab就有14个小任务,再加上本次lab中很多地用到了我们在lab2中编写的宏函数,可能出锅的地方实在太多了

写代码的时候一定要“谨慎”!刚刚提到,本次lab中很多地用到了我们在lab2中编写的宏函数,并且也有很多同学表示是lab2的代码出锅才导致^^^^^TOO LOW^^^^^的出现,这就要求我们在写代码的时候一定要“谨慎”,尽可能考虑到所有的情况,把出锅的可能性降到最低,不给后面的lab添麻烦

四、指导书反馈

1、关于env_alloc函数中将进程状态设置为ENV_RUNNABLE后却没有加入env_sched_list[0]

按理来说,所有状态为ENV_RUNNABLE的进程都应该在就绪链表里,尽管这个进程可能还没有对应的程序代码,一句代码也没法执行,但是它依旧是一个就绪的进程,所以我认为应该在设置后就加入env_sched_list[0]中

2、关于load_icode_mapper函数中的注释和已经给出的代码

个人认为load_icode_mapper函数中的注释和已经给出的代码都对代码的填写存在误导

先看第一步

/*Step 1: load all content of bin into memory. */
    for (i = 0; i < bin_size; i += BY2PG) {
        /* Hint: You should alloc a new page. */
    }

看起来似乎没什么问题,但是如果出现这种情况:其中bin_size < BY2PG,就出问题了,第二个页面的那部分无法写入

OS-lab3实验报告_宏函数_11

并且如果va不是BY2PG对齐,那么有一部分内容需要写到已经alloc的页面,并不是全都要alloc一个新的页面

再看第二步

/*Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`.
    * hint: variable `i` has the value of `bin_size` now! */
    while (i < sgsize) {


    }

这个注释真的就离谱。。。如果我们按照已经给出的架构写,很大概率i != bin_size,但是这里注释却告诉我们此时i == bin_size