1.页表操作

1.1页表项定义

image-20240624160044495

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;
}

/* 获取下级页表的PPN*/
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物理页帧获取

虚拟地址向物理地址转换过程

image-20240620172006572
  • 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;
}
  • 转换为PTE
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的内存映射实现如下

image-20240624165741735

同前面提到的差不多

  • 首先通过VPN[2]结合SATP拿到第三级的PTE 然后不断结合VPN 得到最后的物理地址

  • 首先通过辅助函数拿到虚拟地址的三级VPN

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; // 1_1111_1111 = 0x1ff
vpn.value >>= 9;
}
for (int i = 0; i < 3; i++) {
result[i] = idx[i];
}
}
  • 有了PTE,还需要一个PTPTE 进行管理。因此 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
/* 查找PTE 若没有则新建一个*/
PageTableEntry* find_pte_create(PageTable* pt,VirtPageNum vpn)
{

// 拿到虚拟页号的三级索引,保存到idx数组中
size_t idx[3];
indexes(vpn, idx);
//根节点
PhysPageNum ppn = pt->root_ppn;
//从根节点开始遍历,如果没有pte,就分配一页内存,然后创建一个
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);
//压入栈中
// push(&pt->frames,frame.value);
}
//取出进入下级页表的物理页号
ppn = PageTableEntry_ppn(pte);
}
}
  • find_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)
{
// 拿到虚拟页号的三级索引,保存到idx数组中
size_t idx[3];
indexes(vpn, idx);
//根节点
PhysPageNum ppn = pt->root_ppn;
//从根节点开始遍历,如果没有pte,就分配一页内存,然后创建一个
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;

//printk("ppn:%d\n",ppn.value);
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内存初始化

image-20240624202737775
  • 上图所示,内存由高到低排列,定义内核起始地址为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 + 12810241024)` 即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()
{
// 初始化时 kernelend 需向上取整
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);
// // map kernel text executable and read-only.
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");
// map kernel data and the physical RAM we'll make use of.
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 00x80201000的三级页号的索引为 2 1 1,
  • 通过下图的三次查表就对应上了具体的物理内存

image-20240624205704543

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()
{
// wait for any previous writes to the page table memory to finish.
printk("satp:%lx\n",MAKE_SATP(kernel_pagetable.root_ppn.value));
sfence_vma();

w_satp(MAKE_SATP(kernel_pagetable.root_ppn.value));

// flush stale entries from the TLB.
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
// supervisor address translation and protection;
// holds the address of the page table.
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;
}

// 刷新 TLB.
static inline void sfence_vma()
{
// the zero, zero means flush all TLB entries.
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初始化
trap_init();


while (1)
{
/* code */
}

// task_init();

// timer_init();


// run_first_task();

}

测试

可见:

  • root_ppn:对齐到了0x80250000
  • etext:对齐到了0x80202000 代码段

image-20240624211356863

本节工作总结:

  • 首先对PTEPT进行了定义,并且实现了将PPN转化为物理地址的函数
  • 仿照rcore,实现了内存映射,即由VPN通过三级页表向PPN转换 目标是最终获取物理地址
  • 然后,对内存进行了初始化,定义应用程序的内存空间,按照页对齐的方式对齐了etext代码段数据和 向下取整的方式对齐数据段data并且定义了内存初始化函数frame_allocator_init()
  • 采用恒等映射的方式,将虚拟地址映射到物理地址上去
  • 最后设置SATP开启Sv39分页模式