date: 2019-02-20
tags: OS 6.828
这里会记录阅读6.828课程lecture note的我的个人笔记。可能会中英混杂,不是很适合外人阅读,也请见谅。
我们需要隔离开的address space
每个进程都有自己的内存,耶只能读写自己的内存。
挑战就在于如何能够完成multiplexing的同时保证isolation
pagine机制提供了一个addressing的抽象
CPU -> MMU -> RAM
VA PA
软件只能通过VA进行load/store,而不能通过PA。
kernel告诉MMU该如何进行这个mapping
MMU还可以限制用户能够使用哪些虚拟地址。
x86的上述mapping的基本单元是4KB,这个单元被称为page
同时,mapping是按照4KB对齐的,也就是说每个paging都是start on 4 KB boundary
因为x86的内存为32位,所以后面的12位对应一个page内部的地址,上面提到的page table做的mapping就是用VA前20位对应到PA的前20位。
page table里面的这2^20个entry被称为2^20个page table entry(PTE),我们来看一下一个PTE中有什么:
PTE的前面20位就是对应的PA(实际上是对应的PA中的那个page)的前20位,其被称为physical page number(PPN)。
后面的12位都是flag,如PTEP表示是否存在,PTEW表示是否可写,PTE_U表示user program是否可以使用。
page table被存在哪里呢?
被存在RAM中,MMU会读取或存储PTE
操作系统可以读写PTE
如果page table就仅仅是一个PTE的array,会出现什么问题呢?
首先是太大了,2^20条,每条32bit,整个table就会是4 MB了,这对于早期的机器太大了。
并且对于一个小的程序,它不需要那么多内存,可能只需要几百page,剩下的就浪费了。
所以x86使用了一个"two-level page table"以节省空间
除了在RAM中 分配PTE,还在内存中存一个叫page directory(PD)的东西。
PD也是一个array,其每一个entry被称为PDE,我们来看一下这个PDE的结构,
PDE的前20位也是一个PPN,其指向的page是一个用于存page table的page,存的每个page table会指向1024个PTE。
在PD中有1024个PDE,所以就指向了2^20个PTE。
刚刚提到了对于一个小程序,可能不需要那么多PTE,所以有的PDE可以是invalid,从而可以让address space变得很小。
MMU如何知道page table在RAM的哪里呢?
%cr3
存了PD的地址。PD里面(间接)存了PTE的PA,而这些PTE不一定是连续的。
x86 paging hardware是如何翻译VA的?
首先通过%cr3
找到PD的PA,从而可以加载PD;
然后从VA的前10位找到对应的PT(page table)的PA,从而可以加载PT;
然后用VA的之后10位找到PTE,PTE的前20位,也就是PPN加上VA的最后12位就得到了VA对应的PA。
PTE中的flag
P, W, U
xv6用U来防止用户使用kernel memory
如果这些flag没有被set(没有设为1)会出现什么?
会触发page fault,导致CPU存储寄存器,并强制转化到kernel(进入trap.c
)。
kernel可以选择produce error, kill process或者install a PTE, resume the process
为什么选择mapping而不是其他的,如给一个上下界?
mapping带来的这种indirection让paging hardware可以解决很多问题,如
与其他的很多技巧(这些方法都是啥。。。不知道之后的一讲会不会讲清楚。。。)
为什么在kernel中使用VA?
显然给user process一个page table是很合理的,但是为什么大多数kernel也这么做?
如果没有page table,很容易有memory fragmentation(内存碎片?)
比如先分配64K, 释放,之后分配4K,4K占据了64K的地方,之后再分配64K就没法弄了。
xv6的address space的一个概况:
0x00000000:0x80000000 -- user addresses below KERNBASE
0x80000000:0x80100000 -- map low 1MB devices (for kernel)
0x80100000:? -- kernel instructions/data
? :0x8E000000 -- 224 MB of DRAM mapped here
0xFE000000:0x00000000 -- more memory-mapped devices
为什么用这种布局:
user virtual addresses 从0开始
2GB for user heap to grow contiguously
kernel和user都被map了
kernel永远都被map在固定位置:
kernel线性映射(pa x mapped at va x+0x80000000)
0x80000000
,也就是2G内存下面我们来看代码:
中间的哪些断点之类的东西可能是和现在的版本代码不一样?所以这里为了弄清楚,我来自由发挥一波。
首先在这里需要记录一下,为了方便查找函数出现的位置,经常会使用的指令是
$ grep -n funcname *.[chS]
从main
开始,
// Bootstrap processor starts running C code here.
// Allocate a real stack and switch to it, first
// doing some setup required for memory allocator to work.
int
main(void)
{
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kvmalloc(); // kernel page table
mpinit(); // detect other processors
lapicinit(); // interrupt controller
seginit(); // segment descriptors
picinit(); // disable pic
ioapicinit(); // another interrupt controller
consoleinit(); // console hardware
uartinit(); // serial port
pinit(); // process table
tvinit(); // trap vectors
binit(); // buffer cache
fileinit(); // file table
ideinit(); // disk
startothers(); // start other processors
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()
userinit(); // first user process
mpmain(); // finish this processor's setup
}
main
会先调用kinit1
然后在entrypgdir
的范围里对kernel进行初始化,然后调用kinit2
,其会把剩下的从entrypgdir
到PHYSTOP + KERNBASE
的虚拟内存分给kernel,这两个函数的具体在kalloc.c
中
// Initialization happens in two phases.
// 1. main() calls kinit1() while still using entrypgdir to place just
// the pages mapped by entrypgdir on free list.
// 2. main() calls kinit2() with the rest of the physical pages
// after installing a full page table that maps them on all cores.
void
kinit1(void *vstart, void *vend)
{
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
freerange(vstart, vend); // 注意这里的freerange就是把这个范围里的physical memory free掉
}
void
kinit2(void *vstart, void *vend)
{
freerange(vstart, vend);
kmem.use_lock = 1;
}
之后的userinit
是我们的重点,这个函数的目的就是设置第一个用户process,它在proc.c
中。
void
userinit(void)
{
struct proc *p;
extern char _binary_initcode_start[], _binary_initcode_size[];
p = allocproc();
initproc = p;
if((p->pgdir = setupkvm()) == 0)
panic("userinit: out of memory?");
inituvm(p->pgdir, _binary_initcode_start, (int)_binary_initcode_size);
p->sz = PGSIZE;
memset(p->tf, 0, sizeof(*p->tf));
p->tf->cs = (SEG_UCODE << 3) | DPL_USER;
p->tf->ds = (SEG_UDATA << 3) | DPL_USER;
p->tf->es = p->tf->ds;
p->tf->ss = p->tf->ds;
p->tf->eflags = FL_IF;
p->tf->esp = PGSIZE;
p->tf->eip = 0; // beginning of initcode.S
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
// this assignment to p->state lets other cores
// run this process. the acquire forces the above
// writes to be visible, and the lock is also needed
// because the assignment might not be atomic.
acquire(&ptable.lock);
p->state = RUNNABLE;
release(&ptable.lock);
}
我们这次主要考虑page table相关的内容,所以主要有两个函数,setupkvm()
与inituvm
。首先是setupkvm
:
// There is one page table per process, plus one that's used when
// a CPU is not running any process (kpgdir). The kernel uses the
// current process's page table during system calls and interrupts;
// page protection bits prevent user code from using the kernel's
// mappings.
//
// setupkvm() and exec() set up every page table like this:
//
// 0..KERNBASE: user memory (text+data+stack+heap), mapped to
// phys memory allocated by the kernel
// KERNBASE..KERNBASE+EXTMEM: mapped to 0..EXTMEM (for I/O space)
// KERNBASE+EXTMEM..data: mapped to EXTMEM..V2P(data)
// for the kernel's instructions and r/o data
// data..KERNBASE+PHYSTOP: mapped to V2P(data)..PHYSTOP,
// rw data + free physical memory
// 0xfe000000..0: mapped direct (devices such as ioapic)
//
// The kernel allocates physical memory for its heap and for user memory
// between V2P(end) and the end of physical memory (PHYSTOP)
// (directly addressable from end..P2V(PHYSTOP)).
// This table defines the kernel's mappings, which are present in
// every process's page table.
static struct kmap {
void *virt; // 这一段开始的虚拟地址
uint phys_start; // 实际物理地址起始
uint phys_end; // 实际物理地址结束
int perm;
} kmap[] = {
{ (void*)KERNBASE, 0, EXTMEM, PTE_W}, // I/O space
{ (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0}, // kern text+rodata
{ (void*)data, V2P(data), PHYSTOP, PTE_W}, // kern data+memory
{ (void*)DEVSPACE, DEVSPACE, 0, PTE_W}, // more devices
};
// Set up kernel part of a page table.
pde_t*
setupkvm(void)
{
pde_t *pgdir;
struct kmap *k;
if((pgdir = (pde_t*)kalloc()) == 0) // kalloc会分配4096B的物理内存
return 0;
memset(pgdir, 0, PGSIZE);
if (P2V(PHYSTOP) > (void*)DEVSPACE)
panic("PHYSTOP too high");
for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,
(uint)k->phys_start, k->perm) < 0) {
freevm(pgdir);
return 0;
}
return pgdir;
}
这里的重点函数就是mappages
,
// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned.
static int
mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
{
char *a, *last;
pte_t *pte;
a = (char*)PGROUNDDOWN((uint)va);
last = (char*)PGROUNDDOWN(((uint)va) + size - 1);
for(;;){
if((pte = walkpgdir(pgdir, a, 1)) == 0)
return -1;
if(*pte & PTE_P)
panic("remap");
*pte = pa | perm | PTE_P;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}
在解释mappages
之前,需要先解释一下这里面的walkpgdir
:
// Return the address of the PTE in page table pgdir
// that corresponds to virtual address va. If alloc!=0,
// create any required page table pages.
static pte_t *
walkpgdir(pde_t *pgdir, const void *va, int alloc)
{
pde_t *pde;
pte_t *pgtab;
pde = &pgdir[PDX(va)];
if(*pde & PTE_P){
pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
} else {
if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
return 0;
// Make sure all those PTE_P bits are zero.
memset(pgtab, 0, PGSIZE);
// The permissions here are overly generous, but they can
// be further restricted by the permissions in the page table
// entries, if necessary.
*pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
}
return &pgtab[PTX(va)];
}
walkpgdir
是找寻va
对应的物理地址。
如果PDE不是空指针,同时其存在(PTE_P)被设置了,则再把PDE的物理地址转化为虚拟地址
如果PDE是空的,同时alloc被设置为1了,说明需要分配新的page table了
用va的中间10位来从page table中找到对应的PTE的物理地址
注意PTE的地址指向的值的前20位加上va的后12位就是va对应的pa了。
说完walkpgdir
就可以说回mappages
和setupvm
,其主要就是把kernel的va对应的地址都分配了。
说完setupvm
就说inituvm
,就是把_binary_initcode_start
存在pgdir
的最开始的一个page中。
除去最开始的初始化,在执行一个新的进程的时候也会分配新的内存的,有兴趣可以看一下exec.c
中的代码。