lab3 实验报告
思考题
Thinking 3.1
env_id = (asid << 11) | (1 << 10) | index
,其保证了每一个进程控制块的id唯一。
在进行env_id != envid
判断前仅仅只利用了index后10位进行偏移找到进程控制块e,无法保证高6位的asid信息相同。如果不判断,无法保证所取到的env为所需要的。
Thinking 3.2
A1:
UTOP是用户读写部分的最高地址,ULIM是用户进程的最高地址。
UTOP和ULIM之间的区域为用户进程的进程块还有页表,只能读。
A2:
env_cr3是页目录的物理地址
pgdir[PDX(UVPT)]=env_cr3利用了页目录自映射,对应了页目录的页目录项。
A3:
进程对应的都是虚拟地址;对于进程而言,物理地址只能通过在查询页表获得。
不同的进程有着自己独立的虚拟地址空间和映射关系。不同的进程中相同的虚拟地址可能映射到不同的物理地址,不同的虚拟地址可能映射到相同的物理地址。
在有一些OS中,MMU的填写是由硬件自动完成的,设计师无需花费太多精力考虑物理地址。在我们的MOS中,需要花大量的时间考虑物理地址的填写,是因为其硬件无法自己实现重填,需要软件的参与。
CPU运行和进程内部保存的几乎都是虚拟地址,对于程序而言,使用虚拟地址来运行,虚拟地址到物理地址的转换这一步由OS负责,对进程程序不可见。
Thinking 3.3
来源:通过阅读代码,发现load_elf()
和load_icode_mapper()
存在user_data,使用时,两个函数传入的数据都是进程控制块e。
不能删去此参数,因为user_data是向内层函数int *map()
即load_icode_mapper()
传递的参数。
若缺失,load_icode_mapper()
就无法获取所需的参数
实际例子:C语言标准库stdlib.h 中的qsort()
函数。第3个参数size是为了给函数内部调用compar()
提供信息。qsort()
的声明如下
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void*))
Thinking 3.4
根据va
与va + bin_size
是否页对齐,一共可以分成4种情况。
都考虑到了。
Thinking 3.5
A1:
我认为是虚拟地址:理由有3:
- 此pc值会供CPU使用,而CPU只会发出虚拟地址
- pc指向了一段连续的地址空间,这种“连续性”只有虚拟地址才具有
- 通过阅读函数
load_icode()
,有:env_tf.pc = entry_point
,在load_elf()
中,有*entry_point = ehdr->e_entry
,对于一个elf可执行文件,其内部存储的是虚拟地址
A2:
我认为其对每个进程都是一样的。
entry_point的值为一个虚拟地址,将其值确定下来相当于规定了文件入口位置。虚拟地址相同意味着页表项相同,只是其对应的物理地址不同。OS可以对每个可执行文件执行相同的操作,同时找到正确的物理地址。
因此可知其能降低elf可执行文件和操作系统的复杂度,方便操作。
Thinking 3.6
epc
为cp0_epc
,因为其内部保存的是进程发生中断时的指令地址。当恢复进程时,应从发生中断的指令开始往下执行,因此要将pc
设为cp0_epc
Thinking 3.7
A1:
操作系统在发生时钟中断时将原进程的env_tf
中的内容保存到TIMESTACK
区域
在handle_int
函数中通过SAVE_ALL
实现功能。
在SAVE_ALL
中会根据CP0_CAUSE寄存器中的值选择栈顶,对于时钟中断,会设置栈顶为0x82000000,即TIMESTACK
。
A2:
TIMESTACK
时发生时钟中断时的栈顶指针,KERNEL_SP
是发生其他中断时的栈顶指针。
Thinking 3.8
handle_int:在./lib/genex.S
handle_sys:在./lib/syscall.S
handle_mod和handle_tlb两者都是通过genex.S文件中的宏函数BUILD_HANDLER
实现的。
Thinking 3.9
LEAF(set_timer)
li t0, 0xc8
sb t0, 0xb5000100 #在地址0xb5000100中写入0xc8,设置时钟频率为200
sw sp, KERNEL_SP #保存sp值到KERNEL_SP
setup_c0_status STATUS_CU0|0x1001 0 #设置CP0的SR寄存器,第1,13位置1
jr ra #函数结束,返回
nop
END(set_timer)
timer_irq:
sb zero, 0xb5000110 #在地址0xb5000100中写入0,响应时间中断,暂停时钟
1: j sched_yield #跳转到调度函数,进行调度
nop
j ret_from_exception #跳转到ret_from_exception
nop
Thinking 3.10
有两个env_sched_list用于存储进程队列。
当进程被创建时,插入env_sched_list[0]的队头。
当发生时间片中断,在handle_int函数的最后会跳转到sched_yield函数。
在sched_yield()中,若当前进程还能继续运行,则继续。否则就会选出下一个要运行的进程。(具体选择next_env的方式此处略过)
实验难点展示
时钟中断的过程梳理
个人认为这是本次实验的核心和难点:理解OS处理异常的过程。
时间片轮转算法
- 若当前进程还能运行,就接着运行
- 若当前进程的时间片已经运行完
- 将要结束的进程的进程控制块插入另一列表的尾部
- 在
env_sched_list[cur_head_index]
中寻找下一进程- 当前队列若空,则切换队列,
cur_head_index = 1 - cur_head_index
- 找到
env_status == ENV_RUNNALE
的进程,跳出寻找过程 - 对于
env_status != ENV_RUNNALE
的进程,将其插入另一队列的开头(之后就都会移回来)
- 当前队列若空,则切换队列,
- 如果没有找到,接着在另一队列中寻找下一进程
- 当前队列若空,则返回错误值
- 找到
env_status == ENV_RUNNALE
的进程,跳出寻找过程 - 对于
env_status != ENV_RUNNALE
的进程,将其插入另一队列的开头
- 设置时间片运行次数,并运行选中的进程
体会与感想
本次实验让我们能够创建相关进程并且通过异常处理实现进程调度(时间片轮转)。
lab3与之前的实验有着较强的关联,需要大量使用之前lab中完成的函数,且有很多需要相互调用的新函数,阅读起来难度较大。只有事先了解清楚函数的大概作用,捋清了它和整体之间的关系,才能比较高效的理解代码。感受最明显的就是在load_icode()
,load_icode_mapper()
,load_elf()
三个函数中,如果在写函数之前不知道每一个函数的功能和其在整体之中大概的作用,就很难理解。
此外,在使用函数时还要弄明白使用每个函数的限制条件。例如bzero(start, len)
,其要求start与4对齐,否则会直接循环卡死。在无法保证起始地址与4对齐的地方,可以复制bzero()的代码块,仅使用其中按字节置0的部分。