Lab3实验报告
一、思考题
Thinking 3.1
思考envid2env 函数:
为什么envid2env 中需要判断e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
在通过索引取envs
数组中的第”id“个进程块时只取了envid
的后10位,但是envid
的后10位在生成的时候只与进程页的物理位置有关,要保证一个进程的id号完全对应,仅仅看后十位是不够的,也要确保前22位是一样的,因此e->env_id != envid
这一步确定进程的id确实是传入的envid。
如果没有这步判断则可能出现输入的id并不是进程id号,而仅仅是进程的物理位置与另一个进程相同的情况,从而造成错误。
Thinking 3.2
结合include/mmu.h 中的地址空间布局,思考env_setup_vm 函数:
- UTOP 和ULIM 的含义分别是什么,UTOP 和ULIM 之间的区域与UTOP以下的区域相比有什么区别?
- 请结合系统自映射机制解释代码中pgdir[PDX(UVPT)]=env_cr3的含义。
- 谈谈自己对进程中物理地址和虚拟地址的理解。
UTOP是用户进程读写部分的最高地址;ULIM是用户进程的最高地址,是kseg0和kuseg的分界线。UTOP到ULIM之间的区域是存放用户进程的进程块和页表信息的地方,只能被用户读取而不能被用户更改,而UTOP以下部分是用户进程可以自由读写的部分。
在env_cr3
中保存进程的页目录的物理地址,pgdir[PDX(UVPT)]
是页目录的页目录项;pgdir[PDX(UVPT)]=env_cr3
把页目录的PDX(UVPT)
项映射到了该进程的页目录的自身,即建立了自映射机制。
进程对应的地址都是虚拟地址,通过查询页表得到对应的物理地址;在实验中,不同的进程对于内核的2G的地址空间是相同的,而对于用户的2G有各自相互独立的地址空间,因此不同进程对应着不同的虚拟地址,但有可能会映射到相同的物理地址。
Thinking 3.3
找到 user_data 这一参数的来源,思考它的作用。没有这个参数可不可以?为什么?(可以尝试说明实际的应用场景,举一个实际的库中的例子)
不可以。
user_data
是为了向内层的函数传值,缺乏它的话,传递的函数指针对应的函数就获得不了所需的外部参数。
举例,C语言中的快排qsort
函数中的width
参数是为了给内部调用的compare
函数提供信息:
void qsort(
void* base,
size_t num,
size_t width,
int (*compare)(const void* e1,const void* e2)
);
Thinking 3.4
结合load_icode_mapper 的参数以及二进制镜像的大小,考虑该函数可能会面临哪几种复制的情况?你是否都考虑到了?
有四种情况:首尾均对齐、首对齐尾不对齐、首不对齐尾对齐、首尾均不对齐。
最坏的情况即为指导书中给出的首尾均不对齐的情况。
若内存的首地址没有页对齐,则需要分配一页来存它;若加载text
和data
段后末尾未页对齐,需要再分配一页存储尾部;如果前一段未页对齐,并且bss
段的首地址也未页对齐,需要再分配一页存储,并将相应的地方清空;如果bss
段末尾未对齐,需要单独分配一页存储,并将其清空。
Thinking 3.5
思考上面这一段话,并根据自己在lab2 中的理解,回答:
- 你认为这里的 env_tf.pc 存储的是物理地址还是虚拟地址?
- 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?
env_tf.pc
存储的是虚拟地址。
entry_point
对于每个进程都是一样的;因为entry_point
的值是一个虚拟地址,每次执行时都从一个固定的虚拟地址开始,这种统一对CPU比较友好,可以降低操作系统的复杂度;并且由于进程PCB不同,所以可以映射到不同的物理地址,这种不同可以依据不同的物理地址对应进程的不同含义,增加灵活性。
Thinking 3.6
请查阅相关资料解释,上面提到的epc是什么?为什么要将env_tf.pc设置为epc呢?
epc是CP0的EPC寄存器的值,保存的是进程发生中断时进程的地址;
将env_tf.pc
设置为epc是为了使处理完异常中断重新执行后,可以从发生中断的地方继续向下执行。
Thinking 3.7
关于 TIMESTACK,请思考以下问题:
- 操作系统在何时将什么内容存到了 TIMESTACK 区域
- TIMESTACK 和 env_asm.S 中所定义的 KERNEL_SP 的含义有何不同
操作系统在时钟中断时,把存放CPU寄存器状态的栈的栈顶地址存到了TIMESTACK区域。
TIMESTAKCK是时钟中断时固定的栈顶地址,KERNEL_SP是其他中断时的栈顶地址。
Thinking 3.8
试找出上述 5 个异常处理函数的具体实现位置。
handle_sys
函数在syscall.S
中;
handle_int
、handle_reserved
、handle_tlb
、handle_mod
都在genex.S
中。
Thinking 3.9
阅读 kclock_asm.S 和 genex.S 两个文件,并尝试说出 set_timer 和timer_irq 函数中每行汇编代码的作用。
set_timer
:(见注释)
LEAF(set_timer) //定义set_timer函数
li t0, 0xc8 //将0xb5000100写入0xc8,表示1秒钟中断200次
sb t0, 0xb5000100
sw sp, KERNEL_SP //将sp寄存器的值存到KERNEL_SP中,保存当前栈指针
setup_c0_status STATUS_CU0|0x1001 0 //将状态寄存器的第0位和第12位置1(4号中断)
jr ra //函数返回
nop
END(set_timer) //定义结束
timer_irq
:(见注释)
timer_irq:
sb zero, 0xb5000110 //0xb5000110写入0,关闭实时钟
1: j sched_yield //跳转到调度函数
nop
/*li t1, 0xff
lw t0, delay
addu t0, 1
sw t0, delay
beq t0,t1,1f
nop*/
j ret_from_exception //跳转到ret_from_exception
nop
Thinking 3.10
阅读相关代码,思考操作系统是怎么根据时钟周期切换进程的。
设置了两个链表队列,最开始时在队列1中。定时器负责产生中断,若时间片未用完,则时间片-1;当前进程的时间片用完之后,会将其从当前的队列取出,装入另一个队列的队尾。之后取出当前队列的头部进程,若为RUNNABLE状态,则执行,否则继续查找至找到为止。当前队列若为空,则切换另一个队列查找,如此进行调度。
二、实验难点图示
1、设置进程控制块
这一部分的难点主要在env_alloc
和env_setup_vm
两个函数上。env_alloc
作用是分配一个空闲PCB,在执行过程中调用了env_setup_vm
函数,env_setup_vm
函数的作用是初始化该进程的页目录。
在上面的思考题中已经提到过,在我们实验中,用户态的2G是私有的,并且各不相同,而内核态的2G对所有进程都是相同的。填写env_setup_vm
最大的难点便在于看懂内存分布图。
ULIM以上的页目录项即和内核的页目录项完全相同;ULIM以下的为用户区特有的地址。
ULIM=0x80000000
是操作系统分配给用户的2G地址空间的最大值,UTOP=0x7f400000
是用户能够自由读写的地址空间的最大值。UTOP到ULIM这段空间映射的是记录页面使用情况的4M大小的pages数组,4M进程控制块envs数组和用户页表域的4M虚拟空间,用户不能写只能读,用于使用户进程查看其他进程信息。
具体到env_setup_vm
函数,首先要申请页目录,此时对于用户而言,UTOP以下的区域为用户可以自由读取的区域,页目录要清零,UTOP以上的部分要和内核保持一致,要复制内核;其次为进程设置页目录和其物理地址;最后填入页目录自映射项。
对于env_allc
函数,从env_free_list
中申请出一个进程后进行初始化,并分配新资源,最后从程序链表中移出这一进程。
2、加载二进制镜像
这部分函数的最大难点在于对齐的问题,指导书上给了提示之后,思考时也容易了很多。
这一任务由load_icode
函数完成,而load_icode
函数又靠load_elf
和load_icode_mapper
完成。
在load_icode_mapper
中,要加载一个ELF文件到内存,就要将ELF文件中所有需要加载的segment加载到对应的虚地址va上。但是va和文件大小都不一定对齐4KB,并且若一段内存不满一个页,则仍要分配一整个页面来存储。最开始需要检查开头一段是否对齐,如果不对齐,申请一个页面存储这一段,拷贝也只能拷贝这一段,接着申请之后对齐的页面,最后考虑binsize<sgsize
是否成立,如果是,则将这一段全部赋0。
对于load_elf
,完成对elf文件的解构,并将文件映射到内存。
最后是load_icode
,这是真正的加载二进制镜像的函数。首先分配进程的运行栈空间(这里是用户栈),为栈空间预分配一个页面。即分配一个物理页,然后将用户栈的地址该物理页建立映射,映射的内存空间是 (USTACKTOP - BY2PG, USTACKTOP)
,之后调用load_elf
函数把二进制文件加载到内存,并设置pc寄存器,要运行的进程的代码段预先被载入到了entry_ point
为起点的内存中,运行进程时,CPU从pc所指的位置开始执行二进制码。
3、进程调度
lab3-2里题目少了很多,相较之下sched_yield
的实现难度稍大。这个函数思路很简单,但是处理时一些细节问题很容易导致**^^^^too low^^^^**的问题。
思路:
关于题目的一些理解:
- 使用两个链表,每个链表里存储着的都是待调度的进程,而且两个链表里的结点应该一样,只是存在的时间和顺序不一样;
- 正在执行的进程是存在在当前所持链表的表头位置的,当该进程的时间片用完需要切换进程时,才将该进程从当前链表头删除,添加到另一个链表尾;
一些我踩过的坑🕳:
env_run()
函数必须放在LIST_REMOVE()
、LIST_INSERT()
、count--
等后面,因为env_run()
之后的语句执行不了;- 必须先
LIST_REMOVE()
再LIST_INSERT_TAIL()
,因为删除的时候并不指明是从哪一个链表里删除,如果先INSERT再REMOVE,就会把刚刚插进去的删除掉;
三、体会与感想
这次lab第一部分花费时间较多,花了两天的时间都在断断续续的学习,大概有10个小时;第二部分学习所花费的时间相对较少,但也在解决**^^^TO LOW^^^**问题上花费了不少的时间,可能有6个小时左右。总的来说,感觉前半部分的内容比较多,很多题目在一开始学的时候理解起来还比较困难,不过经过不懈的软磨硬泡,最终也基本能看懂,不过还是还是很遗憾lab3第一次考试挂掉了,原因在于最后调用了env_run()
(不过也可能有其他问题),感觉课上和课下关联不是很大,课下学习成果和课上考试结果貌似没有必然联系,虽然也很心疼丢掉的分,但是还是真正学到东西最重要;第二部分要填的空比较少,个人感觉写汇编的难度比较大,但是Exercise没有考察这方面的知识,上机考试也没有涉及,这次上机很顺利的通过了(泪目)。
四、指导书反馈
第一是关于curenv
,我之前一直以为它和e
一样,是需要自己定义的,也因此出了很多错,后来grep
之后才知道curenv
是全局变量,希望能针对这一点稍微提醒一下。
第二是关于load_icode_mapper
这个函数,这个函数在给的时候只给了for循环,但是其实在for循环之前也有代码要填写,希望能在这里给出提示。
五、残留难点
感觉是指导书中涉及的那部分汇编的代码,让我自己写大概率写不出来。