RISC-V32页式虚拟内存介绍及C语言实现

一、前言

1、 本文是在修改汪辰老师RISC-V操作系统时产生的学习笔记。

2、本文大部分内容是参考risc-v官方的手册和《RISC-V开放架构设计之道》编写,这些资料在网上均能找到对应的pdf。

2、本人水平一般,文章可能有纰漏,欢迎指出。

二、S模式简介

在RISC-V中,有两种内存管理方式,一种是基于物理内存保护(Physical Memory Protection,PMP)的方式,另一种是基于S特权级的页式虚拟内存管理方式,前者实现较为简单,但局限性较高,常用于嵌入式场景,这里我们主要谈的是后者。

S模式是一种可选的特权模式,旨在支持现代类UNIX操作系统,S模式的特权级高于U模式,但低于M模式,与U模式类似,S模式软件不能使用M模式的CSR和指令。

无论位于何种模式,所有异常都默认将控制权转移到M模式的异常处理程序(即跳转到mtvec寄存器填写的地址处执行),但RISC-V提供一种异常委托机制,用于选择性的将中断和异常委托给S模式处理。

但是我们这里为了简化,不采用异常委托的方式,即所有中断和异常仍然交于M模式处理,当处于M模式时,对地址空间的访问将不会经过页表的翻译,而是直接使用实际的物理内存

三、实验设备

后续代码运行在QEMU的virt版中,该版的内存实质被映射到了地址空间中的0x80000000-0x88000000处,共计128M大小。

四、RISC-V32虚拟内存简介

1、分页方案

在RISC-V中,32位的分页方案是Sv32,64位的分页方案是Sv39 or Sv48,这里我们只关注32位的Sv32方案。

Sv32采用二级页表,最大支持4GiB的地址空间,该空间被划分为2的10次方个4MiB的兆页,每个兆页又被进一步划分为2的10次方个4Kib的基页,基页和兆页的结构一致,每个页占用4个字节的空间。

2、页格式

  • V表示该页表是否有效,V=1时有效,如果V=0,则在使用此页时就会触发异常(我不确定是多少号异常)。
  • RWX表示该页是否可读、写、执行,若3位均为0,则代表该表项为一级页表,具体的权限由下面的二级页表来进行精细的管理。注意,并不是说一级页表就一定要将这三位设为0,当一级页表中这三位不都为0时,则表示该4MiB内存的权限全部由一级页表来管理(我没试过和二级页表的设置冲突时会发生什么,但是最好不要这么做)。
  • U表示该页是否为用户页,若为0,则表示不为用户页,则U模式下无法访问此页(会触发异常,但是我不知道是多少号),S模式下能,若U=1,则U模式能访问此页,但是S模式下不能。
  • G表示该映射是否存在于所有虚拟地址空间,RISC-V通用satp寄存器来管理页表的地址,一个操作系统内可以存在多个页表,如果将G=1,则表示该表项会无视satp中设置的其他页表来进行翻译,常用于属于操作系统内核的页(这里其实存在一个问题,可能会出现两个不同页表相同映射的地址表项内容存在冲突,此时会不可预测的使用其中任何一个页表,可能取决于具体的硬件实现)。
  • A表示自从上次清除A位以来,该页是否被访问过。
  • D表示自从上次清除D位以来,该页是否被写入过(AD位通常被用于操作系统换入换出使用)。
  • RSW字段保留给操作系统使用,硬件会忽略此字段
  • PPN存放物理页号,后续会详细介绍该字段。

3、开启分页

RISC-V的分页由satp寄存器管理,,satp寄存器结构如下图所示:

其中MODE字段用于开启分页,0代表不开启任何分页,1代表开启Sv32模式的分页,如果是RISC-V64的stap字段,MODE字段则由4位组成,这里不做过多的介绍。

ASID字段为可选字段,手册没看懂,《RISC-V开放架构设计之道》里说可以降低上下文切换的开销,也没说别的了,全填0就完事了。

PPN字段为一级页表的首地址,注意PPN字段只有22位,是无法在32位地址空间里精确标记的一个地址的,所以其实PPN字段是需要4k对齐的。

在正确写入了satp寄存器后,即可开启页式虚拟内存。

4、地址翻译过程

翻译大致流程如下图所示:

图中VA为目标虚拟地址,处理器在收到一条访存指令时,首先从satp中的PPN字段取出一级页表的地址,从VA的后10位取出目标物理地址在一级页表中的偏移以找到对应的二级页表,注意此时获得的其实是存放在一级页表页表项的PPN字段中的22位数据,这个数据还要乘上4096后加上VA[31:22]才是对应二级页表的地址。

随后将VA中间的10位作为二级页表的偏移,从二级页表中取出物理地址的后20位,最后将从二级页表中取出的数据*4096 + VPN的低12位以获得物理地址。

这个过程其实很明确,唯一让人疑惑的是PPN是一个22位的数据,最后又拼接了一个12位的地址上去,这样以来最后获得的物理地址数据岂不是34位的吗,事实上确实如此,但是二级页表里的数据其实是我们填的,所以我们只要不在二级页表中存放22位的地址就可以了。

五、C语言代码实现

1、页表项结构

结合我们之前所说的页表项结构,直接在c语言里用一个结构体位域实现即可

typedef struct _mempage
{
    uint32_t V : 1;    // 有效位(Valid)
    uint32_t R : 1;    // 可读位(Read)
    uint32_t W : 1;    // 可写位(Write)
    uint32_t X : 1;    // 可执行位(Execute)
    uint32_t U : 1;    // 用户模式位(User)
    uint32_t G : 1;    // 全局位(Global)
    uint32_t A : 1;    // 访问位(Accessed)
    uint32_t D : 1;    // 脏位(Dirty)
    uint32_t RSW : 2;  // 保留供软件使用的位(Reserved for Software Use)
    uint32_t PPN : 20; // 物理页号(Physical Page Number)
}mempage;

