《See MIPS Run Linux》 讀書筆記

http://xenyinzen.wikidot.com/loongson-about:see-mips-run-linux-notes


這裏面寫的是我研讀《See MIPS Run Linux》這本書時的一些讀書筆記,對書上的一些疑點有比較詳細的闡述。還算草稿性質,比較亂,以後有時間再來整理:)

*現在MIPS世界指令集統一標準爲MIPS32/64 R1和R2(Release 2),不再沿用以前所謂的R4K, R24K等芯片命名方式和MIPS I, II, III, IV, V這種指令集方式。

*MIPS指令特點:
1。所有指令都是32位編碼;
2。有些指令有26位供目標地址編碼;有些則只有16位。因此要想加載任何一個32位值,就得用兩個加載指令。16位的目標地址意味着,指令的跳轉或子函數的位置必須在64K以內(上下32K);
3。所有的動作原理上要求必須在1個時鐘週期內完成,一個動作一個階段;
4。有32個通用寄存器,每個寄存器32位(對32位機)或64位(對64位機);
5。本身沒有任何幫助運算判斷的標誌寄存器,要實現相應的功能時,是通過測試兩個寄存器是否相等來完成的;
6。所有的運算都是基於32位的,沒有對字節和對半字的運算(MIPS裏,字定義爲32位,半字定義爲16位);
7。沒有單獨的棧指令,所有對棧的操作都是統一的內存訪問方式。因爲push和pop指令實際上是一個複合操作,包含對內存的寫入和對棧指針的移動;

*MIPS指令的五級流水線:每條指令都包含五個執行階段。
第一階段:從指令緩衝區中取指令。佔一個時鐘週期;
第二階段:從指令中的源寄存器域(可能有兩個)的值(爲一個數字,指定$0~$31中的某一個)所代表的寄存器中讀出數據。佔半個時鐘週期;
第三階段:在一個時鐘週期內做一次算術或邏輯運算。佔一個時鐘週期;
第四階段:指令從數據緩衝中讀取內存變量的階段。從平均來講,大約有3/4的指令在這個階段沒做什麼事情,但它是指令有序性的保證(爲什麼是保證,我還沒看清楚?)。佔一個時鐘週期;
第五階段:存儲計算結果到緩衝或內存的階段。佔半個時鐘週期;
*所以一條指令要佔用四個時鐘週期;

*?書上P27, P28畫的示意圖有點問題,感覺不是很恰當;我自己畫了一個。

*由於MIPS固定指令長度,所以造成其編譯後的二進制文件和內存佔用空間比x86的要大,(x86平均指令長度只有3個字節多一點,而MIPS是4個字節)

*尋址方式:
1。只有一種內存尋址方式。就是基地址加一個16位的地址偏移;

*內存中的數據訪問必須嚴格對齊(至少4字節對齊);

*硬跳轉指令只有26位目標地址,再加上2位的對齊位,可尋址28位的空間,即256M。意思即是說,在一個C程序內,goto語句只能跳轉到它之前的128M和之後的128M這個地址空間之內;
*條件分支指令只有16位跳轉地址,加上2位的對齊位,共18位尋址空間,即256K。意思即是說,在一個C程序內,if語句只能跳轉到它之前的128K和之後的128K這個地址空間之內;

*MIPS默認不把子函數的返回地址(就是調用函數的受害指令地址)存放到棧中,而是存放到$31寄存器中;這對那些葉子函數有利。如果遇到嵌套的函數的話,有另外的機制處理。

*流水線效應。由於採用了高度的流水線,結果產生了一些對程序員來說可見的效應,需要注意。最重要的兩個效應就是分支延遲效應和載入延遲效應。
1。任何一個分支跳轉語句後面的那條語句叫做分支延遲槽。實際上在程序執行到分支語句時,當他剛把要跳轉到的地址填充好(到代碼計數器裏),還沒完成本條指令,分支語句後面的那個指令就執行了。這是因爲流水線效應,幾條指令同時在執行,只是處於不同的階段。具體看書上說提前半條指令執行,沒看懂。分支延遲槽常用被利用起來完成一些參數初始化等相關工作,而不是被浪費了。
2。載入延遲是這樣的。當執行一條從內存中載入數據的命令時,是先載入到高速緩衝中,然後再取到寄存器中,這個過程相對來說是比較慢的。在這個過程完成之前,可能已經有幾條在流水線上的指令被執行了。這幾條在載入指令後被執行的指令就被稱作載入延遲槽。現在就有一個問題,如果後面這幾條指令要用到載入指令所載入的那個數據怎麼辦?一個通用的辦法是,把內部鎖加在數據載入過程上,這樣,當後面的指令要用這條指令時,就只有先停止運行(在ALU階段),等這條數據載入指令完成了後再開始運行。

