Heim  >  Artikel  >  Betrieb und Instandhaltung  >  Analyse der Erstellung von RISC-V-Linux-Startseitentabellen

Analyse der Erstellung von RISC-V-Linux-Startseitentabellen

嵌入式Linux充电站
嵌入式Linux充电站nach vorne
2023-08-01 15:39:361768Durchsuche

Der vorherige Artikel analysierte den Assembly-Startprozess von RISC-V Linux, in dem erwähnt wurde, dass die Umleitung zur Verschiebung das Einschalten der MMU erfordert. Heute analysieren wir die Seitentabellenerstellung von RISC-V Linux.

Hinweis: Dieser Artikel basiert auf dem Linux5.10.111-Kernel -Bit-virtuelle Adresse und 48-Bit-virtuelle Adressadresse. RISC-V Linux verwendet standardmäßig auch das sv39-Format. Die virtuelle Adresse, die physische Adresse und das PTE-Format von sv39 sind wie folgt:

Virtuelles Adressformat:

Physisches Adressformat:

sv32sv39sv48PTE-Format :

Analyse der Erstellung von RISC-V-Linux-Startseitentabellen Die virtuelle Adresse wird durch 39 Bits dargestellt, von denen die niedrigen 12 Bits den Seitenversatz darstellen. Die hohen Bits sind in drei Teile unterteilt: VP N[0], VP N[1] und VP N[. 2], die jeweils die virtuelle Adresse VA in PTE, PMD und PGD darstellen.

Die physische Adresse wird durch 56 Bits dargestellt, die niedrigen 12 Bits stellen den Seitenversatz dar und die hohen Bits sind die physischen Seiten PPN[0], PPN[1] und PPN[2]

PTE speichert die physischen Seiten PPN[ 0], PPN[1] und PPN[2] entsprechen dem PPN in der physischen Adresse; die unteren 10 Bits des PTE stellen die Zugriffsberechtigung der physischen Adresse dar. Wenn RWX alle 0 ist, bedeutet dies, dass die Adresse in gespeichert ist Der PTE ist die physische Adresse der Seitentabelle der nächsten Ebene. Andernfalls bedeutet dies, dass die aktuelle Seitentabelle die Seitentabelle der letzten Ebene ist. Sehen Sie sich das Seitentabellenformat von SV39 an. SV39 verwendet eine dreistufige Seitentabelle: PGD, PMD und PTE, jede Ebenenseitentabelle wird durch 9 Bits dargestellt, das heißt, jede Ebenenseitentabelle verfügt über 512 Seitentabelleneinträge.

Erstellen Sie im Code ein Array mit 512 Elementen, um eine Seitentabelle darzustellen. Ein PTE verfügt über 512 Seitentabelleneinträge. Jeder Seitentabelleneintrag belegt 8 Bytes, 512 * 8 = 4096 Bytes, sodass ein PTE 4 KB darstellt. Ein PMD verfügt außerdem über 512 Seitentabelleneinträge. Jeder Eintrag kann einen PTE darstellen, 512 * 4 K = 2M, sodass ein PMD 2M darstellt. Analog dazu entspricht eine PGD 512 * 2M = 1G. PGDPMDPTE,每一个级页表使用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

Wichtige Schlussfolgerung: PGD steht für 1G, PMD steht für 2M, PTE steht für 4K. Die Standardseitengröße von sv39 beträgt 4K

. Analyse der Erstellung von RISC-V-Linux-Startseitentabellen

Schematische Darstellung des Prozesses der Konvertierung der virtuellen Adresse der Seitentabelle der dritten Ebene in die physische Adresse:

sv39 Der Prozess der Konvertierung der virtuellen Adresse der Seitentabelle der dritten Ebene in die physische Adresse:

MMU Erhält die physische Adresse von PGD über das Satp-Register und kombiniert sie mit dem PGD-Index (d. h. V PN [2]). Finden Sie die PMD, kombinieren Sie sie mit dem PMD-Index (d. h. V PN [1]). Finden Sie den PTE und kombinieren Sie ihn dann mit dem PTE-Index (d. h. V PN[0]), um den Wert von VA im PTE-Index zu erhalten und so die physikalische Adresse zu erhalten.

