内存管理

物理内存初始化

  • 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;
    }