xv6页表

页表系统

一级页表


内存地址64bit,虚拟内存空间使用low39位(蓝色部分,EXT是不使用的地址位);物理内存地址空间使用56位(红色部分)。
虚拟地址的low39中,高27位是index,用来索引页表中的具体"一行"页表项。一个页表项在物理内存中是4096Byte,offset用来索引具体一个Byte。
物理地址的low56中,高44位是物理页号(PPN),低12位直接使用虚拟地址中的offset的12位。
这段地址翻译过程中,虚拟地址的27位用来在页表中索引一个页表项(灰色部分的一行),找到页表项后,可以从中读到44位的PPN,直接对应了物理地址中的44位。然后使用虚拟地址的offset12位,来索引具体的一个Byte。
在这个一级页表设计中,存储页表所需要的物理空间是(2^27)*(64bit)=2^27*8B = 128*8MB= 1G。那么系统中每一个进程都需要1G的物理空间来存储这张页表,以进行虚拟地址对物理地址的转换。

三级页表

在三级页表的设计中,虚拟地址的index被分为3级。最高的一级(9位)用来索引第一级页表,第一级页表在物理内存中存储的地址写在寄存器satp中。从第一级页表中索引到ppn后,可以直接找到第二级页表在物理内存中的位置,再用中间的9bit来索引第二级页表中的页表项。

例如,如果只为了映射一个物理地址时,这种三级页表的方式,只需要在物理内存中存储三张页表,分别对应这张图中的三张页表。因为每一级页表只要找到相应的ppn就行了,所以只需要这3级ppn被存储的页表。那么每张页表的物理空间占用为2^9*64bit=512*8B=4KB,三张页表总共占用12KB。相比之前一级页表的设计,同样为了映射一个物理地址,此时一个进程只需要12KB,之前需要1G。

TLB

指令都使用虚拟地址,例如load/store指令,其使用的虚拟地址需要过一遍页表,才能找到物理地址。
三级也表的潜在缺点是需要加载三个页表,才能把load/store指令中的虚拟地址转换为物理地址。因此,为了避免从物理内存中加载PTE,risc-v cpu把PTE缓存到TLB中。

satp

为了让硬件使用页表,系统内核必须把根页表(三级页表结构中的L2)写到satp寄存器中。然后指令所涉及的虚拟地址都需要使用satp寄存器指向的页表来进行翻译,转换为物理地址。每个cpu有自己的satp寄存器,这样不同的cpu就可以运行不同的进程。

物理内存

物理内存称为DRAM,物理地址的每个Byte都有相应的地址,指令只声明虚拟地址,再由页表系统翻译为物理地址,去load/store具体dram中的数据。

内核地址空间

xv6系统中,每个进程有一个页表,这个页表描述了每个进程的用户地址空间。除此之外,还有一个统一的页表,用来描述内核地址空间。内核控制了它自己的内存地址空间的结构,并且自己可以直接访问物理内存、硬件资源。

下图描述了内核的虚拟地址空间是如何映射到物理地址空间的。
image
QEMU模拟了一个完整的计算机,包括一个物理内存,地址范围是0x80000到0x8640000(即PHYSTOP),QEMU的模拟还包括了I/O设备,例如硬盘。QEMU把设备接口暴露给软件,以内存映射控制寄存器的方式。内核可以与这些设备进行交互,只需要读写相应的物理地址即可。
对于内核来说,设备和ram都是直接映射的方式进行访问的,即,这些资源的虚拟地址完全等于其物理地址。例如,内核,在虚拟内存空间中和物理内存中,都位于0x80000位置。直接映射简化了内核代码对物理内存进行读写的复杂度。例如,当fork给子进程分配了内存时,分配器返回的内存地址,虽然是虚拟内存地址,但实际上就是物理内存地址。

有下面几种内核的虚拟地址,不是直接映射的:

  1. trampoline page(见 trap部分内容)。这个page在用户的页表中都映射在顶部位置。这是一个全局唯一的物理页,在内核的虚拟内存空间中被映射了两次,一次是直接映射的,一次是在虚拟内存空间中的顶部。
    image

  2. kernel stack page。每个进程都有一份内核的栈页,在每个进程的内存地址空间中,kernel stack page下面是不做内存映射的guard page,这个页面是无效页面,所以如果一个内核溢出了该内核的stack page,guard page就可以触发一个异常,如果没有guard page,那么溢出的部分就会错误的写入其他的内核的内存中。

code: 地址空间的创建

pagetable_t

可以是一个内核的页表,也可以是进程的页表。实际上是个64bit ptr
image

walk函数

PGSHIFT:
定义基础位移,sv39中,高12位不用
image

PXSHIFT & PX:
在L2,L1中位移虚拟地址的9bits
image

walk:

image

