1.页表操作 1.1页表项定义
64位页表项布局 :
低10位:存储下级物理页的属性标志位
10-54位:存储下级页表的物理页号
address.h
定义页表项结构体64位以及0-9的标记位
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct { uint64_t bits; }PageTableEntry; #define PTE_V (1 << 0) #define PTE_R (1 << 1) #define PTE_W (1 << 2) #define PTE_X (1 << 3) #define PTE_U (1 << 4) #define PTE_G (1 << 5) #define PTE_A (1 << 6) #define PTE_D (1 << 7)
address.c
定义一些操作PTE的函数
通过一些移位的操作进行判断
**ppn.value = (entry->bits >> 10) & ((1ul << 44) - 1)
**:
entry->bits >> 10
:将页表条目的 bits
字段右移10位,去掉低10位的标志位,得到物理页号部分。
((1ul << 44) - 1)
:生成一个44位的掩码(0xFFFFFFFFFFF),用于提取物理页号部分。这个掩码确保物理页号部分的最高44位保留,而低位的标志位被清除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 PageTableEntry PageTableEntry_new (PhysPageNum ppn, uint8_t PTEFlags) { PageTableEntry entry; entry.bits = (ppn.value << 10 ) | PTEFlags; return entry; } PageTableEntry PageTableEntry_empty () { PageTableEntry entry; entry.bits = 0 ; return entry; } PhysPageNum PageTableEntry_ppn (PageTableEntry *entry) { PhysPageNum ppn; ppn.value = (entry->bits >> 10 ) & ((1ul << 44 ) - 1 ); return ppn; } uint8_t PageTableEntry_flags (PageTableEntry *entry) { return entry->bits & 0xFF ; } bool PageTableEntry_is_valid (PageTableEntry *entry) { uint8_t entryFlags = PageTableEntry_flags(entry); return (entryFlags & PTE_V) != 0 ; }
1.2物理页帧获取 虚拟地址向物理地址转换过程
PTE存放是的可能下一级的PTE 以及最终的 物理地址的PPN,需要访问物理页号对应的物理帧的内存数据,定义了两个辅助函数
将PPN转化为物理地址,并返回该地址对应的字节数组指针
1 2 3 4 5 6 uint8_t * get_bytes_array (PhysPageNum ppn) { PhysAddr addr = phys_addr_from_phys_page_num(ppn); return (uint8_t *) addr.value; }
1 2 3 4 5 6 PageTableEntry* get_pte_array (PhysPageNum ppn) { PhysAddr addr = phys_addr_from_phys_page_num(ppn); return (PageTableEntry*) addr.value; }
1.3 PT与PTE
PT: 页表是一个包含多个 PTE 的数据结构 ,用于存储虚拟地址到物理地址的映射。
PTE: 页表条目是页表中的一个条目 ,表示具体的虚拟地址到物理地址的映射关系。
2.虚实地址映射 rcore
的内存映射实现如下
同前面提到的差不多
address.c
获取虚拟页表的三级索引
从 vpn.value
中提取三级索引:
使用 & 0x1ff
提取低9位(1_1111_1111),这是每级索引的范围(512个条目)。
右移9位,准备提取下一个索引。
从高到低(i = 2, 1, 0)提取三级索引。
1 2 3 4 5 6 7 8 9 10 11 12 void indexes (VirtPageNum vpn, size_t * result) { size_t idx[3 ]; for (int i = 2 ; i >= 0 ; i--) { idx[i] = vpn.value & 0x1ff ; vpn.value >>= 9 ; } for (int i = 0 ; i < 3 ; i++) { result[i] = idx[i]; } }
有了PTE
,还需要一个PT
对PTE
进行管理。因此 PageTable
要保存它根节点的物理页号 root_ppn
作为页表唯一的区分标志。
address.h
PT实现
1 2 3 4 typedef struct { PhysPageNum root_ppn; }PageTable;
现在已经有了页表项PT
,如何在PT中查找PTE
,
传入一个PageTable
,根据此页表的根节点开始遍历,根节点的物理页号是保存在satp
寄存器中的,从页表中根据虚拟地址的页表项索引来取出具体的页表项,如果此页表项为空,则分配一页内存,然后新建一个页表项进行填充。直到三级页表索引完毕,会返回虚拟地址最终对应的三级页表的页表项,此时三级页表的页表项是空的,在进行map时只需要对此页表项赋值就行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 PageTableEntry* find_pte_create (PageTable* pt,VirtPageNum vpn) { size_t idx[3 ]; indexes(vpn, idx); PhysPageNum ppn = pt->root_ppn; for (int i = 0 ; i < 3 ; i++) { PageTableEntry* pte = &get_pte_array(ppn)[idx[i]]; if (i == 2 ) { return pte; } if (!PageTableEntry_is_valid(pte)) { PhysPageNum frame = StackFrameAllocator_alloc(&FrameAllocatorImpl); *pte = PageTableEntry_new(frame,PTE_V); } ppn = PageTableEntry_ppn(pte); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 PageTableEntry* find_pte (PageTable* pt, VirtPageNum vpn) { size_t idx[3 ]; indexes(vpn, idx); PhysPageNum ppn = pt->root_ppn; for (int i = 0 ; i < 3 ; i++) { PageTableEntry* pte = &get_pte_array(ppn)[idx[i]]; if (i == 2 ) { return pte; } if (!PageTableEntry_is_valid(pte)) { return NULL ; } ppn = PageTableEntry_ppn(pte); } }
有了PTE
以及VPN
还有PT
,那么就可以建立虚拟地址与物理地址的之间的映射关系了:只需要将映射的物理页号与虚拟地址通过三级页表中的页表项对应起即可
该函数会根据指定的大小,将连续的虚拟地址映射到连续的物理地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 void PageTable_map (PageTable* pt,VirtAddr va, PhysAddr pa, u64 size ,uint8_t pteflgs) { if (size == 0 ) panic("mappages: size" ); PhysPageNum ppn = floor_phys(pa); VirtPageNum vpn = floor_virts(va); u64 last = (va.value + size - 1 ) / PAGE_SIZE; for (;;) { PageTableEntry* pte = find_pte_create(pt,vpn); assert(!PageTableEntry_is_valid(pte)); *pte = PageTableEntry_new(ppn,PTE_V | pteflgs); if ( vpn.value == last ) break ; vpn.value+=1 ; ppn.value+=1 ; } }
1 2 3 4 5 6 void PageTable_unmap (PageTable* pt, VirtPageNum vpn) { PageTableEntry* pte = find_pte(pt,vpn); assert(!PageTableEntry_is_valid(pte)); *pte = PageTableEntry_empty(); }
3.开启MMU 3.1内存初始化
上图所示,内存由高到低排列,定义内核起始地址为KERNBASE = 0x80200000
,
内核的代码被编译器编译后是由代码段和数据段组成的,可以在os.map
中看见各段的地址空间,
代码段结束的地址设定为etext
,数据段结束的地址设定为kernelend
。
然后指定从内核结束后向上128M
的空间为空闲内存,可以给应用使用的。
os.ld
对链接文件进行修改,指定了链接脚本结束后按照页对齐
定义了两个符号:PROVIDE(etext = .);
,PROVIDE(kernelend = .);
,etext
就代表了内核代码段结束的地址,kernelend
就代表了内核结束的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 OUTPUT_ARCH(riscv) ENTRY(_start) MEMORY { ram (rxai!w) : ORIGIN = 0x80200000, LENGTH = 128M } SECTIONS { skernel = .; /* 定义内核起始内存地址 */ .text : { *(.text .text.*) . = ALIGN(0x1000); /* text结束的地址按照页对齐 */ PROVIDE(etext = .); } >ram .rodata : { *(.rodata .rodata.*) } >ram .data : { . = ALIGN(4096); *(.sdata .sdata.*) *(.data .data.*) PROVIDE(_data_end = .); } >ram .bss :{ *(.sbss .sbss.*) *(.bss .bss.*) *(COMMON) } >ram PROVIDE(kernelend = .); }
初始化:
U模式下可用的内存是从kernelend
开始到 PHYSTOP
结束
``PHYSTOP的定义为
#define PHYSTOP (KERNBASE + 1281024 1024)` 即128M空间的大小
address.c
PGROUNDDOWN
宏:将地址向下取整到页边界。
1 2 3 4 5 6 7 8 9 10 11 12 13 StackFrameAllocator FrameAllocatorImpl; extern char kernelend[];#define PGROUNDDOWN(a) (((a)) & ~(PAGE_SIZE-1)) void frame_alloctor_init () { StackFrameAllocator_new(&FrameAllocatorImpl); StackFrameAllocator_init(&FrameAllocatorImpl, \ ceil_phys(phys_addr_from_size_t (kernelend)), \ ceil_phys(phys_addr_from_size_t (PHYSTOP))); printk("Memoery start:%p\n" ,kernelend); printk("Memoery end:%p\n" ,PHYSTOP); }
3.2 内存映射 采用恒等映射:虚拟地址映射后的物理地址是相同的,这样在启用mmu
后,原先的代码执行逻辑不变。 虚拟内存和物理内存恒等映射
address.c
kvmmake
函数用于构建内核的页表,映射内核代码段和数据段到物理内存。kvminit
函数用于初始化内核页表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 extern char etext[];PageTable kvmmake (void ) { PageTable pt; PhysPageNum root_ppn = StackFrameAllocator_alloc(&FrameAllocatorImpl); pt.root_ppn = root_ppn; printk("root_ppn:%p\n" ,phys_addr_from_phys_page_num(root_ppn)); printk("etext:%p\n" ,(u64)etext); PageTable_map(&pt,virt_addr_from_size_t (KERNBASE),phys_addr_from_size_t (KERNBASE), \ (u64)etext-KERNBASE , PTE_R | PTE_X | PTE_U) ; printk("finish kernel text map!\n" ); PageTable_map(&pt,virt_addr_from_size_t ((u64)etext),phys_addr_from_size_t ((u64)etext ), \ PHYSTOP - (u64)etext , PTE_R | PTE_W | PTE_U) ; printk("finish kernel data and physical RAM map!\n" ); return pt; } PageTable kernel_pagetable; void kvminit () { kernel_pagetable = kvmmake(); }
首先建立一个根页表,从空闲内存中拿出一页,然后映射内核代码段,再映射数据段, 代码段的属性是可执行可读的,数据段的属性是可读可写的,且U模式不可访问。
由于我们现在是将U模式的应用和内核代码一起打包了,所以肯定U模式下的代码肯定是执行不了的,需要后面实现一个读取应用的模块来加载app 。
然后是内核的映射表建立:内核的代码段只占两页内存: 0x80200000
,0x80201000
, 内核根页表放在0x80250000
即空闲内存开始的第一页。虚拟地址0x80200000
的三级页号的索引为 2 1 0 ,0x80201000
的三级页号的索引为 2 1 1 ,
通过下图的三次查表就对应上了具体的物理内存
3.3 开启Sv39分页模式 要开启Sv39
的分页模式,只需要去写satp
的值就行了:设置为Sv39
分页模式,然后将root_ppn
的值写入。这里有一个刷新TLB
的操作。
快表 (TLB, Translation Lookaside Buffer) , 它维护了部分虚拟页号到页表项的键值对。当 MMU 进行地址转换的时候,首先会到快表中看看是否匹配,如果匹配的话直接取出页表项完成地址转换而无需访存;否则再去查页表并将键值对保存在快表中。一旦我们修改 satp 就会切换地址空间,快表中的键值对就会失效(因为快表保存着老地址空间的映射关系,切换到新地址空间后,老的映射关系就没用了)。为了确保 MMU 的地址转换能够及时与 satp 的修改同步,我们需要立即使用 sfence.vma
指令将快表清空,这样 MMU 就不会看到快表中已经过期的键值对了。
address.c
开启分页模式
kvminithart
函数用于初始化当前 CPU 核心的页表。它会设置页表寄存器 satp
,并刷新 TLB(Translation Lookaside Buffer),确保新页表的生效。
宏定义 :
SATP_SV39
:表示 SV39 模式的 satp
值,高 4 位为 1000。
MAKE_SATP(pagetable)
:将页表地址与 SATP_SV39
组合成 satp
寄存器的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #define SATP_SV39 (8L << 60) #define MAKE_SATP(pagetable) (SATP_SV39 | (((u64)pagetable))) void kvminithart () { printk("satp:%lx\n" ,MAKE_SATP(kernel_pagetable.root_ppn.value)); sfence_vma(); w_satp(MAKE_SATP(kernel_pagetable.root_ppn.value)); sfence_vma(); reg_t satp = r_satp(); printk("satp:%lx\n" ,satp); }
riscv.h
定义sfence和satp 两个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static inline void w_satp (reg_t x) { asm volatile ("csrw satp, %0" : : "r" (x)) ; } static inline reg_t r_satp () { reg_t x; asm volatile ("csrr %0, satp" : "=r" (x) ) ; return x; } static inline void sfence_vma () { asm volatile ("sfence.vma zero, zero" ) ; }
4.测试
main.c
修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 extern void frame_alloctor_init () ;extern void kvminit () ;extern void kvminithart () ;void os_main () { printk("hello timer os!\n" ); frame_alloctor_init(); kvminit(); kvminithart(); trap_init(); while (1 ) { } }
测试
可见:
root_ppn
:对齐到了0x80250000
etext
:对齐到了0x80202000
代码段
本节工作总结:
首先对PTE
和PT
进行了定义,并且实现了将PPN
转化为物理地址的函数
仿照rcore
,实现了内存映射,即由VPN
通过三级页表向PPN
转换 目标是最终获取物理地址
然后,对内存进行了初始化,定义应用程序的内存空间,按照页对齐的方式对齐了etext
代码段数据和 向下取整的方式对齐数据段data
并且定义了内存初始化函数frame_allocator_init()
采用恒等映射的方式,将虚拟地址映射到物理地址上去
最后设置SATP
开启Sv39
分页模式