2、填写页表内容

先看代码,后面再解释,page_alloc是一个4k级别的内存分配函数,参数是分配的页的个数。

void mempage_init()
{
    // 初始化0号进程的页表
    // 目前只支持128M的内存,一个一级页表只能管理4M的内存,所以需要32个一级页表
    task0_firstpage = (mempage *)((uint32_t)page_alloc(2) + 2048);
    // mempage* task0_firstpage =  (mempage*)((uint32_t)page_alloc(2));
    //  有32个一级页表,一个一级页表有1024个二级页表项,所以二级页表需要的总空间为
    //  32 * 1024 * 8
    mempage *task0_secpage = (mempage *)page_alloc(64);
    // 初始化一级页表
    for (int i = 0; i < 32; i++)
    {
        if ((uint32_t)(&task0_secpage[i * 1024]) % 4096 != 0)
            printf("err, i = %d", i);
        task0_firstpage[i].PPN = (uint32_t)(&task0_secpage[i * 1024]) / 4096;
        task0_firstpage[i].V = 1;
        task0_firstpage[i].R = 0;
        task0_firstpage[i].W = 0;
        task0_firstpage[i].X = 0;
        task0_firstpage[i].U = 0;
        task0_firstpage[i].G = 0;
        task0_firstpage[i].A = 1;
        task0_firstpage[i].D = 1;
        task0_firstpage[i].RSW = 0;
    }

    // 初始化二级页表
    for (int i = 0; i < 1024 * 32; i++)
    {
        task0_secpage[i].PPN = (0x80000000 + i * 4096) / 4096;
        // task0的页表比较特殊,这里假设它所有页表项都可读写执行
        // 脏读和脏写位如果不用设置为1在硬件的角度上是最快的
        task0_secpage[i].V = 1;
        task0_secpage[i].R = 1;
        task0_secpage[i].W = 1;
        task0_secpage[i].X = 1;
        task0_secpage[i].U = 0;
        task0_secpage[i].G = 0;
        task0_secpage[i].A = 1;
        task0_secpage[i].D = 1;
        task0_secpage[i].RSW = 0;
    }

    // 开启分页
    // ASID字段好像是优化用的,但是不管也行
    reg_t tmp = 0;
    tmp = tmp | 1U << 31 | ((((uint32_t)task0_firstpage - 2048) / 4096) & 0x3FFFFF);
    w_satp(tmp);
}

来一段一段剖析代码,首先是开头

// 初始化0号进程的页表
// 目前只支持128M的内存,一个一级页表只能管理4M的内存,所以需要32个一级页表
task0_firstpage = (mempage *)((uint32_t)page_alloc(2) + 2048);
// mempage* task0_firstpage =  (mempage*)((uint32_t)page_alloc(2));
//  有32个一级页表,一个一级页表有1024个二级页表项,所以二级页表需要的总空间为
//  32 * 1024 * 8
mempage *task0_secpage = (mempage *)page_alloc(64);

代码开头有个很有意思的内容,即task0_firstpage是一级页表的开头,但是为什么要先加上2048呢,因为在qemu的virt版中,内存从0x80000000处开始,前面的地址空间我不想管理,而前面的地址刚好是2048个一级页表的大小,所以先+2048就相当于我们跳过了前面0x80000000这么多的地址,直接设置ram地址的映射。

接着是第二段初始化一级页表和二级页表的代码,这段没什么好说的,无非是照着手册填数据罢了。

// 初始化一级页表
for (int i = 0; i < 32; i++)
{
    if ((uint32_t)(&task0_secpage[i * 1024]) % 4096 != 0)
        printf("err, i = %d", i);
    task0_firstpage[i].PPN = (uint32_t)(&task0_secpage[i * 1024]) / 4096;
    task0_firstpage[i].V = 1;
    task0_firstpage[i].R = 0;
    task0_firstpage[i].W = 0;
    task0_firstpage[i].X = 0;
    task0_firstpage[i].U = 0;
    task0_firstpage[i].G = 0;
    task0_firstpage[i].A = 1;
    task0_firstpage[i].D = 1;
    task0_firstpage[i].RSW = 0;
}

// 初始化二级页表
for (int i = 0; i < 1024 * 32; i++)
{
    task0_secpage[i].PPN = (0x80000000 + i * 4096) / 4096;
    // task0的页表比较特殊,这里假设它所有页表项都可读写执行
    // 脏读和脏写位如果不用设置为1在硬件的角度上是最快的
    task0_secpage[i].V = 1;
    task0_secpage[i].R = 1;
    task0_secpage[i].W = 1;
    task0_secpage[i].X = 1;
    task0_secpage[i].U = 0;
    task0_secpage[i].G = 0;
    task0_secpage[i].A = 1;
    task0_secpage[i].D = 1;
    task0_secpage[i].RSW = 0;
}

接着是最后的开启分页的代码

// 开启分页
// ASID字段好像是优化用的,但是不管也行
reg_t tmp = 0;
tmp = tmp | 1U << 31 | ((((uint32_t)task0_firstpage - 2048) / 4096) & 0x3FFFFF);
w_satp(tmp);

这段代码需要注意的是,因为task0_firstpage我们之前是在申请的内存地址的基础上加上了2048,所以设置页表的时候一定要减去2048,同时因为有效字段只有10位,所以要除以4096,最后将设置的好的值写入到satp寄存器即可,这里的w_satp是一个内联汇编函数,用的时候需要自己实现。

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值