搜索
首页运维linux运维RISC-V Linux启动之页表创建分析

上篇分析了RISC-V Linux的汇编启动过程,其中讲到了relocate重定向需要开启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位代表page offset,高位划分为了三部分:VP N[0]、VP N[1]和VP N[2],分别代表虚拟地址VA在PTE、PMD和PGD中的索引。

物理地址使用56位表示,低12位代表page offset,高位是物理页PPN[0]、PPN[1]和PPN[2]

PTE保存了物理页PPN[0]、PPN[1]和PPN[2],和物理地址中的PPN相对应;PTE的低10位代表物理地址的访问权限,当RWX全为0时,则代表该PTE存储的地址是下一级页表的物理地址,否则代表当前页表是最后一级页表

再看看sv39 的页表格式,sv39使用的是三级页表,PGDPMDPTE,每一个级页表使用9bit表示,即每一级页表都有512个页表项。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在代码中,创建一个有512个元素的数组即代表一个页表。一个PTE有512个页表项,每一个页表项占用8字节,512*8=4096字节,所以一个PTE代表4K。一个PMD也是512个页表项,每一项可代表一个PTE,512 *4 K=2M,所以一个PMD就代表2M。以此类推,一个PGD代表512 * 2M=1G。

RISC-V Linux启动之页表创建分析重要结论:PGD代表1G、PMD代表2M、PTE代表4K。sv39默认的页大小是4K

三级页表虚拟地址转为物理地址过程示意图:

sv39三级页表虚拟地址转为物理地址过程:

🎜MMU通过satp寄存器得到PGD的物理地址,结合PGD index(即V PN[2])找到PMD;找到PMD后,再结合PMD index(即V PN[1])找到PTE,然后结合PTE index(即V PN[0])得到VA在PTE索引中的值,从而得到物理地址。🎜🎜最后在PTE中取出PPN[2]、PPN[1]和PPN[0],再和虚拟地址的低12位offset相加,得到最终的物理地址。🎜

临时页表分析

MMU开启前,需要建立好kernel、dtb、trampoline等页表。以便MMU开启后,并且在内存管理模块运行之前,kernel可以正常初始化,dtb可以正常地被解析。这部分页表都是临时页表,最终的页表在setup_vm_final()建立。

临时页表创建顺序:

首先为fixmap创建早期的PGD、PMD,这时PGD使用early_pg_dir。然后对从kernel开始的前2M内存建立二级页表,此时PGD使用trampoline_pg_dir,为这2M建立的页表也叫作superpage。再然后,对整个kernel创建二级页表,此时PGD使用early_pg_dir。最后为dtb预留4M大小创建二级页表。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页表🎜🎜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语言创建的,代码也比较少。主要就是setup_vm()和setup_vm_final()两个页表创建函数。理解了sv39的一些地址格式后,再去分析源码就比较容易。不过不同kernel版本代码都不一样,需要具体情况具体分析。

本篇提到了setup_vm()会检查kernel入口地址是否2M对齐,如果不对齐kernel无法启动,但其实我们可以解除这个2M对齐限制,将这部分空间利用起来,下篇教大家优化这部分内存。

以上是RISC-V Linux启动之页表创建分析的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:嵌入式Linux充电站。如有侵权,请联系admin@php.cn删除
Debian回收旧版本的方法Debian回收旧版本的方法Apr 13, 2025 am 09:12 AM

本文介绍如何有效清理Debian系统中的旧版本软件和内核,释放磁盘空间并提高系统性能。操作前请务必备份重要数据。一、清除无用软件包使用apt命令行工具可以轻松删除不再需要的软件包及其依赖项:打开终端。执行sudoapt-getautoremove命令自动删除已安装软件包的冗余依赖项。使用sudoapt-getpurge命令删除指定软件包及其配置文件。例如,删除firefox及其配置文件,执行sudoapt-getpurgefirefox。

如何利用Nginx日志提升网站速度如何利用Nginx日志提升网站速度Apr 13, 2025 am 09:09 AM

