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