*MIPS的虛擬地址內存映射空間。
0x0000 0000 ~ 0x7fff ffff
用戶級空間,2GB,要經MMU(TLB)地址翻譯。kuseg。可以控制要不要經過緩衝。

0x8000 0000 ~ 0x9fff ffff
kseg0. 這塊區域爲操作系統內核所佔的區域,共512M。使用時,不經過地址翻譯,將最高位去掉就線性映射到內存的低512M(不足的就裁剪掉頂部)。但要經過緩衝區過渡。

0xa000 0000 ~ 0xbfff ffff
kseg1. 這塊區域爲系統初始化所佔區域,共512M。使用時,不經過地址翻譯,也不經過緩衝區。將最高3位去掉就線性映射到內存的低512M(不足的就裁剪掉頂部)。

0xc000 0000 ~ 0xffff ffff
kseg2. 這塊區域也爲內核級區域。要經過地址翻譯。可以控制要不要經過緩衝。

*MIPS的協處理器
CP0:這是MIPS芯片的配置單元。必不可少,雖然叫做協處理器,但是通常都是做在一塊芯片上。絕大部分MIPS功能的配置,緩衝的控制,異常/中斷的控制,內存管理的控制都在這裏面。所以是一個完整的系統所必不可少的。

*MIPS的高速緩衝
MIPS一般有兩到三級緩衝,其中第一級緩衝數據和指令分開存儲。這樣的好處是指令和數據可以同時存取,提高效率。但缺點是提高了複雜度。第二級緩衝和第三級緩衝(如果有的話)就不再分開存放啦。

緩衝的單元叫做緩衝行。每一行中,有一個tag,然後後面接的是一些標誌位和一些數據。緩衝行按順序線性排列起來,就組成了整個緩衝。

緩衝行的索引和存取有一套完整的機制,另畫圖說明。

*MIPS的異常機制
精確異常的概念:在運行流程中沒有任何多餘效應的異常。即當異常發生時,在受害指令之前的指令被完全執行,而受害指令及後面的指令還沒開始執行(注:說受害指令及後面的指令還沒做任何事情是不對的,實際上受害指令是處於其指令週期的第三階段剛完成,即ALU階段剛完成)。精確異常有有助於保證軟件設計上不受硬件實現的影響。

CP0中的EPC寄存器用於指向異常發生時指令跳轉前的執行位置,一般是受害指令地址。當異常時,是返回這個地址繼續執行。但如果受害指令在分支延遲槽中,則會硬件自動處理使EPC往回指一條指令,即分支指令。在重新執行分支指令時,分支延遲槽中的指令會被再執行一次。

精確異常的實現對流水線的流暢性是有一定的影響的,如果異常太多,系統執行效率就會受到影響。

*異常又分常規異常和中斷兩類。常規異常一般爲軟件的異常,而中斷一般爲硬件異常,中斷可以是芯片內部,也可以是芯片外部觸發產生。

異常發生時,跳轉前最後被執行的指令是其MEM階段剛好被執行完的那條指令。受害指令是其ALU階段剛好執行完的那條指令。

異常發生時,會跳到異常向量入口中去執行。MIPS的異常向量有點特殊,它一般只個2個或幾個中斷向量入口,一個入口給一般的異常使用,一個入口給TLB miss異常使用(這樣的話,可以省下計算異常類型的時間。在這種機制幫助下,系統只用13個時鐘週期就可以把TLB重填好)。

CP0寄存器中有個模式位,SR(BEV),只要設置了,就會把異常入口點轉移到非緩衝內存地址空間中(kseg1)。

MIPS系統把重啓看作一個不可迴歸的異常來處理。
冷啓動:CPU硬件完全被重新配置,軟件重新加載;
熱啓動:軟件完全重新初始化;

MIPS對異常的處理的哲學是給異常分配一些類型,然後由軟件給它們定義一些優先級,然後由同一個入口進入異常分配程序,在分配程序中根據類型及優先級確定該執行哪個對應的函數。這種機制對兩個或幾個異常同時出現的情況也是適合的。

