xv6-内存管理
内存管理
物理内存初始化
xv6 在 main 函数中调用 kinit1 和 kinit2 来初始化物理内存,kinit1 初始化内核末尾到物理内存 4M 的物理内存空间为未使用
kinit2 初始化剩余内核空间到 PHYSTOP 为未使用
kinit1(end, P2V(4*1024*1024)); // phys page allocator
kinit2(P2V(4*1024*1024), P2V(PHYSTOP)); // must come after startothers()kinit2 在内核构建了新页表后,能够完全访问内核的虚拟地址空间,所以在这里初始化所有物理内存,并开始了锁机制保护空闲内存链表。
void
kinit2(void *vstart, void *vend)
{
freerange(vstart, vend);
kmem.use_lock = 1;
}
内核新页表初始化
main 函数通过调用 kvmalloc 函数来实现内核新页表的初始化
pde_t *kpgdir; // for use in scheduler()
void
kvmalloc(void)
{
kpgdir = setupkvm();
switchkvm();
}
// 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 能够有效管理物理内存,让上层只需要考虑虚拟地址空间。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
int use_lock;
struct run *freelist;
} kmem;
xv6 内存管理函数
xv6 通过提供几个接口来实现内核页表的控制和用户页表的控制,xv6 让每个进程都有独立的页表结构,在切换进程时总是需要切换页表;
switchkvm 简单地将 kpgdir 设置为 cr3 寄存器的值,这个页表仅仅在 scheduler 内核线程中使用。
页表和内核栈都是每个进程独有的,xv6 使用结构体 proc 将它们统一起来,在进程切换的时候,他们也往往随着进程切换而切换,内核中模拟出了一个内核线程,它独占内核栈和内核页表 kpgdir,它是所有进程调度的基础。
switchuvm 通过传入的 proc 结构负责切换相关的进程独有的数据结构,其中包括 TSS 相关的操作,然后将进程特有的页表载入 cr3 寄存器,完成设置进程相关的虚拟地址空间环境。
进程的页表在使用前往往需要初始化,其中必须包含内核代码的映射,这样进程在进入内核时便不需要再次切换页表,进程使用虚拟地址空间的低地址部分,高地址部分留给内核,设置页表时通过调用 setupkvm、allocuvm、deallocuvm 接口完成相关操作
// Switch h/w page table register to the kernel-only page table,
// for when no process is running.
void
switchkvm(void)
{
lcr3(V2P(kpgdir)); // switch to the kernel page table
}
// Switch TSS and h/w page table to correspond to process p.
void
switchuvm(struct proc *p)
{
pushcli();
cpu->gdt[SEG_TSS] = SEG16(STS_T32A, &cpu->ts, sizeof(cpu->ts)-1, 0);
cpu->gdt[SEG_TSS].s = 0;
cpu->ts.ss0 = SEG_KDATA << 3;
cpu->ts.esp0 = (uint)proc->kstack + KSTACKSIZE;
// setting IOPL=0 in eflags *and* iomb beyond the tss segment limit
// forbids I/O instructions (e.g., inb and outb) from user space
cpu->ts.iomb = (ushort) 0xFFFF;
ltr(SEG_TSS << 3);
if(p->pgdir == 0)
panic("switchuvm: no pgdir");
lcr3(V2P(p->pgdir)); // switch to process's address space
popcli();
}xv6 vm.c 文件中还提供了 loaduvm 将文件系统上的 i 节点内容读取载入到相应的地址上,通过 allocuvm 接口为用户进程分配内存和设置页表,然后调用 loaduvm 接口将文件系统上的程序载入到内存,便能够为 exec 系统调用提供接口,为用户进程的正式运行做准备。
// Load a program segment into pgdir. addr must be page-aligned
// and the pages from addr to addr+sz must already be mapped.
int
loaduvm(pde_t *pgdir, char *addr, struct inode *ip, uint offset, uint sz)
{
uint i, pa, n;
pte_t *pte;
if((uint) addr % PGSIZE != 0)
panic("loaduvm: addr must be page aligned");
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, addr+i, 0)) == 0)
panic("loaduvm: address should exist");
pa = PTE_ADDR(*pte);
if(sz - i < PGSIZE)
n = sz - i;
else
n = PGSIZE;
if(readi(ip, P2V(pa), offset+i, n) != n)
return -1;
}
return 0;
}在 vm.c 中,copyuvm 负责复制一个新的页表并分配新的内存,新的内存布局和旧的完全一样,xv6 使用这个函数作为 fork 的底层实现
// Given a parent process's page table, create a copy
// of it for a child.
pde_t*
copyuvm(pde_t *pgdir, uint sz)
{
pde_t *d;
pte_t *pte;
uint pa, i, flags;
char *mem;
if((d = setupkvm()) == 0)
return 0;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walkpgdir(pgdir, (void *) i, 0)) == 0)
panic("copyuvm: pte should exist");
if(!(*pte & PTE_P))
panic("copyuvm: page not present");
pa = PTE_ADDR(*pte);
flags = PTE_FLAGS(*pte);
if((mem = kalloc()) == 0)
goto bad;
memmove(mem, (char*)P2V(pa), PGSIZE);
if(mappages(d, (void*)i, PGSIZE, V2P(mem), flags) < 0)
goto bad;
}
return d;
bad:
freevm(d);
return 0;
}uva2ka 将一个用户地址转化为内核地址,也就是通过用户地址找到对应的物理地址,然后退出这个物理地址在内核页表中的虚拟地址并返回,copyout 则调用 uva2ka 则拷贝 p 地址 len 字节到用户地址 va 中
// Map user virtual address to kernel address.
char*
uva2ka(pde_t *pgdir, char *uva)
{
pte_t *pte;
pte = walkpgdir(pgdir, uva, 0);
if((*pte & PTE_P) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
return (char*)P2V(PTE_ADDR(*pte));
}
// Copy len bytes from p to user address va in page table pgdir.
// Most useful when pgdir is not the current page table.
// uva2ka ensures this only works for PTE_U pages.
int
copyout(pde_t *pgdir, uint va, void *p, uint len)
{
char *buf, *pa0;
uint n, va0;
buf = (char*)p;
while(len > 0){
va0 = (uint)PGROUNDDOWN(va);
pa0 = uva2ka(pgdir, (char*)va0);
if(pa0 == 0)
return -1;
n = PGSIZE - (va - va0);
if(n > len)
n = len;
memmove(pa0 + (va - va0), buf, n);
len -= n;
buf += n;
va = va0 + PGSIZE;
}
return 0;
}