BUAA_OS lab3实验报告

一、思考题

1. 思考3.1

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

采用逆序插入,可以使得envs与空闲链表中进程的排布顺序相同,这样使用LIST_FIRST可以从前往后(envs的低序号到高序号)依次取用,便于管理。

2. 思考3.2

思考env.c/mkenvid 函数和envid2env 函数:

• 请你谈谈对mkenvid 函数中生成id 的运算的理解,为什么这么做?

• 为什么envid2env 中需要判断e->env_id != envid 的情况?如果没有这步判断会发生什么情况?

  • mkenvid生成id的具体方式是:时刻记录调用此函数的总次数,将总次数左移11位后再或上该进程块在envs中的次序。引入调用函数次数的静态变量方式可以有效保证id为进程独有,引入进程块位置又方便根据id直接查找进程块。

  • 注意到,根据id获取进程时的方法是e = envs + ENVX(envid),只根据后十位的偏移进行了查找,并不知道因此很可能获取到的是一个存在但高位与id不相对应的进程块,也就是说取到了不该取的进程,由此产生错误。所以取出进程块后还需要进一步判断id是否相等,只有每个进程独有的id完全符合,才能保证取到了正确的进程。

3. 思考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为分配给用户的最高空间,而二者之间的区域用户只能读取,没有权限进行修改。

  • UVPT我的解读是user virtual page table,也就是将页表首地址映射到页目录地址,由此实现自映射机制。

  • 进程的虚拟地址是“假地址”,需要操作系统将其映射到实际存在的物理地址上。

4. 思考3.4

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

查找发现,在env.c文件的load_icode_mapper函数中user_data有这样的应用:

 struct Env *env = (struct Env *)user_data;

load_icode函数中,调用load_elf函数时,user_data对应的位置也是一个进程的指针。

我认为在这里user_data用于显示这是哪一个进程的加载行为,携带调用者的信息,从而找到进程对应的页目录,开始复制相关内容。

5. 思考3.5

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

考虑可能出现的最坏情况(每一个分界点都不巧落在页面内部):

  • 填入起始地址位于页面中部:若此处页面不存在,需要分配页面;若页面存在,无需分配但要注意避免覆盖offset之前已有的数据,须从offset之后位置开始填入。、

  • bin_size部分普通页面(装载内容能覆盖整个页面):可直接alloc后完整复制。

  • bin_size结束位置位于页面中部:前半部分复制装载,后半部分使用bzero清零,注意清零时不要新分配页面或覆盖掉之前装载好的数据。

  • bss部分普通页面:可直接alloc新页面并调用bzero全部清零。

  • bss段结束位置位于页面中部:只需清零bss段位置,其后位置虽不大可能有有用的数据存在,但还是尽量保证不要“越权”。

sg_size大于bin_size时需要自动填充.bss段。

6. 思考3.6

这里的e->env_tf.pc是什么呢?就是在我们计组中反复强调的甚为重要的PC。它指示着进程当前指令所处的位置。你应该知道,冯诺依曼体系结构的一大特点就是:程序预存储,计算机自动执行。我们要运行的进程的代码段预先被载入到了entry_ point为起点的内存中,当我们运行进程时,CPU 将自动从pc 所指的位置开始执行二进制码

思考上面这一段话,并根据自己在lab2 中的理解,回答:

• 我们这里出现的” 指令位置” 的概念,你认为该概念是针对虚拟空间,还是物理内存所定义的呢?

• 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?

  • “指令位置”是针对虚拟空间定义的。

  • entry_point对每个进程是一样的。注意到,在load_elf函数中有这样一句代码:*entry_point = ehdr->e_entry;这说明entry_point都是从elf文件同样的位置中读取出来的,因此属于各进程的这个量是统一的。

7. 思考3.7

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

应设置为env_tf.cp0_epc。因为epc用于存储异常发生时的pc值,这样可以使得异常处理程序结束后pc回到异常发生时的指令继续执行。

8. 思考3.8

思考TIMESTACK 的含义,并找出相关语句与证明来回答以下关于TIMESTACK 的问题:

• 请给出一个你认为合适的TIMESTACK 的定义

