BUAA OS Lab3-2课上测试
BUAA OS Lab3-2 课上测试
一、exam
1.题目
在原有的时间片轮转上修改调度算法。
-
在全局有三个调度队列,记为 env_sched_list[3] ,每个队列中的进程单次运行的时间片数量为进程优先级乘以不同的权重,具体的:
-
-
env_sched_list[0] 中进程单次运行时间片数 = 进程优先级数 * 1
-
env_sched_list[1] 中进程单次运行时间片数 = 进程优先级数 * 2
-
env_sched_list[2] 中进程单次运行时间片数 = 进程优先级数 * 4
-
-
进程创建时全部插入到第一个调度队列(即 env_sched_list[0] )的队首
-
进程时间片用完后,根据自身优先级数值加入到另外两个调度队列队尾,若自身优先级为奇数,则顺次加入到下一个调度队列队尾,若为偶数,则加入到下下个调度队列队尾。当然,进程不再处于原调度队列中。具体地:
-
-
env_sched_list[0] 中的进程时间片耗完后,若优先级为奇数,加入到 env_sched_list[1] 队尾;若优先级为偶数,加入到 env_sched_list[2] 队尾
-
env_sched_list[1] 中的进程时间片耗完后,若优先级为奇数,加入到 env_sched_list[2] 队尾;若优先级为偶数,加入到 env_sched_list[0] 队尾
-
env_sched_list[2] 中的进程时间片耗完后,若优先级为奇数,加入到 env_sched_list[0] 队尾;若优先级为偶数,加入到 env_sched_list[1] 队尾
-
-
sched_yield 函数首先从 env_sched_list[0] 队列开始调度,之后依次按照 0 , 1 , 2 , 0 , 1 , ......的顺序切换队列,且仅在当前队列中没有可运行进程时切换到下一个队列
-
其他关于进程是否可以运行的条件与 lab3 课下要求相同
-
为便于评测,请在每次调用 sched_yield 函数时打印换行符,将连续运行的两进程输出分隔开
2.题目理解与解决步骤
-
env_sched_list队列从2个变成了3个。这意味着我们需要修改原有文件中的env_sched_list(grep -r env_sched_list),包括include/env.h,lib/env.c中的env_sched_list[2]
-
sched_yield中,point(当前调度队列)的轮转:从0、1轮流变化变成0、1、2轮流变化。可以通过取余来实现point的循环变化。
point = (point + 1) % 3;
-
sched_yield中,进程的时间片数量,需要乘上权重。可以先构造int weights[3] = {1, 2, 4}; 再通过weights[point]来获取当前队列对应的权重,即可获取时间片数量
int weights[3] = {1, 2, 4};
count = e->env_pri * weights[point]; -
sched_yield中,进程的插入队列变化,如果env_pri为奇数,则插入下个队列;若为偶数,则插入下下个队列。在获取下一个调度的进程时,同时获取其env_pri,并根据奇偶插入对应队列;其中,“下个队列”、“下下个队列”可以参照point的轮转。
LIST_REMOVE(nexte, env_sched_link); // 记得LIST_REMOVE
if ((nexte->env_pri % 2) == 1) { // 奇数
LIST_INSERT_TAIL(&env_sched_list[(point + 1) % 3], nexte, env_sched_link);
}
else LIST_INSERT_TAIL(&env_sched_list[(point + 2) % 3], nexte, env_sched_link); // 偶数
3.注意点
-
记得修改include/env.h和lib/env.c中的env_sched_list
-
获取队列的第一个进程时,记得LIST_REMOVE
-
每次调用sched_yield,记得printf("\n");(题目要求)
-
需要判断env_status == ENV_RUNNABLE(课下已经完成过了)
4.代码
void sched_yield(void)
{
static int count = 0; // remaining time slices of current env
static int point = 0; // current env_sched_list index
static struct Env* e = NULL;
static struct Env* nexte = NULL;
static int jiFlag = 0; // 奇数flag(emm 这个名字,主要是忘了奇数的英文...)
static int weights[3] = {1,2,4};
if (count == 0 || e == NULL || e->env_status != ENV_RUNNABLE) {
while (1) {
if (LIST_EMPTY(&env_sched_list[point])) point = (point + 1) % 3; // point轮转
nexte = LIST_FIRST(&env_sched_list[point]);
if (nexte->env_pri % 2 == 0) jiFlag = 0; // 判断奇数
else jiFlag = 1;
LIST_REMOVE(nexte, env_sched_link);
// 根据奇偶插入相应队列队尾
if (jiFlag) LIST_INSERT_TAIL(&env_sched_list[(point+1)%3], nexte, env_sched_link);
else LIST_INSERT_TAIL(&env_sched_list[(point+2)%3], nexte, env_sched_link);
if (nexte != NULL && nexte->env_status == ENV_RUNNABLE) {
break;
}
}
// find next env -> renew count
e = nexte;
count = e->env_pri * weights[point];
}
count--;
env_run(e);
//env_run(LIST_FIRST(env_sched_list));
}
二、extra
1. 题目
实现Adel异常的处理。
Adel的触发有两种情况:一种是在用户态试图读取 kuseg 外的地址,另一种是试图从一个不对齐的地址读取字或半字(如 lw $t0, 1($0) 就会触发该异常)。extra测试中只针对第二种进行处理,处理方式如下:
-
如果异常由 lw 指令触发,则将发生异常的指令替换为 lh 指令。
-
如果异常由 lh 指令触发,则将发生异常的指令替换为 lb 指令。
指令替换过程只修改指令 26-31 位,不修改寄存器编号和 offset 字段。
lw、lh、lb指令格式:
opcode | rs | rt | immediate |
---|---|---|---|
31-26 | 25-21 | 20-16 | 15-0 |
指令 | opcode |
---|---|
lb | 100000 |
lh | 100001 |
lw | 100011 |
2. 异常处理的过程
由于这题涉及到异常的处理,因此刚好借此复习和总结下异常处理的流程。
经过课下的学习,我们知道,当发生异常时,硬件将设置好EPC、SR、CAUSE等寄存器后,调整到异常处理入口(也就是异常的分发),接下来就由操作系统开始执行了。
2.1 异常分发(boot/start.S)
进入异常分发函数后,首先取出CAUSE寄存器中的ExcCode,并以此为偏移从异常向量组exception_handlers中获取相应的异常处理函数(handle_xxx)地址,然后跳转到该异常处理函数。
2.2 异常处理函数 handle_xxx (lib/genex.S) do_xxx(lib/traps.c)
handle_xxx的异常处理函数可以通过宏BUILD_HANDLER来构造,因为它们的功能都差不多,基本上就是SAVE_ALL、j do_xxx、j ret_from_exception.
在BUILD_HANDLER中,首先是通过SAVE_ALL来保存现场。然后就跳转到相应的函数中(jal do_xxx),(笔者认为也许do_xxx才算真正的异常处理函数),处理完毕后,跳转到ret_from_exception,从异常中返回。
2.3 异常返回 ret_from_exception
ret_from_exception主要进行恢复现场的操作,即将所有寄存器的值恢复到发生异常前的值,以及将PC设置为EPC。
3. 题目理解与解决步骤
经过以上对异常处理过程的分析,我们可以知道,当我们想要处理一个新的异常种类时,需要完成以下几点:
-
往异常向量组中塞一个对应的处理函数地址,即handle_xxx的地址,塞到ExcCode对应的偏移位置上去。
-
实现handle_xxx函数(通过BUILD_HANDLER),使其跳转到“真正的处理函数”do_xxx
-
实现do_xxx函数,在其中实现相应的处理操作
因此,对于本次的Adel异常,我们需要完成:
-
往异常向量组的偏移4位置上塞入handle_adel的地址(Adel的ExcCode = 4),具体操作为在lib/traps.c中加入
set_except_vector(4, handle_adel);
-
通过BUILD_HANDLER实现handle_adel函数,具体操作为在lib/genex.S中加入
BUILD_HANDLER adel do_adel cli
-
实现do_adel函数,具体操作为在lib/traps.c中实现do_adel函数
根据题目描述,我们需要取出发生异常的指令的机器码并将其修改。但是,这条指令在哪儿呢?
根据前面的分析,我们知道异常处理函数handler_adel会先通过SAVE_ALL保存现场,而对于非时钟中断的异常,会将sp指向KERNEL_SP,然后向下开出大小为TF_SIZE(sizeof(struct Trapframe))的栈空间,把寄存器的值保存进去(具体可以查看SAVE_ALL、get_sp的宏定义)。所以EPC也被保存到这里了!EPC又刚好是发生异常的指令的地址,因此我们就可以通过EPC找到对应的指令了。找到这条指令后,修改其OpCode就不是难事了。
void do_adel(char* sp) {// handler_adel把sp寄存器的值传给do_adel(BUILD_HANDLER中 move a0,sp j do_adel
struct Trapframe *tf;
tf = (struct Trapframe*)sp; // 找到保存所有寄存器的栈空间
u_int instr; // 对应指令
u_int* instr_ptr; // 对应指令的地址(PC)
instr_ptr = tf->cp0_epc; // 获取了epc
instr = *instr_ptr; // 获取对应指令
u_int opcode = instr & 0xFC000000;
opcode = opcode >> 26; // 获取opcode
instr = instr & 0x03FFFFFF; // 把instr的opcode先清0
/* 开始处理opcode */
if (opcode == 0b100011) { // lw 变成 lh
opcode = 0b100001;
}
else { // lh 变成 lb
opcode = 0b100000;
}
opcode = opcode << 26;
instr = instr | opcode;
*instr_ptr = instr; // 修改指令机器码
return;
}
[ There's still one thing you failed to consider ]
[ Guess what that is ]
如果只做到这里,已经可以获得99分啦!(当然,这是助教的怜悯,感谢助教!!)
剩下的1分是什么呢?笔者在课上也没想出来,直接开溜(bushi)
经过群里同学的讨论,可能是延迟槽的判断,即如果BD位为1,那么我们对应的lw/lh/lb指令的地址是EPC+4.