1. 程式人生 > >ARM MMU作用和cache

ARM MMU作用和cache

 

在ARM儲存系統中,使用MMU實現虛擬地址到實際實體地址的對映。為何要實現這種對映?首先就要從一個嵌入式系統的基本構成和執行方式著手。系統上電時,處理器的程式指標從0x0(或者是由0Xffff_0000處高階啟動)處啟動,順序執行程式,在程式指標(PC)啟動地址,屬於非易失性儲存器空間範圍,如ROM、FLASH等。然而與上百兆的嵌入式處理器相比,FLASH、ROM等儲存器響應速度慢,已成為提高系統性能的一個瓶頸。而SDRAM具有很高的響應速度,為何不使用SDRAM來執行程式呢?為了提高系統整體速度,可以這樣設想,利用FLASH、ROM對系統進行配置,把真正的應用程式下載到SDRAM中執行,這樣就可以提高系統的效能。然而這種想法又遇到了另外一個問題,當ARM處理器響應異常事件時,程式指標將要跳轉到一個確定的位置,假設發生了IRQ中斷,PC將指向0x18(如果為高階啟動,則相應指向0vxffff_0018處),而此時0x18處仍為非易失性儲存器所佔據的位置,則程式的執行還是有一部分要在FLASH或者ROM中來執行的。那麼我們可不可以使程式完全都SDRAM中執行那?答案是肯定的,這就引入了MMU,利用MMU,可把SDRAM的地址完全對映到0x0起始的一片連續地址空間,而把原來佔據這片空間的FLASH或者ROM對映到其它不相沖突的儲存空間位置。例如,FLASH的地址從0x0000_0000-0x00ff_ffff,而SDRAM的地址範圍是0x3000_0000-0x31ff_ffff,則可把SDRAM地址對映為0x0000_0000-0x1fff_ffff而FLASH的地址可以對映到0x9000_0000-0x90ff_ffff(此處地址空間為空閒,未被佔用)。對映完成後,如果處理器發生異常,假設依然為IRQ中斷,PC指標指向0x18處的地址,而這個時候PC實際上是從位於實體地址的0x3000_0018處讀取指令。通過MMU的對映,則可實現程式完全執行在SDRAM之中。
在實際的應用中,可能會把兩片不連續的實體地址空間分配給SDRAM。而在作業系統中,習慣於把SDRAM的空間連續起來,方便記憶體管理,且應用程式申請大塊的記憶體時,作業系統核心也可方便地分配。通過MMU可實現不連續的實體地址空間對映為連續的虛擬地址空間。
作業系統核心或者一些比較關鍵的程式碼,一般是不希望被使用者應用程式所訪問的。通過MMU可以控制地址空間的訪問許可權,從而保護這些程式碼不被破壞。
MMU的實現過程,實際上就是一個查表對映的過程。建立頁表(translate table)是實現MMU功能不可缺少的一步。頁表是位於系統的記憶體中,頁表的每一項對應於一個虛擬地址到實體地址的對映。每一項的長度即是一個字的長度(在ARM中,一個字的長度被定義為4位元組)。頁表項除完成虛擬地址到實體地址的對映功能之外,還定義了訪問許可權和緩衝特性等。
MMU的對映分為兩種,一級頁表的變換和二級頁表變換。兩者的不同之處就是所實現的變換地址空間大小不同。一級頁表變換支援1M大小的儲存空間的對映,而二級可以支援64KB、4KB和1KB大小地址空間的對映。
要實現從虛擬地址到實體地址的對映,必然會遇到一個問題,如何找到這個頁表。對於表的查詢,要知道這個表的基地址和偏移地址,在具有MMU功能的處理器中,集成了一個被稱為CP15的協處理器,該協處理器的C2暫存器中用於儲存頁表的基地址,下面以一級頁表變換為例說明MMU實現地址變換的過程。
如圖1.1所示,當處理器訪問一個虛擬地址時,該虛擬地址的[31:20]作為偏移地址與頁基地址結合(基地址必須是64KB對齊的,因此基地址的[13:0]位都為0),得到一個32位的頁表項地址(因為頁表項為4位元組對齊,[1:0]兩位為0)。通過這個頁表項地址可以檢索到該頁表項。頁表項的格式如下:

表 1.3
表 1.4
查詢到頁表項後,根據頁表項的訪問特性(緩衝以及是否允許訪問等)協處理器決定是否允許訪問。如不允許訪問,則協處理器向CPU報告出錯資訊;反之,由頁表項的[31:20]位與虛擬地址的[19:0]一起組成實際的實體地址,實現從虛擬地址到實體地址的對映。

對於實際程式設計工作而言,主要是確定如何編寫頁表中的內容並如何確定頁表項地址。現舉例如下:
假設實體地址為0x36B0_0000~0x36Bf_ffff(1M空間)的一塊連續空間需對映為0x0100_0000~0x010f_ffff的一塊連續空間:
1.確定頁表項中的內容:把實體地址的基地址作為頁表項的高12位(31bit~21bit),填寫訪問屬性。假設可以讀寫,可以讀快取、寫緩衝,這樣該頁表項內容為0x36B0_0C00E;
2.確定頁表基地址,填寫頁表基地址到CP15暫存器的C2中。頁表的基地址要為64KB對齊;
3.計算出偏移地址,把內容填寫到頁表項地址中。頁表項地址=頁表基地址+(實體地址基地址>>18),如頁表基地址為0xA100_0000,那麼,頁表項地址=0xA100_0DAC;
4.將頁表項數值寫到對應的頁表項地址中。上例中,需要向地址0xA100_0DAC中寫入0x36B0_0C00E。