下面是當異常發生時MIPS CPU所做的事情:
1。設置EPC指向迴歸的位置;
2。設置SR(EXL)強迫CPU進入kernel態,並禁止所有中斷響應。
3。設置Cause寄存器,以使軟件可以得到異常的類型信息;還有其它一些寄存器在某些異常時會被設置;
4。CPU開始從異常入口取指令,然後以後的所有事情都交由軟件處理了。

k0和k1寄存器用於保存異常處理函數的地址。
異常處理函數執行完成後,會回到異常分配函數那去,在異常分配函數裏,有一個eret指令,用於迴歸原來被中斷的程序繼續執行;eret指令會原子性地把中斷響應打開(置SR(EXL)),並把狀態級由kernel轉到user級,並返回原地址繼續執行。

*中斷
MIPS CPU有8個獨立的中斷位(在Cause寄存器中),其中,6個爲外部中斷,2個爲內部中斷(可由軟件訪問)。一般來說,片上的時鐘計數/定時器,會連接到一個硬件位上去。

SR(IE)位控制全局中斷響應,爲0的話,就禁止所有中斷;
SR(EXL)和SR(ERL)位(任何一個)如果置1的話,會禁止中斷;
SR(IM)有8位,對應8箇中斷源,要產生中斷,還得把這8位中相應的位置1纔行;

中斷處理程序也是用通用異常入口。但有些新的CPU有變化。

*在軟件中實現中斷優先級的方案
1。給各種中斷定優先級;
2。CPU在運行時總是處於某個優先級(即定義一個全局變量);
3。中斷髮生時,只有等於高於CPU優先級的中斷優先級才能執行;(如果CPU優先級處於最低,那麼所有的中斷都可以執行);
4。同時有多箇中斷髮生時,優先執行優先級最高的那個中斷程序;

*大小端問題
見專圖。

硬件上也有大端小端問題,比如串口通訊,一個字節一個字節的發,首先是低位先發出去。
還有顯卡的顯示,比如顯示黑白圖像,在屏幕上一個點對應顯存中的一位,這時,這個位對應關係就是屏幕右上角那個點對應顯存第一個字節的7號位,即最高位。第一排第8位點對應第一個字節的0號位。

*MIPS上的Linux運行情況

用戶態和核心態:在用戶態,不能隨意訪問內核代碼和數據存放區,只能訪問用戶態空間和內核允許訪問(通過某種機制)的內核頁面。也不能執行CP0相關的指令。用戶態要執行內核的某些服務,就得用系統調用(system_call),在系統調用的最後,是一個eret指令。

任何時候Linux都有至少一個線程在跑,Linux一般不禁止中斷。發生中斷時,其環境是從被中斷線程借來的。

中斷服務程序(ISR)應該短小。

MIPS Linux系統上半地址空間只能用內核特權級訪問。內核不通過TLB地址翻譯。

所有線程都共用相同的內核地址空間,但只有同一組線程才用同一個用戶地址空間(指向同一個mm_struct結構)。

如果物理內存高於512M,那麼不能用kseg0和kseg1來映射高於512M的內存部分。只能用kseg2來映射。kseg2要經過TLB。

從某個方面說,內核就是一組供異常處理函數調用的子程序。內核中,線程調度器就是這樣一個小的子程序。由各個線程(異常處理程序也可以算作一個特殊的線程,換他書上的說法)調用。

MIPS Linux有異常模式,而x86上沒有這個概念。

異常要小心操作。不是僅用軟件鎖就能解決的。

*原子操作
MIPS爲支持操作系統的原子操作,特地加了一組指令 ll/sc。它們這樣來使用:

先寫一句
atomic_block:
ll XX1, XXX2
….
sc XX1, XXX2
beq XX1, zero, automic_block
….

在ll/sc中間寫上你要執行的代碼體,這樣就能保證寫入的代碼體是原子執行的(不會被搶佔的)。

其實,ll/sc兩語句自身並不保證原子執行,但他耍了個花招:
用一個臨時寄存器XX1,執行ll後,把XXX2中的值載入XX1中,然後會在CPU內部置一個標誌位,我們不可見,並保存XXX2的地址,CPU會監視它。在中間的代碼體執行的過程中,如果發現XXX2的內容變了(即是別的線程執行了,或是某個中斷髮生了),就自動把CPU內部那個標誌位清0。執行sc時,把XX1的內容(可能已經是新值了)存入XXX2中,並返回一個值存入XX1中,如果標誌位還爲1,那麼這個返回的值就爲1;如果標誌位爲0,那麼這個返回值就爲0。爲1的話,就表明這對指令中間的代碼是一次性執行完成的,而不是中間受到了某些中斷,那麼原子操作就成功了;爲0的話,就表明原子操作沒成功,執行後面beq指令時,就會跳轉到ll指令重新執行,直到原子操作成功爲止。

