ucoreOS_lab6 實驗報告

所有的實驗報告將會在 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/

練習0:填寫已有實驗

lab6 會依賴 lab1~lab5 ,我們需要把做的 lab1~lab5 的代碼填到 lab6 中缺失的位置上面。練習 0 就是一個工具的利用。這裏我使用的是 Linux 下的系統已預裝好的 Meld Diff Viewer 工具。和 lab5 操作流程一樣,我們只需要將已經完成的 lab1~lab5 與待完成的 lab6 (由於 lab6 是基於 lab1~lab5 基礎上完成的,所以這裏只需要導入 lab5 )分別導入進來,然後點擊 compare 就行了。

compare

然後軟件就會自動分析兩份代碼的不同,然後就一個個比較比較複製過去就行了,在軟件裏面是可以支持打開對比複製了,點擊 Copy Right 即可。當然 bin 目錄和 obj 目錄下都是 make 生成的,就不用複製了,其他需要修改的地方主要有以下六個文件,通過對比複製完成即可:

proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c

根據試驗要求,我們需要對部分代碼進行改進,這裏講需要改進的地方的代碼和說明羅列如下:

  • PCT 中增加了三個與 stride 調度算法相關的成員變量,以及增加了對應的初始化過程;
  • 新增了斜堆數據結構的實現;
  • 新增了默認的調度算法 Round Robin 的實現,具體爲調用 sched_class_* 等一系列函數之後,進一步調用調度器 sched_class 中封裝的函數,默認該調度器爲 Round Robin 調度器,這是在 default_sched.* 中定義的;
  • 新增了 set_priority,get_time 的系統調用;

proc_struct 結構體

我們在原來的實驗基礎上,新增了 9 行代碼:

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 使用
};

alloc_proc() 函數

我們在原來的實驗基礎上,新增了 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;
}

trap_dispatch() 函數

我們在原來的實驗基礎上,新增了 1 行代碼:

run_timer_list(); //更新定時器,並根據參數調用調度算法

這裏主要是將時間片設置爲需要調度,說明當前進程的時間片已經用完了。

所以改進後的 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 之中,源碼如下:

/*
  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 調度執行過程:

  • 在ucore中調用調度器的主體函數(不包括 init,proc_tick)的代碼僅存在在 wakeup_proc 和 schedule,前者的作用在於將某一個指定進程放入可執行進程隊列中,後者在於將當前執行的進程放入可執行隊列中,然後將隊列中選擇的下一個執行的進程取出執行;
  • 當需要將某一個進程加入就緒進程隊列中,則需要將這個進程的能夠使用的時間片進行初始化,然後將其插入到使用鏈表組織的隊列的對尾;這就是具體的 Round-Robin enqueue 函數的實現;
  • 當需要將某一個進程從就緒隊列中取出的時候,只需要將其直接刪除即可;
  • 當需要取出執行的下一個進程的時候,只需要將就緒隊列的隊頭取出即可;
  • 每當出現一個時鐘中斷,則會將當前執行的進程的剩餘可執行時間減 1,一旦減到了 0,則將其標記爲可以被調度的,這樣在 ISR 中的後續部分就會調用 schedule 函數將這個進程切換出去;

請在實驗報告中簡要說明如何設計實現”多級反饋隊列調度算法“,給出概要設計,鼓勵給出詳細設計;

設計如下:

  • 在 proc_struct 中添加總共 N 個多級反饋隊列的入口,每個隊列都有着各自的優先級,編號越大的隊列優先級約低,並且優先級越低的隊列上時間片的長度越大,爲其上一個優先級隊列的兩倍;並且在 PCB 中記錄當前進程所處的隊列的優先級;
  • 處理調度算法初始化的時候需要同時對 N 個隊列進行初始化;
  • 在處理將進程加入到就緒進程集合的時候,觀察這個進程的時間片有沒有使用完,如果使用完了,就將所在隊列的優先級調低,加入到優先級低 1 級的隊列中去,如果沒有使用完時間片,則加入到當前優先級的隊列中去;
  • 在同一個優先級的隊列內使用時間片輪轉算法;
  • 在選擇下一個執行的進程的時候,有限考慮高優先級的隊列中是否存在任務,如果不存在才轉而尋找較低優先級的隊列;(有可能導致飢餓)
  • 從就緒進程集合中刪除某一個進程就只需要在對應隊列中刪除即可;
  • 處理時間中斷的函數不需要改變;

至此完成了多級反饋隊列調度算法的具體設計;

練習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 最小的進程。

接下來針對代碼我們逐步分析,首先完整代碼如下:

* 實現思路:
由於在 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 步長一定會溢出,然後又從 0 開始算,這樣,整個調度算法的比較就沒有意義了。

這說明,我們必須得約定一個最大的步長,使得兩個進程的步進值哪怕其中一個溢出或者都溢出還能夠進行比較。

首先 因爲 步長 和 優先級成反比 可以得到一條: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

如果 make grade 無法滿分,嘗試註釋掉 tools/grade.sh 的 221 行到 233 行(在前面加上“#”)。

這裏我們選用古老的編輯器 Vim,具體操作過程如下:

  • 1、首先按 esc 進入命令行模式下,按下 :221 跳轉至 221 行;
  • 2、按下 Ctrl + v,進入列(也叫區塊)模式;
  • 3、在行首使用上下鍵選擇需要註釋的多行(221~233 行);
  • 4、按下鍵盤(大寫)“I”鍵,進入插入模式;
  • 5、然後輸入註釋符(“//”、“#”等);
  • 6、最後按下“Esc”鍵。

擴展練習

Challenge 1 :實現 Linux 的 CFS 調度算法

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 中增加三個成員:

  • 虛擬運行時間
  • 優先級係數:從 1 開始,數值越大,時間過得越快
  • 斜堆
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
};
算法實現
proc_fair_comp_f

首先需要一個比較函數,同樣根據 完全不需要考慮虛擬運行時溢出的問題。

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;
}
fair_init
static void fair_init(struct run_queue *rq) {
    rq->fair_run_pool = NULL;
    rq->proc_num = 0;
}
fair_enqueue

和 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 ++;
}
fair_dequeue
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 --;
}
fair_pick_next
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;
}
fair_proc_tick

需要更新虛擬運行時,增加的量爲優先級係數。

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算法最佳選擇。

  • 堆基於數組,但是對於調度器來說進程數量不確定,無法使用定長數組實現的堆;
  • ucore 中的 Stride Scheduling 調度算法使用了斜堆,但是斜堆沒有維護平衡的要求,可能導致斜堆退化成爲有序鏈表,影響性能。

綜上所示,紅黑樹因爲平衡性以及非連續所以是CFS算法最佳選擇。

Challenge 2 :在ucore上實現儘可能多的各種基本調度算法(FIFO, SJF,...),並設計各種測試用例,能夠定量地分析出各種調度算法在各種指標上的差異,說明調度算法的適用範圍。

待完成。。。

參考資料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章