全部的实验报告将会在 Github 同步更新,更多内容请移步至Github:https://github.com/AngelKitty/review_the_national_post-graduate_entrance_examination/blob/master/books_and_notes/professional_courses/operating_system/sources/ucore_os_lab/docs/lab_report/html
lab6
会依赖 lab1~lab5
,咱们须要把作的 lab1~lab5
的代码填到 lab6
中缺失的位置上面。练习 0 就是一个工具的利用。这里我使用的是 Linux
下的系统已预装好的 Meld Diff Viewer
工具。和 lab5
操做流程同样,咱们只须要将已经完成的 lab1~lab5
与待完成的 lab6
(因为 lab6
是基于 lab1~lab5
基础上完成的,因此这里只须要导入 lab5
)分别导入进来,而后点击 compare
就好了。node
而后软件就会自动分析两份代码的不一样,而后就一个个比较比较复制过去就好了,在软件里面是能够支持打开对比复制了,点击 Copy Right
便可。固然 bin
目录和 obj
目录下都是 make
生成的,就不用复制了,其余须要修改的地方主要有如下六个文件,经过对比复制完成便可:linux
proc.c default_pmm.c pmm.c swap_fifo.c vmm.c trap.c
根据试验要求,咱们须要对部分代码进行改进,这里讲须要改进的地方的代码和说明罗列以下:git
Round Robin
的实现,具体为调用 sched_class_*
等一系列函数以后,进一步调用调度器 sched_class
中封装的函数,默认该调度器为 Round Robin
调度器,这是在 default_sched.*
中定义的;咱们在原来的实验基础上,新增了 9 行代码:github
int exit_code; //退出码(发送到父进程) uint32_t wait_state; //等待状态 struct proc_struct *cptr, *yptr, *optr; //进程间的一些关系 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 使用
因此改进后的 proc_struct
结构体以下:算法
struct proc_struct { //进程控制块 enum proc_state state; //进程状态 int pid; //进程ID int runs; //运行时间 uintptr_t kstack; //内核栈位置 volatile bool need_resched; //是否须要调度,只对当前进程有效 struct proc_struct *parent; //父进程 struct mm_struct *mm; //进程的虚拟内存 struct context context; //进程上下文 struct trapframe *tf; //当前中断帧的指针 uintptr_t cr3; //当前页表地址 uint32_t flags; //进程 char name[PROC_NAME_LEN + 1]; //进程名字 list_entry_t list_link; //进程链表 list_entry_t hash_link; //进程哈希表 int exit_code; //退出码(发送到父进程) uint32_t wait_state; //等待状态 struct proc_struct *cptr, *yptr, *optr; //进程间的一些关系 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 使用 };
咱们在原来的实验基础上,新增了 6 行代码:编程
proc->rq = NULL; //初始化运行队列为空 list_init(&(proc->run_link));//初始化运行队列的指针 proc->time_slice = 0; //初始化时间片 proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;//初始化各种指针为空,包括父进程等待 proc->lab6_stride = 0;//设置步长为0 proc->lab6_priority = 0;//设置优先级为0
因此改进后的 alloc_proc
函数以下:数组
// alloc_proc - alloc a proc_struct and init all fields of proc_struct static struct proc_struct *alloc_proc(void) { struct proc_struct *proc = kmalloc(sizeof(struct proc_struct)); if (proc != NULL) { proc->state = PROC_UNINIT; //设置进程为未初始化状态 proc->pid = -1; //未初始化的的进程id为-1 proc->runs = 0; //初始化时间片 proc->kstack = 0; //内存栈的地址 proc->need_resched = 0; //是否须要调度设为不须要 proc->parent = NULL; //父节点设为空 proc->mm = NULL; //虚拟内存设为空 memset(&(proc->context), 0, sizeof(struct context));//上下文的初始化 proc->tf = NULL; //中断帧指针置为空 proc->cr3 = boot_cr3; //页目录设为内核页目录表的基址 proc->flags = 0; //标志位 memset(proc->name, 0, PROC_NAME_LEN);//进程名 proc->wait_state = 0;//PCB 进程控制块中新增的条目,初始化进程等待状态 proc->cptr = proc->optr = proc->yptr = NULL;//进程相关指针初始化 proc->rq = NULL;//初始化运行队列为空 list_init(&(proc->run_link)); proc->time_slice = 0;//初始化时间片 proc->lab6_run_pool.left = proc->lab6_run_pool.right = proc->lab6_run_pool.parent = NULL;//初始化指针为空 proc->lab6_stride = 0;//设置步长为 0 proc->lab6_priority = 0;//设置优先级为 0 } return proc; }
咱们在原来的实验基础上,新增了 1 行代码:数据结构
run_timer_list(); //更新定时器,并根据参数调用调度算法
这里主要是将时间片设置为须要调度,说明当前进程的时间片已经用完了。框架
因此改进后的 trap_dispatch
函数以下:
static void trap_dispatch(struct trapframe *tf) { ...... ...... ticks ++; assert(current != NULL); run_timer_list(); //更新定时器,并根据参数调用调度算法 break; ...... ...... }
Round Robin
调度算法的调度思想是让全部 runnable 态的进程分时轮流使用 CPU 时间。Round Robin
调度器维护当前 runnable 进程的有序运行队列。当前进程的时间片用完以后,调度器将当前进程放置到运行队列的尾部,再从其头部取出进程进行调度。
在这个理解的基础上,咱们来分析算法的具体实现。
这里 Round Robin
调度算法的主要实如今 default_sched.c
之中,源码以下:
/* file_path = kern/schedule/default_sched.c */ //RR_init函数:这个函数被封装为 sched_init 函数,用于调度算法的初始化,使用grep命令能够知道,该函数仅在 ucore 入口的 init.c 里面被调用进行初始化 static void RR_init(struct run_queue *rq) { //初始化进程队列 list_init(&(rq->run_list));//初始化运行队列 rq->proc_num = 0;//初始化进程数为 0 } //RR_enqueue函数:该函数的功能为将指定的进程的状态置成 RUNNABLE,而且放入调用算法中的可执行队列中,被封装成 sched_class_enqueue 函数,能够发现这个函数仅在 wakeup_proc 和 schedule 函数中被调用,前者为将某个不是 RUNNABLE 的进程加入可执行队列,然后者是将正在执行的进程换出到可执行队列中去 static void 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 队列末尾 if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {//进程控制块的时间片为 0 或者进程的时间片大于分配给进程的最大时间片 proc->time_slice = rq->max_time_slice;//修改时间片 } proc->rq = rq;//加入进程池 rq->proc_num ++;//就绪进程数加一 } //RR_dequeue 函数:该函数的功能为将某个在队列中的进程取出,其封装函数 sched_class_dequeue 仅在 schedule 中被调用,表示将调度算法选择的进程从等待的可执行的进程队列中取出进行执行 static void 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 --;//就绪进程数减一 } //RR_pick_next 函数:该函数的封装函数一样仅在 schedule 中被调用,功能为选择要执行的下个进程 static struct proc_struct *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; } //RR_proc_tick 函数:该函数表示每次时钟中断的时候应当调用的调度算法的功能,仅在进行时间中断的 ISR 中调用 static void 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,进程须要调度 } } //sched_class 定义一个 c 语言类的实现,提供调度算法的切换接口 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, };
如今咱们来逐个函数的分析,从而了解 Round Robin
调度算法的原理。
首先是 RR_init
函数,函数完成了对进程队列的初始化。
//RR_init函数:这个函数被封装为 sched_init 函数,用于调度算法的初始化,使用grep命令能够知道,该函数仅在 ucore 入口的 init.c 里面被调用进行初始化 static void RR_init(struct run_queue *rq) { //初始化进程队列 list_init(&(rq->run_list));//初始化运行队列 rq->proc_num = 0;//初始化进程数为 0 }
其中的 run_queue 结构体以下:
struct run_queue { list_entry_t run_list;//其运行队列的哨兵结构,能够看做是队列头和尾 unsigned int proc_num;//内部进程总数 int max_time_slice;//每一个进程一轮占用的最多时间片 // For LAB6 ONLY skew_heap_entry_t *lab6_run_pool;//优先队列形式的进程容器 };
而 run_queue 结构体中的 skew_heap_entry 结构体以下:
struct skew_heap_entry { struct skew_heap_entry *parent, *left, *right;//树形结构的进程容器 }; typedef struct skew_heap_entry skew_heap_entry_t;
而后是 RR_enqueue
函数,首先,它把进程的进程控制块指针放入到 rq 队列末尾,且若是进程控制块的时间片为 0,则须要把它重置为 max_time_slice
。这表示若是进程在当前的执行时间片已经用完,须要等到下一次有机会运行时,才能再执行一段时间。而后在依次调整 rq 和 rq 的进程数目加一。
//RR_enqueue函数:该函数的功能为将指定的进程的状态置成 RUNNABLE,而且放入调用算法中的可执行队列中,被封装成 sched_class_enqueue 函数,能够发现这个函数仅在 wakeup_proc 和 schedule 函数中被调用,前者为将某个不是 RUNNABLE 的进程加入可执行队列,然后者是将正在执行的进程换出到可执行队列中去 static void 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 队列末尾 if (proc->time_slice == 0 || proc->time_slice > rq->max_time_slice) {//进程控制块的时间片为 0 或者进程的时间片大于分配给进程的最大时间片 proc->time_slice = rq->max_time_slice;//修改时间片 } proc->rq = rq;//加入进程池 rq->proc_num ++;//就绪进程数加一 }
而后是 RR_dequeue
函数,它简单的把就绪进程队列 rq 的进程控制块指针的队列元素删除,而后使就绪进程个数的proc_num减一。
//RR_dequeue 函数:该函数的功能为将某个在队列中的进程取出,其封装函数 sched_class_dequeue 仅在 schedule 中被调用,表示将调度算法选择的进程从等待的可执行的进程队列中取出进行执行 static void 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 --;//就绪进程数减一 }
接下来是 RR_pick_next
函数,即选取函数。它选取就绪进程队列 rq 中的队头队列元素,并把队列元素转换成进程控制块指针,即置为当前占用 CPU 的程序。
//RR_pick_next 函数:该函数的封装函数一样仅在 schedule 中被调用,功能为选择要执行的下个进程 static struct proc_struct *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; }
最后是 RR_proc_tick
,它每一次时间片到时的时候,当前执行进程的时间片 time_slice 便减一。若是 time_slice 降到零,则设置此进程成员变量 need_resched 标识为 1,这样在下一次中断来后执行 trap 函数时,会因为当前进程程成员变量 need_resched 标识为 1 而执行 schedule 函数,从而把当前执行进程放回就绪队列末尾,而从就绪队列头取出在就绪队列上等待时间最久的那个就绪进程执行。
//RR_proc_tick 函数:该函数表示每次时钟中断的时候应当调用的调度算法的功能,仅在进行时间中断的 ISR 中调用 static void 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,进程须要调度 } }
sched_class
定义一个 c 语言类的实现,提供调度算法的切换接口。
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, };
请理解并分析 sched_calss 中各个函数指针的用法,并结合 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 *p);// 将进程 p 插入队列 rq void (*dequeue) (struct run_queue *rq, struct proc_struct *p);// 将进程 p 从队列 rq 中删除 struct proc_struct* (*pick_next) (struct run_queue *rq);// 返回运行队列中下一个可执行的进程 void (*proc_tick)(struct run_queue* rq, struct proc_struct* p);// timetick 处理函数 };
接下来咱们结合具体算法来描述一下 ucore 调度执行过程:
请在实验报告中简要说明如何设计实现”多级反馈队列调度算法“,给出概要设计,鼓励给出详细设计;
设计以下:
至此完成了多级反馈队列调度算法的具体设计;
首先,根据实验指导书的要求,先用 default_sched_stride_c 覆盖 default_sched.c,即覆盖掉 Round Robin 调度算法的实现。
覆盖掉以后须要在该框架上实现 Stride Scheduling 调度算法。
关于 Stride Scheduling 调度算法,通过查阅资料和实验指导书,咱们能够简单的把思想归结以下:
接下来针对代码咱们逐步分析,首先完整代码以下:
* 实现思路: 因为在 ucore 中使用面向对象编程的思想,将全部与调度算法相关的函数封装在了调度器 sched_class 中,所以其实能够不须要覆盖掉 default_sched.c,只须要将 default_sched_stride_c 更名成 default_sched_stride.c,而后注释掉 default_sched.c 中的 sched_class 的定义,这样因为 default_sched_stride.c 中也有 sched_class 的定义,其余代码在调用调度器的接口的时候就直接调用了新实现的 Stride Scheduling 算法实现的函数了; -------------------------------------------------------------------------------------------- /* file_path = kern/schedule/default_sched.c */ /*code*/ #include <defs.h> #include <list.h> #include <proc.h> #include <assert.h> #include <default_sched.h> #define USE_SKEW_HEAP 1 /* You should define the BigStride constant here*/ /* LAB6: YOUR CODE */ #define BIG_STRIDE 0x7FFFFFFF /* ??? */ /* The compare function for two skew_heap_node_t's and the * corresponding procs*/ //proc_stride_comp_f:优先队列的比较函数,主要思路就是经过步数相减,而后根据其正负比较大小关系 static int 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; } /* * stride_init initializes the run-queue rq with correct assignment for * member variables, including: * * - run_list: should be a empty list after initialization. * - lab6_run_pool: NULL * - proc_num: 0 * - max_time_slice: no need here, the variable would be assigned by the caller. * * hint: see proj13.1/libs/list.h for routines of the list structures. */ //stride_init:进行调度算法初始化的函数,在本 stride 调度算法的实现中使用了斜堆来实现优先队列,所以须要对相应的成员变量进行初始化 static void stride_init(struct run_queue *rq) { /* LAB6: YOUR CODE */ list_init(&(rq->run_list));//初始化调度器类 rq->lab6_run_pool = NULL;//对斜堆进行初始化,表示有限队列为空 rq->proc_num = 0;//设置运行队列为空 } /* * stride_enqueue inserts the process ``proc'' into the run-queue * ``rq''. The procedure should verify/initialize the relevant members * of ``proc'', and then put the ``lab6_run_pool'' node into the * queue(since we use priority queue here). The procedure should also * update the meta date in ``rq'' structure. * * proc->time_slice denotes the time slices allocation for the * process, which should set to rq->max_time_slice. * * hint: see proj13.1/libs/skew_heap.h for routines of the priority * queue structures. */ //stride_enqeue:在将指定进程加入就绪队列的时候,须要调用斜堆的插入函数将其插入到斜堆中,而后对时间片等信息进行更新 static void stride_enqueue(struct run_queue *rq, struct proc_struct *proc) { /* LAB6: YOUR CODE */ #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 ++;//维护就绪队列中进程的数量加一 } /* * stride_dequeue removes the process ``proc'' from the run-queue * ``rq'', the operation would be finished by the skew_heap_remove * operations. Remember to update the ``rq'' structure. * * hint: see proj13.1/libs/skew_heap.h for routines of the priority * queue structures. */ //stride_dequeue:将指定进程从就绪队列中删除,只须要将该进程从斜堆中删除掉便可 static void 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 --;//维护就绪队列中的进程总数 } /* * stride_pick_next pick the element from the ``run-queue'', with the * minimum value of stride, and returns the corresponding process * pointer. The process pointer would be calculated by macro le2proc, * see proj13.1/kern/process/proc.h for definition. Return NULL if * there is no process in the queue. * * When one proc structure is selected, remember to update the stride * property of the proc. (stride += BIG_STRIDE / priority) * * hint: see proj13.1/libs/skew_heap.h for routines of the priority * queue structures. */ //stride_pick_next: 选择下一个要执行的进程,根据stride算法,只须要选择stride值最小的进程,即斜堆的根节点对应的进程便可 static struct proc_struct *stride_pick_next(struct run_queue *rq) { /* LAB6: YOUR CODE */ #if USE_SKEW_HEAP if (rq->lab6_run_pool == NULL) return NULL; struct proc_struct *p = le2proc(rq->lab6_run_pool, lab6_run_pool);//选择 stride 值最小的进程 #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) { 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;//步长设置为优先级的倒数,更新该进程的 stride 值 return p; } /* * stride_proc_tick works with the tick event of current process. You * should check whether the time slices for current process is * exhausted and update the proc struct ``proc''. proc->time_slice * denotes the time slices left for current * process. proc->need_resched is the flag variable for process * switching. */ //stride_proc_tick:每次时钟中断须要调用的函数,仅在进行时间中断的ISR中调用 static void stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) { /* LAB6: YOUR CODE */ if (proc->time_slice > 0) {//到达时间片 proc->time_slice --;//执行进程的时间片 time_slice 减一 } if (proc->time_slice == 0) {//时间片为 0 proc->need_resched = 1;//设置此进程成员变量 need_resched 标识为 1,进程须要调度 } } //sched_class 定义一个 c 语言类的实现,提供调度算法的切换接口 struct sched_class default_sched_class = { .name = "stride_scheduler", .init = stride_init, .enqueue = stride_enqueue, .dequeue = stride_dequeue, .pick_next = stride_pick_next, .proc_tick = stride_proc_tick, };
相比于 RR 调度,Stride Scheduling 函数定义了一个比较器 proc_stride_comp_f。优先队列的比较函数 proc_stride_comp_f
的实现,主要思路就是经过步数相减,而后根据其正负比较大小关系。
//proc_stride_comp_f:优先队列的比较函数,主要思路就是经过步数相减,而后根据其正负比较大小关系 static int 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; }
一样的,咱们来逐个函数的分析,从而了解 Stride Scheduling
调度算法的原理。
首先是 stride_init
函数,开始初始化运行队列,并初始化当前的运行队,而后设置当前运行队列内进程数目为0。
//stride_init:进行调度算法初始化的函数,在本 stride 调度算法的实现中使用了斜堆来实现优先队列,所以须要对相应的成员变量进行初始化 static void stride_init(struct run_queue *rq) { /* LAB6: YOUR CODE */ list_init(&(rq->run_list));//初始化调度器类 rq->lab6_run_pool = NULL;//对斜堆进行初始化,表示有限队列为空 rq->proc_num = 0;//设置运行队列为空 }
而后是入队函数 stride_enqueue,根据以前对该调度算法的分析,这里函数主要是初始化刚进入运行队列的进程 proc 的 stride 属性,而后比较队头元素与当前进程的步数大小,选择步数最小的运行,即将其插入放入运行队列中去,这里并未放置在队列头部。最后初始化时间片,而后将运行队列进程数目加一。
//stride_enqeue:在将指定进程加入就绪队列的时候,须要调用斜堆的插入函数将其插入到斜堆中,而后对时间片等信息进行更新 static void stride_enqueue(struct run_queue *rq, struct proc_struct *proc) { /* LAB6: YOUR CODE */ #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 ++;//维护就绪队列中进程的数量加一 }
里面有一个条件编译:
#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
在 ucore 中 USE_SKEW_HEAP 定义为 1 ,所以 #else 与 #endif 之间的代码将会被忽略。
其中的 skew_heap_insert 函数以下:
static inline skew_heap_entry_t *skew_heap_insert(skew_heap_entry_t *a, skew_heap_entry_t *b, compare_f comp) { skew_heap_init(b); //初始化进程b return skew_heap_merge(a, b, comp);//返回a与b进程结合的结果 }
函数中的 skew_heap_init 函数以下:
static inline void skew_heap_init(skew_heap_entry_t *a) { a->left = a->right = a->parent = NULL; //初始化相关指针 }
函数中的 skew_heap_merge 函数以下:
static inline skew_heap_entry_t *skew_heap_merge(skew_heap_entry_t *a, skew_heap_entry_t *b, compare_f comp) { if (a == NULL) return b; else if (b == NULL) return a; skew_heap_entry_t *l, *r; if (comp(a, b) == -1) //a进程的步长小于b进程 { r = a->left; //a的左指针为r l = skew_heap_merge(a->right, b, comp); a->left = l; a->right = r; if (l) l->parent = a; return a; } else { r = b->left; l = skew_heap_merge(a, b->right, comp); b->left = l; b->right = r; if (l) l->parent = b; return b; } }
而后是出队函数 stride_dequeue
,即完成将一个进程从队列中移除的功能,这里使用了优先队列。最后运行队列数目减一。
//stride_dequeue:将指定进程从就绪队列中删除,只须要将该进程从斜堆中删除掉便可 static void 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 --;//维护就绪队列中的进程总数 }
里面的代码比较简单,只有一个主要函数 :skew_heap_remove。该函数实现过程以下:
static inline skew_heap_entry_t *skew_heap_remove(skew_heap_entry_t *a, skew_heap_entry_t *b, compare_f comp) { skew_heap_entry_t *p = b->parent; skew_heap_entry_t *rep = skew_heap_merge(b->left, b->right, comp); if (rep) rep->parent = p; if (p) { if (p->left == b) p->left = rep; else p->right = rep; return a; } else return rep; }
接下来就是进程的选择调度函数 stride_pick_next
。观察代码,它的核心是先扫描整个运行队列,返回其中 stride 值最小的对应进程,而后更新对应进程的 stride 值,将步长设置为优先级的倒数,若是为 0 则设置为最大的步长。
//stride_pick_next: 选择下一个要执行的进程,根据stride算法,只须要选择stride值最小的进程,即斜堆的根节点对应的进程便可 static struct proc_struct *stride_pick_next(struct run_queue *rq) { /* LAB6: YOUR CODE */ #if USE_SKEW_HEAP if (rq->lab6_run_pool == NULL) return NULL; struct proc_struct *p = le2proc(rq->lab6_run_pool, lab6_run_pool);//选择 stride 值最小的进程 #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) { 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;//步长设置为优先级的倒数,更新该进程的 stride 值 return p; }
最后是时间片函数 stride_proc_tick
,主要工做是检测当前进程是否已用完分配的时间片。若是时间片用完,应该正确设置进程结构的相关标记来引发进程切换。这里和以前实现的 Round Robin
调度算法同样。
//stride_proc_tick:每次时钟中断须要调用的函数,仅在进行时间中断的ISR中调用 static void stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) { /* LAB6: YOUR CODE */ if (proc->time_slice > 0) {//到达时间片 proc->time_slice --;//执行进程的时间片 time_slice 减一 } if (proc->time_slice == 0) {//时间片为 0 proc->need_resched = 1;//设置此进程成员变量 need_resched 标识为 1,进程须要调度 } }
sched_class
定义一个 c 语言类的实现,提供调度算法的切换接口。
struct sched_class default_sched_class = { .name = "stride_scheduler", .init = stride_init, .enqueue = stride_enqueue, .dequeue = stride_dequeue, .pick_next = stride_pick_next, .proc_tick = stride_proc_tick, };
如何证实STRIDE_MAX – STRIDE_MIN <= PASS_MAX?
假如该命题不成立,则能够知道就绪队列在上一次找出用于执行的进程的时候,假如选择的进程是 P,那么存在另一个就绪的进程 P',而且有 P' 的 stride 比 P 严格地小,这也就说明上一次调度出了问题,这和 stride 算法的设计是相违背的;所以经过反证法证实了上述命题的成立;
在 ucore 中,目前 Stride 是采用无符号的32位整数表示。则 BigStride 应该取多少,才能保证比较的正确性?
须要保证
注:BIG_STRIDE 的值是怎么来的?
Stride 调度算法的思路是每次找 stride 步进值最小的进程,每一个进程每次执行完之后,都要在 stride步进 += pass步长,其中步长是和优先级成反比的所以步长能够反映出进程的优先级。可是随着每次调度,步长不断增长,有可能会有溢出的风险。
所以,须要设置一个步长的最大值,使得他们哪怕溢出,仍是可以进行比较。
在 ucore 中,BIG_STRIDE 的值是采用无符号 32 位整数表示,而 stride 也是无符号 32 位整数。也就是说,最大值只能为
若是一个 进程的 stride 已经为
这说明,咱们必须得约定一个最大的步长,使得两个进程的步进值哪怕其中一个溢出或者都溢出还可以进行比较。
首先 由于 步长 和 优先级成反比 能够获得一条:pass = BIG_STRIDE / priority <= BIG_STRIDE
进而获得:pass_max <= BIG_STRIDE
最大步长 - 最小步长 必定小于等于步长:max_stride - min_stride <= pass_max
因此得出:max_stride - min_stride <= BIG_STRIDE
前面说了 ucore 中 BIG_STRIDE 用的无符号 32 位整数,最大值只能为
而又由于是无符号的,所以,最小只能为 0,并且咱们须要把 32 位无符号整数进行比较,须要保证任意两个进程 stride 的差值在 32 位有符号数可以表示的范围以内,故 BIG_STRIDE 为
最终的实验结果以下图所示:
若是 make grade 没法满分,尝试注释掉 tools/grade.sh 的 221 行到 233 行(在前面加上“#”)。
这里咱们选用古老的编辑器 Vim,具体操做过程以下:
:221
跳转至 221 行;CFS 算法的基本思路就是尽可能使得每一个进程的运行时间相同,因此须要记录每一个进程已经运行的时间:
struct proc_struct { ... int fair_run_time; // FOR CFS ONLY: run time };
每次调度的时候,选择已经运行时间最少的进程。因此,也就须要一个数据结构来快速得到最少运行时间的进程, CFS 算法选择的是红黑树,可是项目中的斜堆也能够实现,只是性能不及红黑树。CFS是对于优先级的实现方法就是让优先级低的进程的时间过得很快。
首先须要在 run_queue 增长一个斜堆:
struct run_queue { ... skew_heap_entry_t *fair_run_pool; };
在 proc_struct 中增长三个成员:
struct proc_struct { ... int fair_run_time; // FOR CFS ONLY: run time int fair_priority; // FOR CFS ONLY: priority skew_heap_entry_t fair_run_pool; // FOR CFS ONLY: run pool };
首先须要一个比较函数,一样根据
static int proc_fair_comp_f(void *a, void *b) { struct proc_struct *p = le2proc(a, fair_run_pool); struct proc_struct *q = le2proc(b, fair_run_pool); int32_t c = p->fair_run_time - q->fair_run_time; if (c > 0) return 1; else if (c == 0) return 0; else return -1; }
static void fair_init(struct run_queue *rq) { rq->fair_run_pool = NULL; rq->proc_num = 0; }
和 Stride Scheduling 类型,可是不须要更新 stride。
static void fair_enqueue(struct run_queue *rq, struct proc_struct *proc) { rq->fair_run_pool = skew_heap_insert(rq->fair_run_pool, &(proc->fair_run_pool), proc_fair_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 ++; }
static void fair_dequeue(struct run_queue *rq, struct proc_struct *proc) { rq->fair_run_pool = skew_heap_remove(rq->fair_run_pool, &(proc->fair_run_pool), proc_fair_comp_f); rq->proc_num --; }
static struct proc_struct * fair_pick_next(struct run_queue *rq) { if (rq->fair_run_pool == NULL) return NULL; skew_heap_entry_t *le = rq->fair_run_pool; struct proc_struct * p = le2proc(le, fair_run_pool); return p; }
须要更新虚拟运行时,增长的量为优先级系数。
static void fair_proc_tick(struct run_queue *rq, struct proc_struct *proc) { if (proc->time_slice > 0) { proc->time_slice --; proc->fair_run_time += proc->fair_priority; } if (proc->time_slice == 0) { proc->need_resched = 1; } }
为了保证测试能够经过,须要将 Stride Scheduling 的优先级对应到 CFS 的优先级:
void lab6_set_priority(uint32_t priority) { ... // FOR CFS ONLY current->fair_priority = 60 / current->lab6_priority + 1; if (current->fair_priority < 1) current->fair_priority = 1; }
因为调度器须要经过虚拟运行时间肯定下一个进程,若是虚拟运行时间最小的进程须要 yield,那么必须增长虚拟运行时间,例如能够增长一个时间片的运行时。
int do_yield(void) { ... // FOR CFS ONLY current->fair_run_time += current->rq->max_time_slice * current->fair_priority; return 0; }
遇到的问题:为何 CFS 调度算法使用红黑树而不使用堆来获取最小运行时进程?
查阅了网上的资料以及本身分析,获得以下结论:
- 堆基于数组,可是对于调度器来讲进程数量不肯定,没法使用定长数组实现的堆;
- ucore 中的 Stride Scheduling 调度算法使用了斜堆,可是斜堆没有维护平衡的要求,可能致使斜堆退化成为有序链表,影响性能。
综上所示,红黑树由于平衡性以及非连续因此是CFS算法最佳选择。
综上所示,红黑树由于平衡性以及非连续因此是CFS算法最佳选择。
待完成。。。