進程優先級隊列

更詳細的講解和代碼調試演示過程,請參看視頻
Linux kernel Hacker, 從零構建自己的內核

前幾節,我們實現了進程的調度,并給每個進程賦予相應優先級,優先級高的進程,能夠得到更多的CPU時間。但在演示時,我們發現一個問題,就是,當進程發送切換時,鼠標鍵盤的響應會被卡死,這是因為響應鼠標鍵盤事件的進程被調到后臺,無法獲得CPU運行時間,從而不能處理鼠標或鍵盤事件。

這種情況對用戶來說,是不可接受的。對用戶輸入給予及時回應,直接關系到用戶體驗。所以負責相應用戶輸入的進程,其重要性要比那些只需要在后臺運行,不用和用戶直接交互的進程高很多。由此,我們需要實現的是,只要用戶有輸入,那么負責處理用戶輸入的進程就不能被打斷,為了實現這個目的,我們需要實現進程的優先級隊列。

這里寫圖片描述

如上圖,我們把所有進程根據level分成三個隊列,進程越重要,它對應的level值越低,這樣,當進行進程調度時,進程調度器先查看level等于0的隊列是否還有進程,有的話,只執行該隊列的進程,如果該隊列不止一個進程,那么每個進程根據他們的優先級獲得相應的CPU時間。

如果levelw為0的隊列沒有可調度的進程,那么level為1的隊列中的進程才有獲得調度的機會,以此類推。

由此,我們看看相關數據結構的定義,首先是multi_task.h:

struct TASK {
    int sel, flags;
    int priority;
    int level;
    struct TSS32 tss;
};

#define  MAX_TASKS  5
#define  MAX_TASKS_LV   3
#define  MAX_TASKLEVELS 3

#define  TASK_GDT0  7
#define  SIZE_OF_TASK  120

struct TASKLEVEL {
    int running;
    int now;
    struct TASK *tasks[MAX_TASKS_LV];
};

#define SIZE_OF_TASKLEVEL  (8+ 4*MAX_TASKS_LV)

struct TASKCTL {
    int  now_lv;
    int  lv_change;
    struct TASKLEVEL  level[MAX_TASKLEVELS];
    struct TASK tasks0[MAX_TASKS];
};

TASK結構體增加了一個level變量,用于表明進程的重要性。TASKLEVEL對應于上圖的進程隊列,相同重要性的進程,都存儲在對應TASKLEVEL的tasks數組中。

TASKCTL 也就是進程管理器,其存儲的不再是進程,而是進程的優先級隊列,它要找到重要性最高的進程隊列,從中取出進程進行調度。

multi_task.c 需要進行相應改動:

struct TASKCTL *get_taskctl() {
    return taskctl;
}

void init_task_level(int level) {
    taskctl->level[level].running = 0;
    taskctl->level[level].now = 0;
    int i;
    for (i = 0; i < MAX_TASKS_LV; i++) {
        taskctl->level[i].tasks[i] = 0;
    }
}

init_task_level 對進程優先級隊列進行初始化,running表示改對了中有幾個進程,now表示隊列中,哪個進程正在被調度到前臺進行運行。

struct TASK  *task_init(struct MEMMAN *memman) {
    int  i;
    ....
    
    task = task_alloc();
    task->flags = 2;  //active
    task->priority = 100;
    task->level = 0;
    task_add(task);
    task_switchsub();
    ....
}

上面代碼用于初始化運行CMain函數的進程,它把CMain進程的level設置為0,也就是該進程的重要性最高,只要它不被掛起,那么它始終擁有被調度的權利。task_add會根據進程的重要性,將其加入對應的優先級隊列,一旦有新進程加入,task_switchsub 則修改相應調度信息,以便進程調度器做出適當調動。

void task_run(struct TASK *task,int level, int priority) {
    if (level < 0) {
        level = task->level;
    }

    if (priority > 0) {
        task->priority = priority;
    }

    if (task->flags == 2 && task->level != level) {
        task_remove(task); //change task flags
    }

    if (task->flags != 2) {
        task->level = level;
        task_add(task);
    }

    taskctl->lv_change = 1;
    return;
} 

task_run 的作用是修改進程重要性或優先級,或者把新的進程根據其重要性添加到相應隊列。如果進程的重要性有改動,那么通過task_remove把進程從原有的優先級隊列中去除,然后再通過task_add將進程添加到對應的隊列。