例如,这个walk的参数pagetable应该为satp寄存器中存储的根页表,他是一个uint64_t*
在L2级时,虚拟地址va只会保留对应L2的9bits. 这9bits是L2级页表需要的偏移量。pagetable[PX(level, va)]就可以得到相应pte中的数据,即ppn和flag. 如果这个pte没有发生缺页中断,即PTE_V有效,那么直接到了PTE2PA(*pte), 这里是将pte中存储的值,即ppn+flag right shift 10bits,只留ppn,再left shift12,保留出offset位的位置。 \(pagetable_)PTE2PA(\*pte)直接将这个ppn值转换为指向pagetable的指针。因为L2级页表中PTE的内容ppn就是下一级页表的基地址,所以上面直接转成pagetable,即uint64_t的指针是ok的。
除此之外,还存在情况是假设pte的有效位PTE_V为false,即PTE_V为0,那么如果设置了alloc参数,就直接用kalloc在物理内存中分配一个page。如果不做alloc,那么就直接把这个pte的flag设置为1
最后,L0级页表的PTE并没有设置PTE_V有效。

kvm*函数

用来修改、操作内核的page table

uvm*函数

用来修改、操作用户态的page table

其他函数,可以不区分内核、用户态,执行功能

copyin / copyout

这两个函数分别从用户态的内存空间拷贝数据内容,向用户态的内存空间拷贝数据内容。

kvmmake / kvmmap / kvminit

启动的早期阶段,main函数调用kvminit,kvminit通过kvmmake来创建kernel的页表。此时,xv6的page系统还没有构建,因此此时的物理内存、虚拟内存映射都是direct map的. kvmmake首先在物理内存中分配了一个page,用来保存根页表,然后调用kvmmap,来完成kernel需要的地址翻译。

下图中可以看到这个过程
img

以uart0为例
img

进入mappages
img

PGROUNDDOWN是计算va这个虚拟地址到va+size-1这个虚拟地址在向页表宽度对齐之后的虚拟地址。
这里调用walk设置了alloc,即如果找到的pte如果没有的话,就直接分配这个pte(按理说,此时的page系统还没有构建,所以这里都应该是需要alloc的)

如果walk返回的pte是已经PTE_V有效,则当前行为是映射一个已经存在的pte,不符合构建page系统时的预期,所以panic

接下来把物理地址映射到这个pte,并写PTE_V有效
如果需要映射的起始虚拟地址已经到了结束虚拟地址则返回,否则继续映射下一个pte(a += PGSIZE; pa += PGSIZE)

kvminithart

加载了kernel page table到satp寄存器.
在此之后,cpu就可以使用这个kernel page table来做地址翻译

每个cpu在TLB中缓存了pte,当xv6系统需要更换页表时,必须使cpu禁止tlb中的pte,risv有一个指令sfence.vma用来flush当前cpu的tlb。
img

物理内存分配

内核需要在运行时,为页表、用户内存、内核stack、pipe buffer分配空闲的物理内存。xv6系统使用位于内核末端和PHYSTOP之间的内存来做运行时内存分配。一次分配4KB(一个页),通过把所有的page以链接表的形式组织起来,来追踪哪些页是free的。内存的分配操作,实际上是:从连接表中删掉一个page;释放操作,实际上是:增加一个page到连接表中。

问题记录

Q1

无论几级页表,在页表中都有一个flag的表示,就是把每一个页表项的低10位用来表示flag。具体flag的含义可见【三级页表图】中的位表示。一般是读写权限、有效性等等。

Q2

三级页表设计中,后两级页表是通过ppn来索引的,那么offset呢?下面这段文字回答了这个问题,答案就是offset设置位0,因为三级页表中,一个页表是4KB,对应了一个页表项的大小,那么这时就不需要offset来索引具体的Byte了,直接从页表项的开头读就行。

Q3

课程中有这样一个例子:


第2行中..0表示第一级页表中的pte索引是0,..表示这是第一级。pte 0x000...0021fff801表示这个pte的64bit所存储的数据。这其中包含了44bit的ppn和10bit的flag。pa 0x00...0087ffe000表示的是对应的物理地址。因为物理地址是由44bit的ppn和12bit的offset组成。这里就可以验证一下,这个pa中的ppn和pte中的ppn是完全相同的。需要把pte右移10bit,然后转换成0b表示,pa右移12bit,然后转换成0b表示。是完全相同的
观察第1/2级页表项对应的pa,这些pa的最低12bit都是0x000,第一级页表项对应的pa中offset用来索引第二级页表的物理位置,第二级页表项对应的pa中offset用来索引第三级页表的物理位置。根据Q2中的分析,就知道这些offset为啥是0x000了。

接下来,这里用0x10000000这个pa来右移12bit,得到了虚拟地址的27bit index。这个表述成立的原因是:
在xv6中,将部分物理地址映射到了相同的虚拟地址中。见下:
在memlayout.h中定义了物理地址

根据下图,物理地址直接映射到虚拟地址,所以uart0的虚拟地址也会是0x10000000

在看这个kvmmap的函数声明,第二个参数是va,第三个参数是pa。分别表示虚拟地址和物理地址。所以,虚拟地址uart0对应到物理地址uart0。并且UART0被define成0x10000000

所以,uart0的虚拟地址和物理地址都是0x10000000了,这样之前把pa右移12bit得到27bit的index就成立了。

posted @ 2023-04-30 19:17  ijpq  阅读(88)  评论(0编辑  收藏  举报