隻需30天
從零開始編寫一個五髒俱全的圖形操作係統
39.1KB迷你係統
實現多任務、漢字顯示、文件壓縮,還能聽歌看圖玩遊戲
日本編程天纔
揭開CPU、內存、磁盤以及操作係統底層工作模式的神秘麵紗
第15天
多任務(1)
挑戰任務切換(harib12a)
任務切換進階(harib12b)
做個簡單的多任務(1)(harib12c)
做個簡單的多任務(2)(harib12d)
提高運行速度(harib12e)
測試運行速度(harib12f)
多任務進階(harib12g)
1 挑戰任務切換(harib12a)
“話說,多任務到底是啥呢?”我們今天的內容,就從這個問題開始吧。
多任務,在英語中叫做“multitask”,顧名思義就是“多個任務”的意思。簡單地說,在Windows等操作係統中,多個應用程序同時運行的狀態(也就是同時打開好幾個窗口的狀態)就叫做多任務。
對於生活在現代社會的各位來說,這種多任務簡直是理所當然的事情。比如你會一邊用音樂播放軟件聽音樂一邊寫郵件,郵件寫到一半忽然有點東西要查,便打開Web瀏覽器上網搜索。這對於大傢來說這些都是傢常便飯瞭吧。可如果沒有多任務的話會怎麼樣呢?想寫郵件的時候就必須關掉正在播放的音樂,要查東西的時候就必須先保存寫到一半的郵件,然後纔能打開Web瀏覽器……光想象一下就會覺得太不方便瞭。
然而在從前,沒有多任務反倒是普遍的情形(那個時候大傢不用電腦聽音樂,也沒有互聯網)。在那個年代,電腦一次隻能運行一個程序,如果要同時運行多個程序的話,就得買好幾颱電腦纔行。
就在那個時候,誕生瞭最初的多任務操作係統,大傢都覺得太瞭不起瞭。從現在開始,我們也要準備給“紙娃娃係統”添加執行多任務的能力瞭。連這樣一個小不點兒操作係統都能夠實現多任務,真是讓人不由地感嘆它生逢其時呀。
稍稍思考一下我們就會發現,多任務這個東西還真是奇妙,它究竟是怎樣做到讓多個程序同時運行的呢?如果我們的電腦裏麵裝瞭好多個CPU的話,同時運行多個程序倒也順理成章,但實際上就算我們隻有一個CPU,照樣可以實現多任務。
其實說穿瞭,這些程序根本沒有在同時運行,隻不過看上去好像是在同時運行一樣:程序A運行一會兒,接下來程序B運行一會兒,再接下來輪到程序C,然後再迴到程序A……如此反復,有點像日本忍者的“分身術”呢(笑)。
為瞭讓這種分身術看上去更完美,需要讓操作係統盡可能快地切換任務。如果10秒纔切換一次,那就連人眼都能察覺齣來瞭,同時運行多個程序的戲碼也就穿幫瞭。再有,如果我們給程序C發齣一個按鍵指令,正巧這個瞬間係統切換到瞭程序A的話,我們就不得不等上20秒,纔能重新輪到程序C對按鍵指令作齣反應。這實在是讓人抓狂啊(哭)。
在一般的操作係統中,這個切換的動作每0.01~0.03秒就會進行一次。當然,切換的速度越快,讓人覺得程序是在同時運行的效果也就越好。不過,CPU進行程序切換(我們稱為“任務切換”)這個動作本身就需要消耗一定的時間,這個時間大約為0.0001秒左右,不同的CPU及操作係統所需的時間也有所不同。如果CPU每0.0002秒切換一次任務的話,該CPU處理能力的50%都要被任務切換本身所消耗掉。這意味著,如果同時運行2個程序,每個程序的速度就隻有單獨運行時的1/4,這樣你會覺得開心嗎?如果變成這種結果,那還不如乾脆彆搞多任務呢。
相比之下,即便是每0.001秒切換一次任務,單單在任務切換上麵也要消耗CPU處理能力的10%。大概有人會想,10%也沒什麼大不瞭的吧?可如果你看看速度快10%的CPU賣多少錢,說不定就會恍然大悟,“對啊,隻要優化一下任務切換間隔,就相當於一分錢也不花,便換上瞭比現在更快的CPU嘛……”(笑),你也就明白瞭浪費10%也是很不值得的。正是因為這個原因,任務切換的間隔最短也得0.01秒左右,這樣一來隻有1%的處理能力消耗在任務切換上,基本上就可以忽略不計瞭。
關於多任務是什麼的問題,已經大緻講得差不多瞭,接下來我們來看看如何讓CPU來處理多任務。
當你嚮CPU發齣任務切換的指令時,CPU會先把寄存器中的值全部寫入內存中,這樣做是為瞭當以後切換迴這個程序的時候,可以從中斷的地方繼續運行。接下來,為瞭運行下一個程序,CPU會把所有寄存器中的值從內存中讀取齣來(當然,這個讀取的地址和剛剛寫入的地址一定是不同的,不然就相當於什麼都沒變嘛),這樣就完成瞭一次切換。我們前麵所說的任務切換所需要的時間,正是對內存進行寫入和讀取操作所消耗的時間。
接下來我們來看看寄存器中的內容是怎樣寫入內存裏去的。下麵這個結構叫做“任務狀態段”(task status segment),簡稱TSS。TSS有16位和32位兩個版本,這裏我們使用32位版。顧名思義,TSS也是內存段的一種,需要在GDT中進行定義後使用。
參考上麵的結構定義,TSS共包含26個int成員,總計104字節(摘自CPU的技術資料),我特意把它們分成4行來寫。從開頭的backlink起,到cr3為止的幾個成員,保存的不是寄存器的數據,而是與任務設置相關的信息,在執行任務切換的時候這些成員不會被寫入(backlink除外,某些情況下是會被寫入的)。後麵的部分中我們會用到這裏的設定,不過現在你完全可以先忽略它。
第2行的成員是32位寄存器,第3行是16位寄存器,應該沒必要解釋瞭吧……不對,eip好像到現在還沒講過呢。EIP的全稱是"extended instruction pointer",也就是"擴展指令指針寄存器"的意思。這裏的"擴展"代錶它是一個32位寄存器,也就是說其對應的16位版本叫做IP,類比一下的話,跟EAX與AX之間的關係是一樣的。
EIP是CPU用來記錄下一條需要執行的指令位於內存中哪個地址的寄存器,因此它纔被稱為"指令指針"。如果沒有這個寄存器,記性不好的CPU就會忘記自己正在運行哪裏的程序,於是程序就沒辦法正常運行瞭。每執行一條指令,EIP寄存器中的值就會自動纍加,從而保證一直指嚮下一條指令所在的內存地址。
說點題外話,JMP指令實際上是一個嚮EIP寄存器賦值的指令。JMP 0x1234這種寫法,CPU會解釋為MOV EIP,0x1234,並嚮EIP賦值。也就是說,這條指令其實是篡改瞭CPU記憶中下一條該執行的指令的地址,濛瞭CPU一把。這樣一來,CPU在讀取下一條指令時,就會去讀取0x1234這個地址中的指令。你看,這不就相當於是做瞭一個跳轉嗎?
對瞭,如果你在匯編語言裏用MOV EIP,0x1234這種寫法是會齣錯的,還是不要嘗試的好。在匯編語言中,應該使用JMP 0x1234來代替MOV EIP,0x1234。
如果在TSS中將EIP寄存器的值記錄下來,那麼當下次再返迴這個任務的時候,CPU就可以明白應該從哪裏讀取程序來運行瞭。
按照常識,段寄存器應該是16位的纔對,可是在TSS數據結構中卻定義成瞭int(也就是DWORD)類型。我們可以大膽想象一下,說不定英特爾公司的人將來會把段寄存器變成32位的,這樣想想也挺有意思的呢(笑)。
第4行的ldtr和iomap也和第1行的成員一樣,是有關任務設置的部分,因此在任務切換時不會被CPU寫入。也許你會想,那就和第1行一樣,暫時先忽略好瞭--但那可是絕對不行的!如果鬍亂賦值的話,任務就無法正常切換瞭,在這裏我們先將ldtr置為0,將iomap置為0x40000000就好瞭。
關於TSS的話題暫且先告一段落,我們迴來繼續講任務切換的方法。要進行任務切換,其實還得用JMP指令。JMP指令分為兩種,隻改寫EIP的稱為near模式,同時改寫EIP和CS的稱為far模式,在此之前我們使用的JMP指令基本上都是near模式的。不記得CS是什麼瞭?CS就是代碼段(code segment)寄存器啦。
說起來我們其實用過一次far模式的JMP指令,就在asmhead.nas的"bootpack啓動"的最後一句(見8.5節)。
JMP DWORD 2*8:0x0000001b
這條指令在嚮EIP存入0x1b的同時,將CS置為2*8(=16)。像這樣在JMP目標地址中帶冒號(:)的,就是far模式的JMP指令。
如果一條JMP指令所指定的目標地址段不是可執行的代碼,而是TSS的話,CPU就不會執行通常的改寫EIP和CS的操作,而是將這條指令理解為任務切換。也就是說,CPU會切換到目標TSS所指定的任務,說白瞭,就是JMP到一個任務那裏去瞭。
CPU每次執行帶有段地址的指令時,都會去確認一下GDT中的設置,以便判斷接下來要執行的JMP指令到底是普通的far-JMP,還是任務切換。也就是說,從匯編程序翻譯齣來的機器語言來看,普通的far-JMP和任務切換的far-JMP,指令本身是沒有任何區彆的。
好瞭,枯燥的講解就到這裏,讓我們實際做一次任務切換吧。我們準備兩個任務:任務A和任務B,嘗試從A切換到B。
首先,我們需要創建兩個TSS:任務A的TSS和任務B的TSS。
本次的HariMain節選
struct TSS32 tss_a, tss_b;
嚮它們的ldtr和iomap分彆存入閤適的值。
本次的HariMain節選
tss_a.ldtr = 0;
tss_a.iomap = 0x40000000;
tss_b.ldtr = 0;
tss_b.iomap = 0x40000000;
接著將它們兩個在GDT中進行定義。
本次的HariMain節選
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) ADR_GDT;
set_segmdesc(gdt + 3, 103, (int) &tss;_a, AR_TSS32);
set_segmdesc(gdt + 4, 103, (int) &tss;_b, AR_TSS32);
將tss_a定義在gdt的3號,段長限製為103字節,tss_b也采用類似的定義。
現在兩個TSS都創建好瞭,該進行實際的切換瞭。
我們嚮TR寄存器存入3 * 8這個值,這是因為我們剛纔把當前運行的任務定義為GDT的3號。TR寄存器以前沒有提到過,它的作用是讓CPU記住當前正在運行哪一個任務。當進行任務切換的時候,TR寄存器的值也會自動變化,它的名字也就是"task register"(任務寄存器)的縮寫。我們每次給TR寄存器賦值的時候,必須把GDT的編號乘以8,因為英特爾公司就是這樣規定的。如果你有意見的話,可以打電話找英特爾的大叔投訴哦(笑)。
給TR寄存器賦值需要使用LTR指令,不過用C語言做不到。唉,各位是不是都已經見怪不怪瞭啊?啥?你早就料到瞭?(笑)所以說,正如你所料,我們隻能把它寫進naskfunc.nas裏麵。
本次的HariMain節選
load_tr(3 * 8);
本次的naskfunc.nas節選
_load_tr: ; void load_tr(int tr);
LTR [ESP+4] ; tr
RET
對瞭,LTR指令的作用隻是改變TR寄存器的值,因此執行瞭LTR指令並不會發生任務切換。
要進行任務切換,我們必須執行far模式的跳轉指令,可惜far跳轉這事C語言還是無能為力,這種語言還真是不方便啊。沒辦法,這個函數我們也得在naskfunc.nas裏創建。
本次的naskfunc.nas節選
_taskswitch4: ; void taskswitch4(void);
JMP 4*8:0
RET
也許有人會問,在JMP指令後麵寫個RET有意義嗎?也對,通常情況下確實沒意義,因為已經跳轉到彆的地方瞭嘛,後麵再寫什麼指令也不會被執行瞭。不過,用作任務切換的JMP指令卻不太一樣,在切換任務之後,再返迴這個任務的時候,程序會從這條JMP指令之後恢復運行,也就是執行JMP後麵的RET,從匯編語言函數返迴,繼續運行C語言主程序。
另外,如果far-JMP指令是用作任務切換的話,地址段(冒號前麵的4*8的部分)要指嚮TSS這一點比較重要,而偏移量(冒號後麵的0的部分)並沒有什麼實際作用,會被忽略掉,一般來說像這樣寫0就可以瞭。
現在我們需要在HariMain的某個地方來調用taskswitch(),可到底該寫在哪裏呢?唔,有瞭,就放在顯示"10[sec]"的語句後麵好瞭。也就是說,程序啓動10秒以後進行任務切換。
本次的HariMain節選
} else if (i == 10) { /* 10秒計時器*/
putfonts8_asc_sht(sht_back, 0, 64, COL8_FFFFFF, COL8_008484, "10[sec]", 7);
taskswitch4(); /*這裏! */
} else if (i == 3) { /* 3秒計時器 */
大功告成瞭?不對,我們還沒準備好tss_b呢。在任務切換的時候需要讀取tss_b的內容,因此我們得在TSS中定義好寄存器的初始值纔行。
本次的HariMain節選
tss_b.eip = (int) &task;_b_main;
tss_b.eflags = 0x00000202; /* IF = 1; */
tss_b.eax = 0;
tss_b.ecx = 0;
tss_b.edx = 0;
tss_b.ebx = 0;
tss_b.esp = task_b_esp;
tss_b.ebp = 0;
tss_b.esi = 0;
tss_b.edi = 0;
tss_b.es = 1 * 8;
tss_b.cs = 2 * 8;
tss_b.ss = 1 * 8;
tss_b.ds = 1 * 8;
tss_b.fs = 1 * 8;
tss_b.gs = 1 * 8;
乍看之下,貌似會有很多看不懂的地方吧,我們從後半段對寄存器賦值的地方開始看。這裏我們給cs置為GDT的2號,其他寄存器都置為GDT的1號,asmhead.nas的時候也是一樣的。也就是說,我們這次使用瞭和bootpack.c相同的地址段。當然,如果你用彆的地址段也沒問題,不過這次我們隻是想隨便做個任務切換的實驗而已,這種麻煩的事情還是以後再說吧。
繼續看剩下的部分,關於eflags的賦值,如果把STI後的EFLAGS的值通過io_load_eflags賦給變量的話,該變量的值就顯示為0x00000202,因此在這裏就直接使用瞭這個值,僅此而已。如果還有看不懂的地方,大概就是eip和esp的部分瞭吧。
在eip中,我們需要定義在切換到這個任務的時候,要從哪裏開始運行。在這裏我們先把task_b_main這個函數的內存地址賦值給它。
本次的bootpack.c節選
void task_b_main(void)
{
for (;;) { io_hlt(); }
}
這個函數隻執行瞭一個HLT,沒有任何實際作用,後麵我們會對它進行各種改造,現在就先這樣吧。
task_b_esp是專門為任務B所定義的棧。有人可能會說,直接用任務A的棧不就好瞭嗎?那可不行,如果真這麼做的話,棧就會混成一團,程序也無法正常運行。
本次的HariMain節選
int task_b_esp;
task_b_esp = memman_alloc_4k(memman, 64 * 1024) + 64 * 1024;
總之先寫成這個樣子瞭。我們為任務B的棧分配瞭64KB的內存,並計算齣棧底的內存地址。請各位迴憶一下嚮棧PUSH數據(入棧)的動作,ESP中存入的應該棧末尾的地址,而不是棧開頭的地址。
好瞭,我們已經講解得夠多瞭,現在總算是萬事俱備啦,馬上"make run"一下吧。這個程序如果運行正常的話應該是什麼樣子呢?嗯,啓動之後的10秒內,還是跟以前一樣的,10秒一到便執行任務切換,task_b_main開始運行。因為task_b_main隻有一句HLT,所以接下來程序就全部停止瞭,鼠標和鍵盤也應該都沒有反應瞭。
唔……這樣看起來好像很無聊啊,算瞭,總之我們先來"make run"吧。10秒鍾的等待還真是漫長……哇!停瞭停瞭!
看來我們的首次任務切換獲得瞭圓滿成功。
……
东西不错
评分代码与内容结合,用的ide可能不太熟练,确实是本入门好书,对整体有个大概了解,慢慢在看吧
评分这三本书很不错,表达精简,不含糊
评分跟我想象的不太一样,没什么帮助。
评分书质量不错,,,
评分垃圾书!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
评分京东想钱想疯了吧
评分垃圾,分析了一个c语言写的系统,还不如用Lucene或者Elasticsearch
评分搞活动时候买的 暂时还没看 东西是正品 好评
本站所有內容均為互聯網搜索引擎提供的公開搜索信息,本站不存儲任何數據與內容,任何內容與數據均與本站無關,如有需要請聯繫相關搜索引擎包括但不限於百度,google,bing,sogou 等
© 2025 tushu.tinynews.org All Rights Reserved. 求知書站 版权所有