什么是虚拟地址? ——这里存在某种形式的映射关系;并且映射关系对于实现隔离性来说有帮助。
映射的实现:借助page table(多级)。OS为了(模拟)映射转换,有了kvminit和walk等(其实真正的转换是由MMU实现的)。快速的转换(TLB – 缓存)
page table里面是什么?
typedef uint64 pte_t
typedef uint64* pagetable_t
pagetable[i],即pte,pte是下一个page(或者pagetable)的指针(虚拟地址)。 – 转换后可以变成物理地址,然后再去取page(或者pagetable)
图1 不同进程之间不相互干扰
图2 地址空间相互独立
(如果看不到上面两张图片,可以看图1 图2)
kalloc保存了空余page的列表,如果这个列表为空或者耗尽了,那么kalloc会返回一个空指针,内核会妥善处理并将结果返回给用户应用程序。并告诉用户应用程序,要么是对这个应用程序没有额外的内存了,要么是整个机器都没有内存了。
页表是在硬件中通过处理器和内存管理单元(MMU,Memory Management Unit)实现
(如果图片打不开,可以看图片)
MMU并不会保存page table,它只会从内存中读取page table,然后完成翻译,其中内存的page table地址被保存在SATP寄存器中(Supervisor Address Translation and Protection Register) – 当进程切换的时候,也许呀改变SATP寄存器的内容(一般来说进程切换->页表就需要切换,每个进程一个独立的虚拟存储空间)
内核会写SATP寄存器,写SATP寄存器是一条特殊权限指令。所以,用户应用程序不能通过更新这个寄存器来更换一个地址对应表单,否则的话就会破坏隔离性。所以,只有运行在kernel mode的代码可以更新这个寄存器。
并不是为每一个PA (physical address)创建一个table项,而是为每一个page创建一个表单。RISCV中page size = 4096Bytes (4KB),其他系统中,页面大小也大多数为4K – 深有体会,之前的直接io,必须以4K(页大小)为单位分配内存—人麻了
Virtual address:(EXT - index - offset) 其中index用来查找page,offset查找page中对应的哪一个字节(EXT应该是Extend 扩展)。-- offset必须是12bit,因为对应了一个page的4096个字节
Physical address:(physical page number**(PPN) - offset**) (MMU地址转换,需要将index对应成PPN,offset直接copy过来就行)
在RISC-V中:
虚拟地址:64bit,但是只有39bit拿来使用,有27bit被用来当做index,12bit被用来当做offset。offset必须是12bit,因为对应了一个page的4096个字节。
物理内存地址是56bit,其中44bit是物理page号(PPN,Physical Page Number),剩下12bit是offset完全继承自虚拟内存地址(也就是地址转换时,只需要将虚拟内存中的27bit翻译成物理内存中的44bit的page号,剩下的12bitoffset直接拷贝过来即可)。
why多级页表?因为保存页表太多了,页表需要页表的索引。
这里用的是三层索引结构
打不开上图,可以访问:图片地址
L2 - L1 - L0 各占9bit,其中L2对应的page也是4K大小
一个directory是4096Bytes,就跟page的大小是一样的。Directory中的一个条目被称为PTE(Page Table Entry)是64bits,就像寄存器的大小一样,也就是8Bytes。所以一个Directory page有512个条目(4096 / 8)。 – 因为条目应该相当于下一级页表的首地址,因此使用寄存器大小来存储。
每次MMU翻译(映射)地址,其实都是翻译了三次,因为有三级页表。
当一个PTE是无效的(不在内存中),硬件会返回一个page fault,对于这个page fault,操作系统可以更新 page table并再次尝试指令。
TLB是PTE的缓存,因为前面的三级页表,每次翻译都要访问内存 – 太耗时。
其实OS也并不知道TLB是怎么实现的(硬件实现),但是每次切换进程的时候,都需要清空TLB。
附加 :
学生提问:在这个机制中,TLB发生在哪一步,是在地址翻译之前还是之后?Frans教授:整个CPU和MMU都在处理器芯片中,所以在一个RISC-V芯片中,有多个CPU核,MMU和TLB存在于每一个CPU核里面。RISC-V处理器有L1 cache,L2 Cache,有些cache是根据物理地址索引的,有些cache是根据虚拟地址索引的,由虚拟地址索引的cache位于MMU之前,由物理地址索引的cache位于MMU之后。
学生提问:之前提到,硬件会完成3级 page table的查找,那为什么我们要在XV6中有一个walk函数来完成同样的工作?Frans教授:非常好的问题。这里有几个原因,首先XV6中的walk函数设置了最初的page table,它需要对3级page table进行编程所以它首先需要能模拟3级page table。另一个原因或许你们已经在syscall实验中遇到了,在XV6中,内核有它自己的page table,用户进程也有自己的page table,用户进程指向sys_info结构体的指针存在于用户空间的page table,但是内核需要将这个指针翻译成一个自己可以读写的物理地址。如果你查看copy_in,copy_out,你可以发现内核会通过用户进程的page table,将用户的虚拟地址翻译得到物理地址,这样内核可以读写相应的物理内存地址。这就是为什么在XV6中需要有walk函数的一些原因。
如果打不开上图,可以访问:图片地址
(从图中右侧)可以看到最下面是未被使用的地址,这与主板文档内容是一致的(地址为0)。地址0x1000是boot ROM的物理地址,当你对主板上电,主板做的第一件事情就是运行存储在boot ROM中的代码,当boot完成之后,会跳转到地址0x80000000,操作系统需要确保那个地址有一些数据能够接着启动操作系统。
PLIC是中断控制器(Platform-Level Interrupt Controller)。
CLINT(Core Local Interruptor)也是中断的一部分。所以多个设备都能产生中断,需要中断控制器来将这些中断路由到合适的处理函数。
UART0(Universal Asynchronous Receiver/Transmitter)负责与Console和显示器交互。
VIRTIO disk,与磁盘进行交互。
可以向同一个物理地址映射两个虚拟地址,你可以不将一个虚拟地址映射到物理地址。可以是一对一的映射,一对多映射,多对一映射。
.text 和 .data的区别:
例如Kernel text page被标位R-X,意味着你可以读它,也可以在这个地址段执行指令,但是你不能向Kernel text写数据。通过设置权限我们可以尽早的发现Bug从而避免Bug。对于Kernel data需要能被写入,所以它的标志位是RW-,但是你不能在这个地址段运行指令,所以它的X标志位未被设置。(注,所以,kernel text用来存代码,代码可以读,可以运行,但是不能篡改,kernel data用来存数据,数据可以读写,但是不能通过数据伪装代码在kernel中运行)
用户的进程中的内存是从上面图片的free memory处分配过来的。
如果打不开上图,可以访问:图片链接
(所以说,其实argv是存在stack中的 – 确实,因为是函数的传入参数)
是kernel main()函数调用的一部分,kernel main() 和 xv6的启动过程,可以看6.S081 Lab00 xv6启动过程
kvminit源代码如下
typedef uint64 *pagetable_t; // 512 PTEs
/*
* the kernel's page table.
*/
pagetable_t kernel_pagetable;
/*
* create a direct-map page table for the kernel and
* turn on paging. called early, in supervisor mode.
* the page allocator is already initialized.
*/
void
kvminit()
{
kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);
// uart registers
kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface 0
kvmmap(VIRTION(0), VIRTION(0), PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface 1
kvmmap(VIRTION(1), VIRTION(1), PGSIZE, PTE_R | PTE_W);
// CLINT
kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmmap((uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}
// 函数的第一步是为最高一级page directory分配物理page(注,调用kalloc就是分配物理page)。下一行将这段内存初始化为0。
kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);
// uart 是寄存器地址
// qemu puts UART registers here in physical memory.
// #define UART0 0x10000000L
// uart registers
kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);
用gdb调试(过程同6.S081 Lab00 xv6启动过程)
在keminit处设置断点,然后单步调试,可以看到 输出内容如下(具体可以看Lab 6.S081 Lab3 page tables)
page table 0x0000000087f6e000
..0: pte 0x0000000021fda801 pa 0x0000000087f6a000
.. ..0: pte 0x0000000021fda401 pa 0x0000000087f69000
.. .. ..0: pte 0x0000000021fdac1f pa 0x0000000087f6b000
.. .. ..1: pte 0x0000000021fda00f pa 0x0000000087f68000
.. .. ..2: pte 0x0000000021fd9c1f pa 0x0000000087f67000
这里需要注意的是,当pte的r,w,x标志位都是0的时候,说明pte是属于L2或者L1的。
if ((cur_pte & (PTE_R|PTE_W|PTE_X)) == 0) {
recursive_vmprint((pagetable_t)cur_PA, pg_level - 1);
}
最低一级page directory中的PTE有读写标志位,并且Valid标志位也设置了(Lab3 - 1会用到)
内核会持续的按照这种方式,调用kvmmap来设置地址空间。之后会对VIRTIO0、CLINT、PLIC、kernel text、kernel data、最后是TRAMPOLINE进行地址映射。
代码如下(它是kvminit之后的下一个函数)
// Switch h/w page table register to the kernel's page table,
// and enable paging.
void
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma();
}
代码解析:首先设置了SATP寄存器,kernel_pagetable变量来自于kvminit第一行。所以这里实际上是内核告诉MMU来使用刚刚设置好的page table。
在这条指令之前,还不存在可用的page table,所以也就不存在地址翻译。执行完这条指令之后,程序计数器(Program Counter)增加了4。而之后的下一条指令被执行时,程序计数器会被内存中的page table翻译。
所以这条指令的执行时刻是一个非常重要的时刻。因为整个地址翻译从这条指令之后开始生效,之后的每一个使用的内存地址都可能对应到与之不同的物理内存地址。因为在这条指令之前,我们使用的都是物理内存地址,这条指令之后page table开始生效,所有的内存地址都变成了另一个含义,也就是虚拟内存地址。
这里能正常工作的原因是值得注意的。因为前一条指令还是在物理内存中,而后一条指令已经在虚拟内存中了。比如,下一条指令地址是0x80001110就是一个虚拟内存地址。
– 从这一刻起,运行的时候,就必须经过page table的翻译了,当page table设置出错,就会导致虚拟地址不能被正确翻译 – kernel 会停止运行并panic。
walk是kernel里面模拟MMU的函数,接收pagetable,然后返回va 对应的L0 page table的pte 的地址 – 这个函数的ret是返回了PTE的指针。在之前的kvminit函数中,kvmmap会对每个地址或者每个page调用walk函数。
代码如下
// Return the address of the PTE in page table pagetable
// that corresponds to virtual address va. If alloc!=0,
// create any required page-table pages.
//
// The risc-v Sv39 scheme has three levels of page-table
// pages. A page-table page contains 512 64-bit PTEs.
// A 64-bit virtual address is split into five fields:
// 39..63 -- must be zero.
// 30..38 -- 9 bits of level-2 index.
// 21..39 -- 9 bits of level-1 index.
// 12..20 -- 9 bits of level-0 index.
// 0..12 -- 12 bits of byte offset within the page.
// extract the three 9-bit page table indices from a virtual address.
// #define PXMASK 0x1FF // 9 bits
// #define PXSHIFT(level) (PGSHIFT+(9*(level)))
// #define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)
static pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
if(va >= MAXVA)
panic("walk");
for(int level = 2; level > 0; level--) {
pte_t *pte = &pagetable[PX(level, va)];
if(*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
} else {
if(!alloc || (pagetable = (pde_t*)kalloc()) == 0)
return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE(pagetable) | PTE_V;
}
}
return &pagetable[PX(0, va)];
}
一些问答:
学生:我想知道,在SATP寄存器设置完之后,walk是不是还是按照相同的方式工作?
Frans:是的。它还能工作的原因是,内核设置了虚拟地址等于物理地址的映射关系,这里很重要,因为很多地方能工作的原因都是因为内核设置的地址映射关系是相同的。
学生:每一个进程的SATP寄存器存在哪?
Frans:每个CPU核只有一个SATP寄存器,但是在每个proc结构体,如果你查看proc.h,里面有一个指向page table的指针,这对应了进程的根page table物理内存地址。(可以看)
// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// the sscratch register points here.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
/* 72 */ uint64 t0;
/* 80 */ uint64 t1;
/* 88 */ uint64 t2;
/* 96 */ uint64 s0;
/* 104 */ uint64 s1;
/* 112 */ uint64 a0;
/* 120 */ uint64 a1;
/* 128 */ uint64 a2;
/* 136 */ uint64 a3;
/* 144 */ uint64 a4;
/* 152 */ uint64 a5;
/* 160 */ uint64 a6;
/* 168 */ uint64 a7;
/* 176 */ uint64 s2;
/* 184 */ uint64 s3;
/* 192 */ uint64 s4;
/* 200 */ uint64 s5;
/* 208 */ uint64 s6;
/* 216 */ uint64 s7;
/* 224 */ uint64 s8;
/* 232 */ uint64 s9;
/* 240 */ uint64 s10;
/* 248 */ uint64 s11;
/* 256 */ uint64 t3;
/* 264 */ uint64 t4;
/* 272 */ uint64 t5;
/* 280 */ uint64 t6;
};
proc.h
中的proc
结构体:包含一个自旋锁,父进程指针,…, 内存大小,trapframe的地址,pagetable的首地址。
// Per-process state
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; // Bottom of kernel stack for this process
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // Page table
struct trapframe *tf; // 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)
// added by levi
pagetable_t levi_kernel_pagetable;
};