🎜Nehmen Sie abschließend PPN[2], PPN[1] und PPN[0] aus dem PTE heraus und addieren Sie sie dann zum niedrigen 12-Bit-Offset der virtuellen Adresse, um die endgültige physische Adresse zu erhalten. 🎜

Temporäre Seitentabellenanalyse

Bevor Sie MMU starten, müssen Sie Kernel-, DTB-, Trampolin- und andere Seitentabellen erstellen. Damit nach dem Einschalten der MMU und vor der Ausführung des Speicherverwaltungsmoduls der Kernel normal initialisiert und die DTB normal analysiert werden kann. Dieser Teil der Seitentabelle ist eine temporäre Seitentabelle, und die endgültige Seitentabelle wird in setup_vm_final() erstellt.

Temporäre Seitentabellen-Erstellungssequenz:

Erstellen Sie zunächst eine frühe PGD und PMD für Fixmap. Zu diesem Zeitpunkt verwendet PGD early_pg_dir. Erstellen Sie dann eine sekundäre Seitentabelle für die ersten 2 MB Speicher, beginnend mit dem Kernel. Zu diesem Zeitpunkt verwendet PGD trampoline_pg_dir, die für dieses 2M erstellte Seitentabelle heißt auch superpage. Erstellen Sie dann eine sekundäre Seitentabelle für den gesamten Kernel. Zu diesem Zeitpunkt verwendet PGD early_pg_dir. Reservieren Sie schließlich 4 MB Größe für dtb, um eine sekundäre Seitentabelle zu erstellen. Funktion zur Erstellung von Seitentabellen ">early_pg_dir。然后对从kernel开始的前2M内存建立二级页表,此时PGD使用trampoline_pg_dir,为这2M建立的页表也叫作superpage。再然后,对整个kernel创建二级页表,此时PGD使用early_pg_dir。最后为dtb预留4M大小创建二级页表。

页表创建函数

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)

pgdp:PGD页表

va:虚拟地址

pa

create_pgd_mapping()🎜🎜
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-Seitentabelle🎜🎜va: virtuelle Adresse 🎜🎜pa: physische Adresse 🎜

sz:映射大小,PGDIR_SIZE或PMD_SIZE或PTE_SIZE

prot:PAGE_KERNEL_EXEC/PAGE_KERNEL表示当前是最后一级页表,否则pa代表下一级页表的物理地址

create_pmd_mapping()

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代表下一级页表的物理地址

create_pte_mapping()

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()创建最终页表。

具体细节参考代码中的注释,下面的代码省略了一些不重要的部分。

setup_vm()

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中间有一段内存是空闲的,没有人使用。这个问题我们下篇再讲。

setup_vm_final()

在该函数中开始为整个物理内存做内存映射,通过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页表。

Zusammenfassung

RISC-V Die Erstellung der Seitentabelle beim Start von Linux ist relativ einfach zu verstehen, alles wird in C-Sprache erstellt und der Code ist relativ klein. Die beiden wichtigsten Funktionen zur Erstellung von Seitentabellen sind setup_vm() und setup_vm_final(). Nachdem Sie einige Adressformate von SV39 verstanden haben, ist es einfacher, den Quellcode zu analysieren. Allerdings sind die Codes verschiedener Kernel-Versionen unterschiedlich und erfordern eine detaillierte Analyse spezifischer Situationen.

In diesem Artikel wurde erwähnt, dass setup_vm() prüft, ob die Kernel-Eintragsadresse auf 2M ausgerichtet ist. Wenn sie nicht ausgerichtet ist, kann der Kernel tatsächlich nicht gestartet werden. Tatsächlich können wir diese 2M-Ausrichtungsbeschränkung aufheben und diesen Teil des verwenden Im nächsten Artikel erfahren Sie, wie Sie diesen Teil des Speichers optimieren.

Das obige ist der detaillierte Inhalt vonAnalyse der Erstellung von RISC-V-Linux-Startseitentabellen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:嵌入式Linux充电站. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen