《30天自制操作系統》筆記(12)——多任務入門


《30天自制操作系統》筆記(12)——多任務入門

進度回顧

上一篇介紹了設置顯示器高分辨率的方法。本篇講一下操作系統實現多任務的方法。

什么是多任務

對程序員來說,也許這是廢話,不過還是說清楚比較好。

多任務就是讓電腦同時運行多個程序(如一邊寫代碼一邊聽音樂一邊下載電影)。

電腦的CPU只有固定有限的那么一個或幾個,不可能真的同時運行多個程序。所以就用近似的方式,讓多個程序輪換着運行。當輪換速度夠快(0.01秒),給人的感覺就是"同時"運行了。

 

多任務之不實用版

我們首先從最基本的想法開始,做一個不實用版的多任務作為例子。在學習這個例子的過程中引入真正的多任務必須的TSS、TR、far模式JMP的概念,為后續內容打基礎。

當你向CPU發出任務切換的指令時,CPU會先把寄存器中的值全部寫入內存某處;然后,從內存另一位置把所有寄存器的值讀取出來。這就完成了一次任務切換。

任務切換消耗的時間就是讀寫內存消耗的時間,大概為0.0001秒

任務狀態段TSS

存取全部寄存器的值這件事,當然需要有一個數據結構,這就是"任務狀態段"(Task Status Segment)簡稱TSS。

1 struct TSS32
2 {
3     int backlink, esp0, ss0, esp1, ss1, esp2, ss2, cr3;
4     int eip, eflags, eax, ecx, edx, ebx, esp, ebp, esi, edi;
5     int es, cs, ss, ds, fs, gs;
6     int ldtr, iomap;
7 };

TSS32中第一行(從backlink到cr3)暫時不用理會。

第二、三行(從eip到gs)都是寄存器。其中EIP是CPU用來記錄下一條需要執行的指令位於內存中的地址的寄存器,因此被稱為"指令指針"。實際上JMP指令就是修改了EIP的值。

第四行也不用理會。

