OS_lab3
lab3实验报告
思考题
Thinking 3.1
题面
1 | |
回答
在MOS中,根据自映射机制,将页表和页目录映射到了用户空间中的0x7fc00000-0x80000000(UVPT-ULIM)区域,而由于自映射,所以物理内存上页目录单独占一页,其余二级页表单独占一页,但是在虚空间上某一个二级页表对应的虚拟页和页目录对应的虚拟页实际上重叠的,也就是某个二级页表虚地址同页目录虚地址重合,那么这两者对应的物理页框也应该重合,而页目录的每一个页表项存储的是相应二级页表所在页的基地址,因此在页目录中应该有一个页表项存储的是到页目录这一页本身的物理页框号,也即”自映射页表”。
因此,由于按需分配元组,在初始化对应页目录时需要设置映射到页目录自身的页表项,而其在页目录中的索引应该与虚空间上页目录按4KB大小在所有二级页表中的索引以及第一个二级页表按4MB大小在虚空间上的索引,也就是PDX(UVPT),PDX()的作用是取一个32位二进制数的高10位,也就是按4MB大小在虚空间上的索引,而等式右边的PADDR(e->env_pgdir) | PTE_V中的PADDR(e->env_pgdir)代表页目录的物理基地址,而该物理地址由于是通过分配好的页控制块转换而来,因此一定4KB对齐的,也就是低12位都是0,|``PTE_V也就是为其置有效位,实现了该页表项高20位是自映射到当前所在页的物理页号,而低12位则是相应的权限位,实现了自映射页目录项的设置。
Thinking 3.2
题面
1 | |
回答
相关代码中出现的位置如下:
1 | |
由上述代码可见,在load_icode()函数中调用了elf_load_seg()函数,给其传入的data参数是进程控制块指针,而elf_load_seg()函数通过回调函数map_page(),也就是load_icode_mapper()函数,其中将data参数传递给了load_icode_mapper()函数,在该函数中调用了page_insert()来建立对应进程空间中虚地址与分配的物理页面的映射关系。
因此,不可以没有该参数,若没有该参数将无法提供当前进程的页目录地址以及ASID,无法完成page_insert()函数的功能。
Thinking 3.3
题面
1 | |
回答
相关加载内容如下:

