天天看點

《30天自制作業系統》樣章 多任務(1)——挑戰任務切換(harib12a)

多任務(1) --挑戰任務切換( harib12a)

  “話說,多任務到底是啥呢?”我們今天的内容,就從這個問題開始吧。

  多任務,在英語中叫做 “multitask”,顧名思義就是“多個任務”的意思。簡單地說,在Windows等作業系統中,多個應用程式同時運作的狀态(也就是同時打開好幾個視窗的狀态)就叫做多任務。

  對于生活在現代社會的各位來說,這種多任務簡直是理所當然的事情。比如你會一邊用音樂播放軟體聽音樂一邊寫郵件,郵件寫到一半忽然有點東西要查,便打開 Web浏覽器上網搜尋。這對于大家來說這些都是家常便飯了吧。可如果沒有多任務的話會怎麼樣呢?想寫郵件的時候就必須關掉正在播放的音樂,要查東西的時候就必須先儲存寫到一半的郵件,然後才能打開 Web浏覽器……光想象一下就會覺得太不友善了。

  然而在從前,沒有多任務反倒是普遍的情形(那個時候大家不用電腦聽音樂,也沒有網際網路)。在那個年代,電腦一次隻能運作一個程式,如果要同時運作多個程式的話,就得買好幾台電腦才行。

  就在那個時候,誕生了昀初的多任務作業系統,大家都覺得太了不起了。從現在開始,我們也要準備給“紙娃娃系統”添加執行多任務的能力了。連這樣一個小不點兒作業系統都能夠實作多任務,真是讓人不由地感歎它生逢其時呀。

  稍稍思考一下我們就會發現,多任務這個東西還真是奇妙,它究竟是怎樣做到讓多個程式同時運作的呢?如果我們的電腦裡面裝了好多個 CPU的話,同時運作多個程式倒也順理成章,但實際上就算我們隻有一個 CPU,照樣可以實作多任務。

  其實說穿了,這些程式根本沒有在同時運作,隻不過看上去好像是在同時運作一樣:程式 A運作一會兒,接下來程式 B運作一會兒,再接下來輪到程式 C,然後再回到程式 A……如此反複,有點像日本忍者的“分身術”呢(笑)。

  為了讓這種分身術看上去更完美,需要讓作業系統盡可能快地切換任務。如果 10秒才切換一次,那就連人眼都能察覺出來了,同時運作多個程式的戲碼也就穿幫了。再有,如果我們給程式 C發出一個按鍵指令,正巧這個瞬間系統切換到了程式 A的話,我們就不得不等上 20秒,才能重新輪到程式 C對按鍵指令作出反應。這實在是讓人抓狂啊(哭)。

[img]http://b105.photo.store.qq.com/psb?/V12gMU7j3dMtSc/O8yp5o3Ijpr3KLzUb1amW4ceiB4gMdFH2xRkCZavyGs!/b/YXPVmD7HGwAAYo0OqD4fGwAA[/img]

  在一般的作業系統中,這個切換的動作每 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中進行定義後使用。

[img]http://b105.photo.store.qq.com/psb?/V12gMU7j3dMtSc/sUWVZcicdkmMAG4Kdou2eU1eeLoyIIPmyhQhDEp.5ig!/b/YZOaqT79EwAAYtBhnT6LGwAA[/img]

  參考上面的結構定義,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寄存器中的值就會自動累加,進而保證一直指向下一條指令所在的記憶體位址。

  [size=small]說點題外話, JMP指令實際上是一個向 EIP寄存器指派的指令。 JMP 0x1234這種寫法, CPU會解釋為 MOV EIP,0x1234,并向 EIP指派。也就是說,這條指令其實是篡改了 CPU記憶中下一條該執行的指令的位址,蒙了 CPU一把。這樣一來, CPU在讀取下一條指令時,就會去讀取 0x1234這個位址中的指令。你看,這不就相當于是做了一個跳轉嗎?

  對了,如果你在彙編語言裡用 MOV EIP,0x1234這種寫法是會出錯的,還是不要嘗試的好。在彙編語言中,應該使用 JMP 0x1234來代替MOV EIP,0x1234。[/size]

  如果在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節)。 MP 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。

  [img]http://b104.photo.store.qq.com/psb?/V12gMU7j3dMtSc/UJKBp6zyNzdc0UhOeNlONV9vbZWKN1DkTP5FzfFXGQo!/b/YThhCT7EagAAYhBDAz7BaQAA[/img]

  現在兩個TSS都建立好了,該進行實際的切換了。

  我們向TR寄存器存入3 * 8這個值,這是因為我們剛才把目前運作的任務定義為 GDT的3号。 TR寄存器以前沒有提到過,它的作用是讓 CPU記住目前正在運作哪一個任務。當進行任務切換的時候,TR寄存器的值也會自動變化,它的名字也就是“ task register”(任務寄存器)的縮寫。我們每次給 TR寄存器指派的時候,必須把 GDT的編号乘以 8,因為英特爾公司就是這樣規定的。如果你有意見的話,可以打電話找英特爾的大叔投訴哦(笑)。

  給TR寄存器指派需要使用 LTR指令,不過用 C語言做不到。唉,各位是不是都已經見怪不怪了啊?啥?你早就料到了?(笑)是以說,正如你所料,我們隻能把它寫進 naskfunc.nas裡面。

[img]http://b103.photo.store.qq.com/psb?/V12gMU7j3dMtSc/wy89dzg.vUB0SMF6k0sYxjR3r8TMlW1.kCKig3rN4uU!/b/YQE4bD1JagAAYgjIbT2hagAA[/img]

