Artikel sebelumnya menganalisis proses permulaan pemasangan RISC-V Linux, yang menyebut bahawa pengalihan semula lokasi memerlukan menghidupkan MMU Hari ini kami menganalisis penciptaan jadual halaman RISC-V Linux.
Nota: Artikel ini berdasarkan kernel linux5.10.111
sv32
、sv39
、sv48
RISC-V Linux masing-masing yang menyokong
Alamat fizikal diwakili oleh 56 bit, 12 bit rendah mewakili offset halaman, dan bit tinggi ialah halaman fizikal PPN[0], PPN[1] dan PPN[2]
PTE menyimpan halaman fizikal PPN[ 0], PPN[1] dan PPN[2] sepadan dengan PPN dalam alamat fizikal; 10 bit PTE yang lebih rendah mewakili kebenaran akses alamat fizikal Apabila RWX semuanya 0, ini bermakna alamat disimpan dalam PTE ialah alamat fizikal jadual halaman peringkat seterusnya , jika tidak, ini bermakna jadual halaman semasa ialah jadual halaman peringkat terakhir.
Lihat format jadual halaman sv39 menggunakan jadual halaman tiga peringkat,PGD
, PMD
dan PTE
, setiap jadual halaman peringkat diwakili oleh 9 bit, iaitu, setiap jadual halaman peringkat mempunyai 512 entri jadual halaman. PGD
、PMD
和PTE
,每一个级页表使用9bit表示,即每一级页表都有512个页表项。
在代码中,创建一个有512个元素的数组即代表一个页表。一个PTE有512个页表项,每一个页表项占用8字节,512*8=4096字节,所以一个PTE代表4K。一个PMD也是512个页表项,每一项可代表一个PTE,512 *4 K=2M,所以一个PMD就代表2M。以此类推,一个PGD代表512 * 2M=1G。
重要结论:PGD代表1G
、PMD代表2M
、PTE代表4K
Dalam kod, buat tatasusunan dengan 512 elemen untuk mewakili jadual halaman. PTE mempunyai 512 entri jadual halaman, setiap entri jadual halaman menduduki 8 bait, 512*8=4096 bait, jadi PTE mewakili 4K. PMD juga mempunyai 512 halaman entri jadual, setiap entri boleh mewakili PTE, 512 *4 K=2M, jadi PMD mewakili 2M. Dengan analogi, satu PGD mewakili 512 * 2M = 1G.
Kesimpulan penting: PGD bermaksud 1G
, PMD mewakili 2M
, PTE mewakili 4K
. Saiz halaman lalai sv39 ialah 4K.
Rajah skematik proses menukar alamat maya jadual halaman peringkat ketiga kepada alamat fizikal:
sv39 Proses menukar alamat maya jadual halaman peringkat ketiga kepada alamat fizikal: MMU mendapatkan alamat fizikal PGD melalui daftar satp, dan menggabungkannya dengan indeks PGD (iaitu V PN [2]) Cari PMD selepas menemui PMD, gabungkannya dengan indeks PMD (iaitu V PN[1]) ke cari PTE, dan kemudian gabungkannya dengan indeks PTE (iaitu V PN[0]) untuk mendapatkan nilai VA dalam indeks PTE, dengan itu mendapatkan alamat fizikal. 🎜🎜Akhir sekali, keluarkan PPN[2], PPN[1] dan PPN[0] daripada PTE, dan kemudian tambahkannya pada offset 12-bit rendah alamat maya untuk mendapatkan alamat fizikal terakhir. 🎜Sebelum memulakan MMU, anda perlu mencipta kernel, dtb, trampolin dan jadual halaman lain. Supaya selepas MMU dihidupkan dan sebelum modul pengurusan memori dijalankan, kernel boleh dimulakan secara normal dan dtb boleh dihuraikan secara normal. Bahagian jadual halaman ini ialah jadual halaman sementara dan jadual halaman akhir dibuat dalam setup_vm_final().
Jujukan pembuatan jadual halaman sementara:
Mula-mula buat PGD dan PMD awal untuk fixmap Pada masa ini, PGD menggunakan early_pg_dir
. Kemudian buat jadual halaman kedua untuk memori 2M pertama bermula dari kernel Pada masa ini, PGD menggunakan trampolin_pg_dir
, jadual halaman yang dibuat untuk 2M ini juga dipanggil superpage
. Kemudian, buat jadual halaman kedua untuk keseluruhan kernel Pada masa ini, PGD menggunakan early_pg_dir
. Akhir sekali, tempah saiz 4M untuk dtb untuk mencipta jadual halaman kedua. early_pg_dir
。然后对从kernel开始的前2M内存建立二级页表,此时PGD使用trampoline_pg_dir
,为这2M建立的页表也叫作superpage
。再然后,对整个kernel创建二级页表,此时PGD使用early_pg_dir
。最后为dtb预留4M大小创建二级页表。
void __init create_pgd_mapping(pgd_t *pgdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
pgdp
:PGD页表
va
:虚拟地址
pa
static void __init create_pmd_mapping(pmd_t *pmdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
pgdp
: Jadual halaman PGD🎜🎜va
: alamat maya 🎜🎜pa
: alamat fizikal 🎜sz
:映射大小,PGDIR_SIZE或PMD_SIZE或PTE_SIZE
prot
:PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址
static void __init create_pmd_mapping(pmd_t *pmdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
pmdp
:PMD页表
va
:虚拟地址
pa
:物理地址
sz
:映射大小,PMD_SIZE或PAGE_SIZE
prot
:权限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址
static void __init create_pte_mapping(pte_t *ptep, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
ptep
:PTE页表
va
:虚拟地址
pa
:物理地址
sz
:映射大小,PAGE_SIZE
prot
:权限,PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址
例如,将虚拟地址PAGE_OFFSET映射到物理地址pa,映射大小为4K,创建三级页表PGD、PMD和PTE:
create_pgd_mapping(early_pg_dir,PAGE_OFFSET, (uintptr_t)early_pmd,PGDIR_SIZE,PAGE_TABLE); create_pmd_mapping(early_pmd,PAGE_OFFSET, (uintptr_t)early_pte,PGDIR_SIZE,PAGE_TABLE); create_pte_mapping(early_pte,PAGE_OFFSET, (uintptr_t)pa,PAGE_SIZE,PAGE_KERNEL_EXEC);
这样创建后,MMU就会根据PAGE_OFFSET在PGD中找到PMD,然后再PMD中找到PTE,最后取出物理地址。
RISC-V Linux启动,经历了两次页表创建过程,第一次使用C函数setup_vm()
创建临时页表,第二次使用C函数setup_vm_final()
创建最终页表。
具体细节参考代码中的注释,下面的代码省略了一些不重要的部分。
asmlinkage void __init setup_vm(uintptr_t dtb_pa) { uintptr_t va, pa, end_va; uintptr_t load_pa = (uintptr_t)(&_start); uintptr_t load_sz = (uintptr_t)(&_end) - load_pa; uintptr_t map_size; //load_pa就是kernel加载的其实物理地址 //load_sz就是kernel的实际大小 //page_offset就是kernel的起始物理地址对应的虚拟地址,va_pa_offset是他们的偏移量 va_pa_offset = PAGE_OFFSET - load_pa; //计算得到kernel起始物理地址的物理页,PFN_DOWN是将物理地址右移12位,因为sv39的物理地址的低12位是pa_offset,所以右移12位,得到pfn pfn_base = PFN_DOWN(load_pa); map_size = PMD_SIZE;//PMD_SIZE为2M,在当前,map_size只能为PGDIR_SIZE或PMD_SIZE。这时kernel默认不允许建立PTE。 //检查PAGE_OFFSET是否1G对齐,以及kernel入口地址是否2M对齐 BUG_ON((PAGE_OFFSET % PGDIR_SIZE) != 0); BUG_ON((load_pa % map_size) != 0); //allc_pte_early里面是BUG(),对于临时页表,kernel不允许我们建立PTE pt_ops.alloc_pte = alloc_pte_early; pt_ops.get_pte_virt = get_pte_virt_early; #ifndef __PAGETABLE_PMD_FOLDED pt_ops.alloc_pmd = alloc_pmd_early; pt_ops.get_pmd_virt = get_pmd_virt_early; #endif /* 设置 early PGD for fixmap */ create_pgd_mapping(early_pg_dir, FIXADDR_START, (uintptr_t)fixmap_pgd_next, PGDIR_SIZE, PAGE_TABLE); /* 设置 fixmap PMD */ create_pmd_mapping(fixmap_pmd, FIXADDR_START, (uintptr_t)fixmap_pte, PMD_SIZE, PAGE_TABLE); /* 设置 trampoline PGD and PMD */ create_pgd_mapping(trampoline_pg_dir, PAGE_OFFSET, (uintptr_t)trampoline_pmd, PGDIR_SIZE, PAGE_TABLE); create_pmd_mapping(trampoline_pmd, PAGE_OFFSET, load_pa, PMD_SIZE, PAGE_KERNEL_EXEC); /* * 设置覆盖整个内核的早期PGD,这将使我们能够达到paging_init()。 * 稍后在下面的 setup_vm_final() 中映射所有内存。 */ end_va = PAGE_OFFSET + load_sz; for (va = PAGE_OFFSET; va < end_va; va += map_size) create_pgd_mapping(early_pg_dir, va, load_pa + (va - PAGE_OFFSET), map_size, PAGE_KERNEL_EXEC); /* 为dtb创建早期的PMD */ create_pgd_mapping(early_pg_dir, DTB_EARLY_BASE_VA, (uintptr_t)early_dtb_pmd, PGDIR_SIZE, PAGE_TABLE); /* 为 FDT 早期扫描创建两个连续的 PMD 映射 */ pa = dtb_pa & ~(PMD_SIZE - 1); create_pmd_mapping(early_dtb_pmd, DTB_EARLY_BASE_VA, pa, PMD_SIZE, PAGE_KERNEL); create_pmd_mapping(early_dtb_pmd, DTB_EARLY_BASE_VA + PMD_SIZE, pa + PMD_SIZE, PMD_SIZE, PAGE_KERNEL); dtb_early_va = (void *)DTB_EARLY_BASE_VA + (dtb_pa & (PMD_SIZE - 1)); ...... }
setup_vm()在最开始就进行了kernel入口地址的对齐检查,要求入口地址2M对齐。假设内存起始地址为0x80000000,那么kernel只能放在0x80000000、0x80200000等2M对齐处。为什么会有这种对齐要求呢?
我猜测单纯是为给opensbi预留了2M空间,因为kernel之前还有opensbi,而opensbi运行完之后,默认跳转地址就是偏移2M,kernel只是为了跟opensbi对应,所以设置了2M对齐。
那opensbi需要占用2M这么大?实际上只需要几百KB,因此opensbi和kernel中间有一段内存是空闲的,没有人使用。这个问题我们下篇再讲。
在该函数中开始为整个物理内存做内存映射,通过swapper
页表来管理,并且清除掉汇编阶段的页表。
static void __init setup_vm_final(void) { uintptr_t va, map_size; phys_addr_t pa, start, end; u64 i; /** * 此时MMU已经开启,但是页表还没完全建立。 */ pt_ops.alloc_pte = alloc_pte_fixmap; pt_ops.get_pte_virt = get_pte_virt_fixmap; #ifndef __PAGETABLE_PMD_FOLDED pt_ops.alloc_pmd = alloc_pmd_fixmap; pt_ops.get_pmd_virt = get_pmd_virt_fixmap; #endif /* Setup swapper PGD for fixmap */ create_pgd_mapping(swapper_pg_dir, FIXADDR_START, __pa_symbol(fixmap_pgd_next), PGDIR_SIZE, PAGE_TABLE); /* 为整个物理内存创建页表 */ for_each_mem_range(i, &start, &end) { if (start >= end) break; if (start <= __pa(PAGE_OFFSET) && __pa(PAGE_OFFSET) < end) start = __pa(PAGE_OFFSET); //best_map_size是选择合适的映射大小,kernel入口地址2M对齐或者kernel大小能被2M整除时,map_size就是2M,否则就是4K。 map_size = best_map_size(start, end - start); for (pa = start; pa < end; pa += map_size) { va = (uintptr_t)__va(pa); create_pgd_mapping(swapper_pg_dir, va, pa, map_size, PAGE_KERNEL_EXEC); } } /* 清除fixmap的PMD和PTE */ clear_fixmap(FIX_PTE); clear_fixmap(FIX_PMD); /* 切换到swapper页表,这个是最终的页表,汇编阶段relocate开启MMU的操作,跟下面这句是一样的。 */ csr_write(CSR_SATP, PFN_DOWN(__pa_symbol(swapper_pg_dir)) | SATP_MODE); local_flush_tlb_all();//刷新TLB ...... }
说明:
在setup_vm_final()函数中,通过swapper_pg_dir
页表来管理整个物理内存的访问。并且清除汇编阶段的页表fixmap_pte和early_pg_dir。(本质上就是把该页表项的内容清0,即赋值为0)
最终把swapper_pg_dir
页表的物理地址赋值给SATP
寄存器。这样CPU就可以通过该页表访问整个物理内存。
切换页表通过如下实现:
csr_write(CSR_SATP,PFN_DOWN(_pa(swapper_pg_dir))|SATP_MODE);
在swapper_pg_dir管理的kernel space中,其虚拟地址与物理地址空间的偏移是固定的,为va_pa_offset
(定义在arch/riscv/mm/init.c中的一个全局变量)
注意:swapper_pg_dir管理的是kernel space的页表,即它把物理内存映射到的虚拟地址空间是只能kernel访问的。user space不能访问,用户空间如果访问,必须自行建立页表,把物理地址映射到user space的虚拟地址空间。kernel线程共享这个swapper_pg_dir页表。
RISC-V Penciptaan jadual halaman apabila Linux bermula agak mudah difahami, semuanya dicipta dalam bahasa C, dan kodnya agak kecil. Fungsi penciptaan jadual dua halaman utama ialah setup_vm() dan setup_vm_final(). Selepas memahami beberapa format alamat sv39, lebih mudah untuk menganalisis kod sumber. Walau bagaimanapun, kod versi kernel yang berbeza adalah berbeza dan memerlukan analisis terperinci situasi tertentu.
Artikel ini menyebut bahawa setup_vm() akan menyemak sama ada alamat kemasukan kernel adalah sejajar 2M Jika ia tidak sejajar, kernel tidak boleh dimulakan Tetapi sebenarnya, kita boleh mengangkat sekatan penjajaran 2M ini dan menggunakan bahagian ini ruang. Artikel seterusnya akan mengajar anda cara mengoptimumkan bahagian memori ini.
Atas ialah kandungan terperinci Analisis pembuatan jadual halaman permulaan RISC-V Linux. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!