Xv6--页表

一、页表 Page tables

1、功能

xv6对此的描述:

Page tables are the mechanism through which the operating system provides each process with its own private address space and memory. 
Page tables determine what memory addresses mean, and what parts of physical memory can be accessed. 
They allow xv6 to isolate different process’s address spaces and to multiplex them onto a single physical memory.
Page tables also provide a level of indirection that allows xv6 to perform a few tricks: 
mapping the same memory (a trampoline page) in several address spaces, and guarding kernel and user stacks with an unmapped page. 

总结一下:页表是操作系统提供的一种机制,通过页表,操作系统可以隔离每个进程的地址空间;可以让进程使用比物理内存更多的内存;可以实现再进程之间共享数据。
额外说明:地址空间
物理地址:物理地址就是真实的内存地址,就是CPU可以读写的内存位置,(这里又涉及到 独立编址和统一编址,此处不在详述)
虚拟地址:可以理解成,这个地址实际是不存在的,CPU需要MMU和页表将虚拟地址转换成物理地址才能正常工作,那为什么要设计虚拟地址呢?答案就是页表的功能。
内核地址空间:Xv6分为内核态和用户态,内核地址空间,就是在内核态使用的虚拟地址
用户地址空间:在用户态下使用的虚拟地址

2、硬件支持

Xv6运行在Sv39 RISC-V,64位机器,故CPU寻址是64位,但是只使用了低39位,高位保留。操作系统将页表地址填入satp寄存器,打开MMU,之后CPU会将加载的地址都认为是虚拟地址,寻址时,MMU回将该地址翻译成物理地址,再返回对应内存的值或将值写入正确的内存位置。

3、页表使用说明

在Xv6中,如果是设置一级页表,则39位地址中,12-39位作为page index,页表PTE索引
现在以一级页表为例来说明如何寻址:
VA:virtual address
VA:0x1111001100 = (001 0001 0001 0001 0000 0000 0001 0001 0000 0000)b 39位地址1111001100
VA[12:39] :(001 0001 0001 0000 0000 0001)=0x111001
(page_table[VA[12:39]] << 12) | VA[11:00] = PA[39:00]
可以看到Page Table之所以叫页表,其实他就是真实的存在在内存中的一个表,在操作系统初始化期间,会手动填充这个表,在一级页表中,寻址长度位39位,其中高27位是PTE索引,所以在操作系统初始化期间,会申请一块内存存储这个表,这段内存的大小可以计算出:
表我们可以理解为一个数组,数组的每一项就是物理地址的高27位,这里就算作8字节,一共有0x7FFFFF项,故需要申请 80x7FFFFF个字节的空间,也就是82^27Bytes。可以看到一级页表对内存造成了很大的浪费,故前人们又想出了多级页表的概念。
这里以三级页表为例来说明
同样的将虚拟地址空间分段,在三级页表中,64位地址使用了低39位,高27位分为三段,每段9位,则可以推算出,每一级页表需要82^7Bytes = 8512Bytes,所以每一级页表有512项PTE,51283的内存大小,这大大减少了页表所占用的内存大小;低12位地址,为页表offset,这里用低12为作为offset的原因:页表:首先我们会将内存的地址空间,也就是可以寻址的内存范围,抽象成 ”页“, 每个 页 又设定成 4096个字节也即0x1000,那么我们知道计算机是从0开始计数,故 0 - 4096-1 一共4096项,即0xFFF刚好是十二位,所以通过低12位的offset,可以查找到一个页中的每一个地址。综上所述,通过三级页表,CPU拿到一个VA地址,将一级页表地址写入MMU中,MMU在一级页表中通过VA一级页表索引找出二级页表的地址,再通过VA中二级页表的索引找出三级页表,再通过VA中三级页表索引找出对应的页,再将VA低12位地址offset加上,就找到VA所对应的PA物理地址。
VA[38:30] 9位为第一级页表的index
VA[29:21] 9位为第二级页表的index
VA[20:12] 9位为第三级页表的index
VA[11:00] 12位为页中offset

4、Xv6页表的实现

页表并不是一次性创建成功的,而是在使用的过程中创建的。xv6在建立内核地址空间时,将内核地址空间做了一个布局,可以看到左边是我们要实现的虚拟地址空间,右边时物理地址空间,基本时一个一一映射的关系,区别是在虚拟地址空间将Trampoline和Kstack映射到最高地址,这里设计成这样涉及到中断的相关知识,以后有机会另起一文来补充。
kernel_address

5、代码

main() --> kvminit() --> kvmmake() --> kvmmap() --> mappages() --> walk()

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

// add a mapping to the kernel page table.
// only used when booting.
// does not flush TLB or enable paging.
void
kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(kpgtbl, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

// Create PTEs for virtual addresses starting at va that refer to
// physical addresses starting at pa. va and size might not
// be page-aligned. Returns 0 on success, -1 if walk() couldn't
// allocate a needed page-table page.
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  if(size == 0)
    panic("mappages: size");
  
  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
    if((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if(*pte & PTE_V)
      panic("mappages: remap");
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

// Return the address of the PTE in page table pagetable
// that corresponds to virtual address va.  If alloc!=0,
// create any required page-table pages.
//
// The risc-v Sv39 scheme has three levels of page-table
// pages. A page-table page contains 512 64-bit PTEs.
// A 64-bit virtual address is split into five fields:
//   39..63 -- must be zero.
//   30..38 -- 9 bits of level-2 index.
//   21..29 -- 9 bits of level-1 index.
//   12..20 -- 9 bits of level-0 index.
//    0..11 -- 12 bits of byte offset within the page.
pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc)
{
  if(va >= MAXVA)
    panic("walk");

  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(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)];
}

实际上申请内存并填充页表的就是walk函数。我们的页表要做成的最终结果是:
这里我举例说明,UART0 地址为 0x10000000L,因为这里虚拟地址与物理地址一一对应,所以
VA = 0x10000000, VA[38:30] = 0 VA[29:21] = 0x80 VA[20:12] = 0 VA[11:00] = 0
PA = 0x10000000
现在申请页表地址,假设
一级页表地址为 0x87FFF000 二级页表地址0x87FFE000 三级页表0x87FFD000,每个页表一共512项,每一项共8个字节。
额外说明,页表项叫做PTE,我们知道页表地址一定是4096字节对齐的,所以PTE的低12位在做寻址时肯定会清零,所以PTE的低12位可以作为PTE属性,Xv6只使用了低10位来表示PTE的属性,比如 该内存可读、可写、可执行、是否共享、用户操作权限、内核操作权限等等属性,这里不详述。

现在通过代码执行可以得到
0x87FFF000[0] = ((0x87FFE000 >>12) << 10) | PTE_V.(PTE_V 0x1 << 0 表示该PTE有效)
0x87FFF000[0] = 21FF F801
可以看到,在一级页表的第一项中,存放了二级页表的地址,这里是存放在一级页表的第一项是因为VA[38:30]=0,虚拟地址的一级页表索引位0,同理,三级页表的地址存放在二级页表的0x80位置
0x87FFE000[128] = ((0x87FFD000 >> 12) << 10) | PTE_V (PTE有效)
0x87FFE000[128] = 0x21FFF401
同理物理地址所在的页存放在 三级页表的第0项,因为VA[20:12]=0
0x87FFD000[0] = ((0x10000000 >> 12) << 10) | PTE_V | PTE_W | PTE_R (PTE有效 可读 可写)
0x87FFD000[0] = 0x04000007
至此,我们就将UART0 0X10000000(VA) 映射到了0x10000000(PA) ,之后访问0x10000000就是访问实际的0x10000000这个地址。这就是我们需要用代码实现的过程,可以用gdb调试,最终结果于此相同,Xv6的实现是很巧妙的。

5、总结

首先我们将内存抽象成一个个页,每个页大小4096字节,然后实现虚拟地址空间与物理地址空间映射时,我们要指定该物理地址映射到哪个虚拟地址,映射大小(必须页对齐),我们使用了三级页表,寻址空间只使用了64位地址的低39位,将其分成四段,前三段每段9位,分别对应每一级页表的index,第4段十二位,表示在实际页中偏移。举个例子:
前面我们实现了0x10000000 映射到 0x10000000,三级页表查询
如果我们现在访问0x10000010,访问过程如下
VA = 0x1000 0010 VA[38:30] = 0 ⇒ 一级页表0x87FFF000[0] = 0x21FFF801,该PTE有效,计算出二级页表的地址
((0x21FFF801 >> 10) << 12) = 0x87FFE000(左移时为了将低10位属性位清零,右移12位是为了保持4096字节对齐) ⇒ VA[29:21] = 0x80 = 128 ⇒ 0x87FFE000[128] = 0x21FFF401,该PTE有效,计算出三级页表的地址
(0x21FFF401 >> 10) << 12) = 0x87FFD000 ⇒ VA[20:12] = 0 ⇒ 0x87FFD000[0] = 0x04000007,该PTE有效,并且该页可读可写,该页的物理地址位 ((0x04000007 >> 10) << 12) = 0x10000000 ⇒ 这里找出了VA对应的页,又页偏移,VA[11:0] = 0x010, 0x10000000 + VA[11:00] = 0x10000010,最终就可以访问到指定的物理地址,就实现了一次虚拟地址到物理地址的转换,然后实际这样的地址转换是不需要我们做的,硬件上的MMU就是做这些事情的。

  • 32
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值