相关代码如下:
1 | |
因此该函数需要考虑segment中.text段和.data段组成的内容的起始地址所在页未对齐的页面,后续对齐的页面以及末地址未对齐的页面,还有.bss段所占页面的加载情况。
Thinking 3.4
题面
1 | |
回答
我认为这里应该是虚拟地址,因为在MOS中访问的地址永远是虚拟地址。
Thinking 3.5
题面
1 | |
回答
异常向量组:
1 | |
0、1、2、3号异常处理函数为handle_int、handle_mod
和handle_tlb(2、3相同),根据grep指令寻找到具体实现在kern/genex.S:
1 | |
Thinking 3.6
题面
1 | |
回答
1 | |
Thinking 3.7
题面
1 | |
回答
在genex.S中有如下代码:
1 | |
OS通过相应异常向量组,调用handle_int函数来处理中断,根据**CP0_CAUSE和CP0_STATUS的值来判断当前是否是7号的时钟中断,如果两者都是就跳转到相应的处理程序timer_irq,其通过schedule函数进行进程调度**。
lab3课下
Exercise 3.1
题面
1 | |
回答
由于env_free_list采用的是LIST结构,而env_sched_list采用的是TAILQ结构,因此这两者分别采用LIST_INIT()和TAILQ_INIT()两个宏来完成初始化,需要注意的是,这里传入的参数都是指针形式。由于要求将空闲PCB倒序插入空闲链表中,因此循环变量应从空闲PCB总数减1开始,直到0,使用LIST_INSERT_HEAD()宏插入到空闲链表中,注意宏的相应参数要求。
Exercise 3.2
题面
1 | |
回答
看到虚地址与物理地址建立映射,立马想到lab2中使用过的建立虚地址与物理页框之间的映射关系的函数page_insert,由于映射长度相同,即偏移量相同,因此va + i和pa + i就可以分别代表映射关系的双方,为了获取相应的物理页,可以使用pa2page()宏将pa + i转换为对应的物理页,再通过page_insert函数建立映射关系即可。
Exercise 3.3
题面
1 | |
回答
根据提示即可,将相应页面的引用次数pp_ref加一,并给相应的新建的PCB的页目录首地址修改为分配的物理地址对应的虚地址,借助**page2kva()宏进行虚实地址转换**,注意,env_pgdir是Pde *类型,需要强制类型转换。
Exercise 3.4
题面
1 | |
回答
根据提示作答即可,将题目需要初始化的结构体成员进行相应的赋值,一定要注意异常情况需要返回负值,例如:一开始应当使用LIST_EMPTY()宏来检查env_free_list是否为空,而**env_setup_vm()函数也有可能因为空闲页控制块为空而抛出异常、asid_alloc()函数由于无空闲asid而返回负值等情况,需要提前返回相应异常值**。
Exercise 3.5
题面
1 | |
回答
根据提示一步步填空即可,不过需要注意的是page_alloc()函数是有可能返回空,也就是返回一个负数的,因此需要考虑page_free_list为空的特殊情况。此外对于memcpy的使用可以参照string.c中的用法,注意写入的是带偏移量offset的位置.
1 | |
Exercise 3.6
题面
1 | |
回答
根据提示完成相应填空即可。该函数的作用就是将二进制文件中的所有段加载到内存上,通过ELF_FOREACH_PHDR_OFF遍历所有段的偏移,根据判断其是否可加载的状态(ph->p_type == PT_LOAD),若可加载就通过调用eld_load_seg()函数利用load_icode_mapper回调函数加载相应的段到内存上;否则不进行加载。
Exercise 3.7
题面
1 | |
回答
根据提示一步一步完成,首先利用env_alloc()函数分配一个新的PCB,注意可能会分配失败,即返回了负值,需要判断一下。接着将传入的priority参数赋给新分配的e的env_pri变量成员,设置其状态env_status为ENV_RUNNABLE,紧接着将传入的binary和size作为参数调用load_icode()函数加载相应段内容到内存上,并将分配好的PCB插入env_sched_list中,注意采用的宏不同于之前的,是TAILQ_INSERT_HEAD。**注意此处的field域不再是env_link而是env_sched_link**。
Exercise 3.8
题面
1 | |
回答
根据提示,env_run()函数的主要作用是保存当前进程的上下文、切换当前进程为e和修改全局变量cur_pgdir并用env_pop_tf()函数将切换后的进程的上下文恢复到相应位置异常返回。其中尤其注意env_pop_tf()的用法:extern void env_pop_tf(struct Trapframe *tf, u_int asid) __attribute__((noreturn));,其中第一个参数传入的是指针形式,而第二个参数传入的是ASID,由于ASID只有8位,因此需要对进程控制块提供的env_asid进行高24位置零处理,做法就是将其&一个NASID - 1,NASID宏的值是256。==尤其注意,在这里env_pop_tf()函数是noreturn,不返回的,在这里的不返回不是指不返回返回值,而是该函数的结束将结束调用其的函数,也就是此处env_pop_tf()函数内结束后,env_run()函数就结束了,那么env_run()函数也是noreturn的,因此调用env_run()函数会在该函数中直接结束,并不会去运行逻辑上在env_run()函数之后的语句。==
Exercise 3.9
题面
1 | |
回答
在理解的前提下根据提示,完成填空即可。该汇编代码就是根据相应的异常,跳转到相应的异常处理程序。
1 | |
Exercise 3.10
题面
1 | |
回答
根据提示完成填空即可,注意Linker Script的语法要求。
Exercise 3.11
题面
1 | |
回答
代码如下:
1 | |
根据要求将CP0_COUNT清零,注意mtc0只能将寄存器的值赋给相应的cp0寄存器,并将给定的计数器周期数t0赋给CP0_COMPARE。
Exercise 3.12
题面
1 | |
回答
代码和解析如下:
1 | |
lab3课下难点
本次lab主要涉及两个方面——进程和异常,前者主要主要考察到的是创建、加载和运行切换,而后者则主要涉及到分发与处理。
进程

