L'article précédent analysait le processus de démarrage de l'assembly de RISC-V Linux, qui mentionnait que la redirection de relocalisation nécessite l'activation de la MMU. Aujourd'hui, nous analysons la création de table de pages de RISC-V Linux.
Remarque : cet article est basé sur le noyau linux5.10.111
RISC-V Linux prend en charge sv32
、sv39
、sv48
et d'autres formats d'adresse virtuelle, qui représentent respectivement une adresse virtuelle 32 bits, 38 Adresse virtuelle de 48 bits et adresse d'adresse virtuelle de 48 bits. RISC-V Linux utilise également le format sv39 par défaut. L'adresse virtuelle, l'adresse physique et le format PTE de sv39 sont les suivants :
Format d'adresse virtuelle :
Format d'adresse physique :
Format PTE. :
L'adresse virtuelle est représentée par 39 bits, dont les 12 bits de poids faible représentent le décalage de page. Les bits de poids fort sont divisés en trois parties : VP N[0], VP N[1] et VP N[. 2], qui représentent respectivement l'adresse virtuelle VA dans PTE, PMD et PGD index dans .
L'adresse physique est représentée par 56 bits, les 12 bits faibles représentent le décalage de page et les bits forts sont les pages physiques PPN[0], PPN[1] et PPN[2]
PTE enregistre les pages physiques PPN[ 0], PPN[1] et PPN[2] correspondent au PPN dans l'adresse physique ; les 10 bits inférieurs du PTE représentent l'autorisation d'accès de l'adresse physique. Lorsque RWX est entièrement à 0, cela signifie que l'adresse est stockée dans. le PTE est l'adresse physique de la table de pages de niveau suivant, sinon cela signifie que la table de pages actuelle est la table de pages de dernier niveau.
Regardez le format de table de pages de sv39. sv39 utilise une table de pages à trois niveaux,PGD
, PMD
et PTE
, chaque table de page de niveau est représentée par 9 bits, c'est-à-dire que chaque table de page de niveau a 512 entrées de table de page. 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
Dans le code, créez un tableau de 512 éléments pour représenter un tableau de pages. Un PTE a 512 entrées de table de pages, chaque entrée de table de pages occupe 8 octets, 512*8=4096 octets, donc un PTE représente 4K. Un PMD dispose également de 512 entrées de table de pages, chaque entrée peut représenter un PTE, 512 *4 K=2M, donc un PMD représente 2M. Par analogie, un DPI représente 512 * 2M = 1G.
Conclusion importante : PGD signifie 1G
, PMD représente 2M
, PTE représente 4K
. La taille de page par défaut de sv39 est 4K.
Schéma schématique du processus de conversion de l'adresse virtuelle de la table de pages de troisième niveau en adresse physique :
sv39 Le processus de conversion de l'adresse virtuelle de la table de pages de troisième niveau en adresse physique : MMU obtient l'adresse physique du PGD via le registre satp et la combine avec l'index PGD (c'est-à-dire V PN [2]) Trouvez le PMD après avoir trouvé le PMD, combinez-le avec l'index PMD (c'est-à-dire V PN [1]) pour recherchez le PTE, puis combinez-le avec l'index PTE (c'est-à-dire V PN[0]) pour obtenir la valeur de VA dans l'index PTE, obtenant ainsi l'adresse physique. 🎜🎜Enfin, supprimez PPN[2], PPN[1] et PPN[0] du PTE, puis ajoutez-les au faible décalage de 12 bits de l'adresse virtuelle pour obtenir l'adresse physique finale. 🎜Avant de démarrer MMU, vous devez créer un noyau, un dtb, un trampoline et d'autres tables de pages. Ainsi, après l'activation de la MMU et avant l'exécution du module de gestion de la mémoire, le noyau peut être initialisé normalement et la dtb peut être analysée normalement. Cette partie de la table de pages est une table de pages temporaire et la table de pages finale est créée dans setup_vm_final().
Séquence de création de table de pages temporaire :
Créez d'abord les premiers PGD et PMD pour fixmap. À l'heure actuelle, PGD utilise early_pg_dir
. Créez ensuite une table de pages secondaire pour les 2 premiers Mo de mémoire à partir du noyau. À ce stade, PGD utilise trampoline_pg_dir
, la table des pages créée pour ce 2M est aussi appelée superpage
. Ensuite, créez une table de pages secondaire pour l'ensemble du noyau. À ce stade, PGD utilise early_pg_dir
. Enfin, réservez une taille de 4 Mo à dtb pour créer une table de pages secondaire. 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
: table de pages PGD🎜🎜va
: adresse virtuelle 🎜🎜pa
: adresse physique🎜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 La création de table de pages au démarrage de Linux est relativement facile à comprendre, tout est créé en langage C et le code est relativement petit. Les principales fonctions de création de tables de deux pages sont setup_vm() et setup_vm_final(). Après avoir compris certains formats d'adresse de sv39, il sera plus facile d'analyser le code source. Cependant, les codes des différentes versions du noyau sont différents et nécessitent une analyse détaillée de situations spécifiques.
Cet article mentionne que setup_vm() vérifiera si l'adresse d'entrée du noyau est alignée sur 2M. Si elle n'est pas alignée, le noyau ne peut pas démarrer. Mais en fait, nous pouvons lever cette restriction d'alignement sur 2M et utiliser cette partie du noyau. espace. Le prochain article vous apprendra comment optimiser cette partie de la mémoire.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!