ucore lab6(调度器)
一、实验目的
1.1理解操作系统的调度管理机制
1.2熟悉 ucore 的系统调度器框架,以及缺省的Round-Robin 调度算法
1.3基于调度器框架实现一个(Stride Scheduling)调度算法来替换缺省的调度算法
二、实验内容
实验五完成了用户进程的管理,可在用户态运行多个进程。但到目前为止,采用的调度策略是很简单的FIFO调度策略。本次实验,主要是熟悉ucore的系统调度器框架,以及基于此框架的Round-Robin(RR) 调度算法。然后参考RR调度算法的实现,完成Stride Scheduling调度算法。
三、实验步骤及流程
3.0 练习0:填写已有实验
3.0.1实验要求
本实验依赖实验1/2/3/4/5。请把你做的实验2/3/4/5的代码填入本实验中代码中有“LAB1”/“LAB2”/“LAB3”/“LAB4”“LAB5”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab6的测试应用程序,可能需对已完成的实验1/2/3/4/5的代码进行进一步改进。
3.0.2代码改进
(1)proc_struct(kern/process/proc.h)
添加代码如下:
struct run_queue *rq; //当前的进程在队列中的指针
list_entry_t run_link; // 运行队列的指针
int time_slice; // 该进程剩余的时间片,只对当前进程有效
skew_heap_entry_t lab6_run_pool; // 该进程在优先队列中的节点,仅在 LAB6 使用
uint32_t lab6_stride; // 该进程的调度步进值,仅在 LAB6 使用
uint32_t lab6_priority; // 该进程的调度优先级,仅在 LAB6 使用
(2)alloc_proc函数(kern/process/proc.c)
根据上述结构体增加的定义,增加初始化内容如下:
proc->rq = NULL;
proc->run_link.prev = proc->run_link.next = NULL;
proc->time_slice = 0;
proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;
proc->lab6_stride = 0;
proc->lab6_priority = 0;
(3)trap_dispatch函数(kern/trap/trap.c)
需要更改对于定时器做初始化:
ticks ++;
assert(current != NULL);
run_timer_list(); //更新定时器,并根据参数调用调度算法
3.1 练习1:使用 Round Robin 调度算法(不需要编码)
3.1.1实验要求
完成练习0后,建议大家比较一下(可用kdiff3等文件比较软件)个人完成的lab5和练习0完成后的刚修改的lab6之间的区别,分析了解lab6采用RR调度算法后的执行过程。执行make grade,大部分测试用例应该通过。但执行priority.c应该过不去。
请在实验报告中完成:
·请理解并分析sched_class中各个函数指针的用法,并结合Round Robin 调度算法描ucore的调度执行过程。
·请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计
3.1.2 RR调度算法
(1)RR_init函数
初始化进程队列,并将其进程数量置零。
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list));//初始化运行队列
rq->proc_num = 0;//初始化进程数为0
}
(2)RR_enqueue函数
此函数实现的是一个进程入队的操作:进程队列是一个双向链表,一个进程加入队列的时候,会将其加入到队列的第一位,并给它初始数量的时间片;并更新队列的进程数量。也就是它把进程的进程控制块指针放入到rq队列末尾,且如果进程控制块的时间片为0或者进程的时间片大于分配给进程的最大时间片,则需要把它重置为max_time_slice。然后在依次调整rq和rq的进程数目加一。
RR_enqueue(struct run_queue *rq, struct proc_struct *proc) {
assert(list_empty(&(proc->run_link)));//进程控制块指针非空
list_add_before(&(rq->run_list), &(proc->run_link));//把进程的进程控制块指针放入到rq队列末尾
//进程控制块的时间片为0或者进程的时间片大于分配给进程的最大时间片
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 ++;//就绪进程数加一
}
(3)RR_dequeue函数
从就绪队列中取出这个进程,并将其调用list_del_init删除。同时,进程数量减一。首先确定当前进程控制块指针非空并且进程在就绪队列中,然后将进程控制块指针从就绪队列中删除,最后将就绪进程数减一。
RR_dequeue(struct run_queue *rq, struct proc_struct *proc) {
assert(!list_empty(&(proc->run_link)) && proc->rq == rq);//进程控制块指针非空并且进程在就绪队列中
list_del_init(&(proc->run_link));//将进程控制块指针从就绪队列中删除
rq->proc_num --;//就绪进程数减一
}
(4)RR_pick_next函数
先选取就绪进程队列rq中的队头队列元素,并把队列元素转换成进程控制块指针。最后返回就绪进程。
、通过list_next函数的调用,会从队尾选择一个进程,代表当前应该去执行的那个进程。如果选不出来有处在就绪状态的进程,那么返回NULL,并将执行权交给内核线程idle,idle的功能是不断调用schedule,直到整个系统出现下一个可以执行的进程。
RR_pick_next(struct run_queue *rq) {
list_entry_t *le = list_next(&(rq->run_list)); //选取就绪进程队列rq中的队头队列元素
if (le != &(rq->run_list)) {//取得就绪进程
return le2proc(le, run_link);//返回进程控制块指针
}
return NULL;
}
(5)RR_proc_tick函数
每次产生了时钟中断,代表时间片数量减一。一旦时间片用完了,那么就需要把该进程PCB中的need_resched置为1,代表它必须放弃对于CPU的占有,需要将别的进程调度进来执行,而当前进程需要等待了。即每一次时间片到时的时候,当前执行进程的时间片time_slice便减一。如果time_slice降到零,则设置此进程成员变量need_resched标识为1,这样在下一次中断来后执行trap函数时,会由于当前进程程成员变量need_resched标识为1而执行schedule函数,从而把当前执行进程放回就绪队列末尾,而从就绪队列头取出在就绪队列上等待时间最久的那个就绪进程执行。
RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {//到达时间片
proc->time_slice --;//执行进程的时间片time_slice减一
}
if (proc->time_slice == 0) {//时间片为0
proc->need_resched = 1;//设置此进程成员变量need_resched标识为1,进程需要调度
}
}
(6)sched_class
定义一个c语言类的实现,提供调度算法的切换接口。在schedule初始化的时候,需要填写一个初始化信息,那么这里就填上我们所实现的类函数,那么系统就可以按照这个方式去执行了。
struct sched_class default_sched_class = {
.name = "RR_scheduler",
.init = RR_init,
.enqueue = RR_enqueue,
.dequeue = RR_dequeue,
.pick_next = RR_pick_next,
.proc_tick = RR_proc_tick,
};
3.2 练习2:实现 Stride Scheduling 调度算法(需要编码)
3.2.1实验要求
首先需要换掉RR调度器的实现,即用default_sched_stride_c覆盖default_sched.c。然后根据此文件和后续文档对Stride度器的相关描述,完成Stride调度算法的实现。
3.2.2关键数据结构及知识点
既然需要调度当前stride最小的进程去执行,那么必须要有比较部分相比于RR调度,Stride Scheduling函数定义了一个比较器:其中,a和b是两个进程的指针,通过指针指向的地点从队列中调出这两个进程并拷贝给p个q,使用p和q直接比较他们的stride值,并根据返回值,调整斜堆。
proc_stride_comp_f(void *a, void *b)
{
struct proc_struct *p = le2proc(a, lab6_run_pool); //通过进程控制块指针取得进程a
struct proc_struct *q = le2proc(b, lab6_run_pool);//通过进程控制块指针取得进程b
int32_t c = p->lab6_stride - q->lab6_stride;//步数相减,通过正负比较大小关系
if (c > 0) return 1;
else if (c == 0) return 0;
else return -1;
}
3.2.3代码实现
(1)stride_init函数
初始化函数stride_init。开始初始化运行队列,并初始化当前的运行队,然后设置当前运行队列内进程数目为0。这里的处理和之前的RR无区别。唯一的不同在于,初始化进程队列的时候是对于lab6_run_pool(进程池)进行操作,因为使用了斜堆的数据结构,代码中,为这个变量已经建立好了相应的结构,因此需要这样做。如果还是初始化rq,那么由于rq是基于双向链表实现的,会出现一些错误。
stride_init(struct run_queue *rq) {
/* LAB6: 201808010410 */
list_init(&(rq->run_list));
rq->lab6_run_pool = NULL;//初始化当前进程运行队列为空
rq->proc_num = 0;//运行队列内进程数目为0
}
(2)stride_enqueue函数
这里函数主要是初始化刚进入运行队列的进程 proc 的stride属性,然后比较队头元素与当前进程的步数大小,选择步数最小的运行,即将其插入放入运行队列中去,这里并未放置在队列头部。最后初始化时间片,然后将运行队列进程数目加一。
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: 201808010410 */
#if USE_SKEW_HEAP
rq->lab6_run_pool =
skew_heap_insert(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);//将进程加入就绪队列
#else
assert(list_empty(&(proc->run_link)));
list_add_before(&(rq->run_list), &(proc->run_link));
#endif
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 ++;//进程数加一
}
(3)stride_dequeue函数
里面的代码比较简单,只有一个主要函数 :skew_heap_remove,完成将一个进程从队列中移除的功能,这里使用了优先队列。最后运行队列数目减一。
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
#if USE_SKEW_HEAP
rq->lab6_run_pool =
skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f);
#else
assert(!list_empty(&(proc->run_link)) && proc->rq == rq);
list_del_init(&(proc->run_link));
#endif
rq->proc_num --;
}
(4)stride_pick_next函数
选择进程调度。先扫描整个运行队列,返回其中stride值最小的对应进程,然后更新对应进程的stride值,将步长设置为优先级的倒数,如果为0则设置为最大的步长。
stride_pick_next(struct run_queue *rq) {
/* LAB6: YOUR CODE */
#if USE_SKEW_HEAP
if (rq->lab6_run_pool == NULL) return NULL;
//如果调度队列为空,那么返回NULL,表示没有进程能够被调度,此时idpe内核线程会执行,不停调用schedule函数,直到找到一个符合条件的进程被调度上去。
struct proc_struct *p = le2proc(rq->lab6_run_pool, lab6_run_pool);
#else
list_entry_t *le = list_next(&(rq->run_list));
if (le == &rq->run_list)
return NULL;
struct proc_struct *p = le2proc(le, run_link);
le = list_next(le)
while (le != &rq->run_list)//扫描整个运行队列,返回其中stride值最小的对应进程,然后更新对应进程的stride值
{
struct proc_struct *q = le2proc(le, run_link);
if ((int32_t)(p->lab6_stride - q->lab6_stride) > 0)//这里有一个基于减法的比较
p = q;
le = list_next(le);
}
#endif
if (p->lab6_priority == 0)//优先级为0
p->lab6_stride += BIG_STRIDE;//步长设置为最大值
else p->lab6_stride += BIG_STRIDE / p->lab6_priority;//步长设置为优先级的倒数
return p;
}
(5)stride_proc_tick函数
主要工作是检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换。和RR相同。
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: 201808010410 */
if (proc->time_slice > 0) {//到达时间片
proc->time_slice --;//执行进程的时间片time_slice减一
}
if (proc->time_slice == 0) {//时间片为0
proc->need_resched = 1;//设置此进程成员变量need_resched标识为1,进程需要调度
}
}
(6)stride调度过程
首先是初始化函数stride_init。开始初始化运行队列,并初始化当前的运行队,然后设置当前运行队列内进程数目为0。然后是入队函数stride_enqueue,根据之前对该调度算法的分析,这里函数主要是初始化刚进入运行队列的进程 proc 的stride属性,然后比较队头元素与当前进程的步数大小,选择步数最小的运行,即将其插入放入运行队列中去,这里并未放置在队列头部。最后初始化时间片,然后将运行队列进程数目加一。然后是出队函数stride_dequeue,即完成将一个进程从队列中移除的功能,这里使用了优先队列。最后运行队列数目减一。接下来就是进程的调度函数stride_pick_next,观察代码,它的核心是先扫描整个运行队列,返回其中stride值最小的对应进程,然后更新对应进程的stride值,将步长设置为优先级的倒数,如果为0则设置为最大的步长。最后是时间片函数stride_proc_tick,主要工作是检测当前进程是否已用完分配的时间片。如果时间片用完,应该正确设置进程结构的相关标记来引起进程切换
四、思考题
Q1:(练习1)请理解并分析sched_class中各个函数指针的用法,并结合Round Robin 调度算法描ucore的调度执行过程。
sched_class中各个指针用法如下:
struct sched_class {
const char *name;//调度器名字
void (*init)(struct run_queue *rq);//初始化运行队列
void (*enqueue)(struct run_queue *rq, struct proc_struct *proc);//将进程P插入队列rq
void (*dequeue)(struct run_queue *rq, struct proc_struct *proc);//将进程p从队列rq删除
struct proc_struct *(*pick_next)(struct run_queue *rq);//返回运行队列中下一个可执行的进程
void (*proc_tick)(struct run_queue *rq, struct proc_struct *proc);//时间片处理函数
};
ucore调度执行过程:
RR调度算法的就绪队列在组织结构上也是一个双向链表,只是增加了一个成员变量,表明在此就绪进程队列中的最大执行时间片。而且在进程控制块proc_struct中增加了一个成员变量time_slice,用来记录进程当前的可运行时间片段。这是由于RR调度算法需要考虑执行进程的运行时间不能太长。在每个timer到时的时候,操作系统会递减当前执行进程的time_slice,当time_slice为0时,就意味着这个进程运行了一段时间(这个时间片段称为进程的时间片),需要把CPU让给其他进程执行,于是操作系统就需要让此进程重新回到rq的队列尾,且重置此进程的时间片为就绪队列的成员变量最大时间片max_time_slice值,然后再从rq的队列头取出一个新的进程执行。
Q2:(练习1)请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计。
①首先调度优先级高的队列中的进程。若高优先级中队列中已没有调度的进程,则调度次优先级队列中的进程
②对于同一个队列中的各个进程,按照时间片轮转法调度。比如Q1队列的时间片为N,那么Q1中的作业在经历了N个时间片后若还没有完成,则进入Q2队列等待,若Q2的时间片用完后作业还不能完成,一直进入下一级队列,直至完成。
③在低优先级的队列中的进程在运行时,又有新到达的作业,那么在运行完这个时间片后,CPU马上分配给新到达的作业。
五、运行结果
执行:make grade。如下图所示应用程序检测都输出ok,得分170/170,所以结果正确。
六、实验心得
通过本次实验对RR调度和Stride调度有了更深入的学习与理解,通过验收以及助教老师的提问对这部分内容掌握的更加牢固,这两个调度算法的实现都基于调度类五元组:初始化、入队、出队、选择下一个、中断处理。区别就在于Stride基于比较步长和进程执行进度的思想,要求频繁比较Stride值,因此选用了适应斜堆的函数,就代码而言,差别不大。相信本次实验的内容与收获会对今后的学习起到很好的帮助。