本实验与处理机调度无关,仍然是进程调度(或者说是单处理机上的进程调度),只不过相比实验五的非抢占调度算法,这里使用了时间片轮转算法和步长算法,更加接近实际的操作系统。
本实验使用的编译命令:
make qemu
or
make run-priority # 更普遍
or
make grade # 跑单测
实验执行流程概述
在实验五,创建了用户进程,并让它们正确运行。这中间也实现了FIFO调度策略。可通过阅读实验五下的 kern/schedule/sched.c 的 schedule 函数的实现来了解其FIFO调度策略。与实验五相比,实验六专门需要针对处理器调度框架和各种算法进行设计与实现,为此对ucore的调度部分进行了适当的修改,使得kern/schedule/sched.c 只实现调度器框架,而不再涉及具体的调度算法实现。而调度算法在单独的文件(default_sched.[ch])中实现。
除此之外,实验中还涉及了idle进程的概念。当cpu没有进程可以执行的时候,系统应该如何工作?在实验五的scheduler实现中,ucore内核不断的遍历进程池,直到找到第一个runnable状态的 process,调用并执行它。也就是说,当系统没有进程可以执行的时候,它会把所有 cpu 时间用在搜索进程池,以实现 idle的目的。但是这样的设计不被大多数操作系统所采用,原因在于它将进程调度和 idle 进程两种不同的概念混在了一起,而且,当调度器比较复杂时,schedule 函数本身也会比较复杂,这样的设计结构很不清晰而且难免会出现错误。所以在此次实验中,ucore建立了一个单独的进程(kern/process/proc.c 中的 idleproc)作为 cpu 空闲时的 idle 进程,这个程序是通常一个死循环。
接下来可看看实验六的大致执行过程,在init.c中的kern_init函数增加了对sched_init函数的调用。sched_init函数主要完成了对实现特定调度算法的调度类(sched_class)的绑定,使得ucore在后续的执行中,能够通过调度框架找到实现特定调度算法的调度类并完成进程调度相关工作。为了更好地理解实验六整个运行过程,这里需要关注的重点问题包括:
- 何时或何事件发生后需要调度?
- 何时或何事件发生后需要调整实现调度算法所涉及的参数?
- 如何基于调度框架设计具体的调度算法?
- 如何灵活应用链表等数据结构管理进程调度?
进程状态
在此次实验中,进程的状态之间的转换需要有一个更为清晰的表述,在 ucore中,runnable的进程会被放在运行队列中。值得注意的是,在具体实现中,ucore定义的进程控制块struct proc_struct包含了成员变量state,用于描述进程的运行状态,而running和runnable共享同一个状态(state)值(PROC_RUNNABLE。不同之处在于处于running态的进程不会放在运行队列中。进程的正常生命周期如下:
- 进程首先在 cpu 初始化或者 sys_fork 的时候被创建,当为该进程分配了一个进程控制块之后,该进程进入 uninit态(在proc.c 中 alloc_proc)。
- 当进程完全完成初始化之后,该进程转为runnable态。
- 当到达调度点时,由调度器 sched_class 根据运行队列rq的内容来判断一个进程是否应该被运行,即把处于runnable态的进程转换成running状态,从而占用CPU执行。
- running态的进程通过wait等系统调用被阻塞,进入sleeping态。
- sleeping态的进程被wakeup变成runnable态的进程。
- running态的进程主动 exit 变成zombie态,然后由其父进程完成对其资源的最后释放,子进程的进程控制块成为unused。
- 所有从runnable态变成其他状态的进程都要出运行队列,反之,被放入某个运行队列中。
sched_init()
#define MAX_TIME_SLICE 5
45 void
46 sched_init(void) {
# 初始化定时器链表
47 list_init(&timer_list);
48
# 设置当前的调度器为时间片轮转算法调度器
49 sched_class = &default_sched_class;
50
# 初始化运行时队列
51 rq = &__rq;
52 rq->max_time_slice = MAX_TIME_SLICE;
# 调用RR_init初始化运行时队列
53 sched_class->init(rq);
54
55 cprintf("sched class: %s\n", sched_class->name);
56 }
struct sched_class default_sched_class
50 struct sched_class default_sched_class = {
51 .name = "RR_scheduler",
52 .init = RR_init,
53 .enqueue = RR_enqueue,
54 .dequeue = RR_dequeue,
55 .pick_next = RR_pick_next,
56 .proc_tick = RR_proc_tick,
57 };
RR_init()
7 static void
8 RR_init(struct run_queue *rq) {
9 list_init(&(rq->run_list));
10 rq->proc_num = 0;
11 }
wakeup_proc()
这里出现于实验五不同的地方。
58 void
59 wakeup_proc(struct proc_struct *proc) {
60 assert(proc->state != PROC_ZOMBIE);
61 bool intr_flag;
62 local_intr_save(intr_flag);
63 {
64 if (proc->state != PROC_RUNNABLE) {
65 proc->state = PROC_RUNNABLE;
66 proc->wait_state = 0;
67 if (proc != current) {
# 注意这里,与实验五不同!!!
68 sched_class_enqueue(proc);
69 }
70 }
71 else {
72 warn("wakeup runnable process.\n");
73 }
74 }
75 local_intr_restore(intr_flag);
76 }
schedule()
和实验五稍有不同
78 void
79 schedule(void) {
80 bool intr_flag;
81 struct proc_struct *next;
82 local_intr_save(intr_flag);
83 {
84 current->need_resched = 0;
85 if (current->state == PROC_RUNNABLE) {
86 sched_class_enqueue(current);
87 }
88 if ((next = sched_class_pick_next()) != NULL) {
89 sched_class_dequeue(next);
90 }
91 if (next == NULL) {
92 next = idleproc;
93 }
94 next->runs ++;
95 if (next != current) {
96 proc_run(next);
97 }
98 }
99 local_intr_restore(intr_flag);
100 }
sched_class_enqueue()->RR_enqueue()
即把某进程的进程控制块指针放入到rq队列末尾,且如果进程控制块的时间片为0,则需要把它重置为rq成员变量max_time_slice。这表示如果进程在当前的执行时间片已经用完,需要等到下一次有机会运行时,才能再执行一段时间。
16 static inline void
17 sched_class_enqueue(struct proc_struct *proc) {
18 if (proc != idleproc) {
19 sched_class->enqueue(rq, proc);
20 }
21 }
13 static void
14 RR_enqueue(struct run_queue *rq, struct proc_struct *proc) {
15 assert(list_empty(&(proc->run_link)));
16 list_add_before(&(rq->run_list), &(proc->run_link));
17 if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
# rq->max_time_slice 是 5
18 proc->time_slice = rq->max_time_slice;
19 }
20 proc->rq = rq;
21 rq->proc_num ++;
22 }
sched_class_pick_next()->RR_pick_next()
28 static inline struct proc_struct *
29 sched_class_pick_next(void) {
# 从运行时队列里顺序挑选一个进程PCB
30 return sched_class->pick_next(rq);
31 }
31 static struct proc_struct *
32 RR_pick_next(struct run_queue *rq) {
33 list_entry_t *le = list_next(&(rq->run_list));
34 if (le != &(rq->run_list)) {
35 return le2proc(le, run_link);
36 }
37 return NULL;
38 }
sched_class_dequeue()->RR_dequeue()
把就绪进程队列rq的进程控制块指针的队列元素删除,并把表示就绪进程个数的proc_num减一。
23 static inline void
24 sched_class_dequeue(struct proc_struct *proc) {
25 sched_class->dequeue(rq, proc);
26 }
24 static void
25 RR_dequeue(struct run_queue *rq, struct proc_struct *proc) {
26 assert(!list_empty(&(proc->run_link)) && proc->rq == rq);
27 list_del_init(&(proc->run_link));
28 rq->proc_num --;
29 }
sched_class_proc_tick()->RR_proc_tick()
每次timer到时后,trap函数将会间接调用此函数来把当前执行进程的时间片time_slice减一。如果time_slice降到零,则设置此进程成员变量need_resched标识为1,这样在下一次中断来后执行trap函数时,会由于当前进程程成员变量need_resched标识为1而执行schedule函数,从而把当前执行进程放回就绪队列末尾,而从就绪队列头取出在就绪队列上等待时间最久的那个就绪进程执行。
33 void
34 sched_class_proc_tick(struct proc_struct *proc) {
35 if (proc != idleproc) {
36 sched_class->proc_tick(rq, proc);
37 }
38 else {
39 proc->need_resched = 1;
40 }
41 }
40 static void
41 RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
42 if (proc->time_slice > 0) {
43 proc->time_slice --;
44 }
45 if (proc->time_slice == 0) {
46 proc->need_resched = 1;
47 }
48 }