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
设置为TIMESTACK
或KERNEL_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`.