笔者认为,Linux 操作系统(Operating System)最核心的机制是虚拟内存(Virtual Memory)。因为,操作系统主要作用是将硬件环境抽象起来,给在其中运行的应用(Applications)提供一个统一的、抽象的运行环境(Execution Environment),使得应用(Applications)专注于为用户提供各种各样的业务逻辑(Business Logics)。其中,虚拟内存提供了核心的支撑作用。
从下(底层,baremetal)往上看,运行的程序与外界交互,主要是与主存(Main Memory)、外围设备(Peripheral Devices)交互(Access,Read/Write,IO)都是通过对其对应的地址(Addresses)进行读写操作(Read/Write Operations)来实现的。
而操作系统为了统一管理与外界的交互,使得,在操作系统上运行的应用(Applications)需要间接地(Indrectly)实现地址的访问,也就是通过操作系统来实现交互。那么,虚拟内存就提供了这么一个机制,通过不同的页表(Page Table)为每个应用提供一个独立的地址空间(Address Space),然后,该地址空间的地址,通过对应的页表,转换成实际的物理地址(Physical Address),从而,对外界进行访问。
这里所说的虚拟地址(Virtual Address, VA),主要指的 CPU load/store/branch 等指令所使用的地址。而物理地址(Physical Address, PA),主要指的是SoC上的总线地址,即经过总线的路由(Routing)后,指派到对应的设备上。
也就是说,在没有虚拟地址转换时,虚拟地址等于物理地址。
基于上述概念,以 RISCV Linux 以及 RISCV Sv39 为例,通过一系列的文章,详细地梳理清楚 在 RISCV Sv39 上,Linux 是如何实现其虚拟内存机制的。
(顺便提一句,vmlinux 中的 vm,就是 virtual memory 的缩写)
这是该系列的第一篇文章,主要讲述 RISCV Sv39。
RISCV Sv39 指 64 位的 RISCV CPU 中,其有效虚拟地址位数为 39 位,也就是有效位(Effective bits)为 0 - 38,另,39 - 64 的值与 bit 38 一样。也就是 Two's Complement 的计数方式,方便地址的计算与划分。
The RISC-V Instruction Set Manual: Volume II Privileged Architecture. P 11.4.
Instruction fetch addresses and load and store effective addresses,which are 64 bits, must have bits 63–39 all equal to bit 38, or else a page-fault exception will occur.
The 27-bit VPN is translated into a 44-bit PPN via a three-level page table, while the 12-bit page offset is untranslated.
即,根据RISCV Sv39 规定,39位有效虚拟地址被划分为4段,高位三段为虚拟页表索引号(Virtual Page Number),低12位为 页表偏移量(Page Offset)。
另外,Sv39 规定,物理地址有效位数为 56 位,即 0 - 55。如下:
另外,页表项 PTE (Page Table Entry) 的设计,如下:
这里需要注意的是,在RISCV 64 位的CPU角度上看,其访问的地址(Virtual Address)是64位的。当 CPU 中的虚拟地址转换模式(Virtual Address Translation Mode)被选定后,如 Sv39,那么,64位的CPU访问地址(CPU Access Virtual Address),将装换成虚拟地址转换模式指定的虚拟地址,如 Sv39 虚拟地址 (Sv39 Virtual address)。其转换规则由其转换模式规定,如Sv39 规定,低39位CPU访问地址有效,高位值必须等于 bit 38,类似符号扩展(sign extension)。基于,转换模式,可以找到其对应的虚拟地址转换模式指定的物理地址,如 Sv39 物理地址(Sv39 Physical Address),56位有效位的物理地址。然后,通过 0 扩展模式(zero extension)将其装换成机器级(machine level)物理地址(physical address),也就是上面所说的SoC总线地址(Bus Address)。
也就是,从CPU角度出发,地址的转换如下:
虚拟地址(Virtual Address, VA)/ CPU访问地址(CPU Access Virtual Address)/ 逻辑地址(Logical Address )
-- 符号扩展(sign extension)--
==> Sv39 虚拟地址 (Sv39 Virtual address)(VPN + Page Offset)
-- 虚拟地址转换模式 --
==> Sv39 物理地址(Sv39 Physical Address)(PPN + Page Offset)
-- 0 扩展模式(zero extension)--
==> 机器级(machine level)物理地址(physical address, PA)/ SoC总线地址(Bus Address)
通过 S mode 下的 satp 控制状态寄存器(Control Status Register, CSR),设定虚拟地址转换模式,如 Sv39,即物理根页表号(Root PPN),如下:
其转换过程如下:
其中,虚拟地址到物理地址的转换过程,大致如下:(在不考虑PTE属性的情况下)
#define XLEN (64) // 64 bits RISCV CPU
#define BITS_IN_BYTE (8) // 1 Byte = 8 bits
#define SATP_PPN_MASK (0x0fff'ffff'ffff)
#define PAGE_SIZE_BITS (12) // 4KB per page
#define PAGE_SIZE (1 << PAGE_SIZE_BITS)
#define PTE_SIZE (XLEN / BITS_IN_BYTE) // 8 Bytes per PTE
#define PTE_ENTRIES_PER_PAGE (PAGE_SIZE / PTE_SIZE) // 512 entries per page
#define Level_0_VA_OFFSET_BITS (PAGE_SIZE_BITS + 9 + 9)
#define Level_1_VA_OFFSET_BITS (PAGE_SIZE_BITS + 9)
#define Level_2_VA_OFFSET_BITS (PAGE_SIZE_BITS)PTE* level_0_page_table = (satp & SATP_PPN_MASK) << PAGE_SIZE_BITS
PTE* level_1_page_table = level_0_page_table[(va >> (Level_0_VA_OFFSET_BITS)) & (512 - 1)]
PTE* level_2_page_table = level_1_page_table[(va >> (Level_1_VA_OFFSET_BITS)) & (512 - 1)]
PTE* leaf_PTE = level_2_page_table[(va >> (Level_2_VA_OFFSET_BITS)) & (512 - 1)]
void * pa = leaf_PTE.ppn << Level_2_VA_OFFSET_BITS + (va & (1 << Level_2_VA_OFFSET_BITS - 1))
具体的转换过程,请参考官方手册。
其中,当 PTE 在 level_0_page_table 时,就是 叶表项(leaf_pte),那么该 PTE 描述的 1GB的空间(30 Bits),即
#define XLEN (64) // 64 bits RISCV CPU
#define BITS_IN_BYTE (8) // 1 Byte = 8 bits
#define SATP_PPN_MASK (0x0fff'ffff'ffff)
#define PAGE_SIZE_BITS (12) // 4KB per page
#define PAGE_SIZE (1 << PAGE_SIZE_BITS)
#define PTE_SIZE (XLEN / BITS_IN_BYTE) // 8 Bytes per PTE
#define PTE_ENTRIES_PER_PAGE (PAGE_SIZE / PTE_SIZE) // 512 entries per page
#define Level_0_VA_OFFSET_BITS (PAGE_SIZE_BITS + 9 + 9)
#define Level_1_VA_OFFSET_BITS (PAGE_SIZE_BITS + 9)
#define Level_2_VA_OFFSET_BITS (PAGE_SIZE_BITS)PTE* level_0_page_table = (satp & SATP_PPN_MASK) << PAGE_SIZE_BITS
PTE* leaf_PTE = level_0_page_table[(va >> (Level_0_VA_OFFSET_BITS)) & (512 - 1)]
void * pa = leaf_PTE.ppn << Level_0_VA_OFFSET_BITS + (va & (1 << Level_0_VA_OFFSET_BITS - 1))
同理,当 PTE 在 level_1_page_table 时,就是 叶表项(leaf_pte),那么该 PTE 描述的 2MB的空间(21 Bits),即
#define XLEN (64) // 64 bits RISCV CPU
#define BITS_IN_BYTE (8) // 1 Byte = 8 bits
#define SATP_PPN_MASK (0x0fff'ffff'ffff)
#define PAGE_SIZE_BITS (12) // 4KB per page
#define PAGE_SIZE (1 << PAGE_SIZE_BITS)
#define PTE_SIZE (XLEN / BITS_IN_BYTE) // 8 Bytes per PTE
#define PTE_ENTRIES_PER_PAGE (PAGE_SIZE / PTE_SIZE) // 512 entries per page
#define Level_0_VA_OFFSET_BITS (PAGE_SIZE_BITS + 9 + 9)
#define Level_1_VA_OFFSET_BITS (PAGE_SIZE_BITS + 9)
#define Level_2_VA_OFFSET_BITS (PAGE_SIZE_BITS)PTE* level_0_page_table = (satp & SATP_PPN_MASK) << PAGE_SIZE_BITS
PTE* level_1_page_table = level_0_page_table[(va >> (Level_0_VA_OFFSET_BITS)) & (512 - 1)]
PTE* leaf_PTE = level_1_page_table[(va >> (Level_1_VA_OFFSET_BITS)) & (512 - 1)]
void * pa = leaf_PTE.ppn << Level_1_VA_OFFSET_BITS + (va & (1 << Level_1_VA_OFFSET_BITS - 1))
那么,基于 RISCV Sv39 的虚拟地址转换模式大致上如上所示。