U-Boot記憶體管理
如《 ofollow,noindex">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)個條目。因為U-Boot中的虛實地址對映都是固定對映,而且只有和啟動相關的裝置才需要對映,所以2MB頁表空間基本夠用。等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); } } } }