操作系統lab6實驗報告
實驗五完成了用戶進程的管理,可在用戶態運行多個進程。可是目前的進程調度策略是FIFO策略,而本實驗則會實現Stride Scheduling
調度算法。
練習0:填寫已有實驗
同樣使用一款名爲meld
的軟件進行對比即可,大致截圖如下:
現在將需要修改的文件羅列如下:
proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
然後是一些需要簡單修改的部分,根據註釋的提示,主要是一下兩個函數需要額外加以修改。
alloc_proc函數
這裏alloc_proc
還需要修改一下,完整的代碼如下:
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;
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;
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;
proc->lab6_priority = 0;
}
return proc;
}
相比於lab5,lab6對proc_struct
結構體再次做了擴展,這裏主要是多出了以下部分
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;//步數初始化
proc->lab6_priority = 0;//初始化優先級
具體的解釋見註釋。
trap_dispatch函數
這裏在時鐘產生的地方需要對定時器做初始化,修改的部分如下:
static void
trap_dispatch(struct trapframe *tf) {
......
......
ticks ++;
assert(current != NULL);
run_timer_list(); //更新定時器,並根據參數調用調度算法
break;
......
......
}
練習1 使用Round Robin調度算法
Round Robin
調度算法的調度思想是讓所有 runnable 態的進程分時輪流使用 CPU 時間。Round Robin
調度器維護當前 runnable進程的有序運行隊列。當前進程的時間片用完之後,調度器將當前進程放置到運行隊列的尾部,再從其頭部取出進程進行調度。
在這個理解的基礎上,我們來分析算法的具體實現。
這裏Round Robin
調度算法的主要實現在default_sched.c
之中,源碼如下:
static void
RR_init(struct run_queue *rq) {
list_init(&(rq->run_list));
rq->proc_num = 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 ++;
}
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 --;
}
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 --;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
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_enqueue
函數,
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 ++;
}
看代碼,首先,它把進程的進程控制塊指針放入到rq隊列末尾,且如果進程控制塊的時間片爲0,則需要把它重置爲max_time_slice
。這表示如果進程在當前的執行時間片已經用完,需要等到下一次有機會運行時,才能再執行一段時間。然後在依次調整rq和rq的進程數目加一。
然後是RR_dequeue
函數
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 --;
}
即簡單的把就緒進程隊列rq的進程控制塊指針的隊列元素刪除,然後使就緒進程個數的proc_num減一。
接下來是RR_pick_next
函數。
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;
}
選取函數,即選取就緒進程隊列rq中的隊頭隊列元素,並把隊列元素轉換成進程控制塊指針,即置爲當前佔用CPU的程序。
最後是
static void RR_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;
}
}
觀察代碼,即每一次時間片到時的時候,當前執行進程的時間片time_slice
便減一。如果time_slice
降到零,則設置此進程成員變量need_resched
標識爲1,這樣在下一次中斷來後執行trap函數時,會由於當前進程程成員變量need_resched
標識爲1而執行schedule函數,從而把當前執行進程放回就緒隊列末尾,而從就緒隊列頭取出在就緒隊列上等待時間最久的那個就緒進程執行。
之後是一個對象化,提供一個類的實現,不是重點,這裏不做贅述。
練習2 實現Stride Scheduling調度算法
首先,根據實驗指導書的要求,先用default_sched_stride_c
覆蓋default_sched.c
,即覆蓋掉Round Robin
調度算法的實現。
覆蓋掉之後需要在該框架上實現Stride Scheduling
調度算法。
關於Stride Scheduling
調度算法,經過查閱資料和實驗指導書,我們可以簡單的把思想歸結如下:
- 1、爲每個
runnable
的進程設置一個當前狀態stride,表示該進程當前的調度權。另外定義其對應的pass值,表示對應進程在調度後,stride 需要進行的累加值。 - 2、每次需要調度時,從當前
runnable
態的進程中選擇 stride最小的進程調度。對於獲得調度的進程P,將對應的stride加上其對應的步長pass(只與進程的優先權有關係)。 - 3、在一段固定的時間之後,回到步驟2,重新調度當前stride最小的進程
參照實驗指導書所給的僞代碼能夠更好的理解該調度算法:
接下來針對代碼我們逐步分析,首先完整代碼如下:
#include <defs.h>
#include <list.h>
#include <proc.h>
#include <assert.h>
#include <default_sched.h>
#define USE_SKEW_HEAP 1
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;
if (c > 0) return 1;
else if (c == 0) return 0;
else return -1;
}
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;
}
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 ++;
}
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 --;
}
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);
#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)
p->lab6_stride += BIG_STRIDE;
else p->lab6_stride += BIG_STRIDE / p->lab6_priority;
return p;
}
static void
stride_proc_tick(struct run_queue *rq, struct proc_struct *proc) {
/* LAB6: YOUR CODE */
if (proc->time_slice > 0) {
proc->time_slice --;
}
if (proc->time_slice == 0) {
proc->need_resched = 1;
}
}
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_init
。
開始初始化運行隊列,並初始化當前的運行隊,然後設置當前運行隊列內進程數目爲0。
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
屬性,然後比較隊頭元素與當前進程的步數大小,選擇步數最小的運行,即將其插入放入運行隊列中去,這裏並未放置在隊列頭部。最後初始化時間片,然後將運行隊列進程數目加一。
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
,即完成將一個進程從隊列中移除的功能,這裏使用了優先隊列。最後運行隊列數目減一。
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
,觀察代碼,它的核心是先掃描整個運行隊列,返回其中stride值最小的對應進程,然後更新對應進程的stride值,將步長設置爲優先級的倒數,如果爲0則設置爲最大的步長。
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);
#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
//更新對應進程的stride值
if (p->lab6_priority == 0)
p->lab6_stride += BIG_STRIDE;
else p->lab6_stride += BIG_STRIDE / p->lab6_priority;
return p;
}
最後是時間片函數stride_proc_tick
,主要工作是檢測當前進程是否已用完分配的時間片。如果時間片用完,應該正確設置進程結構的相關標記來引起進程切換。這裏和之前實現的Round Robin
調度算法一樣,所以不贅述。
還有一個優先隊列的比較函數proc_stride_comp_f
的實現,主要思路就是通過步數相減,然後根據其正負比較大小關係。
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;//步數相減,通過正負比較大小關係
if (c > 0) return 1;
else if (c == 0) return 0;
else return -1;
}
另外還有就是一個封裝類的實現,也不詳細解釋了。
實驗結果
在lab6的根目錄下運行make qemu
,得到如下結果:
觀察可知,實驗成功。