所以,我們要注意,插在ll/sc指令中間的代碼必須短小。

據經驗,一般原子操作的循環不會超過3次:)

*系統調用 syscall
系統調用也通過異常入口進入系統內核,選擇8號異常代碼處理函數進行處理,進入系統調用分配函數後,還要根據傳進來的參數再一次分配到具體的功能函數上去。系統調用傳遞參數是在寄存器中進行的。

系統調用號存放在v0中,參數存放在a0-a3。如果參數過多,會有另一套機制來處理。系統調用的返回值通常放在v0中。如果系統調用出錯,則會在a3中返回一個錯誤號。

*異常入口點位於kseg0的底部,是硬件規定的。

*注:地址空間的0x0000 0000是不能用的,從0開始的一個或多個頁不會被映射。

*內存分頁映射有以下優點:
1。隱藏和保護數據;
2。分配連續的地址給程序;
3。擴展地址空間;
4。按需求載入代碼和數據(通過異常方式);
5。便於重定位;
6。代碼和數據在線程中共享,便於交換數據;

所有的線程是平等的,所有的線程都有自己的內存管理結構體;運行於同一地址空間的線程組,共享有大部分這種數據結構。在線程中,保存有本地址空間已經使用的頁面的一個頁表,用來記錄每個已用的虛頁與實際物理頁的映射關係;

*ASID是與虛擬頁高位配合使用。用於描述在TLB和Cache中的不同的線程,只有8位,所以最多隻能同時運行256個線程。這個數字一般來說是夠的。如果超過這個數目了,就要把Cache刷新了重新裝入。所以,在這點上,與x86是不同的。

*MIPS Linux的內存駐留頁表結構
用的是兩級頁表,一個頁表目錄,一個頁表,頁表中的每一項是一個 EntryLo0-1。
(這與x86方式類似)。而沒有用MIPS原生設計的方案。

*TLB的refill過程-硬件部分
1。CPU先產生一個虛擬地址,要到這個地址所對應的物理地址上取數據(或指令)或寫數據(或指令)。
低13位被分開來。然後高19位成爲VPN2,和當前線程的ASID(從EntryHi(ASID)取)一起配合與TLB表中的項進行比較。(在比較過程中,會受到PageMask和G標誌位的影響)
2。如果有匹配的項,就選擇那個。虛擬地址中的第12位用於選取是用左邊的物理地址項還是用右邊的物理地址項。
然後就會考察V和D標誌位,V標誌位表示本頁是否有效,D表示本頁是否已經髒了(被寫過)。
如果V=0,或D=1,就會引發翻譯異常,BadVAddr會保存現在處理的這個虛擬地址,EntryHi會填入這個虛擬地址的高位,還有Context中的內容會被重填。
然後就會考察C標誌位,如果C=1,就會用緩衝作中轉,如果C=0,就不使用緩衝。
這幾級考察都通過了之後,就正確地找到了那個對應的物理地址。
3。如果沒有匹配的項,就會觸發一個TLB refill異常,然後後面就是軟件的工作了;

*TLB的refill過程-軟件部分
1。計算這個虛擬地址是不是一個正確的虛擬地址,在內存頁表中有沒有與它對應的物理地址;如果沒有,則調用地址錯誤處理函數;
2。如果在內存頁表中找到了對應的物理地址,就將其載入寄存器;
3。如果TLB已經滿了,就用random選取一個項丟棄;
4。複製新的項進TLB。

*MIPS Linux中標誌內存頁已經髒了的方式與x86不同。它要耍個把戲:
1。當一個可寫的頁第一次載入內存中時(從磁盤載入?載入的時候就分配一個物理頁,同時就分配個對應的虛擬頁,並在內存頁表中添一個Entry),將其Entry的D標誌位清0;
2。然後,當後面有指令要寫這個頁時,就會觸發一個異常(先載入TLB中判斷),我們在這個異常處理函數中把內存頁表項中的標誌位D置1。這樣後面的就可以寫了。並且,由於這個異常把標誌位改了,我們認爲這個物理頁是髒的了。
3。至於TLB中已經有的那個Entry拷貝還要修改它的D標誌位,這樣這次寫入操作才能繼續入下進行。

*MIPS中的C語言參數傳遞機制?

*MIPS中的堆棧結構及在內存中的分佈?


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章