一、內核啟動時,頁表映射有哪些?
Linux初始化過程,會依次建立如下頁表映射:
1.恒等映射:頁表基地址idmap_pg_dir;
2.粗粒度內核鏡像映射:頁表基地址init_pg_dir;
3.fixmap映射:頁表基地址為init_pg_dir;
4.內核主表映射,包含細粒度內核鏡像映射和線性映射,頁表基地址為swapper_pg_dir;
5.用戶空間頁表映射:頁表基地址task->mm->pgd;
二、恒等映射
????????在我們將uboot和kernel image燒寫到ram后,uboot完成相關硬件的初始化后,最后跳轉到kernel的入口函數(假設地址為0x40000000,那么就會將PC設置為0x40000000),此時,由于MMU尚未開啟,所以程序一直運行在物理地址空間,也就是PC的值,也就是指令地址,都是物理地址。
????????一旦MMU開始后,地址就會經過MMU轉換為物理地址,也就是所有地址就會被認為是虛擬地址,但是,現代處理器大多支持多級流水線,處理器會提前預取多條指令到流水線中,當打開MMU時,CPU已經預取多條指令到流水線中,并且這些指令都是用物理地址預取的;MMU開啟后,將以虛擬地址訪問,這樣繼續訪問流水線中預取的指令(按物理地址預取,比如0x40000488,這個地址就會被認為是虛擬地址),就很容易出錯。由此,便引出了恒等映射,所謂恒等,就是將虛擬地址映射到完全相等的物理地址上去(比如前面地址0x40000488映射到物理地址還是0x40000488),這樣就解決了這個問題。當然這個映射的區域是很小的,恒等映射完成后,啟動MMU,CPU就開啟虛擬地址訪問階段。
簡單看下啟動部分的匯編代碼(head.S)
SYM_CODE_START(primary_entry) //入口函數bl preserve_boot_args // 保存啟動參數到boot_args數組bl el2_setup // w0=cpu_boot_mode,切換到EL1模式adrp x23, __PHYS_OFFSETand x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0bl set_cpu_boot_mode_flag //設置__boot_cpu_mode全局變量bl __create_page_tables //創建恒等映射和內核映像映射頁表/** The following calls CPU setup code, see arch/arm64/mm/proc.S for* details.* On return, the CPU will be ready for the MMU to be turned on and* the TCR will have been set.*/bl __cpu_setup // 為打開MMU做準備b __primary_switch //啟動MMU,最后跳轉到start_kernel(內核C語言部分)
SYM_CODE_END(primary_entry)
其中__create_page_tables函數的功能就是創建恒等映射和內核映像映射頁表,這里先看創建恒等映射頁表。
2.1?__create_page_tables
/** Create the identity mapping.*/adrp x0, idmap_pg_diradrp x3, __idmap_text_start // __pa(__idmap_text_start)mov x5, #VA_BITS_MIN
1:adr_l x6, vabits_actualstr x5, [x6]dmb sydc ivac, x6 // Invalidate potentially stale cache lineadrp x5, __idmap_text_endclz x5, x5cmp x5, TCR_T0SZ(VA_BITS) // default T0SZ small enough?b.ge 1f // .. then skip VA range extensionadr_l x6, idmap_t0szstr x5, [x6]dmb sydc ivac, x6 // Invalidate potentially stale cache linemov x4, #1 << (PHYS_MASK_SHIFT - PGDIR_SHIFT)str_l x4, idmap_ptrs_per_pgd, x51:ldr_l x4, idmap_ptrs_per_pgdmov x5, x3 // __pa(__idmap_text_start)adr_l x6, __idmap_text_end // __pa(__idmap_text_end)map_memory x0, x1, x3, x6, x7, x3, x4, x10, x11, x12, x13, x14
這段匯編的作用的就是創建恒等映射頁表,在調用map_memory的時候,各個參數的值分別如下:
x0:idmap_pg_dir
x3:idmap_text_start
x6:idmap_text_end
x7:SWAPPER_MM_MMUFLAGS
x4:PTRS_PER_PGD
map_memory
其它的作為輸出。再看下宏map_memory的定義
.macro map_memory, tbl, rtbl, vstart, vend, flags, phys, pgds, istart, iend, tmp, count, svadd \rtbl, \tbl, #PAGE_SIZEmov \sv, \rtblmov \count, #0compute_indices \vstart, \vend, #PGDIR_SHIFT, \pgds, \istart, \iend, \countpopulate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmpmov \tbl, \svmov \sv, \rtbl#if SWAPPER_PGTABLE_LEVELS > 3compute_indices \vstart, \vend, #PUD_SHIFT, #PTRS_PER_PUD, \istart, \iend, \countpopulate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmpmov \tbl, \svmov \sv, \rtbl
#endif#if SWAPPER_PGTABLE_LEVELS > 2compute_indices \vstart, \vend, #SWAPPER_TABLE_SHIFT, #PTRS_PER_PMD, \istart, \iend, \countpopulate_entries \tbl, \rtbl, \istart, \iend, #PMD_TYPE_TABLE, #PAGE_SIZE, \tmpmov \tbl, \sv
#endifcompute_indices \vstart, \vend, #SWAPPER_BLOCK_SHIFT, #PTRS_PER_PTE, \istart, \iend, \countbic \count, \phys, #SWAPPER_BLOCK_SIZE - 1populate_entries \tbl, \count, \istart, \iend, \flags, #SWAPPER_BLOCK_SIZE, \tmp.endm
宏各個參數的含義如下:
tbl:頁表起始地址,頁表基地址
rtbl:下一級頁表起始地址,一般是tbl + PAGE_SIZE
vstart:要映射的虛擬地址的起始地址
vend:要映射的虛擬地址的結束地址
flags:最后一級頁表的屬性
phys:要映射的物理地址
pgds:PGD頁表的個數
首先用compute_indices計算虛擬地址對應的pgd頁表的索引,然后用populate_entries填充相關頁表項。
compute_indices
.macro compute_indices, vstart, vend, shift, ptrs, istart, iend, countlsr \iend, \vend, \shiftmov \istart, \ptrssub \istart, \istart, #1and \iend, \iend, \istart // iend = (vend >> shift) & (ptrs - 1)mov \istart, \ptrsmul \istart, \istart, \countadd \iend, \iend, \istart // iend += (count - 1) * ptrs// our entries span multiple tableslsr \istart, \vstart, \shiftmov \count, \ptrssub \count, \count, #1and \istart, \istart, \countsub \count, \iend, \istart
.endm
各個入參含義:
vstart:起始虛擬地址
vend:結束虛擬地址
shift:各級頁表在虛擬地址中的偏移
ptrs:頁表項的個數
istart:vstart索引值
iend:vend索引值
populate_entries
.macro populate_entries, tbl, rtbl, index, eindex, flags, inc, tmp1
.Lpe\@: phys_to_pte \tmp1, \rtblorr \tmp1, \tmp1, \flags // tmp1 = table entrystr \tmp1, [\tbl, \index, lsl #3]add \rtbl, \rtbl, \inc // rtbl = pa next leveladd \index, \index, #1cmp \index, \eindexb.ls .Lpe\@
.endm
各參數含義:
tbl:頁表基地址
rtbl:下級頁表基地址
index:寫入頁表的起始索引
flags:頁表屬性
三、粗粒度內核鏡像映射
????????一般情況下,物理地址都是低端地址(不會超過256T),所以,恒等映射的時候,其實是將用戶空間的一段地址與物理地址建立了映射,所以,MMU啟動后,idmap_pg_dir會寫入TTBR0;然內核鏈接的地址都是高端地址(0xFFFF_xxxx_xxxx_xxxx),它的頁表基地址需要寫入TTBR1,所以還需要創建一張表來映射整個內核鏡像。 這張表的頁表基地址是init_pg_dir。
/** Map the kernel image (starting with PHYS_OFFSET).
*/adrp x0, init_pg_dirmov_q x5, KIMAGE_VADDR // compile time __va(_text)add x5, x5, x23 // add KASLR displacementmov x4, PTRS_PER_PGDadrp x6, _end // runtime __pa(_end)adrp x3, _text // runtime __pa(_text)sub x6, x6, x3 // _end - _textadd x6, x6, x5 // runtime __va(_end)map_memory x0, x1, x5, x6, x7, x3, x4, x10, x11, x12, x13, x14
這里還是調用map_memory宏,前面已經看過。內核鏡像的映射后如下圖:
這里有幾個點需要注意:
(1).idmap.text本身也是屬于內核鏡像的一部分,所以,這部分,其實是映射了兩次;
(2)兩張頁表idmap_pg_dir和init_pg_dir,也都在內核鏡像當中,從vmlinux.lds.S能夠看到其定義:
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
idmap_pg_end = .;.... = ALIGN(PAGE_SIZE);
init_pg_dir = .;
. += INIT_DIR_SIZE;
init_pg_end = .;
(3) 細心的人可能會注意到上面那張映射的圖好像有問題,明明是4級映射,怎么只有PGD,PUD和PMD三張表,這是因為目前我們項目的內核使能了section map機制,在這種機制下,idmap和init_pg_dir的創建會比實際設置值減少一級,將最后一級頁表取消,所以最后一級的頁表每一項能夠映射2M,整張表的映射大小達到了1G,這樣,一張表就能映射整個內核了。
這個最后一級能夠映射2M,和整張表可以映射1G,對于不懂虛擬地址轉換成物理地址的人,可能是一團漿糊,下面簡單介紹一下,為什么?
第一、64位地址,四級頁表,每個頁表4k,就是一個page,能存儲512個entry,每個entry 占用8字節,這是最常見的情況。
內核將一個進程的內存映射表建立好之后,在該進程被調度運行的時候,會將PGD的物理地址放置到MMU的頁表基地址寄存器中,在X86_64架構下,該寄存器為CR3,ARM64架構下,該寄存器為ttbr0_el1和ttbr1_el1,接下來的尋址過程中,就不需要linux來干預了,MMU會通過PGD-PUD-PMD-PTE-PAGE-OFFSET這個過程,根據虛擬地址,找到其對應的物理地址。
第二、拆分一下,64位的地址,MMU如何計算虛擬地址 得出物理地址;
- ? ?ttbr0_el1寄存器中保存著頁表的基地址,也就是存放PGD頁表起始的物理地址,在內核中是申請了一個page,共4096字節,每個表項占8字節,可以存512個表項;要取哪個表項,需要根據虛擬地址 39-47 的PGD的索引,就可以得到下一級PUD的頁表基地址;
- 39-47bit為PGD的索引,該索引可以找到PUD的頁基地址,2的9次方,共512個表項,每個表項占8字節,共4096字節。
- 30-38bit為PUG的索引,該索引可以找到PMD的頁基地址,2的9次方,共512個表項,每個表項占8字節,共4096字節;
- 21-29bit為PMD的索引,該索引可以找到PTE的頁基地址,2的9次方,共512個表項,每個表項占8字節,共4096字節;
- 12-20bit為PTE的索引,該索引可以找到物理內存頁面的基地址,2的9次方,共512個表項,每個表項占8字節,共4096字節;
- 0-11bit為頁內偏移地址,根據頁基地址+偏移量找到對應的物理內存;
第三、上面使用section map 建立頁表到PMD,可以映射2M,是 如何計算?
假如 21-29bit 的索引值計算出來為A,根據A,找到PTE的頁基地址,PTE有512個表項,每個表項代表一個頁面基地址,每個頁4K大小,可以得出公式:
2M = 512* 4K?
第四、1G 的大小是如何計算出?
固定PGD 的索引值,得出固定的PUG索引值,可以得到PMD 的基地址,PMD的基地址開始保存512個表項,一個PMD的表項可以有2M的物理地址范圍,那么就是
1G = 512* 2M
所以init_pg_dir 的映射表,一整張表就能映射整個內核 是指一個PMD?,即一個PMD的頁,512個表項*2M。
四、fixmap映射
????????為什么需要fixmap? 內核啟動流程,首先由uboot將kernel image和dtb拷貝到內存中,并且將dtb物理地址告訴內核。內核啟動后,在mmu已經啟動(之后soc只能通過虛擬地址訪問內存),paging_init還沒完成調用之前,內核啟動過程需要訪問解析dtb,此時就需要將虛擬地址和物理地址進行映射。這就是fixmap機制的產生原因。
fixmap的區域是在編譯階段就確定好的,在fixmap.h能夠看到代碼定義
enum fixed_addresses {FIX_HOLE,#define FIX_FDT_SIZE (MAX_FDT_SIZE + SZ_2M)FIX_FDT_END,FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,FIX_EARLYCON_MEM_BASE,FIX_TEXT_POKE0,#ifdef CONFIG_UNMAP_KERNEL_AT_EL0FIX_ENTRY_TRAMP_DATA,FIX_ENTRY_TRAMP_TEXT,#define TRAMP_VALIAS (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT))#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */__end_of_permanent_fixed_addresses,#define NR_FIX_BTMAPS (SZ_256K / PAGE_SIZE)#define FIX_BTMAPS_SLOTS 7#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)FIX_BTMAP_END = __end_of_permanent_fixed_addresses,FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,FIX_PTE,FIX_PMD,FIX_PUD,FIX_PGD,__end_of_fixed_addresses};
再看下fixmap的虛擬地址是怎么計算的:
#define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT))
這里面的x就對應上面的枚舉值,所以兩個相差為1的枚舉間隔的內存大小剛好是一頁,即4K。
fixmap的映射大致可以分為三類:固定映射,動態映射,查找映射。
固定映射就是在內核啟動過程中,用于分配給指定物理地址設定的映射關系,持續在系統啟動到關機的整個生命周期;
動態映射就是在內核啟動過程中,或啟動完成后,動態地給模塊分配虛擬地址,模塊退出后釋放該虛擬內存,即過程中動態建立映射關系;
查找映射,用于 paing_init,通過 pgd_set_fixmap 給頁目錄表的全局表項 swapper_pg_dir 做虛擬內存映射,映射到具體的 pte 的表項,然后后續就可以根據這個頁表項所映射的內存空間建立模塊的映射關系。
處理流程
映射框架建立:early_fixmap_init
void __init early_fixmap_init(void){pgd_t *pgdp;pud_t *pudp;pmd_t *pmdp;unsigned long addr = FIXADDR_START; //(1)pgdp = pgd_offset_k(addr); // (2)__p4d_populate(pgdp, __pa_symbol(bm_pud), PUD_TYPE_TABLE); // (3)pudp = fixmap_pud(addr); // (4)__pud_populate(pudp, __pa_symbol(bm_pmd), PMD_TYPE_TABLE); //(5)pmdp = fixmap_pmd(addr); //(6)__pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE); //(7)}
(1)拿到FIXMAP的起始地址;
(2)這里獲取addr地址對應pgd全局頁表中的entry,這個pgd全局頁表就是init_pg_dir全局頁表;
#define pgd_offset_k(address) pgd_offset(&init_mm, (address))
#define INIT_MM_CONTEXT(name) .pgd = init_pg_dir,
struct mm_struct init_mm = {.mm_rb = RB_ROOT,.pgd = swapper_pg_dir,...INIT_MM_CONTEXT(init_mm)
}
完成后如下圖所示
early_ioremap_setup
void __init early_ioremap_setup(void){int i;for (i = 0; i < FIX_BTMAPS_SLOTS; i++)slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);}
這個比較簡單,就是將FIX_BTMAP_BEGIN ~ FIX_BTMAP_END的虛擬內存做初始化,依序將各個slots的起始地址填入slot_virt數組中,目的是為后續使用這部分虛擬內存的時候,只需要調用這個數組對應的數組項即可,這段虛擬內存,既被稱為臨時映射虛擬內存區域。
dtb映射
????????內核初始化需要讀取dtb,dtb在燒寫的時候會燒寫到物理內存中的一塊區域,現在開啟MMU后,需要訪問的話,就必須要先建立起映射。 dtb的虛擬地址空間為4M。由于linux規定dtb的size需要小于2M,理論上用一個2M的虛擬地址空間即可完成映射。但是建立dtb頁表需要使用section map,即其最后一級頁表會直接指向2M block為邊界的物理地址。此時若dtb的位置橫跨物理地址2M邊界,就需要為上下兩個2M block都創建頁表才能訪問完整的image,正是基于這個考慮,此處內核為dtb保留了4M的虛擬地址空間。 當前fixmap的映射情況如上面圖6所示,注意此時bm_pte中還沒有填充,若要訪問fixmap的虛擬地址,首先要填充bm_pte。
dtb映射的函數如下:
void *__init fixmap_remap_fdt(phys_addr_t dt_phys, int *size, pgprot_t prot) //(1){const u64 dt_virt_base = __fix_to_virt(FIX_FDT); //(2)int offset;void *dt_virt;offset = dt_phys % SWAPPER_BLOCK_SIZE;dt_virt = (void *)dt_virt_base + offset;create_mapping_noalloc(round_down(dt_phys, SWAPPER_BLOCK_SIZE),dt_virt_base, SWAPPER_BLOCK_SIZE, prot); //(3)return dt_virt;}
(1)入參dt_phys就是dtb的物理起始地址;
(2)獲取fixmap中FIX_FDT的虛擬起始地址;
(3)調用建立映射函數create_mapping_noalloc。
create_mapping_noalloc函數主要調用了__create_pgd_mapping
static void __create_pgd_mapping(pgd_t *pgdir, phys_addr_t phys,unsigned long virt, phys_addr_t size,pgprot_t prot,phys_addr_t (*pgtable_alloc)(int),int flags){unsigned long addr, end, next;pgd_t *pgdp = pgd_offset_pgd(pgdir, virt);phys &= PAGE_MASK;addr = virt & PAGE_MASK;end = PAGE_ALIGN(virt + size);do {next = pgd_addr_end(addr, end); // (1)alloc_init_pud(pgdp, addr, next, phys, prot, pgtable_alloc,flags); //(2)phys += next - addr;} while (pgdp++, addr = next, addr != end);}
由于fixmap大小只有4M,所以共享同一個pgd項,這里獲取的pgdp也就是圖6中的pgdp。 循環就是在根據每一個pgd項創建pud頁表
static void alloc_init_pud(pgd_t *pgdp, unsigned long addr, unsigned long end,phys_addr_t phys, pgprot_t prot,phys_addr_t (*pgtable_alloc)(int),int flags){unsigned long next;pud_t *pudp;p4d_t *p4dp = p4d_offset(pgdp, addr);p4d_t p4d = READ_ONCE(*p4dp);pudp = pud_set_fixmap_offset(p4dp, addr); //(1)do {pud_t old_pud = READ_ONCE(*pudp);next = pud_addr_end(addr, end);if (use_1G_block(addr, next, phys) &&(flags & NO_BLOCK_MAPPINGS) == 0) {pud_set_huge(pudp, phys, prot);BUG_ON(!pgattr_change_is_safe(pud_val(old_pud),READ_ONCE(pud_val(*pudp))));} else {alloc_init_cont_pmd(pudp, addr, next, phys, prot,pgtable_alloc, flags);BUG_ON(pud_val(old_pud) != 0 &&pud_val(old_pud) != READ_ONCE(pud_val(*pudp)));}phys += next - addr;} while (pudp++, addr = next, addr != end);pud_clear_fixmap();}
這里有個關鍵點需要注意,就是對pudp的訪問,首先看(1),pudp獲得是什么
void __set_fixmap(enum fixed_addresses idx,phys_addr_t phys, pgprot_t flags){unsigned long addr = __fix_to_virt(idx); //(2)pte_t *ptep;ptep = fixmap_pte(addr); //(3)if (pgprot_val(flags)) {set_pte(ptep, pfn_pte(phys >> PAGE_SHIFT, flags)); //(4)} else {pte_clear(&init_mm, addr, ptep);flush_tlb_kernel_range(addr, addr+PAGE_SIZE);}}#define __set_fixmap_offset(idx, phys, flags) \({ \unsigned long ________addr; \__set_fixmap(idx, phys, flags); \ ________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1)); \________addr; //(5) \})#define set_fixmap_offset(idx, phys) \__set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL) #define pud_offset_phys(dir, addr) (p4d_page_paddr(READ_ONCE(*(dir))) + pud_index(addr) * sizeof(pud_t)) //(1)#define pud_set_fixmap(addr) ((pud_t *)set_fixmap_offset(FIX_PUD, addr))#define pud_set_fixmap_offset(p4d, addr) pud_set_fixmap(pud_offset_phys(p4d, addr))
(1)計算出FIX_FDT對應的pud項地址,也就是圖7中的entryU的物理地址
(2)獲得FIX_PUD的虛擬地址
(3)計算FIX_PUD的虛擬地址對應的pte項
(4)將entryU的物理地址寫入對應的pte項
(5)返回一個地址,這個地址是fixmap中FIX_PUD中的一個地址,在FIX_PUD中的偏移對應的是圖7中entryU在bm_pud中的偏移
返回的地址就是圖中的pudp,我們知道這是一個虛擬地址,如果頁表沒有相應的映射,訪問是會失敗的。在early_fixmap_init函數中,只是建立了一個映射的框架,并沒有填充pte,所以直接訪問pudp會失敗。剛剛的過程就是在為其建立映射。
現在再來看訪問地址pudp,mmu是怎么映射的,首先FIXMAP共享同一個pgd項和pud項,再看FIXMAP_START和FIX_PUD也屬于同一個pmd項(一個pmd項對應2M空間,FIXMAP_START和FIX_PUD在同一個2M范圍內),而剛剛又為pudp填充了對應的pte項,所以,這個地址是能夠映射成功的,并且是將FIX_PUD與bm_pud建立起了映射。
同樣對FIX_PMD建立映射后如圖8所示
static void init_pmd(pud_t *pudp, unsigned long addr, unsigned long end,phys_addr_t phys, pgprot_t prot,phys_addr_t (*pgtable_alloc)(int), int flags){unsigned long next;pmd_t *pmdp;pmdp = pmd_set_fixmap_offset(pudp, addr); //(1)do {pmd_t old_pmd = READ_ONCE(*pmdp);next = pmd_addr_end(addr, end);if (((addr | next | phys) & ~SECTION_MASK) == 0 &&(flags & NO_BLOCK_MAPPINGS) == 0) {pmd_set_huge(pmdp, phys, prot); //(2)} else {alloc_init_cont_pte(pmdp, addr, next, phys, prot,pgtable_alloc, flags);}phys += next - addr;} while (pmdp++, addr = next, addr != end);pmd_clear_fixmap(); //(3)}
上述代碼完成(1)后,就如圖8所示;
(2)dtb的映射使能了2M的block映射,也就是最后一級為pmd,且其相應的entry會設置為2M對齊的物理地址與prot組合的值,所以會進入到這個分支; 完成后如圖9所示,pte中設置的實際值上面的圖中沒有說明清楚,這里完善了下
(3)清FIX_PMD對應的pte項,因為最后一級已經設置在了pmd,所以pte項也就不再需要了,同樣FIX_PUD對應的pte項也不需要了;完成后如圖10所示
?到這里就完成了對dtb的映射,就可以正常讀取dtb的信息了,dtb中就包含了內存信息,地址范圍,大小等等。然后就會初始化物理頁面分配器,即初始化伙伴系統;有了物理頁面分配器,內核主頁表就可以建立動態映射頁表。
五、內核主頁表建立
內核主表的建立包含了細粒度內核鏡像映射,線性映射。 主要在函數paging_init中完成
void __init paging_init(void){pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir)); //(1)map_kernel(pgdp); //內核細粒度映射map_mem(pgdp); //線性映射pgd_clear_fixmap();cpu_replace_ttbr1(lm_alias(swapper_pg_dir));init_mm.pgd = swapper_pg_dir;memblock_free(__pa_symbol(init_pg_dir),__pa_symbol(init_pg_end) - __pa_symbol(init_pg_dir));memblock_allow_resize();}
細粒度內核鏡像映射
細粒度映射就是為內核的每個段建立頁表
static void __init map_kernel(pgd_t *pgdp){static struct vm_struct vmlinux_text, vmlinux_rodata, vmlinux_inittext,vmlinux_initdata, vmlinux_data;pgprot_t text_prot = rodata_enabled ? PAGE_KERNEL_ROX : PAGE_KERNEL_EXEC;if (arm64_early_this_cpu_has_bti())text_prot = __pgprot_modify(text_prot, PTE_GP, PTE_GP);map_kernel_segment(pgdp, _text, _etext, text_prot, &vmlinux_text, 0,VM_NO_GUARD);map_kernel_segment(pgdp, __start_rodata, __inittext_begin, PAGE_KERNEL,&vmlinux_rodata, NO_CONT_MAPPINGS, VM_NO_GUARD);map_kernel_segment(pgdp, __inittext_begin, __inittext_end, text_prot,&vmlinux_inittext, 0, VM_NO_GUARD);map_kernel_segment(pgdp, __initdata_begin, __initdata_end, PAGE_KERNEL,&vmlinux_initdata, 0, VM_NO_GUARD);map_kernel_segment(pgdp, _data, _end, PAGE_KERNEL, &vmlinux_data, 0, 0);if (!READ_ONCE(pgd_val(*pgd_offset_pgd(pgdp, FIXADDR_START)))) {set_pgd(pgd_offset_pgd(pgdp, FIXADDR_START),READ_ONCE(*pgd_offset_k(FIXADDR_START)));} else if (CONFIG_PGTABLE_LEVELS > 3) {pgd_t *bm_pgdp;p4d_t *bm_p4dp;pud_t *bm_pudp;BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));bm_pgdp = pgd_offset_pgd(pgdp, FIXADDR_START);bm_p4dp = p4d_offset(bm_pgdp, FIXADDR_START);bm_pudp = pud_set_fixmap_offset(bm_p4dp, FIXADDR_START);pud_populate(&init_mm, bm_pudp, lm_alias(bm_pmd));pud_clear_fixmap();} else {BUG();}kasan_copy_shadow(pgdp);}
看函數map_kernel_segment
static void __init map_kernel_segment(pgd_t *pgdp, void *va_start, void *va_end,pgprot_t prot, struct vm_struct *vma,int flags, unsigned long vm_flags){phys_addr_t pa_start = __pa_symbol(va_start);unsigned long size = va_end - va_start;__create_pgd_mapping(pgdp, pa_start, (unsigned long)va_start, size, prot,early_pgtable_alloc, flags);if (!(vm_flags & VM_NO_GUARD))size += PAGE_SIZE;vma->addr = va_start;vma->phys_addr = pa_start;vma->size = size;vma->flags = VM_MAP | vm_flags;vma->caller = __builtin_return_address(0);vm_area_add_early(vma);}
頁表建立過程不再細說,這里在建立之前頁表如上面圖10所示 paging_init的第一步是為swapper_pg_dir的pgd表建立映射,也就是FIX_PGDswapper_pg_dir的pgd表建立映射,與swapper_pg_dir是完成內核映射的頁表基地址。 注意此時pgdp已經指向了swapper_pg_dir,所以后面的pud頁表、pmd頁和pte表都需要動態創建。
線性映射
內核鏡像映射僅僅是對內核鏡像空間做了映射,為了方便內核自由訪問所有物理內存,內核還做了一個線性映射。
static void __init map_mem(pgd_t *pgdp){phys_addr_t kernel_start = __pa_symbol(_text);phys_addr_t kernel_end = __pa_symbol(__init_begin);phys_addr_t start, end;int flags = 0;u64 i;if (rodata_full || debug_pagealloc_enabled())flags = NO_BLOCK_MAPPINGS | NO_CONT_MAPPINGS;memblock_mark_nomap(kernel_start, kernel_end - kernel_start);/* map all the memory banks */for_each_mem_range(i, &start, &end) {if (start >= end)break;__map_memblock(pgdp, start, end, PAGE_KERNEL_TAGGED, flags);}__map_memblock(pgdp, kernel_start, kernel_end,PAGE_KERNEL, NO_CONT_MAPPINGS);memblock_clear_nomap(kernel_start, kernel_end - kernel_start);}
核心就是__map_memblock
static void __init __map_memblock(pgd_t *pgdp, phys_addr_t start,phys_addr_t end, pgprot_t prot, int flags){__create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start,prot, early_pgtable_alloc, flags);}
具體的建立過程和內核鏡像建立過程類似,完成映射后,基本如下圖所示
完成映射后,將init_mm.pgd設置成swapper_pg_dir,同時將TTBR1寄存器改寫成swapper_pg_dir,自此,后面內核的頁表基地址就是swapper_pg_dir了。
六、用戶空間頁表創建
每個進程都有自己的用戶空間,所以每個進程都有自己獨立的頁表。用戶進程創建頁表發生在三個時候:創建進程時,缺頁異常時,進程切換時。
進程創建時
第一步,分配pgd物理頁面 調用順序copy_process->copy_mm->dup_mm->mm_init->mm_alloc_pgd
static inline int mm_alloc_pgd(struct mm_struct *mm){mm->pgd = pgd_alloc(mm);if (unlikely(!mm->pgd))return -ENOMEM;return 0;}pgd_t *pgd_alloc(struct mm_struct *mm){gfp_t gfp = GFP_PGTABLE_USER;if (PGD_SIZE == PAGE_SIZE)return (pgd_t *)__get_free_page(gfp);elsereturn kmem_cache_alloc(pgd_cache, gfp);}
第二步,拷貝父進程頁表,依次拷貝vma 調用路徑 copy_process->copy_mm->dup_mm->dup_mmap->copy_page_range
intcopy_page_range(struct vm_area_struct *dst_vma, struct vm_area_struct *src_vma){pgd_t *src_pgd, *dst_pgd;unsigned long next;unsigned long addr = src_vma->vm_start;unsigned long end = src_vma->vm_end;struct mm_struct *dst_mm = dst_vma->vm_mm;struct mm_struct *src_mm = src_vma->vm_mm;struct mmu_notifier_range range;int ret;ret = 0;dst_pgd = pgd_offset(dst_mm, addr);src_pgd = pgd_offset(src_mm, addr);do {next = pgd_addr_end(addr, end);if (pgd_none_or_clear_bad(src_pgd))continue;if (unlikely(copy_p4d_range(dst_vma, src_vma, dst_pgd, src_pgd,addr, next))) {ret = -ENOMEM;break;}} while (dst_pgd++, src_pgd++, addr = next, addr != end);return ret;}
copy_p4d_range里依次拷貝pud,pmd和pte。
缺頁異常
頁表拷貝完成后,等進程發生寫時拷貝,觸發缺頁異常,在異常服務處理里真正分配物理頁面。
static vm_fault_t wp_page_copy(struct vm_fault *vmf){struct vm_area_struct *vma = vmf->vma;struct mm_struct *mm = vma->vm_mm;struct page *old_page = vmf->page;struct page *new_page = NULL;pte_t entry;int page_copied = 0;struct mmu_notifier_range range;if (is_zero_pfn(pte_pfn(vmf->orig_pte))) {new_page = alloc_zeroed_user_highpage_movable(vma,vmf->address);if (!new_page)goto oom;} else {new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,vmf->address); //分配一個新的物理頁面,并把old_page復制到new_pageif (!new_page)goto oom;if (!cow_user_page(new_page, old_page, vmf)) {put_page(new_page);if (old_page)put_page(old_page);return 0;}}if (mem_cgroup_charge(new_page, mm, GFP_KERNEL))goto oom_free_new;cgroup_throttle_swaprate(new_page, GFP_KERNEL);__SetPageUptodate(new_page);mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma, mm,vmf->address & PAGE_MASK,(vmf->address & PAGE_MASK) + PAGE_SIZE);mmu_notifier_invalidate_range_start(&range);vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {if (old_page) {if (!PageAnon(old_page)) {dec_mm_counter_fast(mm,mm_counter_file(old_page));inc_mm_counter_fast(mm, MM_ANONPAGES);}} else {inc_mm_counter_fast(mm, MM_ANONPAGES);}flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));entry = mk_pte(new_page, vma->vm_page_prot);entry = pte_sw_mkyoung(entry);entry = maybe_mkwrite(pte_mkdirty(entry), vma);ptep_clear_flush_notify(vma, vmf->address, vmf->pte);page_add_new_anon_rmap(new_page, vma, vmf->address, false);lru_cache_add_inactive_or_unevictable(new_page, vma);set_pte_at_notify(mm, vmf->address, vmf->pte, entry);update_mmu_cache(vma, vmf->address, vmf->pte);if (old_page) {page_remove_rmap(old_page, false);}new_page = old_page;page_copied = 1;} else {update_mmu_tlb(vma, vmf->address, vmf->pte);}if (new_page)put_page(new_page);pte_unmap_unlock(vmf->pte, vmf->ptl);mmu_notifier_invalidate_range_only_end(&range);if (old_page) {if (page_copied && (vma->vm_flags & VM_LOCKED)) {lock_page(old_page); /* LRU manipulation */if (PageMlocked(old_page))munlock_vma_page(old_page);unlock_page(old_page);}put_page(old_page);}return page_copied ? VM_FAULT_WRITE : 0;oom_free_new:put_page(new_page);oom:if (old_page)put_page(old_page);return VM_FAULT_OOM;}
進程切換
主要完成兩件事
(1)設置進程的
ASID到TTBR1_EL1
(2)設置mm->pgd到TTBR0_EL1完成地址空間切換
調用路徑:context_switch->switch_mm_irqs_off->switch_mm->__switch_mm->check_and_switch_context->cpu_switch_mm->cpu_do_switch_mm
void cpu_do_switch_mm(phys_addr_t pgd_phys, struct mm_struct *mm){unsigned long ttbr1 = read_sysreg(ttbr1_el1);unsigned long asid = ASID(mm);unsigned long ttbr0 = phys_to_ttbr(pgd_phys);/* Skip CNP for the reserved ASID */if (system_supports_cnp() && asid)ttbr0 |= TTBR_CNP_BIT;/* SW PAN needs a copy of the ASID in TTBR0 for entry */if (IS_ENABLED(CONFIG_ARM64_SW_TTBR0_PAN))ttbr0 |= FIELD_PREP(TTBR_ASID_MASK, asid);/* Set ASID in TTBR1 since TCR.A1 is set */ttbr1 &= ~TTBR_ASID_MASK;ttbr1 |= FIELD_PREP(TTBR_ASID_MASK, asid);write_sysreg(ttbr1, ttbr1_el1); //(1)isb();write_sysreg(ttbr0, ttbr0_el1); //(2)isb();post_ttbr_update_workaround();}
(1) ASID寫到TTBR1_EL1;
(2) 新進程頁表基地址pgd,填入ttbr0_el1