《30天自制操作系統(tǒng)》筆記(12)
本文關(guān)鍵詞:30天自制操作系統(tǒng),由筆耕文化傳播整理發(fā)布。
《30天自制操作系統(tǒng)》筆記(12)——多任務(wù)入門(mén)
進(jìn)度回顧上一篇介紹了設(shè)置顯示器高分辨率的方法。本篇講一下操作系統(tǒng)實(shí)現(xiàn)多任務(wù)的方法。
什么是多任務(wù)對(duì)程序員來(lái)說(shuō),也許這是廢話,不過(guò)還是說(shuō)清楚比較好。
多任務(wù)就是讓電腦同時(shí)運(yùn)行多個(gè)程序(如一邊寫(xiě)代碼一邊聽(tīng)音樂(lè)一邊下載電影)。
電腦的CPU只有固定有限的那么一個(gè)或幾個(gè),不可能真的同時(shí)運(yùn)行多個(gè)程序。所以就用近似的方式,讓多個(gè)程序輪換著運(yùn)行。當(dāng)輪換速度夠快(0.01秒),給人的感覺(jué)就是"同時(shí)"運(yùn)行了。
多任務(wù)之不實(shí)用版
我們首先從最基本的想法開(kāi)始,做一個(gè)不實(shí)用版的多任務(wù)作為例子。在學(xué)習(xí)這個(gè)例子的過(guò)程中引入真正的多任務(wù)必須的TSS、TR、far模式JMP的概念,為后續(xù)內(nèi)容打基礎(chǔ)。
當(dāng)你向CPU發(fā)出任務(wù)切換的指令時(shí),CPU會(huì)先把寄存器中的值全部寫(xiě)入內(nèi)存某處;然后,從內(nèi)存另一位置把所有寄存器的值讀取出來(lái)。這就完成了一次任務(wù)切換。
任務(wù)切換消耗的時(shí)間就是讀寫(xiě)內(nèi)存消耗的時(shí)間,大概為0.0001秒。
任務(wù)狀態(tài)段TSS存取全部寄存器的值這件事,當(dāng)然需要有一個(gè)數(shù)據(jù)結(jié)構(gòu),這就是"任務(wù)狀態(tài)段"(Task Status Segment)簡(jiǎn)稱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)暫時(shí)不用理會(huì)。
第二、三行(從eip到gs)都是寄存器。其中EIP是CPU用來(lái)記錄下一條需要執(zhí)行的指令位于內(nèi)存中的地址的寄存器,因此被稱為"指令指針"。實(shí)際上JMP指令就是修改了EIP的值。
第四行也不用理會(huì)。
TSS中的信息會(huì)存儲(chǔ)到內(nèi)存某處(記為X),而X的地址會(huì)注冊(cè)到GDT中。(不知道什么是GDT?請(qǐng)查看這里)
寄存器TR寄存器TR是作用是讓CPU記住當(dāng)前在運(yùn)行哪個(gè)任務(wù)。其存儲(chǔ)的值是"當(dāng)前任務(wù)所在的段號(hào)*8"。只需在操作系統(tǒng)啟動(dòng)時(shí)對(duì)其賦值一次,以后進(jìn)行任務(wù)切換時(shí),CPU會(huì)自動(dòng)調(diào)整TR的值。給TR賦值只能用匯編實(shí)現(xiàn)。
[ESP+
LTR指令只是改變TR的值,不會(huì)發(fā)生任務(wù)切換。所以我感覺(jué)TR像是一個(gè)標(biāo)識(shí)變量。正是由于這一點(diǎn)我才有了后文的猜想。
切換任務(wù)就是執(zhí)行JMP指令
JMP指令分兩種,即"只改寫(xiě)EIP的near模式"與"同時(shí)改寫(xiě)EIP和CS的far模式"。CS是代碼段寄存器(code segment)。
平時(shí)使用的都是near模式。
在asmhead.nas中跳轉(zhuǎn)到bootpack.c中的主函數(shù)用的是far模式,即
1 JMP DWORD 2 * 8: 0x0000001b
這條指令在向EIP寫(xiě)入0x1b時(shí),也向CS寫(xiě)入2*8(即16)。
像這樣在JMP目標(biāo)地址中帶冒號(hào)(:)的,就是far模式。
切換任務(wù)時(shí),我們使用far模式的JMP指令。
CPU執(zhí)行far模式的JMP指令前,會(huì)根據(jù)GDT中注冊(cè)的TSS情況,判斷JMP的目標(biāo)地址是可執(zhí)行代碼還是TSS。如果是可執(zhí)行代碼,那么CPU就認(rèn)為這只是一個(gè)普通的far模式的JMP;如果是TSS,則認(rèn)為這是一個(gè)任務(wù)切換指令,會(huì)切換到目標(biāo)地址指定的TSS所記錄的任務(wù)中,也就是JMP到另一個(gè)任務(wù)那里去了。
所以普通的far模式的JMP和任務(wù)切換的JMP指令,其機(jī)器碼是同一個(gè)。
Demo:兩個(gè)任務(wù)切換我們把操作系統(tǒng)啟動(dòng)時(shí)運(yùn)行的程序記作任務(wù)A,即如下代碼。
1 void HariMain(void) 2 { timer_ts = timer_alloc(); 5 timer_init(timer_ts, &fifo, 2); 6 timer_settime(timer_ts, 2); (;;) { 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); } } putfonts8_asc_sht(sht_back, , 7); putfonts8_asc_sht(sht_back, , 6); } 29 } 30 } 31 }
任務(wù)A:操作系統(tǒng)啟動(dòng)時(shí)程序下面是任務(wù)B執(zhí)行的函數(shù)。
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); (;;) { 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(); farjmp(0, 3 * 8); 22 timer_settime(timer_ts, 2); 23 } 24 } 25 } 26 }
任務(wù)B任務(wù)A執(zhí)行0.02秒后就進(jìn)入farjmp(0, 4 * 8);,從而自行切換到任務(wù)B,任務(wù)B執(zhí)行0.02秒后就進(jìn)入farjmp(0, 3 * 8);,從而自行切換到任務(wù)A。周而復(fù)始。
像這種在應(yīng)用代碼中編寫(xiě)任務(wù)切換的方式,明顯不實(shí)用。不過(guò)用于研究多任務(wù)還是很方便的。
多任務(wù)截圖沒(méi)有意義,就此作罷。
真正的多任務(wù)
大體上說(shuō),實(shí)現(xiàn)多任務(wù)的方法就是利用前面提到的定時(shí)器PIT(Programmable Interval Timer)能夠定時(shí)產(chǎn)生中斷的功能,在其中斷處理函數(shù)中實(shí)現(xiàn)任務(wù)切換的目的。
時(shí)間片輪轉(zhuǎn)調(diào)度算法是一種最基本的任務(wù)調(diào)度算法,它讓每個(gè)任務(wù)依次執(zhí)行相同的一段時(shí)間(如0.01秒)。在此基礎(chǔ)上,可以為任務(wù)添加"休眠"、"優(yōu)先級(jí)"等功能和屬性,根據(jù)屬性值調(diào)整執(zhí)行時(shí)間和執(zhí)行順序。
1 void inthandler20(int *esp) 2 { ts = 0; 5 for (;;) { (timer->timeout > timerctl.count) { 8 break; 9 } timer->flags = TIMER_FLAGS_ALLOC; 12 if (timer != mt_timer) { 13 fifo32_put(timer->fifo, timer->data); 14 } else { } } 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) (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 }
為了簡(jiǎn)化非核心代碼,我用demo版的mt_taskswitch(void)代替了有復(fù)雜數(shù)據(jù)結(jié)構(gòu)的真實(shí)版本,這樣方便理解整個(gè)代碼的原理。其中的farjmp是用匯編實(shí)現(xiàn)的。
FAR [ESP+
根據(jù)C語(yǔ)言編譯器的規(guī)則,調(diào)用這個(gè)farjmp函數(shù)時(shí),在[ESP+4]處存放了EIP的值,在 [ESP+8]處存放了CS的值。給eip賦值0,給cs賦值要切換到的任務(wù)所在的段號(hào)(乘8),就可以正確調(diào)用farjmp。
一般發(fā)生JMP后,不會(huì)執(zhí)行后面的RET指令了。但是,,執(zhí)行任務(wù)切換的JMP后,再返回這個(gè)任務(wù)的時(shí)候,程序會(huì)從JMP指令之后的地方恢復(fù)運(yùn)行,也就是JMP后面這個(gè)RET指令會(huì)被執(zhí)行。因此這里的RET必不可少。
任務(wù)切換的時(shí)機(jī)我提出一個(gè)問(wèn)題,任務(wù)切換是在farjmp中執(zhí)行JMP FAR [ESP+4]時(shí)發(fā)生的嗎?
從不實(shí)用版的代碼看來(lái),答案應(yīng)該是"是"。因?yàn)榇_實(shí)在任務(wù)A執(zhí)行了farjmp中的JMP FAR [ESP+4]指令后切換到了任務(wù)B,之后切換回任務(wù)A時(shí),又從JMP FAR [ESP+4]指令后面的RET指令開(kāi)始執(zhí)行了。
但是在真正的多任務(wù)中,CPU調(diào)用中斷處理函數(shù),在inthandler20中執(zhí)行了farjmp中的JMP FAR [ESP+4]。如果答案是"是",那么此時(shí)就會(huì)從中斷處理函數(shù)中切換到另一個(gè)任務(wù)A中了。
可是,還會(huì)不會(huì)切換回中斷處理函數(shù)inthandler20呢?
如果會(huì),那么中斷處理函數(shù)不就也成了一個(gè)任務(wù)嗎?
如果會(huì),那么中斷處理函數(shù)函還沒(méi)有return不就中斷了嗎?此時(shí)再來(lái)一個(gè)中斷的話,會(huì)怎么樣呢?棧就亂套了。
如果不會(huì),那么中斷處理函數(shù)最前面用匯編寫(xiě)的PUSH各種寄存器的指令就沒(méi)有相應(yīng)的POP指令了呀,時(shí)間一長(zhǎng)棧就溢出了呀。這明顯不對(duì)。
所以,我猜想只有一種情況是可行的。那就是:farjmp中的JMP FAR [ESP+4]指令并沒(méi)有完成任務(wù)切換,它只是讓CPU記錄了一個(gè)標(biāo)識(shí)(比如上文的寄存器TR的作用),標(biāo)識(shí)應(yīng)該運(yùn)行的任務(wù)是X。然后,當(dāng)CPU完成中斷處理函數(shù),再次執(zhí)行某個(gè)任務(wù)A中的指令時(shí),它會(huì)發(fā)現(xiàn)"現(xiàn)在應(yīng)該執(zhí)行任務(wù)X"中的指令了,所以它就切換到任務(wù)X中去。任務(wù)切換實(shí)際上此時(shí)才完成。
我查了一些資料,只能暫時(shí)作此猜想。
多任務(wù)優(yōu)化為了提高多任務(wù)運(yùn)行效率,下面就對(duì)其進(jìn)行優(yōu)化。
休眠和喚醒"休眠"就是從tasks鏈表中去掉一個(gè)任務(wù)A,"喚醒"就是把這個(gè)任務(wù)A重新加入tasks鏈表。
休眠的時(shí)機(jī):任務(wù)A的消息隊(duì)列為空(沒(méi)有待處理的消息)時(shí)。
喚醒的時(shí)機(jī):任務(wù)A的消息隊(duì)列獲得新的消息時(shí)。
任務(wù)優(yōu)先級(jí)把任務(wù)分到Level0、Level1、Level2這三個(gè)層中的一個(gè),當(dāng)Level0有活動(dòng)的任務(wù)(非休眠狀態(tài))時(shí),只在Level0的任務(wù)間切換。當(dāng)Level0沒(méi)有任務(wù)或均處于休眠狀態(tài)時(shí),在Level1的任務(wù)間切換。Level2同理。
閑置任務(wù)這與"哨兵"的思路相同。就是在Level2中添加一個(gè)只HLT的任務(wù)。如果操作系統(tǒng)里沒(méi)有其他任何任務(wù)的話,就會(huì)執(zhí)行這個(gè)"哨兵"任務(wù),即HLT掉(直至有中斷發(fā)生)。
哨兵的好處就是簡(jiǎn)化代碼,使得邏輯處理沒(méi)有特殊情況。
總結(jié)操作系統(tǒng)利用CPU的far模式的JMP指令、寄存器TR、GDT、TSS和PIT中斷這些功能實(shí)現(xiàn)了多任務(wù),可見(jiàn)CPU在設(shè)計(jì)時(shí)就考慮到了計(jì)算機(jī)要具有多任務(wù)處理的能力。也就是說(shuō),CPU、PIC等硬件支持什么功能,操作系統(tǒng)才能實(shí)現(xiàn)什么功能。這又肯定了硬件為操作系統(tǒng)提供API的看法。
請(qǐng)查看下一篇《《30天自制操作系統(tǒng)》筆記(13)——總結(jié)》
本文關(guān)鍵詞:30天自制操作系統(tǒng),由筆耕文化傳播整理發(fā)布。
本文編號(hào):176119
本文鏈接:http://sikaile.net/wenshubaike/mishujinen/176119.html