riscv-xv6单步调试3 虚拟内存的初始化

0. 序

这一部分主要记录main函数如何初始化xv6的虚拟内存,main函数依次调用了kinit(), kvminit(), kvminithart()。

1. kinit()函数

struct run {
  struct run *next;
};

struct {   //kmem是用于管理物理页面的数据结构,就是一个链表。
  struct spinlock lock;  
  struct run *freelist;
} kmem;
 //最终达成的效果是:kmem本身是存放在内核的全局数据段里面,
 //然后它的freelist字段保存了第一个页面的物理地址
 //而第一个页面的物理地址的前8个字节又保存了第二个页面的物理地址……
 
void
kinit()
{    // end是内核代码结束后的第一个字节的地址,这以后到PHYSTOP的物理内存
     //是可以释放的,PHYSTOP是物理内存RAM的最高地址。
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

void  //释放  pa_start 到 pa_end 这一段的物理页面
freerange(void *pa_start, void *pa_end)
{
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

void      //简单地把pa指向的物理页面加入到链表中
kfree(void *pa)
{    
  struct run *r;

  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  memset(pa, 1, PGSIZE);

  r = (struct run*)pa;

  acquire(&kmem.lock);
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

void *   //分配出链表头指向的物理页面,并返回该页面的物理地址。
kalloc(void)
{
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}

因此调用kinit函数后,物理内存就由kmem这个数据结构控制好了。
在这里插入图片描述
利用gdb查看end的值,再反汇编查看内核代码数据结束的地址:
在这里插入图片描述
可以看到,在disk这个全局数据结构开始的物理位置是0x80023000,考虑到其占的大小,end值为0x80026000是比较合理的。

2. kvminit()函数

pagetable_t kernel_pagetable;

void
kvminit(void)
{
  kernel_pagetable = kvmmake();
}

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);
  // map kernel stacks
  proc_mapstacks(kpgtbl);
  
  return kpgtbl;
//可以看到,kvmmake建立起了内核虚拟地址空间到物理内存空间的映射,
//包括硬件和RAM,这里的映射都是恒等映射,保证了后面开启虚存机制后
//指令和数据的地址仍能够翻译到正确的位置。例外的是以trampoline地址
//起始的汇编代码映射了两次,一次是内核代码段的恒等映射,同时还把它
//映射到了内核虚拟地址空间的最高处。
//疑问,这里为什么没有建立CLINT硬件映射的代码??怀疑它这里写漏了?
//在lab里面的xv6代码是有建立CLINT硬件映射的代码的。
}
void
kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm);
//该函数在内部调用了mappages函数,效果和mappages相同
//作为一层封装,多了一个错误检查(调用panic)

int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm); //va是虚拟地址,pa是物理地址,页表项的访问权限由perm决定。
//该函数在内部调用了work函数,且alloc=1。
//该函数的作用是根据给出的pagetable,设置好从va开始的大小为size的区域
//对应的页表项,使得它们将会映射到从pa开始,大小为size的物理页面。

pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc);
//在 alloc=0 时,walk函数的作用是模仿mmu,利用给定的页表pagetable
//和虚拟地址va,返回该va在pagetable中对应的页表项指针。
//对应数据结构为pte_t。在 alloc=1 时,如果在翻译地址的过程中发现某级页表
//中应该对应va的表项不存在,则会调用kalloc.c中的kalloc函数分配一页,然后
//填写该表项使其指向分配出的页面(在alloc=0时会简单地报错)。


总之,kvminit函数建立了初始化了kernel_pagetable这个全局页表,建立了内核虚拟地址空间到物理空间的映射。

3. kvminithart()函数

#define SATP_SV39 (8L << 60)

#define MAKE_SATP(pagetable) (SATP_SV39 | (((uint64)pagetable) >> 12))

void  //将satp装入页表
kvminithart()
{
  w_satp(MAKE_SATP(kernel_pagetable));
  sfence_vma(); //清除与地址翻译有关的所有缓存(例如PLB中的条目)
}

下图是satp寄存器的结构,前44位是根页表的物理地址的高44位(因为实际页表位置要求4KB对齐),因此最多支持 2 56 2^{56} 256字节的物理地址空间。
satp寄存器的结构
mode字段含义如下:
在这里插入图片描述
在start函数中在设置satp的值为0也可以理解了,这样设置了mode字段为0,从而关闭虚拟内存。从代码中可以看到,xv6的mode值为8,即翻译模式为39位地址的翻译模式。查看xv6代码的maxva:
在这里插入图片描述
少设置一位是为了避免地址的符号拓展?地址为什么要进行符号拓展?目前还不理解这里

update:

以下引自spec中4.4.1节对sv39的描述,因为如果bit38为1的话,要求bit 63-39都为1,为了避免这样的符号拓展的麻烦,所以xv6少使用了一半的地址空间。

Instruction fetch addresses and load and store effective addresses, which are 64 bits, must have bits 63–39 all equal to bit 38, or else a page-fault exception will occur.

4. procinit()函数

#define KSTACK(p) (TRAMPOLINE - ((p)+1)* 2*PGSIZE) //memorylayout.h
#define TRAMPOLINE (MAXVA - PGSIZE)
//这部分代码位于proc.c中,每个进程设置了指向内核栈的指针
struct proc proc[NPROC];
// initialize the proc table at boot time.

void   //这是之前在kvmmake函数中调用的,为每个进程分配一个内核栈
proc_mapstacks(pagetable_t kpgtbl) { //并建立好页表的映射
  struct proc *p;
  
  for(p = proc; p < &proc[NPROC]; p++) {
    char *pa = kalloc();
    if(pa == 0)
      panic("kalloc");
    uint64 va = KSTACK((int) (p - proc));
    kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
  }
}

void
procinit(void)
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
  initlock(&wait_lock, "wait_lock");
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");
      p->kstack = KSTACK((int) (p - proc)); //设置了指针指向该内核栈      
  }  //注意是指向栈底部,而且是虚拟地址。
}

5. 小结

记录以下函数的作用:kalloc,mappages,walk,freepage,kfree。
疑问:为什么没有映射CLINT?

6. update

感谢灵隐寺未来职工, qianhao2579这两位哥们的留言,我发现之前对于CLINT的理解出了问题。

  • mtime, mtimecmp是两个内存映射寄存器(而不是集成在cpu上的类似于通用寄存器那种),写入mtimecmp实际上是写入到Core Local Interrupt (CLINT),下面的引用来自riscv的参考手册。

Platforms provide a real-time counter, exposed as a memory-mapped machine-mode read-write register, mtime. mtime must run at constant frequency, and the platform must provide a mechanism for determining the timebase of mtime.

The mtime register has a 64-bit precision on all RV32 and RV64 systems. Platforms provide a 64-bit memory-mapped machine-mode timer compare register (mtimecmp), which causes a timer interrupt to be posted when the mtime register contains a value greater than or equal to the value in the mtimecmp register.

  • 上面另外值得注意的是,这俩内存映射的寄存器只能在M态下访问。
  • 另外是手册上对satp寄存器的描述

The satp register is an SXLEN-bit read/write register, formatted as shown in Figure 4.11 for SXLEN=32 and Figure 4.12 for SXLEN=64, which controls supervisor-mode address translation and protection.

从描述上看来,应该认为M态是没有mmu翻译的,即地址会被直接认为是物理地址(虽然我并没有查到相应的说明,不过从mmu的定义来看上面的推断是合理的)。

  • 现在再回头看riscv-xv6对时钟中断的处理,因为mtimecmp寄存器只能在M态进行修改,因此也就没必要把clint的地址映射到虚拟内存中了。在源码中把mtvec寄存器对应的地址设置为timevec函数(start,c),当触发时钟中断后,处理器进入M态,同时跳转到timevec函数(kernelvec.S),timevec函数修改mtimecmp寄存器后手动触发一个软中断,这样调用mret返回后马上会因为这个软中断进入S态,然后有相应的时钟中断处理。这一切的时钟中断处理逻辑并不需要映射CLINT(因为映射了也没啥用)
  • 最后是现在的两个疑问:为什么设计时S态不能访问mtimecmp寄存器,导致用一种这样别扭的方式实现时钟中断?以及CLINT的msip bits是如何与mip寄存器中的msip bit对应上的
  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值