OS-lab3

OS-lab3

上机

exam

我们要修改调度函数,实现多用户的(公平)进程调度,就是在Env结构体了新加一个属性:u_int env_user([0,4])(测试程序会给你赋值),最多不超过5名用户。简言之,在需要调度新的进程的时候你需要统计每个用户的总时间片选,总时间片选保存static int[] 数组中,然后你需要选出当前有进程的用户并且总时间片选最少的(相同则id最小)的用户,然后选出遍历队列选出第一个当前用户的进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void schedule(int yield)
{
static int count = 0; // remaining time slices of current env
struct Env *e = curenv;
static int user_time[5]; //总时间片选
int able[5] = {0, 0, 0, 0, 0}; //记录有进程的用户
struct Env *temp;
TAILQ_FOREACH(temp, &env_sched_list, env_sched_link)
{
able[temp->env_user] = 1; //循环队列,搜索所有有进程的用户
}
/* We always decrease the 'count' by 1.
*
* If 'yield' is set, or 'count' has been decreased to 0, or 'e' (previous 'curenv') is
* 'NULL', or 'e' is not runnable, then we pick up a new env from 'env_sched_list' (list of
* all runnable envs), set 'count' to its priority, and schedule it with 'env_run'. **Panic
* if that list is empty**.
*
* (Note that if 'e' is still a runnable env, we should move it to the tail of
* 'env_sched_list' before picking up another env from its head, or we will schedule the
* head env repeatedly.)
*
* Otherwise, we simply schedule 'e' again.
*
* You may want to use macros below:
* 'TAILQ_FIRST', 'TAILQ_REMOVE', 'TAILQ_INSERT_TAIL'
*/
/* Exercise 3.12: Your code here. */
if (yield || !e || !count || e->env_status != ENV_RUNNABLE)
{
if (e != NULL && e->env_status == ENV_RUNNABLE)
{
TAILQ_REMOVE(&env_sched_list, e, env_sched_link);
TAILQ_INSERT_TAIL(&env_sched_list, e, env_sched_link);
user_time[e->env_user] += e->env_pri; // 时间片选+=env_pri
}
if (TAILQ_EMPTY(&env_sched_list))
{
panic("no runnable envs");
}
else
{
int i = 0;
int flag = -1;
int min = 114514;
for (i = 0; i < 5; i++) //遍历找到时间片最小的用户
{
if (able[i] && user_time[i] < min)
{
min = user_time[i];
flag = i;
}
}
TAILQ_FOREACH(temp, &env_sched_list, env_sched_link) //找到当前用户队列中第一个进程,并run
{
if (temp->env_user == flag)
{
break;
}
}
e = temp;
count = e->env_pri;
}
}
count--;
env_run(e);
}

extra

完成overflow的异常处理

  • 请在完成异常处理函数后修改 lib/traps.c ,将你自己编写的异常处理函数加入异常向量组 中的对应位置。
  • 大家可以使用 lib/genex.S 中定义的 BUILD_HANDLER 宏来构建自己的异常处理函数,构建方法可以参考已有的 handle_tlb 等处理函数。
  • 异常处理(对应指令有三个add,sub,addi)
    • 对于addi,addi $t, s,imm:处理为s, imm : 处理为s = $t/2 + imm/2; epc += 4;
    • add和sub,转换成addu和subu指令

难点是怎么从进程空间中取出指令,我们知道代码段对应页权限肯定没有PTE_D,因此在用户空间tlb访问就会异常,合理的方法是在内核态中异常处理函数,将进程中的env_va转换成物理地址,并将物理地址转换为kseg0地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void do_ov(struct Trapframe *tf)
{
u_long env_va = tf->cp0_epc;
Pde *cpgdir = curenv->env_pgdir;
curenv->env_ov_cnt++;
Pte *pte;
pgdir_walk(cpgdir, env_va, 0, &pte); //找到二级页表项
u_long va = KADDR((*pte & ~(0xFFF)) + (env_va & 0xFFF)); //转换为kseg0虚拟地址,记着*pte一定要取高20位,或者用宏PTE_ADDR,还要加上页内偏移
u_int ins = *(int *)va; //取指令
if (ins & (1 << 29))
{ // addi
int s = (ins & (31 << 21)) >> 21; //取s寄存器
int t = (ins & (31 << 16)) >> 16; //取t寄存器
int imm = ins & (0xFFFF); //立即数
tf->regs[t] = tf->regs[s] / 2 + imm / 2;
tf->cp0_epc += 4;
printk("addi ov handled\n");
}
else if (ins & (1 << 1))
{ // sub
printk("sub ov handled\n");
*(int *)va = ins | 1; //指令改为subu
}
else
{ // add
printk("add ov handled\n");
*(int *)va = ins | 1; //改为addu
}
}

实验报告

一、思考题

thinking 3.1

完成进程页目录的自映射,并且将标志位设置为有效

thinking 3.2

来自elf_laod_seg函数传入,利用回调函数将程序段加载到进程制定虚拟空间中,data参数是必须的,否则我们无法得到程序段的地址

thinking 3.3

  • va是否是页对齐的
  • 将全部短内容加载后,实际占用的filesize是否等于memsize,如果不是则需要申请新的页空间

thinking 3.4

我们知道,进程切换是需要陷入内核态的,所以epc存储的是虚拟地址

thinking 3.5

在genex.S中

thinking 3.6

  • enable_irq 设置cp0状态寄存器允许中断,即项CP0_STATUS写入(STATUS_CU0 | STATUS_IM4 | STATUS_IEc)
  • timer_irq 表示响应时钟中断,执行schedule函数

thinking 3.7

先初始化时钟并且设置中断可相应,当时间片用完时表示要切换进程,发出中断陷入内核,进入handle_int函数,然后根据识别中断类型,是时钟中断然后跳转到schedule函数

二、实验难点

回调函数icode_mapper

根据elf_load_seg需要理解加载段内容到进程中可能碰到的情况,考虑是否也对齐

env_setup_vm函数实现将envs和pages拷贝到用户空间

很抽象,首先pages和evs是放在内核态代码全局变量附近,完成将这两个放到用户态指定位置,我们知道用户态是通过tlb和页表访问的,所以只需要将这两个控制块所在的页映射到用户态能访问到的UTOP到UVPT之间(在map_segment实现)但这只是实现了从内核态到内核态的平移,目的是在进程页表初始化的时候复制到进程页表,这是很巧妙的复制方法,不同虚拟空间的数据拷贝

schedule

如果要切换进程,需要判断当前队列是否为空,当前进程的状态是否仍是RUNNABLE,如果是的话需要把它放到队尾。并且切换进程是env_run所干的事

三、实验体会

经过lab0-lab3,发现linux系统实现的很巧妙,如果我们从0搭建一个操作系统真的是难上加难,所以要学习他们的思想,比如将一些很难得任务切分成小任务,然后对函数进行封装,比如加载段内容到进程中。

OS变得越来越抽象,想要学好必须要真正读懂,如果只是参考往届代码,根据提示补全代码,在上机的时候是肯定做不出来的,对于已经写好的宏和函数,要理解它们的功能和实现,在上机写的时候才能灵活应用


OS-lab3
https://etherialize.github.io/2023/04/17/OS-lab3/
作者
HZY
发布于
2023年4月17日
许可协议