网站性能优化离不开对访问日志的深入分析。Nginx日志记录了用户访问网站的详细信息,巧妙利用这些数据,可以有效提升网站速度。本文将介绍几种基于Nginx日志的网站性能优化方法。一、用户行为分析与优化通过分析Nginx日志,我们可以深入了解用户行为,并据此进行针对性优化:高频访问IP识别:找出访问频率最高的IP地址,针对这些IP地址优化服务器资源配置,例如增加带宽或提升特定内容的响应速度。状态码分析:分析不同HTTP状态码(例如404错误)出现的频率,找出网站导航或内容管理中的问题,并进

debian readdir如何实现文件排序debian readdir如何实现文件排序Apr 13, 2025 am 09:06 AM

在Debian系统中,readdir函数用于读取目录内容,但其返回的顺序并非预先定义的。要对目录中的文件进行排序,需要先读取所有文件,再利用qsort函数进行排序。以下代码演示了如何在Debian系统中使用readdir和qsort对目录文件进行排序:#include#include#include#include//自定义比较函数,用于qsortintcompare(constvoid*a,constvoid*b){returnstrcmp(*(

debian readdir如何支持远程文件系统debian readdir如何支持远程文件系统Apr 13, 2025 am 09:03 AM

在Debian系统中,readdir函数用于读取目录内容。要使其支持远程文件系统,需确保远程文件系统已正确挂载到本地。以下步骤详细说明如何实现:一、选择合适的协议:选择合适的远程文件系统协议至关重要,例如NFS、Samba、FTP、SSHFS等。不同协议的配置方法差异较大。二、安装必要软件包:根据所选协议,安装相应的软件包。例如,NFS需要nfs-common或nfs-kernel-server;Samba需要samba;SSHFS需要fuse和sshfs。使用apt-getinst

debian readdir的兼容性如何debian readdir的兼容性如何Apr 13, 2025 am 09:00 AM

readdir函数是Linux系统中用于读取目录内容的标准工具,在Debian及大多数Linux发行版中均可用。作为稳定且广泛使用的发行版,Debian的readdir函数通常具有良好的兼容性,能与标准C库(例如glibc)及其他Linux工具无缝集成。Debian的更新日志和安全公告中鲜有提及readdir函数的兼容性问题。例如,Debian12.10的更新主要集中在安全性和稳定性改进,这些更新一般不会影响readdir等核心系统工具的兼容性。如果您在

Debian下Tomcat日志配置在哪Debian下Tomcat日志配置在哪Apr 13, 2025 am 08:57 AM

本文介绍如何在Debian系统中配置Tomcat日志。Tomcat日志配置文件通常位于/path/to/tomcat/conf/logging.properties。通过修改此文件,您可以自定义日志级别、格式和输出位置。日志文件存放位置Tomcat日志文件默认存储在$CATALINA_BASE/logs目录下。$CATALINA_BASE指的是Tomcat的安装根目录,如果未指定,则与$CATALINA_HOME(Tomcat安装目录)相同。常用Linux命令查看Tomcat日志以下是一些常

Debian如何清理回收站文件Debian如何清理回收站文件Apr 13, 2025 am 08:54 AM

本文介绍三种在Debian系统中清空回收站的方法,选择最适合您的方式即可。方法一:图形界面(GUI)对于使用图形界面的Debian用户(例如GNOME或KDE),清理回收站非常简单:打开文件管理器:点击桌面上的文件管理器图标(通常是一个文件夹),或使用快捷键Ctrl E。找到回收站:在文件管理器中找到并点击“回收站”或“垃圾桶”图标。清空回收站:在回收站窗口中,点击“清空回收站”或类似的按钮,确认操作即可。方法二:命令行界面(CLI)如果您更熟悉命令行,可以使用终端进行

Debian如何回收不再使用的包Debian如何回收不再使用的包Apr 13, 2025 am 08:51 AM

本文介绍如何在Debian系统中清理无用软件包,释放磁盘空间。第一步:更新软件包列表确保你的软件包列表是最新的:sudoaptupdate第二步:查看已安装的软件包使用以下命令查看所有已安装的软件包:dpkg--get-selections|grep-vdeinstall第三步:识别冗余软件包利用aptitude工具查找不再需要的软件包。aptitude会提供建议,帮助你安全地删除软件包:sudoaptitudesearch'~pimportant'此命令列出标记

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
4 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

Dreamweaver Mac版

Dreamweaver Mac版

视觉化网页开发工具