Chapter3 Page Table
页表让每个进程都拥有自己独立的虚拟内存地址,从而实现内存隔离。
Paging Hardware
Basic Concepts
虚拟内存:是硬件异常、硬件地址翻译、主存、磁盘文件和内核软件的完美交互,他为每个进程提供了一个大的、一致和私有的地址空间。虚拟内存提供了3个重要能力:
- 它将主存看成是一个存储在磁盘上的地址空间的高速缓存, 在主存中只保存活动区域, 并根据需要在磁盘和主存之间来回传送数据, 通过这种方式,它高效地使用了主存。
- 它为每个进程提供了一致的地址空间, 从而简化了内存管理。
- 它保护了每个进程的地址空间不被其他进程破坏。
使用虚拟寻址, CPU通过生成一个虚拟地址(Virtual Address, VA)来访问主存,这 个虚拟地址在被送到内存之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址 的任务叫做地址翻译(address translation)。就像异常处理一样, 地址翻译需要CPU硬件 和操作系统之间的紧密合作。CPU芯片上叫做内存管理单元(Memory Management Unit, MMU)的专用硬件, 利用存放在主存中的查询表来动态翻译虚拟地址, 该表的内容由操作 系统管理。
虚拟地址:作为提醒,RISC-V指令(包括用户和内核)操纵虚拟地址。
物理地址:机器的RAM,即物理内存,是用物理地址来索引的。
页表硬件:RISC-V的页表硬件通过将每个虚拟地址映射到一个物理地址,将这两种地址连接起来。
Translation From VA To PA
xv6运行于Sv39 RISC-V,即在64位地址中只有最下面的39位被使用作为虚拟地址,其中底12位是页内偏移,高27位是页表索引,即4096()字节作为一个page,一个进程的虚拟内存可以有 个page,对应到页表中就是个page table entry (PTE)。每个PTE有一个44位的physical page number (PPN)用来映射到物理地址上和10位flag,总共需要54位,也就是一个PTE需要8字节存储。即每个物理地址的高44位是页表中存储的PPN,低12位是页内偏移,一个物理地址总共由56位构成。
更详细地说,分页硬件通过使用39位中的前27位索引到页表中找到一个PTE,并制作一个56位的物理地址,其前44位来自PTE中的PPN,其后12位是从原始虚拟地址中复制的。
在实际中,页表并不是作为一个包含了个PTE的大列表存储在物理内存中的,而是采用了三级树状的形式进行存储,这样可以让页表分散存储。每个页表就是一页。第一级页表是一个4096字节的页,包含了512个PTE(因为每个PTE需要8字节),每个PTE存储了下级页表的页物理地址,第二级列表由512个页构成,第三级列表由512*512个页构成。因为每个进程虚拟地址的高27位用来确定PTE,对应到3级页表就是最高的9位确定一级页表PTE的位置,中间9位确定二级页表PTE的位置,最低9位确定三级页表PTE的位置。如下图所示。第一级根页表的物理页地址存储在satp寄存器中,每个CPU拥有自己独立的satp
PTE flag可以告诉硬件这些相应的虚拟地址怎样被使用,比如PTE_V表明这个PTE是否存在,PTE_R、PTE_W、PTE_X控制这个页是否允许被读取、写入和执行,PTE_U控制user mode是否有权访问这个页,如果PTE_U=0,则只有supervisor mode有权访问这个页
Kernel Address Space
Xv6为每个进程维护一个页表,描述每个进程的用户地址空间,而对于内核,只有一个单一的页表描述内核的地址空间。
内核对其地址空间的布局进行调控,使其能够以可预测的虚拟地址访问物理内存和各种硬件资源。
内核虚拟地址主要有两个部分:
- From KERNBASE To PHYSTOP:QEMU模拟一台计算机,包括从物理地址0x80000000开始,至少持续到0x86400000的RAM(物理内存),xv6称之为PHYSTOP。
- Below KERNVASE (I/O Devices):QEMU的模拟还包括I/O设备,如磁盘接口。QEMU将设备接口作为内存映射的控制寄存器暴露给软件,这些寄存器位于物理地址空间的0x80000000以下。
Direct Mapping:内核使用 "直接映射 "获得RAM和内存映射的设备寄存器,也就是说,将资源映射到与物理地址相等的虚拟地址。
直接映射简化了读取或写入物理内存的内核代码。
有一些不是直接映射的内核虚拟地址:
- The trampoline page.(和user pagetable在同一个虚拟地址,以便在user space和kernel space之间跳转时切换进程仍然能够使用相同的映射,真实的物理地址位于kernel text中的
trampoline.S) - The kernel stack pages.每个进程有一个自己的内核栈kstack,每个kstack下面有一个没有被映射的guard page,guard page的作用是防止kstack溢出影响其他kstack。当进程运行在内核态时使用内核栈,运行在用户态时使用用户栈。注意:还有一个内核线程,这个线程只运行在内核态,不会使用其他进程的kstack,内核线程没有独立的地址空间。
Code: Creating An Address Space
xv6中和页表相关的代码在kernel/vm.c中。最主要的结构体是pagetable_t,这是一个指向页表的指针。kvm开头的函数都是和kernel virtual address相关的,uvm开头的函数都是和user virtual address相关的,其他的函数可以用于这两者
几个比较重要的函数:
walk:给定一个虚拟地址和一个页表,返回一个PTE指针。walkaddr:查找virtual address对应的physical address。首先调用walk()函数获取对应的PTE,之后把其中的physical address取出来。
freewalk:递归地释放页表页所占用的内存。mappages:给定一个页表、一个虚拟地址和物理地址,创建一个PTE以实现相应的映射。uvm开头的对user page table进行管理。exec()的实现proc_pagetable()初始化pagetable。uvmcreate()。创建一个空的user page table,首先调用kalloc()分配一页page,其次调用memset()函数对该页表进行初始化清零工作。- 之后会
mappages(),主要是装载trampoline code和TRAPFRAME,但是装载失败,会调用uvmfree()进行page table的释放。 uvmfree()。该函数主要被proc_pagetable()函数和proc_freepagetable()函数调用。先调用uvmunmap()->kfree()释放用户内存页,再调用freewalk()释放对应的页表页。如果sz=0,则只需要释放对应的page-table pages。uvmalloc:为进程1. 分配物理内存2. 分配PTE。如果分配成功就继续,否则就调用kfree()对内存进行释放和uvmdealloc()对PTE归零操作。uvmdealloc()。该函数主要在uvmalloc()函数中被调用,用于当kalloc()失败或者mappages()失败时的撤销动作。其主要用于撤销user pages,使进程的size从oldsz变为newsz。如果newsz ≥ oldsz,就什么都不做,否则就调用uvmunmap()来删除从newsz开始的n页pages。uvmunmap()。从va开始删除n页pages的PTE,也就是删除了va到pa的映射关系。首先调用walk()查询va对应的PTE,之后将其置为0。可选择是否释放对应的物理内存,通过调用kfree()完成。
uvmclear()。该函数主要被exec()调用。用于标记一个PTE为user态不可用,用作user stack guard page。首先调用walk()函数找到该PTE,并将对应的位PTE_U置为false。uvmcopy()。给定一对父子进程的页表地址,复制父进程的页表和物理内存到子进程中去。该函数主要用于fork()函数的调用。
kvminit用于创建kernel的页表,使用kvmmap来设置映射。kvminithart将kernel的页表的物理地址写入CPU的寄存器satp中,然后CPU就可以用这个kernel页表来翻译地址了。procinit(kernel/proc.c)为每一个进程分配(kalloc)kstack。KSTACK会为每个进程生成一个虚拟地址(同时也预留了guard pages),kvmmap将这些虚拟地址对应的PTE映射到物理地址中,然后调用kvminithart来重新把kernel页表加载到satp中去。copyout。从kernel(src)拷贝数据到user(dstva)。首先调用walkaddr()查找dstva的pa,之后调用memmove()进行拷贝。copyin()。从user(srcva)拷贝数据到kernel(dst)。首先调用walkaddr()查找srcva的pa,之后调用memmove()进行拷贝。copyinstr()。将一个以 null 结尾的字符串从user复制到kernel。
1.
kinit(),首先释放end→PHYSTOP之间的内存,其中end代表first address after kernel。2.
kvminit()与kvmmake()函数。也可以证明kernel text和kernel data已经在内存中了,因为全程都只是调用了kvmmap()函数对这一段内存建立了映射关系,而没有涉及到内存的分配。对kernel stack的一个设置,这一块因为之前没有被分配过内存,就需要首先kalloc()再kvmmap()从而先分配内存,后装载对应的PTE到页表中。
Physical memory allocation for kernel
内核必须在运行时为页表、用户内存、内核堆栈和管道缓冲区分配和释放物理内存。
xv6对kernel space和PHYSTOP之间的物理空间在运行时进行分配,分配以页(4096 bytes)为单位。分配和释放是通过对空闲页链表进行追踪完成的,分配空间就是将一个页从链表中移除,释放空间就是将一页增加到链表中。
kernel的物理空间的分配函数在kernel/kalloc.c中,每个页在链表中的元素是struct run,每个run存储在空闲页本身中。这个空闲页的链表freelist由spin lock保护,包装在struct kmem中。
User space memory
每个进程有自己的用户空间下的虚拟地址,这些虚拟地址由每个进程自己的页表维护,用户空间下的虚拟地址从0到MAXVA。
当进程向xv6索要更多用户内存时,xv6先用kalloc来分配物理页,然后向这个进程的页表增加指向这个新的物理页的PTE,同时设置这些PTE的flag。
该图是一个进程在刚刚被exec调用时的用户空间下的内存地址,stack只有一页,包含了exec调用的命令的参数从而使main(argc, argv)可以被执行。stack下方是一个guard page来检测stack溢出,一旦溢出将会产生一个page fault exception。
Code:sbrk & exec
sbrk是一个可以让进程增加或者缩小用户空间内存的system call。调用
exec是一个system call,为以ELF格式定义的文件系统中的可执行文件创建用户空间。
- 虚拟页号(VPN)
- 页表项(PTE)
- 物理帧号(PFN)
参考
深入理解计算机系统