void task_switch(void) {
    struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
    struct TASK *new_task, *now_task = tl->tasks[tl->now];
    tl->now++;
    if (tl->now == tl->running) {
        tl->now = 0;
    }
 
    if (taskctl->lv_change != 0) {
        task_switchsub();
        tl = &taskctl->level[taskctl->now_lv];
    }

    new_task = tl->tasks[tl->now];
    timer_settime(task_timer, new_task->priority);
    if (new_task != now_task && new_task != 0) {
        farjmp(0, new_task->sel);
    }

    return;
}

task_switch 被時鐘中斷調用,它的邏輯發送不小變化。taskctl->now_lv表示當前正則被調度的優先級隊列,例如,如果now_lv的值是0,那么表示當前level等于0的隊列中的進程才可以獲得被調度的機會。task_switch 通過taskctl->now_lv得知哪個進程隊列應該被調度,然后從該隊列中,取出task對象進行執行。如果有新的進程加入,或有進程的重要性被改變了,那么taskctl->lv_change的值就不等于0。假設當前獲得調度機會的是level值為1的隊列中的進程,但是有level等于0的進程對象添加到了level等于0的隊列中,那么此時,調度算法就會停止從level為1的隊列中去調度進程,而是切換到level為0的隊列,從中獲取要調度的進程。

int  task_sleep(struct TASK *task) {
   struct TASK *cur_task = 0;

   if (task->flags == 2) {
       cur_task = task_now();
       task_remove(task);
  
       if (task == cur_task) {
          task_switchsub();
          cur_task = task_now();
        
          if (cur_task != 0)
          {
              farjmp(0, cur_task->sel);
          }
       }
   }

   return 0;
}

struct TASK *task_now(void) {
    struct TASKLEVEL *tl = &taskctl->level[taskctl->now_lv];
    return tl->tasks[tl->now];
}

void task_add(struct TASK *task) {
    struct TASKLEVEL *tl = &taskctl->level[task->level];
    tl->tasks[tl->running] = task;
    tl->running++;
    task->flags = 2;
    return;
} 

task_sleep 用于刪除進程,如果需要殺掉某個進程,那么可以使用該函數剝奪指定進程對象獲得調度的權利。task_remove負責把進程從它所在的隊列中刪除,如果當前被掛起的進程是正在運行的進程,那么task_sleep會選擇下一個合適的進程進行調度執行。

task_now 用于返回當前正在被調用的進程對象,task_add把給定進程加入對應的優先級隊列。

void task_remove(struct TASK *task) {
    int i ;
    struct TASKLEVEL *tl = &taskctl->level[task->level];
    for (i = 0; i< tl->running; i++) {
        if (tl->tasks[i] == task) {
            tl->tasks[i] = 0;
            break;
        }
    }

    tl->running--;
    if (i < tl->now) {
        tl->now--;
    }

    if (tl->now >= tl->running) {
        tl->now = 0;
    } 

    task->flags = 1;

    for (; i < tl->running; i++) {
        tl->tasks[i] = tl->tasks[i+1];
    }

    return;
}

void task_switchsub(void) {
    int i;
    for (i = 0; i < MAX_TASKLEVELS; i++) {
        if (taskctl->level[i].running > 0) {
           break;
        }
    }

    taskctl->now_lv = i;
    taskctl->lv_change = 0;
}

task_remove主要負責把進程對象從隊列中刪除,并修改相應的隊列數據。一旦有進程刪除或添加,那么進程調度需要作出對應的調整,task_switchsub的作用是根據進程的添加或刪除,修改進程調度器的相應信息,進而改變進程調度器的調度行為。

最后是主入口函數CMain也做相應改動:

void CMain(void) {
    ....
    for (i = 0; i < 2; i++) {
       ....

       ....
       task_run(task_b[i], 1, (i+1)*5);
    }
    ....
}

主要到,我們把運行兩個窗體的進程其重要性設置成1,也就是只要運行主入口函數的進程不掛起,那么運行兩個窗體的進程就不能得到執行。

本節代碼改動雖多,但邏輯簡單,理解起來應該不難,經過上面的改動后,系統運行的情況如下:


這里寫圖片描述

從上圖看出,如果CMain進程運行時,上面兩個窗體原來的計數字符串沒有顯示,這是因為兩個窗體的進程得不到調度的機會。由于CMain進程負責相應鼠標鍵盤,因此,此時我們移動鼠標就不再出現卡頓的情形。

當CMain進程被掛起后,兩個窗體的進程獲得執行機會,因而窗體中的計算字符串就出現了:


這里寫圖片描述

更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號:


這里寫圖片描述
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容