이전 기사에서는 RISC-V Linux의 어셈블리 시작 프로세스를 분석했는데, 재배치 리디렉션을 위해서는 MMU를 켜야 한다고 언급했습니다. 오늘은 RISC-V Linux의 페이지 테이블 생성을 분석합니다.
참고: 이 문서는 linux5.10.111 커널
RISC-V Linux는 sv32
、sv39
、sv48
및 기타 가상 주소 형식을 지원하며 각각 32비트 가상 주소 38을 나타냅니다. -비트 가상 주소 및 48비트 가상 주소 주소. RISC-V Linux도 기본적으로 sv39 형식을 사용합니다. sv39의 가상 주소, 물리적 주소 및 PTE 형식은 다음과 같습니다.
가상 주소 형식:
물리적 주소 형식:
PTE 형식 :
가상 주소는 39비트로 표현되며, 그 중 하위 12비트는 페이지 오프셋을 나타냅니다. VP N[0], VP N[1] 및 VP N[. 2]는 각각 PTE, PMD 및 PGD 인덱스의 가상 주소 VA를 나타냅니다.
물리적 주소는 56비트로 표현되고, 하위 12비트는 페이지 오프셋을 나타내며, 높은 비트는 물리 페이지 PPN[0], PPN[1], PPN[2]입니다.
PTE는 물리 페이지 PPN[을 저장합니다. 0], PPN[1], PPN[2]은 물리 주소의 PPN에 해당하며, PTE의 하위 10비트는 물리 주소의 액세스 허용을 나타냅니다. RWX가 모두 0이면 저장된 주소를 의미합니다. PTE는 다음 레벨 페이지 테이블의 물리적 주소입니다. 그렇지 않으면 현재 페이지 테이블이 마지막 레벨 페이지 테이블이라는 의미입니다.
sv39의 페이지 테이블 형식을 보세요. sv39는 3단계 페이지 테이블인PGD
, PMD
및 PTE
, 각 레벨 페이지 테이블은 9비트로 표시됩니다. 즉, 각 레벨 페이지 테이블에는 512개의 페이지 테이블 항목이 있습니다. 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
코드에서 페이지 테이블을 나타내기 위해 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
. sv39의 기본 페이지 크기는 4K입니다.
3레벨 페이지 테이블의 가상 주소를 물리 주소로 변환하는 과정의 모식도:
sv39 3레벨 페이지 테이블의 가상 주소를 물리 주소로 변환하는 과정: MMU satp 레지스터를 통해 PGD의 물리적 주소를 얻고 이를 PGD 인덱스(예: V PN[2])와 결합합니다. PMD를 찾은 후 이를 PMD 인덱스(예: V PN[1])와 결합합니다. PTE를 찾은 다음 이를 PTE 인덱스(즉, V PN[0])와 결합하여 PTE 인덱스의 VA 값을 가져와서 물리적 주소를 얻습니다. 🎜🎜마지막으로 PTE에서 PPN[2], PPN[1], PPN[0]을 꺼낸 다음 이를 가상 주소의 하위 12비트 오프셋에 추가하여 최종 물리적 주소를 얻습니다. 🎜MMU를 시작하기 전에 커널, dtb, 트램폴린 및 기타 페이지 테이블을 생성해야 합니다. 그래야 MMU가 켜진 후 메모리 관리 모듈이 실행되기 전에 커널이 정상적으로 초기화되고 dtb도 정상적으로 구문 분석될 수 있습니다. 페이지 테이블의 이 부분은 임시 페이지 테이블이며, 최종 페이지 테이블은 setup_vm_final()에서 생성됩니다.
임시 페이지 테이블 생성 순서:
먼저 수정맵용 초기 PGD 및 PMD를 생성합니다. 이때 PGD는 early_pg_dir
. 그런 다음 커널에서 시작하여 첫 번째 2M 메모리에 대한 보조 페이지 테이블을 만듭니다. 이때 PGD는 trampoline_pg_dir
, 이 2M을 위해 생성된 페이지 테이블은 슈퍼페이지
. 그런 다음 전체 커널에 대한 보조 페이지 테이블을 만듭니다. 이때 PGD는 early_pg_dir
. 마지막으로, 보조 페이지 테이블을 생성하기 위해 dtb에 4M 크기를 예약합니다. 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
: PGD 페이지 테이블🎜🎜va
: 가상 주소 🎜🎜pa
: 물리적 주소 🎜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 Linux 시작 시 페이지 테이블 생성은 비교적 이해하기 쉽고, 모두 C 언어로 생성되며, 코드도 비교적 작습니다. 주요 2페이지 테이블 생성 함수는 setup_vm()과 setup_vm_final()입니다. sv39의 주소 형식 중 일부를 이해하고 나면 소스 코드를 분석하는 것이 더 쉬울 것입니다. 그러나 다양한 커널 버전의 코드는 다르며 특정 상황에 대한 자세한 분석이 필요합니다.
이 기사에서는 setup_vm()이 커널 항목 주소가 2M 정렬되어 있는지 확인한다고 언급했습니다. 정렬되지 않으면 커널을 시작할 수 없습니다. 그러나 실제로는 이 2M 정렬 제한을 해제하고 이 부분을 활용할 수 있습니다. 다음 기사에서는 메모리의 이 부분을 최적화하는 방법을 설명합니다.
위 내용은 RISC-V Linux 시작 페이지 테이블 생성 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!