Skip to the content.

xv6-riscv中使用的物理内存分配单位是页(Page),也就是4KB(4096B)。 也就是说,干啥都要按页来处理。

内存的初始化过程,就是下面三个步骤:

main()
{
    // ...
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    // ...
}

其中,kinit将内核未分配的所有内存,都写为01;kvminit映射内核需要的所有页表; kvminithart需要在所有hart上执行,开启分页。

分页是一个物理实现,打开分页后,不管是用户还是内核,都将使用虚拟内存地址进行 内存的访问,无法直接读取物理内存地址。在satp寄存器中写入根页表的地址就开启了分页, 非常简单。

开启分页(初始化到kvminithart)后通过QEMU控制台中可以查看分页情况:

(qemu) help info mem
info mem  -- show the active virtual memory mappings
(qemu) info mem
vaddr            paddr            size             attr
---------------- ---------------- ---------------- -------
000000000c000000 000000000c000000 0000000000400000 rw-----
0000000010000000 0000000010000000 0000000000002000 rw-----
0000000080000000 0000000080000000 0000000000002000 r-x--a-   // 内核代码
0000000080002000 0000000080002000 0000000000006000 r-x----
0000000080008000 0000000080008000 0000000000001000 rw-----
0000000080009000 0000000080009000 0000000000001000 rw---a-
000000008000a000 000000008000a000 0000000007ff6000 rw-----
0000003ffff7f000 0000000087f78000 0000000000040000 rw-----
0000003ffffff000 0000000080007000 0000000000001000 r-x----  // trampoline

但是这个内存布局有几个巧妙之处:

kinit()

kinit初始化内核的内存页面,大概流程如下图:

kmem内包含了一个spinlock,就是为了保护freelist(空闲页链表),防止并发问题。 操作freelist链表的时候使用锁包裹。

end是kerenl.ld中定义的,表示.bss段末尾位置,可以说是内核内存的heap初始位置。

kfree把页内的内存值都设为0x01,不设为0。

struct run 定义的很巧妙,本身只包含一个指针;所以pa既可以是uint64类型的物理地址, 也可以是页首地址,也可以是链表成员!!!

struct run {
  struct run *next;
};

kvminit()

kvminit 设置内核页表,内核页表和用户页表采用的是相同的数据结构pagetable_t,而且这个类型也很普通, 就是一个指针。其实开始也不太理解,为啥就是个指针。其实页表类型,它所指向的就是一个内存页面,而 这个指针就是页的首地址,或者说它是页内第一个PTE的地址。

// vm.c
pagetable_t kernel_pagetable;
// riscv.h
typedef uint64 pte_t;
typedef uint64 *pagetable_t; // 512 PTEs

页表就是用来索引物理页地址到虚拟页地址的,但是页表本身也需要页来存储,所以页表本身也存放在分配 的页中,一个页4096B,一个页表项(PTE=44+10)占用64bit(8B),所以一个页可以存放512个PTE。这也是 xv6采用的Sv39内存结构的重要特性。

内核页表的创建由kvmmake()完成,就是按照内核布局进行映射

// Make a direct-map page table for the kernel.
pagetable_t
kvmmake(void)
{
  pagetable_t kpgtbl;

  kpgtbl = (pagetable_t) kalloc();
  memset(kpgtbl, 0, PGSIZE);

  // uart registers
  kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);

  // virtio mmio disk interface
  kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);

  // PLIC
  kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  // map kernel text executable and read-only.
  kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);

  // map kernel data and the physical RAM we'll make use of.
  kvmmap(kpgtbl, (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(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  // allocate and map a kernel stack for each process.
  proc_mapstacks(kpgtbl);
  
  return kpgtbl;
}

完成后就出现了(qemu)info mem所显示的内存布局了。kvmmap是主要实现,它又封装了mappages函数…大概过程如下:

里面的walk()是比较难理解的,它主要是模拟了riscv分页情况下虚拟地址如何查找物理地址的过程:

每个PTE有效的是44位PPN是指向页表位置或者物理位置的指针(因为都是页对齐的,所以省略12位),10位flags是页权限; 比如L2的PTE.ppn«12 –> L1 页表的初始位置; L0 PTE.ppn«12 –> 物理页面地址。

这两个宏定义了页表项pte如何与物理地址pa转换:

// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
#define PTE2PA(pte) (((pte) >> 10) << 12)

kvminithart()

这个函数反而最简单,清空TLB然后切换页表。

void
kvminithart()
{
  // wait for any previous writes to the page table memory to finish.
  sfence_vma();

  w_satp(MAKE_SATP(kernel_pagetable));

  // flush stale entries from the TLB.
  sfence_vma();
}

将跟页表的地址,写入到satp中,但是不是简单的写入,还有一个MODE需要一块写入,那就是证明xv6使用Sv39类型的虚拟地址SATP_SV39。

// use riscv's sv39 page table scheme.
#define SATP_SV39 (8L << 60)
#define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12))

可以参考riscv手册查看satp寄存器的格式: