4. 匯編語(yǔ)言。為什么要學(xué)匯編?可能有些人會(huì)學(xué)得匯編難理解,而且現(xiàn)在C語(yǔ)言已經(jīng)可以很方便地編程了,所以不想學(xué)匯編,其實(shí)C語(yǔ)言再怎么方便強(qiáng)大,最后還是要通過(guò)編譯器轉(zhuǎn)換為匯編語(yǔ)言再由匯編轉(zhuǎn)換為機(jī)器碼,才能在機(jī)器中執(zhí)行。可以說(shuō),掌握了匯編之后你一定會(huì)對(duì)”代碼是怎么在CPU里面執(zhí)行的“這個(gè)哲學(xué)命題有進(jìn)一步的了解。另外,不學(xué)匯編你還真寫(xiě)不出一個(gè)操作系統(tǒng)內(nèi)核來(lái),因?yàn)椴僮飨到y(tǒng)的最低層是要直接操作CPU寄存器的,C語(yǔ)言無(wú)法實(shí)現(xiàn),只能用匯編寫(xiě)出來(lái),再由上層的C語(yǔ)言調(diào)用。匯編的書(shū)很多,這里就不介紹了,找一本去狂敲上個(gè)把星期就大概掌握了。
5. 另外你還要懂得計(jì)算機(jī)原理以及單片機(jī),其實(shí)單片機(jī)就是一臺(tái)閹割版的計(jì)算機(jī),你得對(duì)CPU寄存器,數(shù)據(jù)總線,地址總線,以及執(zhí)行方式這些有一定的了解才行,這方面的書(shū)也挺多的,不過(guò)介紹兩本個(gè)人覺(jué)得寫(xiě)得挺好的書(shū)供課外閑讀,《編程卓越之道》1、2卷,這本書(shū)大體上介紹了高級(jí)語(yǔ)言是怎么樣在CPU里面執(zhí)行的,另外也對(duì)CPU內(nèi)部結(jié)構(gòu)做了一些介紹,比那些課內(nèi)教材寫(xiě)得好,有空可以去看一下。
最后介紹一本《嵌入式實(shí)時(shí)操作系統(tǒng)UCOS II》,這本書(shū)介紹了UCOS II這個(gè)操作系統(tǒng)的內(nèi)部源代碼以及實(shí)現(xiàn)原理,我就是從這本書(shū)中學(xué)到了怎樣寫(xiě)一個(gè)可以用的操作系統(tǒng)內(nèi)核。
/**************************************************************************************/
什么是操作系統(tǒng)?其實(shí)就是一個(gè)程序, 這個(gè)程序可以控制計(jì)算機(jī)的所有資源,對(duì)資源進(jìn)行分配,包括CPU時(shí)間,內(nèi)存,IO端口等,按一定規(guī)則分配給所需要的進(jìn)程(進(jìn)程?也就是一個(gè)程序,可以單獨(dú)執(zhí)行),并且自動(dòng)控制讓CPU可以執(zhí)行多個(gè)互不相關(guān)的任務(wù),按照書(shū)中的介紹,一個(gè)操作系統(tǒng)需要具備四個(gè)要素:進(jìn)程調(diào)度、內(nèi)存管理、IO管理、文件管理。
那怎么樣可以讓CPU同時(shí)執(zhí)行多個(gè)任務(wù)呢?首先想象一下如果讓CPU執(zhí)行單道程序,它會(huì)從MAIN函數(shù)開(kāi)始一直順序地執(zhí)行下去,CPU里面有一個(gè)叫PC的寄存器,也就是程序計(jì)數(shù)器,它永遠(yuǎn)指向下一條要執(zhí)行的指令的存放地址,因?yàn)榇蠖鄶?shù)情況下指令都是逐條執(zhí)行的,所以PC寄存器也只是簡(jiǎn)單地加一,所以大家都叫它”程序計(jì)數(shù)器“,從PC寄存器的特點(diǎn)也許我們可以做點(diǎn)文章?比如人為地讓PC寄存器指到另外一段程序的入口地址,那CPU不就自動(dòng)地跑到另一段程序了么?哈哈。假如我們可以這樣做,那沒(méi)錯(cuò),CPU確定是跑到別人的領(lǐng)地去執(zhí)行代碼了,問(wèn)題是:怎么樣讓它回來(lái)繼續(xù)執(zhí)行?換句話說(shuō),PC寄存器改變之后CPU 已經(jīng)不知道剛剛這段程序執(zhí)行到哪里了,亦即跑不回來(lái)了,就像斷了線的風(fēng)箏。呃。。這問(wèn)題麻煩。。解決了這個(gè)問(wèn)題就似乎有點(diǎn)苗頭了。。
好吧,我們來(lái)看看有一個(gè)很相似的問(wèn)題,就是單片機(jī)在執(zhí)行代碼的時(shí)候,突然有一個(gè)中斷信號(hào)過(guò)來(lái)了,單片機(jī)馬上就屁顛屁顛地跑到中斷服務(wù)程序里面去執(zhí)行了,執(zhí)行完畢之后,奇怪??!它怎么還記得跑回來(lái)原來(lái)的地方?。???OH NO .它是怎么辦到的。其實(shí)這里還要介紹另外一個(gè)寄存器叫SP的,即:STACK POINTER堆棧指針,這個(gè)指針指向一個(gè)內(nèi)存的地址,里面存放了一些數(shù)據(jù)。首先,單片機(jī)遇到中斷信號(hào)的時(shí)候,它就把當(dāng)前的PC寄存器的值保存到SP所指的地址,這就相當(dāng)于它記住了當(dāng)前執(zhí)行的地方,叫做斷點(diǎn)保護(hù),然后PC寄存器就指向中斷服務(wù)程序的地址,下一個(gè)時(shí)刻CPU就自動(dòng)執(zhí)行中斷服務(wù)程序里面的代碼了,執(zhí)行完畢之后中斷服務(wù)程序調(diào)用了一個(gè)指令:RETI,這條指令叫返回指令,在函數(shù)結(jié)束之后調(diào)用,它會(huì)自動(dòng)從SP指針指向的地址把值取出來(lái)放到PC寄存器里面,然后CPU就會(huì)自動(dòng)回到之前斷掉的地方繼續(xù)執(zhí)行了!基于這個(gè)原理,我們可以回到上面的問(wèn)題:首先,讓CPU把當(dāng)前的PC保存起來(lái),然后把PC指向別段程序地址,CPU就跑到別人的領(lǐng)地去執(zhí)行了,執(zhí)行完了之后我們可以把SP指向的內(nèi)容放回PC,這樣調(diào)用RET指令之后,CPU就會(huì)回到原來(lái)的地方繼續(xù)執(zhí)行了??!貌似這個(gè)問(wèn)題完美地解決了?。?/div>
可是還有一個(gè)關(guān)鍵的問(wèn)題:CPU在執(zhí)行當(dāng)前代碼的時(shí)候 CPU里面所有的寄存器都保存的當(dāng)前這個(gè)程序所用到的值,比如做加法的時(shí)候用到PSW寄存器的進(jìn)位標(biāo)志位,如果此時(shí)切換到別的任務(wù),那再回到當(dāng)前程序的時(shí)候,這些值都會(huì)被改變,CPU會(huì)陷入混亂然后直接跑飛!!解決這問(wèn)題同樣要靠SP同學(xué),在切換任務(wù)的時(shí)候我們把所有寄存器依次入到SP指向的地址,稱為入棧操作,每次入棧SP指針的值都會(huì)加一或者減一,視不同CPU而定。而要恢復(fù)的時(shí)候,就從SP指向的地址依次把值取出來(lái)放回原來(lái)的地方,稱為彈棧操作。最后才彈出地址到PC寄存器,下一時(shí)刻,CPU自動(dòng)跑到原來(lái)的地址繼續(xù)執(zhí)行,從CPU的角度看就像沒(méi)有發(fā)生任務(wù)切換一樣,一切依舊,繼續(xù)工作。如果CPU的執(zhí)行速度夠快,切換速度也夠快,這樣就可以給人感覺(jué)CPU同時(shí)在執(zhí)行很多任務(wù),這就是操作系統(tǒng)里面最基本的原理。
SO,解釋完原理,我們首先來(lái)就來(lái)實(shí)現(xiàn)簡(jiǎn)單的任務(wù)切換,這里的難點(diǎn)就在于:執(zhí)行這一動(dòng)作必須要操作CPU的寄存器,而C語(yǔ)言是無(wú)法實(shí)現(xiàn)的,這就是為什么要用到匯編的原因了,所有操作系統(tǒng)的最底層代碼都是用匯編語(yǔ)言實(shí)現(xiàn)的,否則根本無(wú)法實(shí)現(xiàn)任務(wù)切換。下面要介紹匯編里面的幾條相關(guān)指令。PS:雖然每種CPU的匯編都不同,但是基本原理還是相通的。
第一條:CALL。函數(shù)調(diào)用指令,當(dāng)我們要調(diào)用一個(gè)函數(shù)的時(shí)候,就會(huì)用到CALL這條指令,它執(zhí)行再?gòu)膫€(gè)動(dòng)作,第一,先把當(dāng)前的PC值保存起來(lái),即現(xiàn)場(chǎng)保護(hù),第二,把要調(diào)用的函數(shù)的入口地址送到PC,這樣,在下一時(shí)刻到來(lái)的時(shí)候,CPU就自動(dòng)跳轉(zhuǎn)到特定的函數(shù)入口地址開(kāi)始執(zhí)行了。
第二條:RET/RETI。當(dāng)一個(gè)函數(shù)執(zhí)行完畢的時(shí)候,需要返回到原來(lái)執(zhí)行的地方,這時(shí)候就要調(diào)用 RET指令(在中斷函數(shù)中返回的時(shí)候調(diào)用RETI指令)。它把SP指向的數(shù)據(jù),即上一次調(diào)用CALL時(shí)保存的那個(gè)地址原來(lái)到PC,這樣,當(dāng)下一時(shí)刻到來(lái)的時(shí)候,CPU就會(huì)跳回到原來(lái)的地方了。實(shí)際上函數(shù)調(diào)用過(guò)程就是這樣的,所以有時(shí)候一些簡(jiǎn)單簡(jiǎn)短的函數(shù)寧愿用#define宏定義寫(xiě)出來(lái),因?yàn)檫@樣寫(xiě)出來(lái)就不用使用調(diào)用/返回過(guò)程,節(jié)省了時(shí)間。
第三/四條:PUSH/POP。這兩個(gè)指令是兩兄弟,即入棧及出棧。關(guān)于堆棧的特性說(shuō)明一下:堆棧這種結(jié)構(gòu)的特性就是后進(jìn)先出,就像疊盤(pán)子一樣,最后疊上去的盤(pán)子會(huì)被最先取出,這種原理非常好用,想象一下函數(shù)嵌套的時(shí)候發(fā)生的一切,就是利用到這種思路。PUSH指令用到把寄存器的值保存起來(lái),它會(huì)把值到保存到SP指針?biāo)傅牡胤?。POP指令則把數(shù)據(jù)從SP所指的地址恢復(fù)到原來(lái)的寄存器中。
用這幾條指令,我們就可以寫(xiě)出一個(gè)任務(wù)切換函數(shù)了,不過(guò)寫(xiě)之前還要說(shuō)明一下什么叫人工堆棧。其實(shí)上,一個(gè)程序在執(zhí)行的時(shí)候,它會(huì)用到一塊內(nèi)存空間用于保存各種變量,比如調(diào)用函數(shù)的時(shí)候這塊地方會(huì)用于保存地址以及寄存器,而在執(zhí)行一些復(fù)雜算法的時(shí)候,如果CPU的寄存器已經(jīng)用完了,這塊地方也會(huì)作為臨時(shí)中間變量的存放區(qū),另外,在向一個(gè)函數(shù)傳遞參數(shù)的時(shí)候,比如:printf(a,b,c,d,e....),如果參數(shù)過(guò)多,多余的參數(shù)也會(huì)先存放到這塊地方。所以說(shuō),這塊地方就像是這個(gè)程序的倉(cāng)庫(kù)一樣,存放著要用的東西。如果是在單道程序中,顯然這樣用沒(méi)問(wèn)題,但是如果是多道程序的話,問(wèn)題就來(lái)了,因?yàn)槿绻腥蝿?wù)共用那塊區(qū)域,那舊任務(wù)保存的東西就會(huì)被新任務(wù)所沖掉,CPU一下子就瘋掉了。。解決的辦法就是:每個(gè)任務(wù)都給它提供一塊專用的區(qū)域,這塊專用區(qū)域就叫人工堆棧,每個(gè)任務(wù)都不一樣,保證了不會(huì)相互沖突。
PS:因?yàn)?1單片機(jī)的內(nèi)存太小,基本無(wú)法實(shí)現(xiàn)多任務(wù),實(shí)現(xiàn)了也不實(shí)用,所以硬件平臺(tái)我選用了AVR單片機(jī)ATMEGA16,有1KB內(nèi)存,應(yīng)該夠用了,花了兩天時(shí)間把AVR的匯編指令看了一遍
首先,當(dāng)需要切換任務(wù)的時(shí)候,要先把當(dāng)前的所有寄存器全部入棧,在AVR單片機(jī)中有32個(gè)通用寄存器R0-R31,還有PC指針,PSW程序狀態(tài)寄存器,這些都要入棧,所以需要的內(nèi)存挺多的?,F(xiàn)在的編譯器都支持在線匯編,就是在C語(yǔ)言里面嵌入?yún)R編語(yǔ)言,方便得多,下面我宏定義了一組入棧操作:PUSH_REG(),里面是用PUSH指令把32個(gè)寄存器全部入棧
#define PUSH_REG()
{_asm("PUSH R0" "PUSH R1" "PUSH R2" "PUSH R3"
"PUSH R4" "PUSH R5" "PUSH R6" "PUSH R7"
"PUSH R8" "PUSH R9" "PUSH R10" "PUSH R11"
"PUSH R12" "PUSH R13" "PUSH R14" "PUSH R15"
"PUSH R16" "PUSH R17" "PUSH R18" "PUSH R19"
"PUSH R20" "PUSH R21" "PUSH R22" "PUSH R23"
"PUSH R24" "PUSH R25" "PUSH R26" "PUSH R27"
"PUSH R28" "PUSH R29" "PUSH R30" "PUSH R31" ); }
入完棧完接下來(lái)要保護(hù)當(dāng)前程序的SP指針,以便下次它要返回的時(shí)候能找到該人工堆棧的地址:
OS_LastThread->ThreadStackTop=(OS_DataType_ThreadStack *)SP;
這一句用C語(yǔ)言就可以實(shí)現(xiàn)了。
接下來(lái)關(guān)于當(dāng)前這段程序的現(xiàn)場(chǎng)算是保護(hù)好了,然后找到要切換到的任務(wù)的人工堆棧地址,把它賦給SP指針,如下:
SP=(uint16_t)OS_CurrentThread->ThreadStackTop;
出棧跟入棧的語(yǔ)法差不多,只是出棧順序要相反:
POP_REG();
接下來(lái),要調(diào)用一條很重要的指令了?。。〈肆钜怀?,CPU就乖乖地切換任務(wù)了!
_asm("RET");
調(diào)用返回指令,它就從SP里面取出函數(shù)地址放到PC,注意他取出的是剛剛放入SP指向地址的函數(shù)入口,所以它會(huì)返回到新任務(wù)執(zhí)行。
就這樣,一個(gè)操作系統(tǒng)里面最核心的”任務(wù)調(diào)度器“的模型就這樣簡(jiǎn)單地實(shí)現(xiàn)了,操作系統(tǒng)里面所作的跟任務(wù)切換有關(guān)的事情到最后都要調(diào)用到這個(gè)任務(wù)調(diào)度器,現(xiàn)在我們實(shí)現(xiàn)調(diào)度器了,相當(dāng)于成功了1/3,接下來(lái)的事情就是考慮在什么情況下調(diào)用這個(gè)調(diào)度器。
技術(shù)專區(qū)
評(píng)論