TSS中的信息會存儲到內存某處(記為X),而X的地址會注冊到GDT中。(不知道什么是GDT?請查看這里

寄存器TR

寄存器TR是作用是讓CPU記住當前在運行哪個任務。其存儲的值是"當前任務所在的段號*8"。只需在操作系統啟動時對其賦值一次,以后進行任務切換時,CPU會自動調整TR的值。給TR賦值只能用匯編實現。

1 _load_tr:        ; void load_tr(int tr);
2         LTR        [ESP+4]            ; tr
3         RET

LTR指令只是改變TR的值,不會發生任務切換。所以我感覺TR像是一個標識變量。正是由於這一點我才有了后文的猜想。

 

切換任務就是執行JMP指令

JMP指令分兩種,即"只改寫EIP的near模式"與"同時改寫EIP和CS的far模式"。CS是代碼段寄存器(code segment)。

平時使用的都是near模式。

在asmhead.nas中跳轉到bootpack.c中的主函數用的是far模式,即

 1 JMP DWORD 2 * 8: 0x0000001b  

這條指令在向EIP寫入0x1b時,也向CS寫入2*8(即16)。

像這樣在JMP目標地址中帶冒號(:)的,就是far模式。

切換任務時,我們使用far模式的JMP指令。

CPU執行far模式的JMP指令前,會根據GDT中注冊的TSS情況,判斷JMP的目標地址是可執行代碼還是TSS。如果是可執行代碼,那么CPU就認為這只是一個普通的far模式的JMP;如果是TSS,則認為這是一個任務切換指令,會切換到目標地址指定的TSS所記錄的任務中,也就是JMP到另一個任務那里去了。

所以普通的far模式的JMP和任務切換的JMP指令,其機器碼是同一個。

Demo:兩個任務切換

我們把操作系統啟動時運行的程序記作任務A,即如下代碼。

 1 void HariMain(void)
 2 {
 3     /**/
 4     timer_ts = timer_alloc();
 5     timer_init(timer_ts, &fifo, 2);
 6     timer_settime(timer_ts, 2);
 7     /**/
 8     for (;;) {
 9         io_cli();
10         if (fifo32_status(&fifo) == 0) {
11             io_stihlt();
12         } else {
13             i = fifo32_get(&fifo);
14             io_sti();
15             if (i == 2) {
16                 farjmp(0, 4 * 8);
17                 timer_settime(timer_ts, 2);
18             } else if (256 <= i && i <= 511) { /* 鍵盤數據 */
19                 /**/
20             } else if (512 <= i && i <= 767) { /* 鼠標數據 */
21                 /**/
22             } else if (i == 10) { /* 10秒計時器 */
23                 putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
24             } else if (i == 3) { /* 3秒計時器 */
25                 putfonts8_asc_sht(sht_back, 0, 80, COL8_FFFFFF, COL8_008484, "3[sec]", 6);
26             } else if (i <= 1) { /* 光標用計時器 */
27                 /**/
28             }
29         }
30     }
31 }
任務A:操作系統啟動時程序

下面是任務B執行的函數。

 1 void task_b_main(void)
 2 {
 3     struct FIFO32 fifo;
 4     struct TIMER *timer_ts;
 5     int i, fifobuf[128];
 6 
 7     fifo32_init(&fifo, 128, fifobuf);
 8     timer_ts = timer_alloc();
 9     timer_init(timer_ts, &fifo, 1);
10     timer_settime(timer_ts, 2);
11 
12     for (;;) {
13         io_cli();
14         if (fifo32_status(&fifo) == 0) {
15             io_sti();
16             io_hlt();
17         } else {
18             i = fifo32_get(&fifo);
19             io_sti();
20             if (i == 1) { /* 任務切換 */
21                 farjmp(0, 3 * 8);
22                 timer_settime(timer_ts, 2);
23             }
24         }
25     }
26 }
任務B

任務A執行0.02秒后就進入farjmp(0, 4 * 8);,從而自行切換到任務B,任務B執行0.02秒后就進入farjmp(0, 3 * 8);,從而自行切換到任務A。周而復始。

像這種在應用代碼中編寫任務切換的方式,明顯不實用。不過用於研究多任務還是很方便的。

多任務截圖沒有意義,就此作罷。

 

真正的多任務

大體上說,實現多任務的方法就是利用前面提到的定時器PIT(Programmable Interval Timer)能夠定時產生中斷的功能,在其中斷處理函數中實現任務切換的目的。

時間片輪轉調度算法是一種最基本的任務調度算法,它讓每個任務依次執行相同的一段時間(如0.01秒)。在此基礎上,可以為任務添加"休眠"、"優先級"等功能和屬性,根據屬性值調整執行時間和執行順序。

 1 void inthandler20(int *esp)
 2 {
 3     /**/
 4     char ts = 0;
 5     for (;;) {
 6         /* timers的計時器全部在工作中,因此不用確認flags */
 7         if (timer->timeout > timerctl.count) {
 8             break;
 9         }
10         /* 超時 */
11         timer->flags = TIMER_FLAGS_ALLOC;
12         if (timer != mt_timer) {
13             fifo32_put(timer->fifo, timer->data);
14         } else {
15             ts = 1; /* mt_timer超時 */
16         }
17         timer = timer->next; /* 將下一個計時器的地址賦給timer */
18     }
19     timerctl.t0 = timer;
20     timerctl.next = timer->timeout;
21     if (ts != 0) {
22         mt_taskswitch();
23     }
24     return;
25 }
26 void mt_taskswitch(void)
27 {/* demo演示只有兩個固定任務的切換過程 */
28     if (mt_tr == 3 * 8) {
29         mt_tr = 4 * 8;
30     } else {
31         mt_tr = 3 * 8;
32     }
33     timer_settime(mt_timer, 2);
34     farjmp(0, mt_tr);
35     return;
36 }

為了簡化非核心代碼,我用demo版的mt_taskswitch(void)代替了有復雜數據結構的真實版本,這樣方便理解整個代碼的原理。其中的farjmp是用匯編實現的。

1 _farjmp:        ; void farjmp(int eip, int cs);
2         JMP        FAR    [ESP+4]                ; eip, cs
3         RET

根據C語言編譯器的規則,調用這個farjmp函數時,在[ESP+4]處存放了EIP的值,在 [ESP+8]處存放了CS的值。給eip賦值0,給cs賦值要切換到的任務所在的段號(乘8),就可以正確調用farjmp。

一般發生JMP后,不會執行后面的RET指令了。但是,執行任務切換的JMP后,再返回這個任務的時候,程序會從JMP指令之后的地方恢復運行,也就是JMP后面這個RET指令會被執行。因此這里的RET必不可少。

任務切換的時機

我提出一個問題,任務切換是在farjmp中執行JMP FAR [ESP+4]發生的嗎?

從不實用版的代碼看來,答案應該是"是"。因為確實在任務A執行了farjmp中的JMP FAR [ESP+4]指令后切換到了任務B,之后切換回任務A時,又從JMP FAR [ESP+4]指令后面的RET指令開始執行了。

但是在真正的多任務中,CPU調用中斷處理函數,在inthandler20中執行了farjmp中的JMP FAR [ESP+4]。如果答案是"是",那么此時就會從中斷處理函數中切換到另一個任務A中了。

可是,還會不會切換回中斷處理函數inthandler20呢?

如果會,那么中斷處理函數不就也成了一個任務嗎?

如果會,那么中斷處理函數函還沒有return不就中斷了嗎?此時再來一個中斷的話,會怎么樣呢?棧就亂套了。

如果不會,那么中斷處理函數最前面用匯編寫的PUSH各種寄存器的指令就沒有相應的POP指令了呀,時間一長棧就溢出了呀。這明顯不對。

所以,我猜想只有一種情況是可行的。那就是:farjmp中的JMP FAR [ESP+4]指令並沒有完成任務切換,它只是讓CPU記錄了一個標識(比如上文的寄存器TR的作用),標識應該運行的任務是X。然后,當CPU完成中斷處理函數,再次執行某個任務A中的指令時,它會發現"現在應該執行任務X"中的指令了,所以它就切換到任務X中去。任務切換實際上此時才完成。

我查了一些資料,只能暫時作此猜想。

多任務優化

為了提高多任務運行效率,下面就對其進行優化。

休眠和喚醒

"休眠"就是從tasks鏈表中去掉一個任務A,"喚醒"就是把這個任務A重新加入tasks鏈表。

休眠的時機:任務A的消息隊列為空(沒有待處理的消息)時。

喚醒的時機:任務A的消息隊列獲得新的消息時。

任務優先級

把任務分到Level0、Level1、Level2這三個層中的一個,當Level0有活動的任務(非休眠狀態)時,只在Level0的任務間切換。當Level0沒有任務或均處於休眠狀態時,在Level1的任務間切換。Level2同理。

閑置任務

這與"哨兵"的思路相同。就是在Level2中添加一個只HLT的任務。如果操作系統里沒有其他任何任務的話,就會執行這個"哨兵"任務,即HLT掉(直至有中斷發生)。

哨兵的好處就是簡化代碼,使得邏輯處理沒有特殊情況。

總結

操作系統利用CPU的far模式的JMP指令、寄存器TR、GDT、TSS和PIT中斷這些功能實現了多任務,可見CPU在設計時就考慮到了計算機要具有多任務處理的能力。也就是說,CPU、PIC等硬件支持什么功能,操作系統才能實現什么功能。這又肯定了硬件為操作系統提供API的看法。

 

請查看下一篇《《30天自制操作系統》筆記(13)——總結》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM