MIT 6.S081 第三章笔记 | 页表
页表让每个进程都拥有自己独立的虚拟内存,从而实现内存隔离。
3.1 Paging hardware
用户和内核都只能操作虚拟地址(\(virtual \space address\)),但是实际物理内存使用物理地址(\(physical \space address\))来索引。页表提供了虚拟地址到逻辑地址的转换。
xv6只使用了64位地址空间中的低39位,其中高\(27\)位为页面号,低\(12\)位为页内偏移,即\(4096(2^{12})\)字节一个\(page\),同kernel/riscv.h
中PGSIZE
相同,一个进程的虚拟内存可以有\(2^{27}\)个\(page\),对应到\(2^{27}\)个页表项\((page \space table \space entries,
PTEs)\)。每个\(PTE\)存储\(44\)位的物理地址和10位标记,总共\(54\)位,即一个PTE需要8字节来存储。每个物理地址高
\(44\) 位是PTE中存储, 后\(12\)位用页内偏移。一个物理地址总共用\(56\)位表示。
RISC-V页表并不是整个存储在内存中的(因为很难找到空闲的整块内存存储),而是采用三级树结构,来使得页表空间可以动态分配和离散存储。每个页表就是一页。第一级页表是一个4096字节的页,包含了512个PTE(因为每个PTE需要8字节),每个PTE存储了下级页表的页物理地址。第二级列表由512个页构成(\(512 *
4096字节\)),第三级列表由512*512个页构成。因为每个进程虚拟地址的高27位用来确定PTE,对应到3级页表就是最高的9位确定一级页表PTE的位置(即偏移量),中间9位确定二级页表PTE的位置,最低9位确定三级页表PTE的位置。每一页内地址是连续的,但是不同页之间内存不一定连续。如下图所示。第一级根页表的物理地址存储在satp
寄存器中,每个CPU核心拥有自己独立的satp
。
PTE flag可以告诉硬件这些相应的虚## 3.4 Physical memory allocation
xv6在运行时分配或释放页表、用户内存、内核栈、管道缓冲区等各种物理内存的不同用途。xv6中这些内存都分配在内核数据的末位和PHYSTOP之间,每次分配4096字节,即4KB空间。
分配和释放是通过对空闲页链表进行追踪完成的(kernel/kalloc.c:struct kmem
),分配空间就是将一个页从链表中移除,释放空间就是将一页增加到链表中。
kernel的物理空间的分配函数在kernel/kalloc.c
中,每个页在链表中的元素是struct run
,每个run存储在空闲页本身中。这个空闲页的链表freelist
由spin lock
保护,包装在struct kmem
中。
kinit
:初始化所有空闲内存列表,在内核刚启动的时候调用。从kernel end
到PHYSTOP
之间的所有内存都按页清空,并存放在freelist
中。freerange
:将range中的每一个页面都调用一次free()
来将其插入到freelist
的末尾。
3.5 进程地址空间
3.2 Kernel address space
QEMU
会模拟一块从0x80000000
开始的内存,到至少0x88000000
。0x80000000
以下的地址被视为直接与设备交互,而不是内存。
## 3.4 Physical memory allocation
xv6在运行时分配或释放页表、用户内存、内核栈、管道缓冲区等各种物理内存的不同用途。xv6中这些内存都分配在内核数据的末位和PHYSTOP之间,每次分配4096字节,即4KB空间。
分配和释放是通过对空闲页链表进行追踪完成的(kernel/kalloc.c:struct kmem
),分配空间就是将一个页从链表中移除,释放空间就是将一页增加到链表中。
kernel的物理空间的分配函数在kernel/kalloc.c
中,每个页在链表中的元素是struct run
,每个run存储在空闲页本身中。这个空闲页的链表freelist
由spin lock
保护,包装在struct kmem
中。
kinit
:初始化所有空闲内存列表,在内核刚启动的时候调用。从kernel end
到PHYSTOP
之间的所有内存都按页清空,并存放在freelist
中。freerange
:将range中的每一个页面都调用一次free()
来将其插入到freelist
的末尾。
3.5 进程地址空间
trampoline page
在用户空间和内核空间中都在同一个虚拟地址。以便能在user
和kernel
间切换时方便的访问。- kernel stack page:每个进程有一个自己的内核栈kstack,每个kstack下面有一个没有被映射的guard page,guard page的作用是防止kstack溢出影响其他kstack。
3.3 Code: creating an address space
大多数和管理页表相关的代码都存放在kernel/vm.c
中。核心结构体是pagetable_t
,实际是一个指向一块8字节内存的指针(见kernel/riscv.h
最后一行)。
重要的函数:
walk
(kernel/vm.c
): 模拟RISC-V三级分页硬件。给定虚拟地址和页表,返回最终页表的PTE。mappages
:给定一个页表,虚拟地址和物理地址,通过在页表中写入PTE来建立映射。kvminit
:调用kvmmap
来创建内核页表的映射。kvminithart
: 把kernel页表的物理地址写入satp寄存器。从w_satp
这行代码后,页表开始启用,地址都变成虚拟地址。
每个RISC-V CPU会把PTE缓存到Translation Look-aside Buffer (TLB)中,当xv6更改了页表时,必须通知CPU来取消掉当前的TLB,取消当前TLB的函数是sfence.vma(),在kvminithart中被调用
3.4 Physical memory allocation
xv6在运行时分配或释放页表、用户内存、内核栈、管道缓冲区等各种物理内存的不同用途。xv6中这些内存都分配在内核数据的末位和PHYSTOP之间,每次分配4096字节,即4KB空间。
分配和释放是通过对空闲页链表进行追踪完成的(kernel/kalloc.c:struct kmem
),分配空间就是将一个页从链表中移除,释放空间就是将一页增加到链表中。
kernel的物理空间的分配函数在kernel/kalloc.c
中,每个页在链表中的元素是struct run
,每个run存储在空闲页本身中。这个空闲页的链表freelist
由spin lock
保护,包装在struct kmem
中。
kinit
:初始化所有空闲内存列表,在内核刚启动的时候调用。从kernel end
到PHYSTOP
之间的所有内存都按页清空,并存放在freelist
中。freerange
:将range中的每一个页面都调用一次free()
来将其插入到freelist
的末尾。
3.5 User space address
每个进程都有一个单独的页表,当内核在进程之间切换的时候,他也会修改对于的页表。
当进程向内核索要更多用户内存的时候,xv6会调用kalloc
来分配物理内存。然后会向进程页表中添加新的PTE项,并附带PTE_W
、PTE_R
、PTR_U
和PTE_V
标志。
从这里可以看到页表的几个使用好处:1.
不同进程的页表将同样的用户地址映射到不同的内存中,从而每个进程都拥有独自的内存。2.
每个进程的虚拟地址都一段从0开始的连续地址,但实际的物理地址并不用为连续的。3.
内核将所有进程运行状态都存储在虚拟空间顶部的trampoline
页面中(没有设置PTE_U
权限),防止用户进程修改其他数据。
3.6 Code : exec
exec是一个system call
,为以ELF格式定义的文件系统中的可执行文件创建用户空间。
exec先检查头文件中是否有ELF_MAGIC
来判断这个文件是否是一个ELF格式定义的二进制文件,用proc_pagetable
来为当前进程创建一个还没有映射的页表,然后用uvmalloc
来为每个ELF
segment分配物理空间并在页表中建立映射,然后用loadseg
来把ELF
segment加载到物理空间当中。注意uvmalloc
分配的物理内存空间可以比文件本身要大。
接下来exec分配user
stack,它仅仅分配一页给stack,通过copyout
将传入参数的string放在stack的顶端,然后在ustack的下方分配一个guard page
如果exec检测到错误,将跳转到bad标签,释放新创建的pagetable并返回-1。exec必须确定新的执行能够成功才会释放进程旧的页表(proc_freepagetable(oldpagetable, oldsz)),否则如果system call不成功,就无法向旧的页表返回-1