全部的實驗報告將會在 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算法最佳選擇。
待完成。。。