Lab3主题:page tables
源码:https://github.com/InQing/xv6-operating-system/tree/pgtbl
问题的引入:从上一节的系统调用我们知道,用户进程如果要获取内核的数据,必须通过copyout函数实现传递,同理,内核获取用户进程数据也需要通过copyin函数。然而,这样是通过软件方法模拟三级页表的遍历从而实现的,有没有更高效的方式呢?
有——那就是通过硬件。
我们都知道,页表是通过硬件遍历寻址的,MMU,TLB也都是硬件支持,所以速度更快。
(一)前置知识:页表详解
(1)页表映射
- 虚拟地址:
RISC-V中,地址是64位的,但仅用低位39位表示虚拟地址。39位的虚拟地址中,27位为页表项(PTE)的编号,12位为offest - PTE:
内存中,每个PTE的自身大小占4B
PTE由物理块号(PPN)和flags构成。
每个物理块大小为2^12字节(页长)=4KB。物理块号对应物理内存中高44位的地址,物理块号的44位+offset(在物理块中对于物理块起始地址的偏移量)的12位构成了一个物理地址。 - 虚拟地址到物理地址的映射关系:
虚拟地址=PTE编号+offset
PTE编号+页表基地址->物理块号
物理块号+offset=物理地址 - 寻址过程:
(1)MMU从satp寄存器中获取页表的基地址
(2)从虚拟地址中取出PTE编号与offest,根据页表的基地址,MMU在内存中找到该页表,然后遍历页表,由PTE编号找到相应的PTE
(3)从PTE中取出PNN,如果是单级页表,则PNN+offest则为最终物理地址,如果是多级页表,则PNN作为下一级页表的基地址
(2)多级页表
- 多级页表的优势
多级页表的好处是节约页表本身大小所占用的内存
RISC-V中,每级页表占用8bit,即每级页表维护2^8=512个页表项。
查找一个映射,从L2->L1->L0,只需要访问3*512的条目。而如果只有单级页表,一页中包含2^27个条目,需要全部放入内存中。 - 三级页表
SV39中,每个进程维护一张用户地址空间页表和一张内核地址空间页表,每个页表都是三级的。
高地址为内核空间,低地址为用户空间 - 三级页表的映射过程
- 逻辑地址划分:
三级 | 二级 | 一级 | 偏移量 |
---|---|---|---|
L2 | L1 | L0 | Offset |
9bits | 9bits | 9bits | 12bits |
- 从satp寄存器中取出三级页表的基地址A3
计算三级页表条目地址(PTE):A3+L2x4 (每个PTE占4字节)
读取该PTE对应的物理块号PNN,读取结果作为二级页表的基地址A2 - 获取二级页表的基地址A2
计算二级页表条目地址(PTE):A2+L1x4 (每个PTE占4字节)
读取该PTE对应的物理块号PNN,读取结果作为一级页表的基地址A1 - 获取一级页表的基地址A1
计算一级页表条目地址(PTE):A1+L0x4 (每个PTE占4字节)
读取该PTE对应的物理块号PNN,读取结果作为最终物理页面的基地址A0 - 最终物理地址为:A0+offest
另外说明一下,kernel里u开头的函数代表用户进程的,k开头的函数代表内核态的,其中有vm的就是和虚拟内存相关的
(二)Print a page table
(1)实验要求
写一个函数vmprint(pagetable_t pagetable),用于打印页表,包括页表深度,PTE与PA(物理地址,其实可以理解为PNN)
(2)实验思路
按照提示,分析一下freewalk
void freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for (int i = 0; i < 512; i++)
{
pte_t pte = pagetable[i];
if ((pte & PTE_V) && (pte & (PTE_R | PTE_W | PTE_X)) == 0)
{
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
}
else if (pte & PTE_V)
{
panic("freewalk: leaf");
}
}
kfree((void *)pagetable);
}
通过分析可知:
- pagetable:页表基地址。i:PTE编号。pte(pagetable[i]):PTE,由PNN与flag组成。child:下一级页表的基地址,如果已经是一级页表了,那就是最终物理地址。PTE2PA:将PNN转为物理地址,原型为
#define PTE2PA(pte) (((pte) >> 10) << 12)
,可以看到,实际上就是清除了PTE的标志位,然后扩展成一个56位的物理地址(PNN本身44位) - pte & PTE_V:判断该PTE是否valid
- pte & (PTE_R | PTE_W | PTE_X):判断PTE是否可读、写、或执行。通过这个条件可以判断是否是最后一级页表,因为只有最后一级的pte才对应最终物理地址,一定满足读写可执行之一的条件,而第二、第三级的pte对应的只是下一级页表的索引地址,不需要读写或可执行
- 页表的三级遍历由递归实现
(3)实验代码
根据上述分析,容易给出代码。
因为要打印起始页表的地址,不适合放入递归里写,所以拆成了两个函数。
注意,打印的… … …表示的是页表的深度而非级数,事实上是从三级页表开始索引,但深度是一。
void vmprint(pagetable_t pagetable)
{
printf("page table %p\n", pagetable);
printwalk(pagetable, 1); // 递归打印页表
}
void printwalk(pagetable_t pagetable, int depth)
{
for (int i = 0; i < 512; i++)
{
pte_t pte = pagetable[i];
if (pte & PTE_V)
{
// PTE合法
uint64 child = PTE2PA(pte);
// 按format打印level,PTE与PA
switch (depth)
{
case 1:
printf("..");
break;
case 2:
printf(".. ..");
break;
case 3:
printf(".. .. ..");
break;
}
printf("%d: pte %p pa %p\n", i, pte, child);
// 只有在页表的最后一级,才可读写或可执行
// 若不在最后一级,则继续递归
if ((pte & (PTE_R | PTE_W | PTE_X)) == 0)
printwalk((pagetable_t)child, depth + 1);
}
}
}
最后,在exec.c里添加调用
...
if(p->pid == 1)
vmprint(p->pagetable);
return argc; // this ends up in a0, the first argument to main(argc, argv)
(三)A kernel page table per process
(1)实验要求
xv6原本的设计是,每个用户进程维护各自的用户页表,但当进入内核态时,所有进程都切换到同一张内核页表,这张内核页表是全局共享的
该实验的目的是让每个进程都有一张属于自己的内核页表,这一步的作用会在下一个实验揭晓。
(2)实验步骤
**1.**在PCB中添加内核页表
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
struct proc *parent; // Parent process
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
->pagetable_t kernelpagetable; // kernel page table for per process
struct trapframe *trapframe; // data page for trampoline.S
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
};
2. 初始化内核页表函数 proc_kvminit()
先来看看 kvminit() 函数,它是初始化 全局内核页表 的
void kvminit()
{
kernel_pagetable = (pagetable_t)kalloc()