轉自:https://blog.csdn.net/qq_16777851/article/details/81074077
1.什么是mmu
MMU是Memory Management Unit的縮寫,中文名是內存管理單元,它是中央處理器(CPU)中用來管理虛擬存儲器、物理存儲器的控制線路,同時也負責虛擬地址映射為物理地址,以及提供硬件機制的內存訪問授權,多用戶多進程操作系統。
物理地址:(英語:physical address),也叫實地址(real address)、二進制地址(binary address),它是在地址總線上,以電子形式存在的,使得數據總線可以訪問主存的某個特定存儲單元的內存地址。
虛擬地址:虛擬地址是相對於物理地址來說的。虛擬地址的提出,主要是為了解決在操作系統中,多線程內存地址重復,大進程在小內存運行等問題 , 在32位系統中,虛擬地址空間中有4G,在操作系統中程序中使用的都是虛擬地址
2.mmu有什么作用
簡單的說,mm的作用有兩點,地址翻譯和內存保護。
在處理器上我們會運行一個操作系統,如linux,windows等,用戶編寫的源程序,需要經過編譯,鏈接,生成可執行程序,人后被操作系統加載執行。在鏈接的時候同chan常我們要指定一個鏈接腳本,鏈接腳本的作用有很多,其中一個的作用是控制可執行文件的section的符號的內存布局,也就是控制ke'z可執行程序將來要在內存中哪里放置。操作系統會按照可執行程序的要求將其加載到內存的對應地址執行。假如用戶A編寫的應用程序的鏈接地址范圍是0x100-0x200,用戶B編寫的應用程序的鏈接地址范圍是0x100-0x200,這是很有可能的。因為給操作系統提供應用程序的開發者很多,不可能為每個開發者限定使用那些內存。這樣,執行程序A的時候就不能執行程序B,執行程序B的時候就不能執行程序A,因為它們執行時會覆蓋對方內存中的程序。為了解決這個問題,必須引入虛擬地址,為此操作系統和處理器都做了處理,添加了mmu,讓其進行地址翻譯。在程序載入內存的時候,操作系統會為其建立地址翻譯表,處理器執行不同應用程序的時候,使用不同的地址翻譯表。如下圖所示。
ProgramA被加載到物理地址地址0x500-0x600處,ProgramB被加載到物理地址0x700-0x800處,同時建立了各自的地址翻譯表,當處理器要執行ProgramB時,會使用ProgramB對應的地址翻譯表,比如讀取ProgramB地址0x100處的指令,那么經過地址翻譯表可知0x100對應實際內存的0x700處,所以實際讀取的就是0x700處的指令。同樣的,當處理器要執行ProgramA時,會使用ProgramA對應的地址翻譯表,這樣就避免了之前提到的內存沖突問題,有了MMU的支持,操作系統就可以輕松實現多任務了。
上圖CPU給出的地址稱之為虛擬地址,經過MMU翻譯后的地址稱之為物理地址。
MMU的地址翻譯功能還可以為用戶提供比實際大得多的內存空間。用戶在編寫程序的時候並不知道運行該程序的計算機內存大小,如果在鏈接的時候指定程序被加載到地址Addr處,而運行該程序的計算機內存小於Addr,那么程序就無法執行,有了MMU后,程序員就不用關心實際內存大小,可以認為內存大小就是“2^指令地址寬度”。MMU會將超過實際內存的虛擬地址翻譯為物理地址進行訪問。
地址翻譯表存儲在內存中,如果采用圖10.1中的方式:地址翻譯表的表項是一個虛擬地址對應一個物理地址,那么會占用太多的內存空間,為此,需要修改翻譯方式,常用的有三種:頁式、段式、段頁式,這也是三種不同的內存管理方式。
頁式內存管理將虛擬內存、物理內存空間划分為大小固定的塊,每一塊稱之為一頁,以頁為單位來分配、管理、保護內存。此時MMU中的地址翻譯表稱為頁表(Page Table),每個任務或進程對應一個頁表,頁表由若干個頁表項(PTE:Page Table Entry)組成,每個頁表項對應一個虛頁,內含有關地址翻譯的信息和一些控制信息。在頁式內存管理方式中地址由頁號和頁內位移兩部分組成,其地址翻譯方式如圖10.2所示。
使用虛擬地址中的虛頁號查詢頁表得到對應的物理頁號,然后與虛擬地址中的頁內位移組成物理地址。比如:頁大小是256字節,虛擬地址是0x104,可知對應的虛頁號是0x1,頁內位移是0x4,假如通過頁表翻譯得到的對應物理頁號是0x7,那么0x104對應的物理地址就是0x704。使用頁表方式進行地址翻譯可以有效減少地址翻譯表占用的內存空間,還是以圖10.1為例,頁大小是256字節,此時每個程序對應的頁表就只有兩項,如圖10.3所示。
段式內存管理將虛擬內存、物理內存空間划分為段進行管理,段的大小取決於程序的邏輯結構,可長可短,一般將一個具有共同屬性的程序代碼和數據定義在一個段中。每個任務和進程對應一個段表(Section Table),段表由若干個段表項(STE:Section Table Entry)組成,內含地址映像信息(段基址和段長度)等內容。在段式虛擬存儲器中,地址分為段號、段內位移兩部分,使用段表進行地址翻譯的過程與使用頁表進行地址翻譯的過程是相似的。
段頁式內存管理是在內存分段的基礎上再分頁,即每段分成若干個固定大小的頁。每個任務或進程對應有一個段表,每段對應有自己的頁表。在訪問存儲器時,由CPU經頁表對段內存儲單元進行尋址。
2、內存保護
內存保護也叫權限管理,除了具有地址翻譯的功能外,還提供了內存保護功能。采用頁式內存管理時可以提供頁粒度級別的保護,允許對單一內存頁設置某一類用戶的讀、寫、執行權限,比如:一個頁中存儲代碼,並且該代碼不允許在用戶模式下執行,那么可以設置該頁的保護屬性,這樣當處理器在用戶模式下要求執行該頁的代碼時,MMU會檢測到並觸發異常,從而實現對代碼的保護。特別是在處理應用程序時,如果一個應用程序寫的比較爛,出現了指針越界或棧溢出,程序跑飛等情況,因為不能訪問別的程序的地址,所以不會影響到別的應用程序的運行。比如在操作系統下,應用程序不能訪問寄存器,而操作系統可以。比如應用程序的只讀數據段不能被寫,否則會發生段錯誤。
3、大容量app在小資源系統運行
在嵌入式系統中,假如內存容量只有256M大,而應用程序卻有1G大時,通常一個程序中,程序執行的比較多的是順序指令,所以在運行1G的程序時,操作系統會先加載一小部分到內存中,當執行完這一部分或發生跳轉發現內存中沒有要跳轉地址的指令時,操作系統再加載需要跳轉部分的程序到其鏈接地址(虛擬地址),加載完后再繼續執行。內次加載程序,都需要建立一個動態的地址映射表。當物理內存加載滿后,操作系統會選擇性的將最早之前加載入物理內存的程序置換到外部flash等存儲器中,再加載需要用到的一塊程序。因為置換需要時間,所以當使用存儲容量較小內存的嵌入式系統后,讓其運行大程序,使用可能會有一定的卡頓現象。
3.arm的mmu
下圖中是arm支持的幾種頁表大小一級每種頁表可以管理的內存單元數量。
通常使用段式頁表作為一級頁表,使用頁式式頁表作為二級頁表。
在關閉了子頁(subpages)功能后可以使用下面三種作為一級頁表。
下圖分別是超級段,和段以及粗頁表的描述。
粗頁以1k為單位管理頁表,段以1M為單位管理頁表,超級短以16M為單位管理頁表。
上面三者都可以作為一級頁表使用。
從上圖我們可以看到,超級頁表可以管理40位數據寬度也就是1T容量的內存。主要是為64位系統而發明的。
結合上下圖的描述我們可以看到,supersection和section是通過bit18來區分的。
段和頁的區分是有bit【0,1】來區分的。
1.下面是超級段和段的區別。
在使能子頁(subpages)功能后,可以使用下面二種作為一級頁表。
在說明一級頁表轉換之前我們先引入一個概念。
首先,我們要分清ARM CPU上的三個地址:虛擬地址(VA,Virtual Address)、變換后的虛擬地址(MVA,Modified Virtual Address)、物理地址(PA,Physical Address)
啟動MMU后,CPU核對外發出虛擬地址VA,VA被轉換為MVA供MMU使用,在這里MVA被轉換為PA;最后通過PA讀寫實際設備
MMU的作用就是負責虛擬地址(virtual address)轉化成物理地址(physical address)。 32位的CPU的虛擬地址空間達到4GB,在一級頁表中使用4096個描述符來表示這4GB的空間,每個描述符代表1M的虛擬地址,要么存儲了它的對應物理地址的起始地址,要么存儲了下一級頁表的地址。使用MVA[31:20]來索引一級頁表(4096個描述符)(因為用MVA的高12位來索引,因此大小為 2^12 = 4096)
由協處理器CP15中的寄存器C2(高18位,即[31:14]為轉換表基地址,低14位為0)用來存放一級轉換表基地址,指向2^14=16KB整除的存儲器即16K對齊,這個存儲區稱為一級轉換表;MVA的高12位,即位[31:20]作為一級轉換表的地址索引,因此一級轉換表具有2^12=4096項,每一項的地址為32位,最高的18位[31:14]為寄存器C2的高18位,中間12位為MVA的高12位[31:20],最低2位為0b00。每一項的內容稱為一個描述符,在段(Section)下,一級描述符的高12位為大小為1MB的段基地址,段內地址(偏移地址)為MVA的低20位,即段內每個存儲器的地址是這樣組成:高12位為一級描述符的高12位,低20位MVA的低20位。這樣,借助於寄存器C2和一級描述符,將一個MVA轉換成一個PA。(在這里一定要注意:MVA的高12位是用來索引4096個項的,然后使用項的內容(即描述符)的高12位為段的高12位,類似於指針里面存放地址,4096項類似指針,描述符類似指針里面的內容)
下圖是使用一級頁表后,地址的轉換過程圖。(以段式頁表為例)
從上往下看:
translation tabe base:簡稱ttb,稱為轉換表基址,存放在cp15的c2寄存器的高18位,低12位為0。所以將來我們寫程序存放ttb的基地址一定要以16kb對齊。
modified virtual address:簡稱mva,稱為轉換后的虛擬地址(即在32bit系統中具有4G訪問空間的虛擬地址),它的高12bit總共4096個項,用來作為該虛擬地址在ttb中的索引。它的低20位,是作為將來找到對應的物理內存的偏移。其本省也是在虛擬內存中的偏移。
address of first-level descriptor:一級地址描述符,它是結合ttb,以及偏移量mva,找到的具體的頁表。即具體段(section)在那個位置。
first-level descriptor:以及頁表描述符,上一步既然知道了是存放在一級頁表的哪個位置了,直接取出其中的高12位,即找到了物理地址所在的段。
physcical address:既然上一步已經找到了物理地址所在的段,那么只需要加上低20位的段內偏移即找到了具體的那個物理地址了。(偏移在物理地址和虛擬地址中是一樣的,區別只是在段地址)
2.粗頁表
一級頁表做粗頁表用的比較少,這里就不分析了
3.二級頁表
二級頁表主要有兩種
大頁表,每頁管理64k
小頁表,每頁管理4k
和一級頁表一樣,使能或不使能subpages分為兩種情況
前面包括一級頁表都沒有說,頁表沒一項的內容,這里統一說明一下。
[0-1]用來識別頁表類型,比如段式,大頁,小頁等
[2-3]用來識別是否使能cache和write buffer
TEX是擴展的類型字段。
[4-5]是用來做權限管理的
其中S R是在cp15的c1協處理器中,用來做系統保護和rom保護的。
nG S XN
其余的就是不同大小的頁的基地址。
以64KB管理的二級粗頁表的映射形式。
以4KB管理的二級粗頁表的映射形式。
4.編程實踐
下面使用上面講的最詳細的段頁表為例,在裸機的情況下開啟MMU,實現虛擬地址映射。
在現代處理器中,為了使內存的速度跟得上CPU的速度,通常在芯片內部做了緩存(cache)。在啟動了cache后,程序的運行效率會極大的提高。
arm中又把cache分為指令cache,又稱(icache),和數據cache,又稱(dcache)。
其中icache可以隨時開啟,隨時關閉,但dcache必須在開啟了MMU后,才能啟動。
在啟動cache后,arm其實才可以稱為哈佛結構(數據指令分開)
否則,在不開啟的情況下,其實還是馮洛伊曼結構。
我的ddr的地址范圍是0x3000000~0x4fffffff
為了驗證我的MMU確實開啟了,所以把程序的鏈接地址改為了0xB0000000,同時把0xB0000000起始的1M空間(我的裸機程序很小,遠小於1M)映射到了0x30000000
首先看一下我的鏈接腳本
-
SECTIONS
-
{
-
. = 0xb0000000;
-
__code_start = .;
-
. = ALIGN(4);
-
.text :
-
{
-
start.o
-
*(.text)
-
}
-
. = ALIGN(4);
-
.rodata :
-
{
-
*(.rodata)
-
}
-
. = ALIGN(4);
-
.data :
-
{
-
data_load_add = LOADADDR(.data);
-
data_start = .;
-
*(.data)
-
data_end = .;
-
}
-
. = ALIGN(4);
-
.bss :
-
{
-
bss_start = .;
-
*(.bss) *(.COMMON)
-
bss_end = .;
-
}
-
}
接下來是頁表的建立。
因為我在裸機中並沒有使用很多東西,所以映射的不是所有4G空間,只映射了我用到的。
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
/* 虛擬地址向物理地址映射 */
-
static void create_tlb(unsigned int *ttb,unsigned int va,unsigned int pa, int io)
-
{
-
int index;
-
-
index = va / 0x100000;
-
-
if(io)
-
ttb[index] = (pa & 0xfff00000) | MMU_SECTION_IO;
-
else
-
ttb[index] = (pa & 0xfff00000) | MMU_SECTION_MEM;
-
}
-
-
-
/* 創建一級頁表
-
* VA PA CB
-
* 0 0 11
-
*
-
* 512M
-
* 0x30000000 0x30000000 11
-
* ......
-
* 0x4ff00000 0x4ff00000 11
-
*
-
* 0xd0000000 0xd0000000 11
-
*
-
* SFR
-
* 0xe0000000 0xe0000000 00
-
* ......
-
* 0xfff00000 0xfff00000 00
-
*
-
* framebuffer
-
* 0x40000000 0x40000000 00
-
*
-
* link address
-
* 0xb0000000 0x30000000 11
-
*/
-
-
/* 創建一個一級的段頁表 */
-
void create_page_table(void)
-
{
-
/* 頁表在哪 0x4f000000 16k對齊 */
-
unsigned int *ttb = (unsigned int *)0x4f000000;
-
unsigned int va,pa;
-
-
/* 1.irom */
-
create_tlb(ttb, 0,0,MMU_MEM);
-
-
/* 2.sdram 512M*/
-
va = 0x30000000;
-
pa = 0x30000000;
-
for( va = 0x30000000; va < 0x4fffffff; va += 0x100000 )
-
{
-
create_tlb(ttb,va,pa, MMU_MEM);
-
pa += 0x100000;
-
}
-
-
/* 3.irom/iram */
-
create_tlb(ttb, 0xd0000000,0xd0000000, MMU_MEM);
-
-
/* 4.sfr */
-
va = 0xe0000000;
-
pa = 0xe0000000;
-
for( va = 0xe0000000; va < 0xfff00000; va += 0x100000)
-
{
-
create_tlb(ttb,va,pa, MMU_IO);
-
pa += 0x100000;
-
}
-
-
/* 5.framebuffer */
-
create_tlb(ttb, 0x40000000,0x40000000, MMU_IO);
-
-
/* 6. link address */
-
create_tlb(ttb, 0xb0000000,0x30000000, MMU_MEM);
-
}
-
-
-
下面是初始化部分(bootloader)
-
-
__reset_exception:
-
-
/* 開發板制鎖*/
-
ldr r0, = 0xe010e81c
-
ldr r1, = 0x301
-
str r1, [r0]
-
-
/* 關閉看門狗 */
-
ldr r0, = 0xe2700000
-
mov r1, # 0
-
str r1, [r0]
-
-
/* 下面有調用c函數設置SVC棧地址 */
-
ldr sp, = 0xd0037d80
-
-
/* 啟動icache */
-
bl enable_icache
-
-
/* 設置時鍾 */
-
bl init_clock
-
-
/* 初始化DDR */
-
bl sdram_init
-
-
/* 創建頁表 */
-
bl create_page_table
-
-
/* 使能mmu */
-
bl enable_mmu
-
-
/* 代碼重定位 */
-
bl copy2sdram
-
-
/* 清bss段 */
-
bl clear_bss
-
-
/* 從iram跳轉到ddr */
-
ldr pc, = sdram
-
sdram:
-
bl uart0_init
-
-
/* 開irq中斷 */
-
mrs r0, cpsr
-
bic r0, r0, # 1<<7
-
msr cpsr, r0
-
-
ldr sp, = 0x45000000
-
-
/* 調用main函數 */
-
bl main
-
-
b .
-
-
enable_icache:
-
mrc p15, 0, r1, c1, c0, 0 @Read Control Regist
-
orr r1, r1,#( 1<<12) @enable instructon cache
-
//bic r1, r1,#(1<<12)
-
mcr p15, 0, r1, c1, c0, 0
-
mov pc, lr
-
-
enable_mmu:
-
/* translation table base write cp5 */
-
ldr r1, = 0x4f000000
-
mrc p15, 0, r2, c2, c0, 0 @ Read Translation Table Base Register
-
orr r2, r2, r1
-
mcr p15, 0, r2, c2, c0, 0 @ Write Translation Table Base Register
-
-
/* set domain 0xffffffff */
-
ldr r0, = 0xffffffff
-
mcr p15, 0, r0, c3, c0, 0 @ Read Domain Access Control Register
-
-
-
/* enable i/d canche */
-
mrc p15, 0, r1, c1, c0, 0 @Read Control Regist
-
orr r1, r1,#( 1<<12) @enable instructon cache
-
orr r1, r1,#( 1<<2) @enable data cache
-
orr r1, r1,#( 1<<0) @enable mmu
-
mcr p15, 0, r1, c1, c0, 0 @write Control Regist
-
mov pc, lr
初始化要注意的點:
1.sdram標號之前的代碼都應該是位置無關碼(不能使用全局變量,靜態變量,字符串,初始化過的局部數組等)。
2.因為頁表放置在ddr中,所以創建頁表必須在ddr初始化之后。
3.因為我的鏈接地址在0xB0000000,所以代碼重定位時必須要能使用0xB0000000的空間。所以我把啟動mmu放在了,重定位前面。同時開啟MMU時要使用頁表基地址,所以頁表也必須在開啟MMUq前先建立。
5.效果
啟動了MMU前,我的刷屏速度大概在每秒幾幀。
開啟了MMU和cache后,我的刷屏速度差不多可以達到每秒二十對幀。
有一點要說明的是,我把framebuffer的顯存映射成MEM即可以使用cache和buffer后,刷新速度感覺比映射成IO速度快了一倍。
主要原因是因為我是一整屏的刷顏色,所以dcache很快就滿了,然后硬件自動把整個dcache刷回內存。所以速度比直接訪問的IO要快,當
每次只刷一小塊部分,速度反而會比IO方式慢。