[img]http://b107.photo.store.qq.com/psb?/V12gMU7j3dMtSc/*dNF3tFMFiJaBAEkJUmniQjOnG*xpA6R*BGTHkCJ*dM!/b/YfaKzj89CwAAYmem1D*iCgAA[/img]

  對了,LTR指令的作用隻是改變 TR寄存器的值,是以執行了 LTR指令并不會發生任務切換。要進行任務切換,我們必須執行 far模式的跳轉指令,可惜 far跳轉這事 C語言還是無能為力,這種語言還真是不友善啊。沒辦法,這個函數我們也得在 naskfunc.nas裡建立。

[img]http://b106.photo.store.qq.com/psb?/V12gMU7j3dMtSc/MM9erdaP7Lv0WHRIEp*dorPuRscdComJsZmgVksJxrE!/b/YW58Nz*8CQAAYh8rQj*kCQAA[/img]

  也許有人會問,在 JMP指令後面寫個 RET有意義嗎?也對,通常情況下确實沒意義,因為已經跳轉到别的地方了嘛,後面再寫什麼指令也不會被執行了。不過,用作任務切換的 JMP指令卻不太一樣,在切換任務之後,再傳回這個任務的時候,程式會從這條 JMP指令之後恢複運作,也就是執行JMP後面的RET,從彙編語言函數傳回,繼續運作 C語言主程式。

  另外,如果 far-JMP指令是用作任務切換的話,位址段(冒号前面的 4*8的部分)要指向 TSS這一點比較重要,而偏移量(冒号後面的 0的部分)并沒有什麼實際作用,會被忽略掉,一般來說像這樣寫 0就可以了。

  現在我們需要在 HariMain的某個地方來調用 taskswitch(),可到底該寫在哪裡呢?唔,有了,就放在顯示“ 10[sec]”的語句後面好了。也就是說,程式啟動 10秒以後進行任務切換。

[img]http://b100.photo.store.qq.com/psb?/V12gMU7j3dMtSc/Ca3Wo7AR3QnKN3w24y.LHmHyRONFjvzxJ9MDuTnE95o!/b/YUklrTuVjQAAYuqhrjtJkQAA[/img]

大功告成了?不對,我們還沒準備好 tss_b呢。在任務切換的時候需要讀取 tss_b的内容,是以我們得在TSS中定義好寄存器的初始值才行。

[img]http://b106.photo.store.qq.com/psb?/V12gMU7j3dMtSc/IlXAk9m0R.z37a5rOIW*rTAwLfrlKpcW.3c5gDGEsj8!/b/YQmBNz86CgAAYib.NT*zCQAA[/img]

[img]http://b106.photo.store.qq.com/psb?/V12gMU7j3dMtSc/s6*fMa1ZL68wR2.Jths28n6cb0ladAC.CSrv19lRDng!/b/YZZwND*ECQAAYlZfMT.3CQAA[/img]

  乍看之下,貌似會有很多看不懂的地方吧,我們從後半段對寄存器指派的地方開始看。這裡我們給cs置為GDT的2号,其他寄存器都置為 GDT的1号,asmhead.nas的時候也是一樣的。也就是說,我們這次使用了和bootpack.c相同的位址段。當然,如果你用别的位址段也沒問題,不過這次我們隻是想随便做個任務切換的實驗而已,這種麻煩的事情還是以後再說吧。

  繼續看剩下的部分,關于 eflags的指派,如果把 STI後的EFLAGS的值通過 io_load_eflags賦給變量的話,該變量的值就顯示為 0x00000202,是以在這裡就直接使用了這個值,僅此而已。如果還有看不懂的地方,大概就是 eip和esp的部分了吧。

。。。。。。

 在eip中,我們需要定義在切換到這個任務的時候,要從哪裡開始運作。在這裡我們先把 task_b_main這個函數的記憶體位址指派給它。

[img]http://b101.photo.store.qq.com/psb?/V12gMU7j3dMtSc/e4igmgAOkHIj*HZEi0ltxHtYQ5A.t.z.nHXIJtfiTAM!/b/YRBxNjyJjgAAYuMZPjy8jwAA[/img]

  這個函數隻執行了一個 HLT,沒有任何實際作用,後面我們會對它進行各種改造,現在就先這樣吧。

  task_b_esp是專門為任務 B所定義的棧。有人可能會說,直接用任務 A的棧不就好了嗎?那可不行,如果真這麼做的話,棧就會混成一團,程式也無法正常運作。

[img]http://b103.photo.store.qq.com/psb?/V12gMU7j3dMtSc/JgDaRW27t89eNG1OsTTcpEywQdb.obLGJXWbeFLiypk!/b/Yfo.bz2EawAAYrqeZz26agAA[/img]

  總之先寫成這個樣子了。我們為任務B的棧配置設定了64KB的記憶體,并計算出棧底的記憶體位址。

請各位回憶一下向棧PUSH資料(入棧)的動作,ESP中存入的應該棧末尾的位址,而不是棧開

頭的位址。

  好了,我們已經講解得夠多了,現在總算是萬事俱備啦,馬上“make run”一下吧。這個程

序如果運作正常的話應該是什麼樣子呢?嗯,啟動之後的10秒内,還是跟以前一樣的,10秒一到便執行任務切換,task_b_main開始運作。因為task_b_main隻有一句HLT,是以接下來程式就全部停止了,滑鼠和鍵盤也應該都沒有反應了。

  唔……這樣看起來好像很無聊啊,算了,總之我們先來“make run”吧。10秒鐘的等待還真

是漫長……哇!停了停了!

  看來我們的首次任務切換獲得了圓滿成功。

[img]http://b103.photo.store.qq.com/psb?/V12gMU7j3dMtSc/3cshy8ahcgzC7dVNmR.0fuklgt*DGdotI4LyyJdeWQg!/b/YR8eZj1qaQAAYm1AbD2xagAA[/img]

繼續閱讀