第16天
繼續多任務之旅。前一天實現了兩個任務之間自動切換,今天開始寫一個更通用的多任務切換程序。
首先定義存儲每個任務的數據結構。
struct TASK {
int sel, flags;
struct TSS32 tss;
};
sel表現段先擇器,也就是CS的值,flags用于標記該任務是否被使用。
再創建一個用于存儲操作系統中所有任務的數據結構。
struct TASKCTL {
int running;
int now;
struct TASK *tasks[MAX_TASKS];
struct TASK tasks0[MAX_TASKS];
};
數據結構有了,然后進行操作,首先我們想創建一個任務,先要獲得TASKCTL中的某一個task0。
struct TASK *task_alloc(void)
{
int i;
struct TASK *task;
for (i = 0; i < MAX_TASKS; i++) {
if (taskctl->tasks0[i].flags == 0) {
task = &taskctl->tasks0[i];
task->flags = 1;
task->tss.eflags = 0x00000202;
task->tss.eax = 0;
task->tss.ecx = 0;
task->tss.edx = 0;
task->tss.ebx = 0;
task->tss.ebp = 0;
task->tss.esi = 0;
task->tss.edi = 0;
task->tss.es = 0;
task->tss.ds = 0;
task->tss.fs = 0;
task->tss.gs = 0;
task->tss.ldtr = 0;
task->tss.iomap = 0x40000000;
return task;
}
}
return 0;
}
在TASKCTL中尋找一個還未使用的task用于存儲,并對task結構進行初使化賦值,然后返回task的地址。
操作系統一開始運行的時候是單任務的,在進行到多任務管理之前,要先初使化TASKCTL數據結構,并為TASK數組申請內存空間,在多任務功能創建完畢之后,還要把自己本身納入多任務管理的范圍內。也就是說操作系統一啟動,一開機時候,顯示了桌面,第一個任務就是它自己本身。
struct TASK *task_init(struct MEMMAN *memman)
{
int i;
struct TASK *task;
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
taskctl = (struct TASKCTL *) memman_alloc_4k(memman, sizeof (struct TASKCTL));
for (i = 0; i < MAX_TASKS; i++) {
taskctl->tasks0[i].flags = 0;
taskctl->tasks0[i].sel = (TASK_GDT0 + i) * 8;
set_segmdesc(gdt + TASK_GDT0 + i, 103, (int) &taskctl->tasks0[i].tss, AR_TSS32);
}
task = task_alloc();
task->flags = 2;
taskctl->running = 1;
taskctl->now = 0;
taskctl->tasks[0] = task;
load_tr(task->sel);
task_timer = timer_alloc();
timer_settime(task_timer, 2);
return task;
}
首先定義一個TASKCTL類型的變量,并分配內存空間,然后用循環語句為這個變量賦初始值,flags全部賦為0,因為還未開始使用。然后為每個任務分配gdt序號。再申請一個task,把操作系統當前運行的任務放進去,并設置2ms的定時器。
用task_alloc函數取得task變量之后,再調用task_run函數運行。
void task_run(struct TASK *task)
{
task->flags = 2;
taskctl->tasks[taskctl->running] = task;
taskctl->running++;
return;
}
在init_task函數中已經設置了2ms的定時器,定時器超時的時候,會調用task_switch函數
void task_switch(void)
{
timer_settime(task_timer, 2);
if (taskctl->running >= 2) {
taskctl->now++;
if (taskctl->now == taskctl->running) {
taskctl->now = 0;
}
farjmp(0, taskctl->tasks[taskctl->now]->sel);
}
return;
}
先設置2ms定時器,然后判斷任務數,任務數如果只有一個就不用切換了。如果多于1個,那么切換到下一個任務。如果已經是最后一個任務,那么就運行第一個任務,重新循環一次。改造之后的多任務程序看上去就好多了,不管什么任務,只要alloc一個,放進run里面,操作系統會自動且平均分配2ms的時間運行。平均分配時間也有缺點,如果一個任務創建之后都沒有使用,那么也分配2ms的話就太浪費cpu的計算能力了,我們就實現讓任務休眠的機制。
void task_sleep(struct TASK *task)
{
int i;
char ts = 0;
if (task->flags == 2) { /* 如果指定任務處于喚醒狀態 */
if (task == taskctl->tasks[taskctl->now]) {
ts = 1; /* 讓自己休眠的話,稍后需要進行任務切換 */
}
/* 尋找task所在的位置 */
for (i = 0; i < taskctl->running; i++) {
if (taskctl->tasks[i] == task) {/* 在這里 */
break;
}
}
taskctl->running--;
if (i < taskctl->now) {
taskctl->now--; /* 需要移動成員,要相應地處理 */
}
/* 移動成員 */
for (; i < taskctl->running; i++) {
taskctl->tasks[i] = taskctl->tasks[i + 1];
}
task->flags = 1; /* 不工作的狀態 */
if (ts != 0) {
/* 任務切換 */
if (taskctl->now >= taskctl->running) {
/* 如果now的值出現異常,則進行修正 */
taskctl->now = 0;
}
farjmp(0, taskctl->tasks[taskctl->now]->sel);
}
}
return;
}
首先判斷準務休眠的任務是不是當前正在運行的任務。然后尋找將要休眠的任務所處于TASKCTL變量中的位置,然后將這個位置覆蓋,如果判斷是正在運行的任務馬上切換任務。將下來的問題是接收鼠標、鍵盤或者其它中斷后,如何喚醒體眠的任務。
每次中斷發生后都會往消息隊列中發送數據,如果喚醒某一個任務也應該從隊列入手。改造隊列的數據結構,增加存儲TASK指針的字段。
struct FIFO32 {
int *buf;
int p, q, size, free, flags;
struct TASK *task;
};
然后在中斷處理程序往消息隊列寫入數據的時候將任務喚醒,我們修改一下入隊函數。
int fifo32_put(struct FIFO32 *fifo, int data)
{
if (fifo->free == 0) {
fifo->flags |= FLAGS_OVERRUN;
return -1;
}
fifo->buf[fifo->p] = data;
fifo->p++;
if (fifo->p == fifo->size) {
fifo->p = 0;
}
fifo->free--;
if (fifo->task != 0) {
if (fifo->task->flags != 2) {
task_run(fifo->task);
}
}
return 0;
}
增加了return 0之前的5行,就是說中斷處理程序往隊列中寫入消息的時候,判斷當前隊列所代表的任務是否處于活動狀態,如果休眠的話那就喚醒。
接下來我們另外再創建3個任務,每個任務都顯示一下窗口,在窗口中只做一件事情,那就是不停得計數并把計數結果顯示到窗口上。
要實現也比較簡單,先創建3個TASK類型的指針,再調用task_alloc函數分配任務存儲空間。創建SHEET指針,再調用sheet_alloc函數分配存儲空間。再調用task_run函數運行。
我在看源代碼時候看到3個窗口任務的入口地址都是task_b_main函數,突然有一個疑問,入口地址都是一樣的的,那么這個函數中定義的變量和消息隊列會不會混淆。前前后后看了好幾遍,task_b[i]->tss.esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024 - 8;這句程序為每個任務申請了不同的??臻g。程序入口函數中定義的int i; int fifobuf[128],雖然是在入口函數中直接定義,但是C語言分配內存空間的時候是把棧中的內存空間拿過來使用。所以雖然的任務入口函數是一樣的,程序的入口也在內存中的同一位置,但是每個任務所使用的數據都是不一樣的。通過近半個小時的思考,我覺得對C語言的內存分配方式有了更加深入的了解。
我們現在為每個任務平均分配了2ms的運行時間,但是如果要把操作系統做得更好,肯定要分出任務的輕重緩急,也就是要設置每個任務的優先級。我們可以設置10 個等級,分配運行的時間從0.01秒~0.1秒。在TASK結構體中增加int priority字段,用于表示優先級。我們把任務a設置成10,也就是說任務a運行的時間有0.1秒,但是由于a不運行的時候會自動休眠,所以也不會影響其他任務的運行。
運用為任務分配定時器的時間方式是最簡單的方式。如果任務A是最重要的,只是給A設置高一點的優先級,那么其他任務還是會運行。有時候我們會碰到一種情況,希望如果任務A需要運行,那么在任務A運行完之前其它任務都不能運行。
我們假設給任務分3個等級,分別是level0~2。其中level0優先級最高,如果level0里的任務需要運行,那么,level1和2都不能運行。
之前我們處理多任務的數據結構有2層,首先是表示具體任務的TASK結構,然后是把TASK結構統一管理的TASKCTL結構?,F在我們在這兩者之前增加TASKLEVEL結構,用于表示任務的級別關系。
struct TASK {
int sel, flags;
int level, priority;
struct TSS32 tss;
};
level變量表示任務所處的級別。
struct TASKLEVEL {
int running; /* 正在運行的任務數量 */
int now; /* 這個變量表示正在運行的是哪個任務 */
struct TASK *tasks[MAX_TASKS_LV];
};
struct TASKCTL {
int now_lv; /* 現在活動中的LEVEL */
char lv_change; /* 在下次任務切換時是否需要改變LEVEL */
struct TASKLEVEL level[MAX_TASKLEVELS];
struct TASK tasks0[MAX_TASKS];
};
現在TASKCTL不再直接是管理任務,而是管理TASKLEVEL,再由TASKLEVEL管理各個任務。書中處理這部分代碼不是很復雜,我也就不貼出來了。主要注意的地方是task_switch函數里如果TASKCTL中lv_change字段為1就要重新查看LEVEL是否有新的更高級的層次任務需要執行。