mmu.h源码中给出了XV6虚拟地址的构成,及所代表的含义
mmu.h中还有页表的相关信息,每个页目录都与1024条记录,每一个页表中也有1024条记录,每一页的大小4096字节,也就是4kb。
// Page directory and page table constants. #define NPDENTRIES 1024 // # directory entries per page directory #define NPTENTRIES 1024 // # PTEs per page table #define PGSIZE 4096 // bytes mapped by a page
内核加载到内存后,在main函数中,首先调用kinit1(),由于这时候的虚拟地址 [KERNBASE, KERNBASE+4MB) 映射到 物理地址[0, 4MB),所以内核实际能用的虚拟地址空间显然是不足以完成正常工作的,所以初始化过程中需要重新设置页表。如图:
xv6在main函数中调用kinit1和kinit2来初始化物理内存。
kinit1初始化内核末尾到物理内存4M的物理内存空间为未使用。调用freerange将空闲内存加入到空闲内存链表中
void kinit1(void *vstart, void *vend) { initlock(&kmem.lock, "kmem"); kmem.use_lock = 0; freerange(vstart, vend); } kinit1(end, P2V(4*1024*1024));
在内核构建了新页表后,能够完全访问内核的虚拟地址空间,然后kinit2初始化剩余内核空间到PHYSTOP为未使用,开始了锁机制保护空闲内存链表。
void kinit2(void *vstart, void *vend) { freerange(vstart, vend); kmem.use_lock = 1; } kinit2(P2V(4*1024*1024), P2V(PHYSTOP));
之前说调用kinit2之前需要构建新的页表,main函数通过调用kvmalloc函数来实现内核新页表的初始化。通过初始化,最后内存布局和地址空间如下:内核末尾物理地址到物理地址PHYSTOP的内存空间未使用 ;虚拟地址空间KERNBASE以上部分映射到物理内存低地址相应位置
void kvmalloc(void) { kpgdir = setupkvm(); switchkvm(); }
在setupkvm中,调用mappages来建立内核所需的虚拟地址到物理地址映射。阅读mappages的源码,对于每一个待映射的虚拟地址,调用walkpgdir找到该地址对应的PTE地址,然后保存对应的物理地址、权限。
在说一下 walkpgdir,这个函数用来计算PTE(页表条目)的地址,他会根据va的前十位来先找到在页目录的条目。如果该条目不存在,说明该页表不存在;如果alloc参数被设置,walkpgdir会分配页表页并将其物理地址放到页目录中。最后用虚拟地址的接下来10位来找到其在页表中的 PTE地址。
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; } //mmu.h #define PDX(va) (((uint)(va) >> PDXSHIFT) & 0x3FF) 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)]; }
另外,在setupkvm中,所有的映射,最后都存储在kmap这样一个结构体中,下面这是kmap的结构
// 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 };
xv6对上层提供kalloc和kfree接口来管理物理内存,上层无需知道具体的细节,kalloc返回虚拟地址空间的地址,kfree以虚拟地址为参数,通过kalloc和kfree能够有效管理物理内存,让上层只需要考虑虚拟地址空间。
kalloc分配4kb(相当于一页的大小)的物理内存,并返回一个内核可以使用的指针,如果返回0说明这页无法被分配。
kfree以虚拟地址为参数,用来释放一页的内存空间
void kfree(char *v) { struct run *r; if((uint)v % PGSIZE || v < end || v2p(v) >= PHYSTOP) panic("kfree"); // Fill with junk to catch dangling refs. memset(v, 1, PGSIZE); if(kmem.use_lock) acquire(&kmem.lock); r = (struct run*)v; r->next = kmem.freelist; kmem.freelist = r; if(kmem.use_lock) release(&kmem.lock); } // Allocate one 4096-byte page of physical memory. // Returns a pointer that the kernel can use. // Returns 0 if the memory cannot be allocated. char* kalloc(void) { struct run *r; if(kmem.use_lock) acquire(&kmem.lock); r = kmem.freelist; if(r) kmem.freelist = r->next; if(kmem.use_lock) release(&kmem.lock); return (char*)r; }
xv6将未分配的内存组成一个链表,一个结构体
struct run { struct run *next; }; struct { struct spinlock lock; int use_lock; struct run *freelist; } kmem;
xv6实际上通过保存虚拟地址空间的freelist,然后通过页表找到物理地址。由此,层调用的只需要想着“虚拟地址a对应的一页释放为空闲页”“分配一页返回虚拟地址给我”即可。