进程创建
由于进程控制块(PCB)是CPU感知进程的唯一标识,因此创建进程实际上就是创建PCB,根据PCB的env_status不同,也就是进程状态的不同,分别采用调度队列env_sched_list和空闲队列env_free_list来管理PCB,前者便于管理已分配PCB和对进程调度,采用的是类似的TAILQ结构,这是因为在进程进行切换的时候会涉及到将某个进程插入队列尾的情况,而LIST结构并未实现相应宏方法。后者用来存储空闲的PCB,采用的是lab2的LIST结构。
在PCB中有两个用来标识该进程的代号,一个是env_id,一个是env_asid,简单的来说,后者是为了解决原来切换页表时全部无效化TLB页表项带来的频繁TLB Miss问题,便于TLB标识虚拟地址空间。而前者是进程的总标识,只在进程完全结束后才被释放供其他进程使用,而后者是需要等到进程被销毁或者其对应TLB被清空时,其ASID就可被分配给其他进程,两者的释放时机略有不同,可以理解为ASID就是为了实现切换页表TLB不全清空而生。
此后通过env_alloc()函数创建通过下列步骤相应的PCB:
从
env_free_list中申请一个空闲PCB。手动初始化PCB。
为新进程初始化页目录。
将该PCB从空闲链表中去除,用于使用。
加载
这部分指导书的讲解并不多,主要是要去理解下列函数的用途:
1 | |
此后再通过env_create()函数在创建完一个新的PCB的基础上去创建进程。
进程运行与切换
也就是env_run()函数的实现,大致功能如下:
保存当前进程上下文信息,一般保存在
env_tf成员变量中,实验中存放寄存器状态的地方是KSTACKTOP以下的一个sizeof(TrapFrame)大小的区域中,将后者拷贝到前者,就可达到保存进程上下文的效果。切换
curenv为即将运行进程e。设置全局变量
cur_pgdir为当前进程页目录地址。调用
env_pop_tf函数,恢复现场,异常返回。
==尤其注意,在这里env_pop_tf()函数是noreturn,不返回的,在这里的不返回不是指不返回返回值,而是该函数的结束将结束调用其的函数,也就是此处env_pop_tf()函数内结束后,env_run()函数就结束了,那么env_run()函数也是noreturn的,因此调用env_run()函数会在该函数中直接结束,并不会去运行逻辑上在env_run()函数之后的语句==。
中断异常
部分机制在计组中有所涉略,此处就指明新的出现的过程。
处理流程
处理异常大致流程:
- 设置
EPC指向异常返回的地址。 - 置
EXL位,强制CPU进入内核态(进入特权模式)并禁止中断,可不能中断套娃。 - 设置
Cause寄存器,记录异常发生原因。 - CPU开始从异常入口处取指,此后交由异常处理程序处理。
异常分发
异常分发程序将检测发生了哪种异常,并调用相应的异常处理程序,一般被要求固定在某个物理地址上,以确保正确的跳转。
实验的异常分发程序如下:
1 | |
此外,.text.exc_gen_entry段和.text.tlb_miss_entry段需被linker Script当置到特定位置,分别放置在0x80000180和0x80000000处,都是异常处理程序入口。实验系统中CPU发生异常(除用户态地址的TLB Miss异常)会自动跳转到前者,否则跳转到后者。
异常向量组
exception_handlers称为异常向量组:
1 | |
GNU C的扩展语法:[first ... last] = value,用于对数组某个区间上的元素赋同值,重叠的允许覆盖:
例如:int arr[5] = {[0 ... 3] = 1, [2 ... 4] = 2}; <=> int arr[5] = {1, 1, 2, 2, 2}
一些异常如下:
- 0号异常 处理函数为
handle_int,中断,由时钟中断、控制台中断等造成。 - 1号异常 处理函数
handle_mod,存储异常,存储操作时该页被标记为只读。 - 2号异常
handle_tlb,TLB load异常 - 3号异常
handle_tlb,TLB store异常 - 8号异常
handle_sys,系统调用,用户进程执行syscall指令陷入内核。
时钟中断
中断处理流程:
- 异常分发,判断出当前异常为中断异常,随后进入相应的中断处理程序。
- 中断处理程序进一步判断Cause寄存器中由几号中断位引发的中断,然后进入不同的中断服务函数。
- 中断处理完成,通过
ret_from_exception函数恢复现场,继续执行。
时钟中断出现原因:与时间片轮转调度算法密切相关。MOS通过硬件定时器产生的时钟中断来判断一个进程的时间片结束。当时间片结束时当前进程挂起,MOS从调度队列中选取合适的进程运行。MOS利用CP0中内置的Timer产生时钟中断。
具体有两种控制Timer的寄存器,Count与Compare寄存器,前者按某种仅与CPU流水线频率相关频率自增,后者保持不变。当两者值相等且不为0时,触发时钟中断。RESET_KCLOCK将Count清零,将Compare置为期望的计时器周期数。MOS中,时钟中断初始化发生在调度执行每一个进程前,也就是env_pop_tf调用RESET_KCLOCK,又在RESTORE_aLL中恢复Status寄存器,开启中断。
时钟中断一产生会触发4KC硬件的异常中断处理流程,地址指向0x80000180,跳转到相应代码段执行,对时钟引起的中断,通过.text_exc_gen_entry代码段分发,调用handle_int函数处理,其根据Cause值判断是否为7号中断位引发的时钟中断,是则执行timer_irq,跳转到schedule执行。
进程调度
算法:用N*TIMER_INTERVAL来量化时间片长度,此处的N是进程的优先级,遍历调度链表找出当前的最优被调度进程。schedule函数被调用时,(该函数也是本次实验的难点,一定要注意认真理解清楚了再下手操作,==尤其注意运行进程时都要将count–、因阻塞而切换不需要再删除队首、env_run()函数的noreturn属性等==)当前正运行进程在curenv中,其剩余时间片长度存储在count中,当下列情况发生时,进行进程切换:
- 尚未调度过任何进程,即
curenv为NULL. - 当前进程已用完时间片,
count== 0. - 当前进程不再就绪,被阻塞或退出.
yield参数指定须发生切换.
无需切换时,只需让count自减一,调用env_run函数,继续执行当前进程,发生切换时,还需判断当前进程是否仍就绪,若是将其移至调度链表末尾,之后选中调度链表首部进程来调度运行,count设置为其优先级。
TLB Miss异常的产生与处理
产生
4Kc的MMU对kuseg段的地址映射,仅由TLB完成。正常情况下,TLB命中虚地址的访存过程全由硬件完成,OS不插手,但是当TLB在转换时发现TLB中还没对应的映射项目,就会产生一个TLB Miss异常。硬件会打断无法继续进行的访存操作,陷入内核并跳转执行OS的对应异常处理程序(tlb_miss_entry),由OS查找页表,填上缺失的TLB项,再从异常返回,重执行打断的访存。
相应的异常处理程序为handle_tlb,其通过宏BUILD_HANDLER包装了do_tlb_refill函数完成TLB重填:
硬件:发生TLB Miss时,CPU将引发地址装入BadVAddr中、虚页号填入EntryHi中的VPN域,Cause中的ExcCode域写为TLBL(读请求)或TLBS(写请求)。
软件:从BadVAddr中取出引发地址,在cur_pgdir中查找该地址对应的物理地址与权限位,然后将相应物理页号和权限位填入EntryLo的PFN域和权限位,用tlbwr将EntryHi和EntryHi中相应内容随机写入TLB中的一项中,最后调用ret_from_exception从异常返回。
心得体会
此次作业的代码量不算大,但是要真正理解进程创建、加载到切换运行而阅读的代码量是巨大的,大量的函数需要去厘清相应实现和关联调用,着实是个巨大的挑战,为此也花了近10个小时来完成课下。
尽管如此,还是在课上折戟,本质还是并没有真正理解一些函数的性质,像env_run()函数的noreturn属性等,这些本来应该在课下解决的却堆积到了课上,导致了课上的败北,值得敲响警钟。
总而言之,此次lab需要真正理解每一行代码、每一个函数的作用,只有这样才能对完成题目得心应手,之后的lab需要去贯彻落实这一点!




