实验目的
- 理解操作系统的调度管理机制
- 熟悉 ucore 的系统调度器框架,以及缺省的Round-Robin 调度算法
- 基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法
熟悉ucore的系统调度器框架,以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,完成Stride Scheduling调度算法。
进程状态
进程的正常生命周期如下:
- 进程首先在 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态变成其他状态的进程都要出运行队列,反之,被放入某个运行队列中。
内核抢占点
调度本质上体现了对CPU资源的抢占。对于用户进程而言,由于有中断的产生,可以随时打断用户进程的执行,转到操作系统内部,从而给了操作系统以调度控制权,让操作系统可以根据具体情况(比如用户进程时间片已经用完了)选择其他用户进程执行。这体现了用户进程的可抢占性(preemptive)。但如果把ucore操作系统也看成是一个特殊的内核进程或多个内核线程的集合,那ucore是否也是可抢占的呢?其实ucore内核执行是不可抢占的(non-preemptive),即在执行“任意”内核代码时,CPU控制权可被强制剥夺。这里需要注意,不是在所有情况下ucore内核执行都是不可抢占的,有以下几种“固定”情况是例外:
- 进行同步互斥操作,比如争抢一个信号量、锁(lab7中会详细分析);
- 进行磁盘读写等耗时的异步操作,由于等待完成的耗时太长,ucore会调用shcedule让其他就绪进程执行。
这几种情况其实都是由于当前进程所需的某个资源(也可称为事件)无法得到满足,无法继续执行下去,从而不得不主动放弃对CPU的控制权。如果参照用户进程任何位置都可被内核打断并放弃CPU控制权的情况,这些在内核中放弃CPU控制权的执行地点是“固定”而不是“任意”的,不能体现内核任意位置都可抢占性的特点。我们搜寻一下实验五的代码,可发现在如下几处地方调用了shedule函数:
表一:调用进程调度函数schedule的位置和原因
编号 | 位置 | 原因 |
---|---|---|
1 | proc.c::do_exit | 用户线程执行结束,主动放弃CPU控制权。 |
2 | proc.c::do_wait | 用户线程等待子进程结束,主动放弃CPU控制权。 |
3 | proc.c::init_main | 1. initproc内核线程等待所有用户进程结束,如果没有结束,就主动放弃CPU控制权; 2. initproc内核线程在所有用户进程结束后,让kswapd内核线程执行10次,用于回收空闲内存资源 |
4 | proc.c::cpu_idle | idleproc内核线程的工作就是等待有处于就绪态的进程或线程,如果有就调用schedule函数 |
5 | sync.h::lock | 在获取锁的过程中,如果无法得到锁,则主动放弃CPU控制权 |
6 | trap.c::trap | 如果在当前进程在用户态被打断去,且当前进程控制块的成员变量need_resched设置为1,则当前线程会放弃CPU控制权 |
仔细分析上述位置,第1、2、5处的执行位置体现了由于获取某种资源一时等不到满足、进程要退出、进程要睡眠等原因而不得不主动放弃CPU。第3、4处的执行位置比较特殊,initproc内核线程等待用户进程结束而执行schedule函数;idle内核线程在没有进程处于就绪态时才执行,一旦有了就绪态的进程,它将执行schedule函数完成进程调度。这里只有第6处的位置比较特殊:
if (!in_kernel) {
……
if (current->need_resched) {
schedule();
}
}
这里表明了只有当进程在用户态执行到“任意”某处用户代码位置时发生了中断,且当前进程控制块成员变量need_resched为1(表示需要调度了)时,才会执行shedule函数。这实际上体现了对用户进程的可抢占性。如果没有第一行的if语句,那么就可以体现对内核代码的可抢占性。但如果要把这一行if语句去掉,我们就不得不实现对ucore中的所有全局变量的互斥访问操作,以防止所谓的racecondition现象,这样ucore的实现复杂度会增加不少。
练习0:填写已有实验
直接对比合入代码,需要做如下修改:
kern/process/proc.c->alloc_proc
// LAB6
proc->rq = NULL;
// 初始化run_link
list_init(&(proc->run_link));
// memset(&proc->run_link, 0, sizeof(list_entry_t));
proc->time_slice = 0;
// 初始化lab6_run_pool
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;
// memset(&proc->lab6_run_pool, 0, sizeof(skew_heap_entry_t));
proc->lab6_stride = 0;
proc->lab6_priority = 1;
kern/trap/trap.c->trap_dispatch
case IRQ_OFFSET + IRQ_TIMER:
ticks ++;
assert(current != NULL);
sched_class_proc_tick(current);
break;
练习1: 使用 Round Robin 调度算法(不需要编码)
完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件)个人完成的lab5和练习0完成后的刚修改的lab6之间的区别,分析了解lab6采用RR调度算法后的执行过程。执行make grade,大部分测试用例应该通过。但执行priority.c应该过不去。
请在实验报告中完成:
-
请理解并分析sched_class中各个函数指针的用法,并结合Round Robin 调度算法描ucore的调度执行过程
sched_class代码如下:
struct sched_class { // the name of sched_class const char *name; // Init the run queue void (*init)(struct run_queue *rq); // put the proc into runqueue, and this function must be called with rq_lock void (*enqueue)(struct run_queue *rq, struct proc_struct *proc); // get the proc out runqueue, and this function must be called with rq_lock void (*dequeue)(struct run_queue *rq, struct proc_struct *proc); // choose the next runnable task struct proc_struct *(*pick_next)(struct run_queue *rq); // dealer of the time-tick void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc); /* for SMP support in the future * load_balance * void (*load_balance)(struct rq* rq); * get some proc from this rq, used in load_balance, * return value is the num of gotten proc * int (*get_proc)(struct rq* rq, struct proc* procs_moved[]); */ };
init->RR_init
即初始化
enqueue->RR_enqueue
某个进程加入队列中
dequeue->RR_dequeue
某个进程出队
pick_next->RR_pick_next
在就绪队列中选择一个最适合运行的进程(选择进程但不将从队列中移除,通过dequeue出队)
proc_tick->RR_proc_tick
在时钏中断处理例程中被调用,以减小当前运行进程的剩余时间片。若时间片耗尽,则设置当前进程的
need_resched
为1。 -
请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计
多级反馈队列调度算法(Multilevel Feedback Queue Scheduling)
参考:多级反馈队列调度算法
假设系统中有3个反馈队列Q1,Q2,Q3,时间片分别为2,4,8
设有3个作业J1,J2,J3分别在时间 0 ,1,3时刻到达。而它们所需要的CPU时间分别是3,2,1个时间片。
**1、**时刻0 J1到达。于是进入到队列1 , 运行1个时间片 , 时间片还未到,此时J2到达。
**2、**时刻1 J2到达。 由于同一队列采用先来先服务,于是J2等待。 J1在运行了1个时间片后,已经完成了在Q1中的2个时间片的限制,于是J1置于Q2等待被调度。当前处理机分配给J2。
**3、**时刻2 J1进入Q2等待调度,J2获得CPU开始运行。
**4、**时刻3 J3到达,由于同一队列采用先来先服务,故J3在Q1等待调度,J1也在Q2等待调度。
**5、**时刻4 J2处理完成,由于J3,J1都在等待调度,但是J3所在的队列比J1所在的队列的优先级要高,于是J3被调度,J1继续在Q2等待。
**6、**时刻5 J3经过1个时间片,完成。
**7、**时刻6 由于Q1已经空闲,于是开始调度Q2中的作业,则J1得到处理器开始运行。 J1再经过一个时间片,完成了任务。于是整个调度过程结束。
练习2: 实现 Stride Scheduling 调度算法(需要编码)
首先需要换掉RR调度器的实现,即用default_sched_stride_c覆盖default_sched.c。然后根据此文件和后续文档对Stride度器的相关描述,完成Stride调度算法的实现。
后面的实验文档部分给出了Stride调度算法的大体描述。这里给出Stride调度算法的一些相关的资料(目前网上中文的资料比较欠缺)。
- strid-shed paper location1
- strid-shed paper location2
- 也可GOOGLE “Stride Scheduling” 来查找相关资料
上面给的资料打不开,可以参考论文:
执行:make grade。如果所显示的应用程序检测都输出ok,则基本正确。如果只是priority.c过不去,可执行 make run-priority 命令来单独调试它。大致执行结果可看附录。( 使用的是 qemu-1.0.1 )。
请在实验报告中简要说明你的设计实现过程。
参考ucore的文档Stride Scheduling基本思路
而该算法的基本思想如下:
- 为每个runnable的进程设置一个当前状态stride,表示该进程当前的调度权。另外定义其对应的pass值,表示对应进程在调度后,stride 需要进行的累加值。
- 每次需要调度时,从当前 runnable 态的进程中选择 stride最小的进程调度。
- 对于获得调度的进程P,将对应的stride加上其对应的步长pass(只与进程的优先权有关系)。
- 在一段固定的时间之后,回到 2.步骤,重新调度当前stride最小的进程。
BigStride应该设置为无符号整型的最大值也就是01111111111111111111111111111111
,即:
#define BIG_STRIDE 0x7fffffff
具体代码如下:
stride_init
static void
stride_init(struct run_queue *rq) {
list_init(&rq->run_list);
rq->lab6_run_pool = NULL;
rq->proc_num = 0;
}
stride_enqueue
static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
rq->lab6_run_pool = skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {
proc->time_slice = rq->max_time_slice;
}
proc->rq = rq;
rq->proc_num++;
}
stride_dequeue
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
rq->lab6_run_pool = skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
rq->proc_num--;
}
stride_pick_next
static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
skew_heap_entry_t* she = rq->lab6_run_pool;
if (she != NULL) {
struct proc_struct *proc = le2proc(she, lab6_run_pool);
proc->lab6_stride += BIG_STRIDE / proc->lab6_priority;
return proc;
}
return NULL;
}
stride_proc_tick
static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice--;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
执行结果如下所示:
扩展练习 Challenge 1 :实现 Linux 的 CFS 调度算法
在ucore的调度器框架下实现下Linux的CFS调度算法。可阅读相关Linux内核书籍或查询网上资料,可了解CFS的细节,然后大致实现在ucore中。
参考链接:
后面再实现
扩展练习 Challenge 2 :在ucore上实现尽可能多的各种基本调度算法(FIFO, SJF,…),并设计各种测试用例,能够定量地分析出各种调度算法在各种指标上的差异,说明调度算法的适用范围。
后面再实现