• 请为你的定义在实验中找出合适的代码段作为证据(请对代码段进行分析)

• 思考TIMESTACK 和第18 行的KERNEL_SP 的含义有何不同

  • TIMESTACK指向一块栈空间的最高地址,从TIMESTACK开始向下的sizeof(struct Trapframe)空间中存储着进程当前的运行状态。

  • 代码段依据:

     .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

    这段汇编代涉及到了TIMESTACK地址,其作用是当检测到中断时将TIMESTACK地址存入sp寄存器,之后在中断时就可以将当前进程状态存入这一地址起始的位置了。

  • 观察发现,汇编代码中根据不同条件选择将sp设置为TIMESTACKKERNEL_SP。首先获取CP0_CAUSE,之后对中断位进行判断,若为时钟中断,将sp设置为TIMESTACK,否则设置为KERNEL_SP

9. 思考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
  sw sp, KERNEL_SP
 setup_c0_status STATUS_CU0|0x1001 0
  jr ra
 
  nop
 END(set_timer)

line13,14:将1写入地址0xb5000100,其中0x100的偏移量表示设置时钟频率,此处设置表示1秒中断1次。

line15:将sp设置为KERNEL_SP

line16:调用宏函数,设置CP0_STATUS值。

line17:返回上一级。

10. 思考3.10

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

这里给大家提供另一个实现方法:

如果使用这个实现方法,要注意:1、进程阻塞(ENV_NOT_RUNNABLE)时,要无条件让出时间片;2、进程结束之后要将其从队列中清除(当然本实验两个进程都是死循环,不存在这种情况)。

我们的操作系统中设置了两个调度队列,同时每个进程有自己的时间片。当前进程时间片用完时,进程状态变为ENV_NOT_RUNNABLE,或进程运行过程中自行变为阻塞状态,此时将其取下放入另一个调度队列的尾部,再从当前队列取出下一个ENV_RUNNABLE的进程开始运行。当前调度队列为空时,切换到另一个调度队列,于是两个调度队列相互合作,一个用于提供运行进程,另一个接收非RUNNABLE的进程,由此实现进程的调度。

产生时钟中断时,pc跳转至异常处理代码,handle_int函数读取异常产生原因和CPU状态,判断为时钟中断后跳转到time_irq函数,进而跳转到我们写的sched_yield函数开始进程调度。由此实现了根据时钟周期切换进程。

二、实验难点

1. load_icode_mapper

我的代码如下:

 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);
 
         Pte *temp;
 
         u_int32_t my_copy_size;
 
     /*Step 1: load all content of bin into memory. */
         i = 0;
         if(offset != 0) {
                 p = page_lookup(env -> env_pgdir, va, &temp);
                 if(p == 0) {
                         if(page_alloc(&p) != 0) {
                                 printf("page_alloc in func load_icode_mapper failed\n");
                                 return -E_NO_MEM;
                        }
                         page_insert(env -> env_pgdir, p, va, PTE_R);
                }
                 if(BY2PG - offset <= bin_size) {
                         my_copy_size = BY2PG - offset;
                }
                 else {
                         my_copy_size = bin_size;
                }
                 bcopy((void *)bin, (void *)(offset + page2kva(p)), my_copy_size);
                 i = my_copy_size;
        }
     for (; i < bin_size; i += my_copy_size) {
         /* Hint: You should alloc a new page. */
                 r = page_alloc(&p);
                 if(r != 0) {
                         printf("page_alloc in func load_icode_mapper failed 1\n");
                         return r;
                }
 
                 if(bin_size - i > BY2PG) {
                         my_copy_size = BY2PG;
                }
                 else {
                         my_copy_size = bin_size - i;
                }
                 bcopy((void *)(bin + i), (void *)page2kva(p), my_copy_size);
 
                 r = page_insert(env -> env_pgdir, p, va + i, PTE_R);
                 if(r != 0) {
                         printf("page_insert in load_icode_mapper failed\n");
                         return r;
                }
 
    }
     /*Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`.
     * hint: variable `i` has the value of `bin_size` now! */
         offset = i - ROUNDDOWN(i, BY2PG);
         if(offset != 0) {
                 p = page_lookup(env -> env_pgdir, va + i, &temp);
                 if(offset + sgsize - i < BY2PG) {
                         my_copy_size = sgsize - i;
                }
                 else {
                         my_copy_size = BY2PG - offset;
                }
                 bzero((void *)(page2kva(p) + offset), my_copy_size);
                 i += my_copy_size;
        }
     while (i < sgsize) {
                 r = page_alloc(&p);
                 if(r != 0) {
                         printf("page_alloc in func load_icode_mapper failed 2\n");
                         return r;
                }
 
                 r = page_insert(env -> env_pgdir, p, va + i, PTE_R);
                 if(r != 0) {
                         printf("load_icode_mapper step2 failed\n");
                         return r;
                }
                 bzero((void *)page2kva(p), BY2PG);
                 i += BY2PG;
    }
     return 0;
 }
 