ARM920T的MMU與Cache

Cache是高效能CPU解決匯流排訪問速度瓶頸的方法,然而它的使用卻是需要權衡的,因為快取本身的動作,如塊拷貝和替換等,也是很消耗CPU時間的。MMU的重要性勿庸置疑,ARM920T(和ARM720T)集成了MMU是其最大的賣點;有了MMU,高階的作業系統(虛擬地址空間,平面地址,程序保護等)才得以實現。二者都挺複雜,並且在920T中又高度耦合,相互配合操作,所以需要結合起來研究。同時,二者的操作物件都是記憶體,記憶體的使用是使用MMU/Cache的關鍵。另外,MMU和Cache的控制暫存器不佔用地址空間,CP15是操縱MMU/Cache的唯一途徑。

Cache/Write Buffer的功能

Cache通過預測CPU即將要訪問的記憶體地址(一般都是順序的),預先讀取大塊記憶體供CPU訪問,來減少後續的記憶體總線上的讀寫操作,以提高速度。然而,如果程式中長跳轉的次數很多,Cache的命中率就會顯著降低,隨之而來,大量的替換操作發生,於是,過多的記憶體操作反而降低了程式的效能。

ARM920T內部採用哈佛結構,將內部指令匯流排和資料匯流排分開,分別連線到ICache和DCache,再通過AMBA匯流排介面連線到ASB總線上去訪問記憶體。Cache由Line組成,Line是Cache進行塊讀取和替換的單位。

Writer Buffer是和DCache相逆過程的一塊硬體,目的也是通過減少memory bus的訪問來提高效能。

MMU的功能

在記憶體中維護一張或幾張表,就看你怎麼給記憶體劃分page和section了。通過CP15指定好轉換表的位置,920T的硬體會自動將轉換表的一部分讀到TLB中。CPU每次進行記憶體讀寫時,發出虛擬地址,參照TLB中的轉換錶轉換到實體地址,並讀取相應entry中的資訊,以決定是否可以有許可權讀寫和快取。

mmugen這個工具就是幫你構造這個表的,省的自己寫程式了。

操作MMU,實際上就是如何分配和使用你的記憶體,並記錄在translationtable裡。

ARM920T中,MMU的每條entry包括Cachable和Buffable位來指定相應的記憶體是否可以用Cache快取。此處就是MMU與Cache的互動作用處。

實際上,MMU和Cache的使用是作業系統設計者根據系統軟硬體配置而考慮的事情。作業系統針對分配給應用程式的地址空間作記憶體保護和快取優化。在沒有作業系統的情況下,就需要我們自己來掌控它們了。其中,主要是合理分配記憶體。

我認為,以下幾點需要著重考慮:

1) 安全第一! -- 避免MMU和Cache的副作用。

當你在無OS的裸機上開發程式時,初始化執行環境的程式碼很重要,比如:各種模式堆疊指標的初始化;將程式碼和RW data從ROM拷貝到RAM;初始化.bss段(zero initialized)空間等。此時會有大量的記憶體操作,如果你enable了Cache,那麼在拷貝完程式碼之後,一定要invalidate ICache和flush DCache。否則將會出現快取中的程式碼或資料與記憶體中的不一致,程式跑飛。

另外,有時候我們需要自己作loader來直接執行ELF檔案,情況也是一樣,拷貝完程式碼後一定要重新整理Cache,以免不測。

還有,對硬體的操作要小心。很多暫存器值都是被硬體改變的,讀寫時,要保證確實訪問到它的地址。首先,在C語言程式碼中宣告為volatile變數,以防止記憶體讀寫被編譯器優化掉;另外,設定好TLB,使得暫存器對映的地址空間不被快取。

總之,快取和記憶體中程式碼的不一致,是一定要避免的。

2) 弄巧成拙! -- 只對頻繁訪問的地址空間進行Cache優化。

我們很清楚自己的程式中,那裡有大量的運算,哪裡有無數的迴圈或遞迴,而這正是Cache的用武之地,我們將這些空間進行快取將大大提高執行速度。但是,很多函式或子程式往往僅僅執行很少幾次,若是對它們也快取,只會撿了芝麻丟了西瓜,造成不必要的快取和替換操作,反而增加了系統負擔,降低了整體效能。

3) 斷點哪兒去了? -- 如何除錯“加速”了的程式碼?

據我所知,一般,debugger都是通過掃描地址匯流排,在斷點處暫停CPU。ARM9TDMI中整合的JTAG除錯口,也是這樣。

當我們除錯使用Cache的程式碼時,將會出現問題。比如:CPU訪問某斷點所在地址之前的地址時,發生快取操作,斷點處程式碼被提前讀入Cache,此時地址總線上出現了斷點地址,CPU被debugger暫停,並且斷點之後的指令也被Cache快取。於是,當你從斷點處step時,程式卻停不了了,因為地址總線上不再出現斷點之後的下一個地址了。
再舉個例子:
int i,a;
for (i=0; i<100; i++) {
-> a++;
}

當地址總線上第一次出現斷點地址時,CPU暫停;之後,就再也不會停了。因為,之後CPU會從cache中直接去程式碼了。(當然,後來,Cache的程式碼有可能會被替換掉,斷點又可到達。) 所幸的是,我用的debugger提供JTAG Monitor,允許斷點跟蹤使用cache的程式。