作業系統實驗六實驗報告
實驗六:排程器
練習0:填寫已有實驗
使用meld
可以簡單地將前幾個lab的程式碼填入lab6中,但是要注意在這次實驗中,部分程式碼需要做出修改,如下,主要是trap_dispatch
、alloc_proc
這兩個函式
kern/trap/trap.c
中lab5的部分程式碼
/* LAB6 YOUR CODE */
/* you should upate you lab5 code
* IMPORTANT FUNCTIONS:
* sched_class_proc_tick
*/
ticks++;
assert(current != NULL);
sched_class_proc_tick(current); //注意,需要到kern/schedule/sched.c中將sched_class_proc_tick前的static去掉,否則會出現未定義錯誤
break;
kern/process/proc.c
中lab5的部分程式碼
//LAB6 YOUR CODE : (update LAB5 steps)
/*
* below fields(add in LAB6) in proc_struct need to be initialized
* struct run_queue *rq; // running queue contains Process
* list_entry_t run_link; // the entry linked in run queue
* int time_slice; // time slice for occupying the CPU
* skew_heap_entry_t lab6_run_pool; // FOR LAB6 ONLY: the entry in the run pool
* uint32_t lab6_stride; // FOR LAB6 ONLY: the current stride of the process
* uint32_t lab6_priority; // FOR LAB6 ONLY: the priority of process, set by lab6_set_priority(uint32_t)
*/
proc->rq = NULL; //初始化執行佇列為空
list_init(&(proc->run_link)); //初始化執行佇列指標
proc->time_slice = 0; //初始化時間片為0
proc->lab6_run_pool.parent = proc->lab6_run_pool.left = proc->lab6_run_pool.right = NULL; //初始化優先佇列的相關指標為空
proc->lab6_stride = 0; //初始化步數為0
proc->lab6_priority = 0 ; //初始化優先順序為0(優先順序越大越優先)
練習1:使用Round Robin排程演算法
Round Robin演算法的基本原理是讓所有執行狀態的程序分時輪流使用處理機。當前程序的時間片用完之後,排程器將當前程序放置到執行佇列的尾部,再從其頭部取出程序進行排程。程序控制塊中增加了一個成員變數proc->time_slice
,用來記錄程序當前的可執行時間片段。在每個時鐘中斷到時的時候,作業系統會遞減當前執行程序的時間片,當時間片為0時,就意味著這個程序需要把CPU讓給其他程序執行,於是作業系統就需要讓此程序重新回到執行佇列的尾部,且重置此程序的時間片為就緒佇列的成員變數最大時間片值,然後再從佇列頭部取出一個新的程序執行。
Round Robin排程演算法的實現包含在default_sched.c
中,原始碼及解釋如下
static void
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list)); //初始化執行佇列
rq->proc_num = 0; //執行佇列中程序個數初始化為0
}
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)); //將程序加入到執行佇列
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 ++; //執行佇列程序數加1
}
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 --; //執行佇列程序數減1
}
static struct proc_struct *
RR_pick_next(struct run_queue *rq) {
list_entry_t *le = list_next(&(rq->run_list));
if (le != &(rq->run_list)) {
return le2proc(le, run_link); //返回佇列中待執行程序的第一個
}
return NULL;
}
static void
RR_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
if (proc->time_slice > 0) {
proc->time_slice --; //每一個時鐘中斷程序的時間片減1
}
if (proc->time_slice == 0) {
proc->need_resched = 1; //當程序時間片用完時進行程序切換
}
}
struct sched_class default_sched_class = { //將RR排程演算法包裝成統一的介面供ucore使用
.name = "RR_scheduler",
.init = RR_init,
.enqueue = RR_enqueue,
.dequeue = RR_dequeue,
.pick_next = RR_pick_next,
.proc_tick = RR_proc_tick,
};
ucore系統通過定義在kern/schedule/sched.c
中的schedule
函式真正對程序進行排程,原始碼及解釋如下
void
schedule(void) {
bool intr_flag; //對全域性變數操作,加鎖
struct proc_struct *next;
local_intr_save(intr_flag);
{
current->need_resched = 0; //將當前程序設定為不需要排程
if (current->state == PROC_RUNNABLE) { //若當前程序依然是執行狀態,則加入到執行佇列中
sched_class_enqueue(current);
}
if ((next = sched_class_pick_next()) != NULL) { //若執行佇列中還有其他程序,則根據演算法挑選出某一個出隊
sched_class_dequeue(next);
}
if (next == NULL) { //若沒有其他執行程序,則設定為idleproc程序
next = idleproc;
}
next->runs ++; //下一個執行的程序執行次數加1
if (next != current) {
proc_run(next); //執行下一個程序
}
}
local_intr_restore(intr_flag);
}
根據理論課內容,多級反饋佇列演算法MLFQ的基本思想如下:
- 多個執行佇列,程序首先進入最高優先順序的佇列,並可在佇列間移動
- 不同佇列的時間片大小不同,時間片大小隨優先級別增加而減小
- 若程序在當前時間片下沒有處理完成,則下降到低一級的佇列
- 高一級的佇列沒有程序時才會排程低一級佇列的程序,並且高一級佇列有程序時可以搶佔低一級的程序
圖中只是一個最簡單的示意圖,實際有更復雜的實現,多級反饋佇列的每一個佇列都可以有自己的演算法(FCFS、RR、etc),時間片也可以設定為各種不同的值,參考《作業系統概念》,多級反饋佇列的排程程式可由下述引數來定義:
- 佇列數量
- 每個佇列的排程演算法
- 用以確定何時升級到更高優先順序佇列的方法
- 用以確定何時降級到更低優先順序佇列的方法
- 用以確定程序在需要服務時應進入哪個佇列的方法
這裡不給出多級反饋佇列的詳細實現,僅提供一個概念和需要考慮的一些因素
練習2:實現Stride Scheduling排程演算法
Stride Scheduling排程演算法參考下文
這裡簡單介紹一下ucore中將要實現的Stride Scheduling排程演算法,基於這個演算法將給每個程序控制塊新增兩個關鍵成員變數stride
和priority
,前者表示這個程序的排程權,後者表示這個程序的優先順序
演算法的基本思想如下:
- 定義一個足夠大的值
BIG_STRIDE = 0x7FFFFFFF
(為什麼是0x7FFFFFFF
將在文末給出解釋),當一個程序排程後將在其排程權上加上一個步長即stride += BIG_STRIDE / priority
(若優先順序為0則直接加上BIG_STRIDE
) - 每次需要排程時,從佇列中選取排程權
stride
最小的程序進行排程
可以證明由於每次選擇stride
最小的排程,且排程後加上BIG_STRIDE / priority
,則優先順序越高單次增加越小,被排程的次數也越多,即該演算法為每個程序分配的時間與其優先順序成正比。
參考Round Robin的介面實現,Stride Scheduling的實現及解釋如下,設計優先佇列的操作可以參考libs/skew_heap.h
:
#define USE_SKEW_HEAP 1 //通過這個巨集來切換實現Stride Scheduling演算法的資料結構(斜堆(優先佇列)1、連結串列0)
/* You should define the BigStride constant here*/
/* LAB6: YOUR CODE */
#define BIG_STRIDE 0x7FFFFFFF /*you should give a value, and is ??? */
//0x7FFFFFFF = 0111 1111 ... 1111 最大的正數
/* The compare function for two skew_heap_node_t's and the
* corresponding procs*/
static int
proc_stride_comp_f(void *a, void *b)
{
struct proc_struct *p = le2proc(a, lab6_run_pool);
struct proc_struct *q = le2proc(b, lab6_run_pool);
int32_t c = p->lab6_stride - q->lab6_stride; //排程權(型別是無符號數uint32_t)相減並按有符號數進行比較,原理將在文末給出解釋
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 libs/list.h for routines of the list structures.
*/
static void
stride_init(struct run_queue *rq) {
/* LAB6: YOUR CODE
* (1) init the ready process list: rq->run_list
* (2) init the run pool: rq->lab6_run_pool
* (3) set number of process: rq->proc_num to 0
*/
list_init(&(rq->run_list)); //初始化執行佇列
rq->lab6_run_pool = NULL; //優先佇列指標初始化為NULL
rq->proc_num = 0; //執行佇列中程序數初始化為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 libs/skew_heap.h for routines of the priority
* queue structures.
*/
static void
stride_enqueue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE
* (1) insert the proc into rq correctly
* NOTICE: you can use skew_heap or list. Important functions
* skew_heap_insert: insert a entry into skew_heap
* list_add_before: insert a entry into the last of list
* (2) recalculate proc->time_slice
* (3) set proc->rq pointer to rq
* (4) increase 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
//下面與RR的實現相同
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 libs/skew_heap.h for routines of the priority
* queue structures.
*/
static void
stride_dequeue(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE
* (1) remove the proc from rq correctly
* NOTICE: you can use skew_heap or list. Important functions
* skew_heap_remove: remove a entry from skew_heap
* list_del_init: remove a entry from the list
*/
#if USE_SKEW_HEAP
rq->lab6_run_pool = skew_heap_remove(rq->lab6_run_pool, &(proc->lab6_run_pool), proc_stride_comp_f); //從優先佇列中移除
#else
//下面與RR的實現相同
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 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 libs/skew_heap.h for routines of the priority
* queue structures.
*/
static struct proc_struct *
stride_pick_next(struct run_queue *rq) {
/* LAB6: YOUR CODE
* (1) get a proc_struct pointer p with the minimum value of stride
(1.1) If using skew_heap, we can use le2proc get the p from rq->lab6_run_poll
(1.2) If using list, we have to search list to find the p with minimum stride value
* (2) update p;s stride value: p->lab6_stride
* (3) return p
*/
#if USE_SKEW_HEAP
//優先佇列的頭結點就是排程權最小的程序,直接取出並呼叫le2proc獲得相應的程序
if(rq->lab6_run_pool == NULL){
return NULL;
}
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);
while((le = list_next(le)) != &(rq->run_list)){
struct proc_struct *tmp = le2proc(le, run_link);
if((int32_t)(tmp->lab6_stride - p->lab6_stride) < 0){
p = tmp;
}
}
#endif
if(p->lab6_priority == 0){
p->lab6_stride += BIG_STRIDE; //注意由於優先順序初始化為0,因此要考慮程序優先順序為0的情況,否則會出現divide error
}
else{
p->lab6_stride += BIG_STRIDE / p->lab6_priority;
}
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.
*/
static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
//對時間片的處理與RR的實現一樣
if (proc->time_slice > 0) {
proc->time_slice --;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
//封裝的實現,參考RR
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,
};
下面解釋一下如何對兩個程序的排程權進行比較proc_stride_comp_f
及BIG_STRIDE = 0x7FFFFFFF
的原因:
從定義上可以知道,排程權被定義為32位無符號數,而隨著每次排程後增加BIG_STRIDE / priority
最終會導致溢位,正確獲得兩個程序排程權的真實大小很關鍵,利用函式proc_stride_comp_f
實現,本質上是直接對兩個32位無符號的排程權求差,並化為32位有符號數與0進行比較,來確定兩個程序排程權的大小
int32_t c = p->lab6_stride - q->lab6_stride;
if (c > 0) return 1; //p->lab6_stride > q->lab6_stride
else if (c == 0) return 0; //p->lab6_stride == q->lab6_stride
else return -1; //p->lab6_stride < q->lab6_stride
假設存在兩個32位無符號數a,b分別表示排程權,初始時a=b,定義步長s = BIG_STRIDE / priority <=S(S為最大步進)
(1)排程b後b+s=B,若B未溢位,且a-B=a-(b+s)=-s<0而不會下溢有符號數,則s<2^31,此時a-B<0成立
(2)排程b後b+s=B,若B溢位,此時B溢位意味著b+s>2^32,B=b+s-2^32,由(1)可知s<2^31那麼必有a=b>2^31,為了a-B<0即a-B的值上溢有符號數,需要使a-(b+s-2^32)>=2^31則s<=2^31
綜上可見s<2^31即最大步進S的精確值為S=2^31-1=0x7FFFFFFF
總結
完成後可以修改#define USE_SKEW_HEAP
的值後再呼叫make grade
可以獲得如下輸出,說明利用優先佇列和連結串列的實驗均成功
//USE_SKEW_HEAP = 0
badsegment: (3.4s)
-check result: OK
-check output: OK
divzero: (1.6s)
-check result: OK
-check output: OK
softint: (1.6s)
-check result: OK
-check output: OK
faultread: (1.6s)
-check result: OK
-check output: OK
faultreadkernel: (1.6s)
-check result: OK
-check output: OK
hello: (1.6s)
-check result: OK
-check output: OK
testbss: (1.7s)
-check result: OK
-check output: OK
pgdir: (1.6s)
-check result: OK
-check output: OK
yield: (1.6s)
-check result: OK
-check output: OK
badarg: (1.6s)
-check result: OK
-check output: OK
exit: (1.6s)
-check result: OK
-check output: OK
spin: (1.8s)
-check result: OK
-check output: OK
waitkill: (2.3s)
-check result: OK
-check output: OK
forktest: (1.7s)
-check result: OK
-check output: OK
forktree: (1.7s)
-check result: OK
-check output: OK
matrix: (10.8s)
-check result: OK
-check output: OK
priority: (11.6s)
-check result: OK
-check output: OK
Total Score: 170/170
//USE_SKEW_HEAP = 1
badsegment: (3.3s)
-check result: OK
-check output: OK
divzero: (1.6s)
-check result: OK
-check output: OK
softint: (1.6s)
-check result: OK
-check output: OK
faultread: (1.6s)
-check result: OK
-check output: OK
faultreadkernel: (1.6s)
-check result: OK
-check output: OK
hello: (1.6s)
-check result: OK
-check output: OK
testbss: (1.7s)
-check result: OK
-check output: OK
pgdir: (1.6s)
-check result: OK
-check output: OK
yield: (1.6s)
-check result: OK
-check output: OK
badarg: (1.6s)
-check result: OK
-check output: OK
exit: (1.6s)
-check result: OK
-check output: OK
spin: (1.8s)
-check result: OK
-check output: OK
waitkill: (2.1s)
-check result: OK
-check output: OK
forktest: (1.6s)
-check result: OK
-check output: OK
forktree: (1.6s)
-check result: OK
-check output: OK
matrix: (10.5s)
-check result: OK
-check output: OK
priority: (11.7s)
-check result: OK
-check output: OK
Total Score: 170/170