RISC-V Linux起動ページテーブル作成解析

リリース: 2023-08-01 15:39:36
転載
1771 人が閲覧しました

前回の記事では、RISC-V Linux のアセンブリ起動プロセスを分析し、再配置リダイレクトには MMU をオンにする必要があると述べましたが、今回は RISC-V Linux のページ テーブルの作成を分析します。

#注: この記事は linux5.10.111 カーネルに基づいています

#sv39 mmu

RISC-V Linux は、

sv32sv39sv48 およびその他の仮想アドレス形式をサポートしており、それぞれ 32 ビットの仮想アドレス、38 ビットの仮想アドレスを表します。ビット仮想アドレスと 48 ビット仮想アドレス。 RISC-V Linux もデフォルトで sv39 形式を使用します。sv39 の仮想アドレス、物理アドレス、および PTE 形式は次のとおりです:

仮想アドレス形式:

RISC-V Linux起動ページテーブル作成解析
物理アドレス形式:

RISC-V Linux起動ページテーブル作成解析
PTE 形式:

RISC-V Linux起動ページテーブル作成解析
仮想アドレスは 39 ビットで表され、下位 12 ビットはページ オフセットを表し、上位ビットは VP N[0]、VP N[1]、VP N[2] の 3 つの部分に分かれており、それぞれ PTE の仮想アドレス VA のインデックスを表します。 、PMDおよびPGD。

物理アドレスは 56 ビットで表され、下位 12 ビットはページ オフセットを表し、上位ビットは物理ページ PPN[0]、PPN[1]、PPN[2]

PTE です。物理アドレスの PPN に対応する物理ページ PPN[0] 、 PPN[1] 、 PPN[2] を保存します; PTE の下位 10 ビットは物理アドレスのアクセス権を表します。すべて 0 の場合、PTE に格納されているアドレスが次のレベルのページ テーブルの物理アドレスであることを意味します。そうでない場合は、現在のページ テーブルが最後のレベルのページ テーブル であることを意味します。

sv39 のページ テーブル形式を見てください。sv39 は、

PGDPMDPTE という 3 レベルのページ テーブルを使用します。レベル ページ テーブルは 9 ビットで表されます。つまり、各レベル ページ テーブルには 512 個のページ テーブル エントリがあります。

コードでは、ページ テーブルを表す 512 個の要素を含む配列を作成します。 PTE には 512 のページ テーブル エントリがあり、各ページ テーブル エントリは 8 バイト (512*8=4096 バイト) を占めるため、PTE は 4K を表します。 PMD には 512 個のページ テーブル エントリもあり、各エントリは PTE、512 *4 K=2M を表すことができるため、PMD は 2M を表します。類推すると、1 PGD は 512 * 2M = 1G を表します。

重要な結論: PGD は 1G を表し、PMD は 2M を表し、PTE は 4K を表します。 sv39 のデフォルトのページ サイズは 4K です。

第 3 レベルのページ テーブルの仮想アドレスを物理アドレスに変換するプロセスの概略図:

RISC-V Linux起動ページテーブル作成解析

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 に対して作成されたページ テーブルは、superpage とも呼ばれます。次に、カーネル全体のセカンダリ ページ テーブルを作成します。このとき、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

: 物理アドレス

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页表。

概要

RISC-V Linux 起動時のページテーブル作成は比較的理解しやすいです。 C言語で作成されており、コードは比較的小さいです。主な 2 つのページ テーブル作成関数は、setup_vm() と setup_vm_final() です。 sv39 のアドレス形式のいくつかを理解すると、ソース コードの解析が容易になります。ただし、カーネルのバージョンが異なるとコードが異なるため、特定の状況を詳細に分析する必要があります。

この記事では、setup_vm() はカーネル エントリ アドレスが 2M アライメントされているかどうかをチェックすると述べました。アライメントされていない場合、カーネルは起動できません。しかし、実際には、この 2M アライメント制限を解除して、次の記事では、メモリのこの部分を最適化する方法について説明します。

#

以上がRISC-V Linux起動ページテーブル作成解析の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:嵌入式Linux充电站
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート