函数选择

我们逐行分析以下三个关键函数:

  1. kvminit():初始化内核页表
  2. kvmmake():创建并映射内核页表
  3. mappages():完成虚拟地址到物理地址的映射

kvminit() – 初始化内核页表

位置:kernel/vm.c

void kvminit(void)
{
  kernel_pagetable = kvmmake();  // 创建页表结构
}
  • 全局变量 kernel_pagetable 是一个内核页表指针(pagetable_t 类型,本质是 uint64*)。
  • 实际工作都由 kvmmake() 完成。

kvmmake() – 创建页表并设置内核映射

pagetable_t
kvmmake(void)
{
  pagetable_t pagetable;
  pagetable = (pagetable_t) kalloc();  // 分配一个物理页作为 root 页表
  memset(pagetable, 0, PGSIZE);        // 清零

  // 映射 trampoline(用于 trap/ret)
  kvmmap(pagetable, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);

  // 映射内核 text 段(只读 + 可执行)
  kvmmap(pagetable, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);

  // 映射内核 data 段(只读 + 可写)
  kvmmap(pagetable, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);

  // 映射设备内存(UART、PLIC、VIRTIO 等)
  kvmmap(pagetable, UART0, UART0, PGSIZE, PTE_R | PTE_W);
  kvmmap(pagetable, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  kvmmap(pagetable, PLIC, PLIC, 0x400000, PTE_R | PTE_W);

  return pagetable;
}

分析:

  • kalloc() 分配的页是物理地址,但 xv6 中通过 direct mapping,可以直接作为虚拟地址用;
  • 每次 kvmmap() 调用的参数:

    • 第一个是目标页表;
    • 第二个是虚拟地址(VA);
    • 第三个是对应的物理地址(PA);
    • PGSIZE 是页大小(通常 4096);
    • 最后是权限位(读写执行)。

kvmmap()mappages()

void
kvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if (mappages(pagetable, va, pa, sz, perm) != 0)
    panic("kvmmap");
}

kvmmap() 就是一层封装,调用 mappages() 处理映射。


重点函数:mappages()

int
mappages(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm)
{
  uint64 a, last;
  pte_t *pte;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + sz - 1);

  for (;;){
    pte = walk(pagetable, a, 1);  // 取出/创建 PTE 指针
    if (pte == 0)
      return -1;
    if (*pte & PTE_V)             // 已经映射了
      panic("remap");

    *pte = PA2PTE(pa) | perm | PTE_V;  // 写入物理页号 + 权限 + valid 位

    if (a == last)
      break;

    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

理解:

  • vava + sz 范围内每一页做一次映射;
  • walk() 负责找到对应的三级页表项,如果需要就创建;
  • PA2PTE(pa) 把物理地址变成 PFN(右移 12 位);
  • | perm | PTE_V 把权限位与 valid 位合并。

walk() – 模拟 Sv39 页表查找过程

pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  for (int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if (*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else {
      if (!alloc || (pagetable = (pde_t*)kalloc()) == 0)
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE((uint64)pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

解释:

  • Sv39 使用三级页表,每级 9 bits;
  • PX(level, va) 宏会取出第 level 级页表索引(level=2 是最高级);
  • walk() 遍历每一级,如果没有页表页,就在 alloc==1 时新建一个;
  • 最终返回最底层页表(level 0)的条目的地址。

小结:完整地址空间建立过程

main() -> kvminit()
         -> kvmmake()
            -> kalloc()           // 分配 root 页表
            -> kvmmap() * N     // 映射 trampoline、text、data、设备等
               -> mappages()      // 为每页创建映射
                  -> walk()       // 遍历多级页表,找到 PTE
                     -> kalloc() // 必要时创建中间页表页

每个 PTE 保存了:

  • PFN(物理页号)
  • 权限标志位(R/W/X)
  • PTE_V:有效位