我没有完全按照课程组给的提示进行。根据我的理解,我使用一个变量i记录elf文件中已完成加载的位置。

本函数大致可以分为四个部分:

  • 对齐起始地址与offset部分

  • 全页面加载bin_size

  • 对齐bin_size末尾与bss段起始部分

  • bss段全页面清零

本函数曾一度出现诡异的问题:尝试运行代码时,调用函数处显示这一函数没有完成执行就开始了死循环,于是我开始在这一函数内部加printf调试,结果显示return之前一切平安,只有return这一句炸了???疑惑了很久,后来在循环里每一句加了printf之后才发现是page2kva写成了page2pa。后来反思可能是因为在循环中错误地把用于返回的地址覆盖了,于是函数不能正常返回了。

2. sched_yield

在我编写此函数的时候,有一个默认条件:存在于env_sched_list中的进程全部是ENV_RUNNABLE的,也就是说我认为如果进程变为非RUNNABLE,会人为地将其从调度队列中移除。

调度算法的可视化实现过程如下图:

三、体会与感想

本次lab3-2竟然爆出了lab2-1的bug!花了一晚上才定位到是queue.h中的LIST_INSERT_TAIL出现问题。虽然我考虑到了LIST为空的情况,但没有设置作为head的节点的prev,于是bug兜兜转转变成了TOOLOW😢.

lab3花费时间竟然高达30小时,感觉大部分时间都在盯着屏幕思考,尤其是load_icode_mapper函数,甚至花费了一整个下午才写好。为保安全后来还把每个函数全部逻辑验证了一遍。

lab3-1上机也让我意识到了课下读懂提供的代码的必要性。课上提到可以仿照env_free函数时,我由于匆忙完全没有读懂这个函数的行为,于是无脑地照抄了逻辑,后来才发现这样做是不行的。

也意识到了c语言根基十分不稳固,比如课上的时候竟然开着dev尝试了半天struct的初始化,竟然试图拿着一个没有初始化的指针赋值,看来有时间还是要恶补一下这些基础问题。

四、指导书反馈

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. */
    }
 /*Step 2: alloc pages to reach `sgsize` when `bin_size` < `sgsize`.
     * hint: variable `i` has the value of `bin_size` now! */
     while (i < sgsize) {
    }

给我的感觉好像是Step1中i只是在记录整个页面,至于偏移需要自己通过计算建立映射;而Step2中似乎i变成了在记录装载成功的量,于是写了一半的我立刻逻辑崩溃....后来直接摒弃的提示的束缚构建自己的逻辑了。

我觉得如果一定要给提示的话,不如直接提示“第一步你需要运用bcopy装载.text和.data段,第二步你需要运用bzero将.bss段清零,期间,你需要注意页面不完整、偏移等问题”。

五、残留难点

env_sched_list存储的内容依旧有疑惑,注意到很多同学都在调度算法中用while循环查找RUNNABLE的进程,而我没有这样做。我的疑惑在于这个队列是否能保证内部进程都是RUNNABLE的?如果是我写的代码,我会在进程状态改变时立刻将其从队列中取出,但不知道评测函数会不会随意修改里面进程的状态。

posted @ 2021-06-28 10:41  菠菜白菜花菜  阅读(940)  评论(0编辑  收藏  举报