? ? ? ? 在 Linux 使用虛擬地址前,需要先配置頁表,這就是 setup_vm() 的作用。然而,Linux 的頁表配置,并不是一次過完成的,分了兩個階段,如下:
? ? ? ? 在 setup_vm() 中,主要初始化了:
1.?struct kernel_mapping kernel_map __ro_after_init;
2.?pgd_t early_pg_dir[PTRS_PER_PGD] __initdata __aligned(PAGE_SIZE);
3.?pgd_t trampoline_pg_dir[PTRS_PER_PGD] __page_aligned_bss;
? ? ? ? 其中,early 指的是 early boot 階段,即 完整的虛擬頁表還沒有配置完成,即 swapper_pg_dir 還未可用。
? ? ? ? 在 linux/arch/riscv/mm/init.c 中,查看 setup_vm() 代碼,可以直觀地看出 kernel_map?各成員變量的賦值,如下:
kernel_map.virt_addr = KERNEL_LINK_ADDR;
kernel_map.phys_addr = (uintptr_t)(&_start);
kernel_map.size = (uintptr_t)(&_end) - kernel_map.phys_addr;
kernel_map.va_pa_offset = PAGE_OFFSET - kernel_map.phys_addr;
kernel_map.va_kernel_pa_offset = kernel_map.virt_addr - kernel_map.phys_addr;
????????通過 GDB,可以查看賦值后的結果,如下:
? ? ? ? 對應的結構定義如下:
1. virt_addr 是 kernel 鏈接地址,也是 kernel 初始地址。
2. phys_addr 是 kernel 的物理內存地址。
3. size 是 kernel 的大小。
? ? ? ? 接下來,就在?early_pg_dir 里配置?fixmap 。
? ? ? ? 也就是,從create_pgd_mapping() 的聲明可以看出,
void __init create_pgd_mapping(pgd_t *pgdp,uintptr_t va, phys_addr_t pa,phys_addr_t sz, pgprot_t prot);
????????該語句的作用是,在根頁表 early_pg_dir 里,
1. 配置一項 PTE;
2. 用于映射?FIXADDR_START (VA)到?fixmap_pgd_next(PA);
3. 其大小為?PGDIR_SIZE(1GB);
4. PTE屬性為?PAGE_TABLE。
? ? ? ? 其定義如下,是用于創建首級頁表(Page Global Directory, pgd):
void __init create_pgd_mapping(pgd_t *pgdp,uintptr_t va, phys_addr_t pa,phys_addr_t sz, pgprot_t prot)
{// 訪問地址:// 如果未啟動 MMU,則是物理地址// 如果啟動MMU,則是虛擬地址// pdg_t *pgdp 為訪問地址// 在 setup_vm() 中 pgdp, pa 為物理地址,通過 PC 相對尋址,給入// 下一級頁表的訪問地址pgd_next_t *nextp;// 下一級物理地址phys_addr_t next_phys;// 當前頁表索引號,即根頁表 VPN,// 給定 Sv39,三級頁表,VA = VPN[2](9) | VPN[1](9) | VPN[0](9) | OFFSET(12)// pgd_idx = VA.VPN[2]uintptr_t pgd_idx = pgd_index(va);// 如果映射大小為 1GB,即 PGDIR_SIZE(30 bits),那么只需在根頁表配置一項PTE即可if (sz == PGDIR_SIZE) {// 如果對應PTE未配置,則配置新值,即 PPN[2] == 0if (pgd_val(pgdp[pgd_idx]) == 0)// 即設置 PPN[2] = pa.PPN[2]pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);return;}// 否則,需要在根頁表與下一級頁表都要配置PTE。// 也就是配置大小 小于 1GBif (pgd_val(pgdp[pgd_idx]) == 0) {// 如果根頁表對應的PTE未配置,則配置新值// 申請下一級頁表,pmd,并返回其物理地址。// 在 setup_vm 中,是返回 靜態的 early_pmd 的物理地址。next_phys = alloc_pgd_next(va);// 配置根頁表對應的 PTE,其 PPN = (next_pyhs >> 12)pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);// 獲取下一級頁表的虛擬地址// 在 setup_vm 中,物理地址等于虛擬地址// 即 early_pmd 的物理地址nextp = get_pgd_next_virt(next_phys);// 零化memset(nextp, 0, PAGE_SIZE);} else {// 如果根頁表對應的PTE已存在,則直接獲取下一級頁表的物理地址next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx]));// 獲取下一級頁表的訪問地址nextp = get_pgd_next_virt(next_phys);}// 構建下一級頁表,如 pmd// 在 setup_vm 中,為 early_pmdcreate_pgd_next_mapping(nextp, va, pa, sz, prot);
}
? ? ? ? 對應的?create_pmd_mapping()? 也與?create_pgd_mapping() 類似,是用于創建倒數中級頁表(Page Medium Directory, pmd),定義如下:
static void __init create_pmd_mapping(pmd_t *pmdp,uintptr_t va, phys_addr_t pa,phys_addr_t sz, pgprot_t prot)
{pte_t *ptep;phys_addr_t pte_phys;uintptr_t pmd_idx = pmd_index(va);if (sz == PMD_SIZE) {if (pmd_none(pmdp[pmd_idx]))pmdp[pmd_idx] = pfn_pmd(PFN_DOWN(pa), prot);return;}if (pmd_none(pmdp[pmd_idx])) {pte_phys = pt_ops.alloc_pte(va);pmdp[pmd_idx] = pfn_pmd(PFN_DOWN(pte_phys), PAGE_TABLE);ptep = pt_ops.get_pte_virt(pte_phys);memset(ptep, 0, PAGE_SIZE);} else {pte_phys = PFN_PHYS(_pmd_pfn(pmdp[pmd_idx]));ptep = pt_ops.get_pte_virt(pte_phys);}create_pte_mapping(ptep, va, pa, sz, prot);
}
? ? ? ??create_pte_mapping() 是用于末級頁表(Page Table Entry,pte),與上述定義類似,如下:
static void __init create_pte_mapping(pte_t *ptep,uintptr_t va, phys_addr_t pa,phys_addr_t sz, pgprot_t prot)
{uintptr_t pte_idx = pte_index(va);BUG_ON(sz != PAGE_SIZE);if (pte_none(ptep[pte_idx]))ptep[pte_idx] = pfn_pte(PFN_DOWN(pa), prot);
}
? ? ? ? 如果,多于三級頁表,還有 PUD (Page Upper Directory), P4D(Page 4th Directory)。
即, 如果五級頁表的話,有 PGD --> P4D --> PUD --> PMD --> PTE 。
? ? ? ? 從上面定義來看,構建頁表項時,其大小只能是每級頁表中定義的一個,如 PGDIR_SIZE, PMD_SIZE, PAGE_SIZE。
? ? ? ? 梳理清楚?create_pgd_mapping(),?create_pmd_mapping(),?create_pte_mapping(),三個函數后,回到 setup_vm(),看看它是如何配置?early_pg_dir,?fixmap_pmd,?trampoline_pg_dir,trampoline_pmd的。
? ? ? ? 首先是在 early_pg_dir 根頁表中,配置 fixmap 的映射,其大小為 1GB,屬性為 頁表。也就是說,fixmap_pgd_next 為?fixmap_pmd,是一個中級頁表,其整個描述大小為 1GB = 512 × 2MB。
/* Setup early PGD for fixmap */create_pgd_mapping(early_pg_dir, FIXADDR_START,(uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE);
? ? ? ? 然后是 fixmap_pmd 中級頁表中,配置?fixmap_pte 末級頁表,其大小為 2MB,屬性為 頁表。如下:
/* Setup fixmap PMD */create_pmd_mapping(fixmap_pmd, FIXADDR_START,(uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE);
? ? ? ? 同理,設置了?trampoline_pg_dir 與?trampoline_pmd,如下:
/* Setup trampoline PGD and PMD */
create_pgd_mapping(trampoline_pg_dir, kernel_map.virt_addr,(uintptr_t)trampoline_pmd, PGDIR_SIZE, PAGE_TABLE);
create_pmd_mapping(trampoline_pmd, kernel_map.virt_addr,kernel_map.phys_addr, PMD_SIZE, PAGE_KERNEL_EXEC)
? ? ? ? 在配置trampoline_pmd頁表時,映射了 內核虛擬地址 到 內核的物理地址,大小為 2MB,屬性為內核執行頁。也就是說,可以通過虛擬地址 0xffffffff80000000 去訪問 物理地址 0x200200000,范圍是 2MB。這里覆蓋了 內核的前2MB內容。而內核大小大于2MB,因此,trampoline_pg_dir 的頁表配置并不能完全覆蓋整個內核所有的代碼段、數據段 及 設備樹。所以,只能作為臨時頁表來使用。即,其中 a2?為 early_pg_dir:
?????????early_pg_dir 頁表配置覆蓋了整個 linux kernel 的代碼段、數據段 及 設備樹,如下:
/** Setup early PGD covering entire kernel which will allow* us to reach paging_init(). We map all memory banks later* in setup_vm_final() below.*/create_kernel_page_table(early_pg_dir, true);/* Setup early mapping for FDT early scan */create_fdt_early_page_table(__fix_to_virt(FIX_FDT), dtb_pa);
? ? ? ? 對應的定義如下:
/** Setup a 4MB mapping that encompasses the device tree: for 64-bit kernel,* this means 2 PMD entries whereas for 32-bit kernel, this is only 1 PGDIR* entry.*/
static void __init create_fdt_early_page_table(uintptr_t fix_fdt_va,uintptr_t dtb_pa)
{
#ifndef CONFIG_BUILTIN_DTBuintptr_t pa = dtb_pa & ~(PMD_SIZE - 1);/* Make sure the fdt fixmap address is always aligned on PMD size */BUILD_BUG_ON(FIX_FDT % (PMD_SIZE / PAGE_SIZE));/* In 32-bit only, the fdt lies in its own PGD */if (!IS_ENABLED(CONFIG_64BIT)) {create_pgd_mapping(early_pg_dir, fix_fdt_va,pa, MAX_FDT_SIZE, PAGE_KERNEL);} else {create_pmd_mapping(fixmap_pmd, fix_fdt_va,pa, PMD_SIZE, PAGE_KERNEL);create_pmd_mapping(fixmap_pmd, fix_fdt_va + PMD_SIZE,pa + PMD_SIZE, PMD_SIZE, PAGE_KERNEL);}dtb_early_va = (void *)fix_fdt_va + (dtb_pa & (PMD_SIZE - 1));
#else/** For 64-bit kernel, __va can't be used since it would return a linear* mapping address whereas dtb_early_va will be used before* setup_vm_final installs the linear mapping. For 32-bit kernel, as the* kernel is mapped in the linear mapping, that makes no difference.*/dtb_early_va = kernel_mapping_pa_to_va(XIP_FIXUP(dtb_pa));
#endifdtb_early_pa = dtb_pa;
}
static void __init create_kernel_page_table(pgd_t *pgdir, bool early)
{uintptr_t va, end_va;end_va = kernel_map.virt_addr + kernel_map.size;for (va = kernel_map.virt_addr; va < end_va; va += PMD_SIZE)create_pgd_mapping(pgdir, va,kernel_map.phys_addr + (va - kernel_map.virt_addr),PMD_SIZE,early ?PAGE_KERNEL_EXEC : pgprot_from_va(va));
}
? ? ? ? 至此,setup_vm()的工作大致完成了,也就是將 kernel_map,?trampoline_pg_dir 及?early_pg_dir 配置完成,使得后續的 relocate 可以正常執行,切入虛擬地址訪問機制。