更詳細的講解和代碼調試演示過程,請參看視頻
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進程被掛起后,兩個窗體的進程獲得執行機會,因而窗體中的計算字符串就出現了:
更多技術信息,包括操作系統,編譯器,面試算法,機器學習,人工智能,請關照我的公眾號: