如《Linux內核內存管理架構》一文中提到,linux內核中的內存管理支持內存地址映射、內存分配、內存回收、內存碎片管理、頁面緩存等眾多功能。但U-Boot做為啟動引導程序,其核心功能就是引導內核鏡像,所以其內存管理功能並不用像Linux內核中的內存管理一樣功能齊全。U-Boot中沒有內存分配、回收、緩存等功能,內存管理其實只做一件事:虛實地址映射,而且是固定映射。
為了提高效率,現代處理器的內存管理都由MMU(Memory Management Unit)硬件單元實現(示意圖如下),其核心模塊主要有TLB和page table。
不同的CPU體系的MMU架構差異很大,有的MMU可以跳過,有的不可以;有的MMU分為L1MMU、L2MMU;有的TLB分為頁式TLB、段式TLB;有的page table分為page entry、block entry;有的TLB reload和page table 查找由MMU硬件實現(ARM、x86, PowerPC),有的則是由軟件實現(MIPS, Alpha)等。U-Boot的虛實地址映射,一般能跳過MMU就跳過,能不使用頁表就不用頁表,總之,怎么簡單怎么來。
可見,U-Boot中的內存管理的實現與CPU架構和MMU強相關,本文挑選了PowerPC e500,MIPS,ARMv8三款處理器並對其MMU架構進行分析,並討論它們在U-Boot中的內存虛實地址映射實現。
U-Boot PowerPC內存管理
下圖是PowerPC e500的MMU結構框圖。
MMU地址映射過程中涉及到3種地址形式:
- 32位有效地址EA:軟件可以直接訪問的地址;
- 41位虛擬地址VA:經過段映射的過渡地址,由32位EA和AS以及8位PID位組成;
- 36位物理地址RA:也稱為實地址,由36位地址總線訪問的地址空間。
PID0-2 用來存放當前進程有效地址的進程ID號,主要作用是在進程上下文切換時,提高TLB刷新的精准性。
LAW(Local Access Window) 用於描述PowerPC處理器物理地址空間的划分,其中LAWBAR用於指定基址,LAWAR用於指定此空間用作PCI、Local Bus還是DDR等設備的空間。
e500支持2種形式的TLB:TLB0和TLB1。
TLB0支持固定4K頁大小映射,512個entry最大可以映射512*4K=2M的物理地址空間,需要動態更新,會產生TLB miss異常。TLB0靈活,可以滿足復雜系統應用的要求。
TLB1 是一種段式映射,有效地址和物理地址之間是一一對應的關系。TLB1支持可變頁大小映射,16個entry 支持4K~4G頁大小的映射,最大可映射16*4G =64G的物理地址空間,不需要動態更新,不會產生TLB miss異常。TLB1不夠靈活,無法滿足復雜系統應用的要求。
PowerPC e500核心的MMU是無法跳過的,所以只能通過MMU來映射地址空間。雖然MMU中的TLB1段式映射不夠靈活,但是簡單,可以滿足U-Boot中的內存固定映射需求。PowerPC對內存虛實地址映射的處理是先設置DDR內存的物理地址(LAW),再把虛擬地址到物理地址的映射關系寫到TLB1中。
相關代碼實現:
phys_size_t fixed_sdram(void) { // ... 初始化DDR配置參數ddr_cfg_regs ddr_size = (phys_size_t) CONFIG_SYS_SDRAM_SIZE * 1024 * 1024; fsl_ddr_set_memctl_regs(&ddr_cfg_regs, 0); if (set_ddr_laws(CONFIG_SYS_DDR_SDRAM_BASE, ddr_size, LAW_TRGT_IF_DDR_1) < 0) { printf("ERROR setting Local Access Windows for DDR\n"); return 0; } return ddr_size; } unsigned int setup_ddr_tlbs_phys(phys_addr_t p_addr, unsigned int memsize_in_meg) { /// ... 計算size for (i = 0; size && i < 8; i++) { /// ... 計算ram_tlb_address, p_addr, ram_tlb_index, tlb_size set_tlb(1, ram_tlb_address, p_addr, MAS3_SX|MAS3_SW|MAS3_SR, wimge, 0, ram_tlb_index, tlb_size, 1); } return memsize_in_meg; } unsigned int setup_ddr_tlbs(unsigned int memsize_in_meg) { return setup_ddr_tlbs_phys(CONFIG_SYS_DDR_SDRAM_BASE, memsize_in_meg); }
U-Boot MIPS內存管理
MIPS的虛擬地址空間分為多個段,即Kseg0-3和Kuseg。其中kseg0/kseg1可以跳過MMU,支持直接映射,各512MB。kseg2/3和Kuseg必須經過MMU。
因為U-Boot階段對內存的需求量很小,512MB內存空間已足夠滿足需要,所以MIPS對內存虛實地址映射的處理是先設置好DDR內存的物理地址BAR,把其虛擬地址設置到kseg0/1即可。沒有必要經過MMU和TLB。換句話說,如果CPU需要訪問高於512M的DDR內存物理地址空間,必須通過MMU地址轉換。
相關代碼實現:
#define mem_map(x) (void *)(CAC_BASE + (x))
U-Boot ARMv8內存管理
ARMv8的MMU結構如下圖,其支持:
- L1指令TLB,全相連,48個entry,支持4KB, 64KB和1MB頁面大小;
- L1數據TLB,全相連,32個entry,支持4KB, 64KB和1MB頁面大小;
- L2 TLB,4路組相連,1024個entry,支持4KB, 64KB和1MB頁面大小;
- page table 查找由MMU中的Translation Control Unit (TCU) 硬件實現;
ARMv8的page table 結構如下。其分為4級,每級頁表有512個條目,支持如下3種表條目描述符。
- Table descriptor,指向下一級table;
- Page descriptor,指向一個4KB(或64KB、1MB)大小page size的頁面;
- Block descriptor,指向一個block size的內存區域;在1級頁表中block size是1GB,2級頁表中block size是2MB。
與上面提到的PowerPC、MIPS不同,ARMv8的MMU即無法跳過,TLB也不支持段式映射。所以只能通過page table完成虛實地址映射。這就帶來一個問題:在DDR內存准備好之前,page table又放在哪里?
ARMv8核心的處理器片內包含一塊2MB大小的OCRAM(On Chip RAM),在DDR RAM准備好之前,MMU table就放在CONFIG_SYS_FSL_OCRAM_BASE處,最大支持EARLY_PGTABLE_SIZE(0x5000)個條目。2MB大小的頁表在Linux內核中是遠遠不夠的,但在U--Boot中已足夠,因為:
- page table支持塊映射(1GB,2MB),對於連續的大塊地址空間映射,block entry能極大簡化page table的大小;
- U-Boot中的虛實地址映射都是固定映射;
- 只有和啟動相關的設備和地址空間才需要映射,其數量有限。
等DDR內存初始化好后,再將table放到DRAM中。
相關代碼實現:
void setup_pgtables(void) { int i; if (!gd->arch.tlb_fillptr || !gd->arch.tlb_addr) panic("Page table pointer not setup."); create_table(); /* Now add all MMU table entries one after another to the table */ for (i = 0; mem_map[i].size || mem_map[i].attrs; i++) add_map(&mem_map[i]); } static void add_map(struct mm_region *map) { u64 *pte; u64 virt = map->virt; u64 phys = map->phys; u64 size = map->size; u64 attrs = map->attrs | PTE_TYPE_BLOCK | PTE_BLOCK_AF; u64 blocksize; int level; u64 *new_table; while (size) { pte = find_pte(virt, 0); if (pte && (pte_type(pte) == PTE_TYPE_FAULT)) { debug("Creating table for virt 0x%llx\n", virt); new_table = create_table(); set_pte_table(pte, new_table); } for (level = 1; level < 4; level++) { pte = find_pte(virt, level); if (!pte) panic("pte not found\n"); blocksize = 1ULL << level2shift(level); if (size >= blocksize && !(virt & (blocksize - 1))) { /* Page fits, create block PTE */ *pte = phys | attrs; virt += blocksize; phys += blocksize; size -= blocksize; break; } else if (pte_type(pte) == PTE_TYPE_FAULT) { /* Page doesn't fit, create subpages */ new_table = create_table(); set_pte_table(pte, new_table); } else if (pte_type(pte) == PTE_TYPE_BLOCK) { split_block(pte, level); } } } }