riscv-xv6单步调试4 异常处理初始化和进程调用

0. 序

   主要记录xv6异常处理的初始化和第一个进程的建立。main函数中主要有trapinit()函数和trapinithart()函数与异常初始化部分相关。

1. trapinit()函数

void    //初始化了一把锁??有关锁的部分以后会写把....
trapinit(void)
{
  initlock(&tickslock, "time");
}

2. trapinithart()函数

// set up to take exceptions and traps while in the kernel.
void  //把kernelvec地址装入stvec寄存器,该寄存器的作用在1.5中记录
trapinithart(void)
{
  w_stvec((uint64)kernelvec);
}

kernelvec对应的汇编代码记录在kernelvec.S文件中。调试输出
在这里插入图片描述
注意最低两位为0,因此没有开启向量中断,即所有的异常都会跳转到该地址

3. userinit()函数

   在userinit函数中,调用了allocproc,uvminit。而在allocproc中调用了proc_pagetable函数, allocpid函数。proc_pagetable中调用了uvmcreate函数。

3.1 uvmcreate函数和proc_pagetable函数

// create an empty user page table.
// returns 0 if out of memory.
pagetable_t
uvmcreate()
{  
  pagetable_t pagetable;
  pagetable = (pagetable_t) kalloc();
  if(pagetable == 0)
    return 0;
  memset(pagetable, 0, PGSIZE);
  return pagetable;
}

   就是调用kalloc函数分配一个页表页?感觉这个封装没太必要地说、、、

#define TRAPFRAME (TRAMPOLINE - PGSIZE)  //memorylayout.h

pagetable_t
proc_pagetable(struct proc *p)
{
  pagetable_t pagetable;

  // An empty page table.
  pagetable = uvmcreate();
  if(pagetable == 0)
    return 0;

  // map the trampoline code (for system call return)
  // at the highest user virtual address.
  // only the supervisor uses it, on the way
  // to/from user space, so not PTE_U.
  if(mappages(pagetable, TRAMPOLINE, PGSIZE,
              (uint64)trampoline, PTE_R | PTE_X) < 0){
    uvmfree(pagetable, 0);
    return 0;
  }

  // map the trapframe just below TRAMPOLINE, for trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  } //p->trapframe的初始化在allocproc中,对应的是一个物理地址

  return pagetable;
}

总的来说,调用了uvmcreate+两次mappages的效果是,建立了一个有trampoline和trapframe的页表映射的页表,可以看到,trapframe在地址空间中位于trampoline的下面一页。

3.2 allocpid()函数和allocproc()函数

int
allocpid() {
  int pid;
  
  acquire(&pid_lock);
  pid = nextpid;
  nextpid = nextpid + 1;
  release(&pid_lock);

  return pid;
}

这个函数很简单了,就是分配一个pid出去,nextpid是一个全局变量

static struct proc*
allocproc(void)
{
  struct proc *p;

  for(p = proc; p < &proc[NPROC]; p++) {
    acquire(&p->lock);
    if(p->state == UNUSED) {
      goto found;
    } else {
      release(&p->lock);
    }
  }
  return 0;
     //第一部分,找到一个进程组中未使用的进程
found:
  p->pid = allocpid();
  p->state = USED;

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p); //这里初始化了p->trapframe,拿到一个物理地址
    release(&p->lock);
    return 0;
  }

  // An empty user page table.
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Set up new context to start executing at forkret,
  // which returns to user space.
  memset(&p->context, 0, sizeof(p->context));
  p->context.ra = (uint64)forkret;
  p->context.sp = p->kstack + PGSIZE; //这样sp就指向了内核栈的顶部
  //注意进程的内核栈是在kernel_pagetable中有表项,进程的pagetable中是
  //没有表项的,因此进程内核栈对该进程是不可见的。
  return p;
}

context这个数据结构如下:

// Saved registers for kernel context switches.
struct context {  //proc.h
  uint64 ra;
  uint64 sp;

  // callee-saved
  uint64 s0;
  uint64 s1;
  uint64 s2;
  uint64 s3;
  uint64 s4;
  uint64 s5;
  uint64 s6;
  uint64 s7;
  uint64 s8;
  uint64 s9;
  uint64 s10;
  uint64 s11;
};

现在可以总结出allocproc这个函数的作用:在进程队列中拿到一个未使用的proc,给proc的trapframe分配一个物理页,为这个proc建一个有trampoline和trapframe映射的页表,然后设置这个进程context中的返回地址ra为forkret,sp为kstack+pgsize(所以kstack应该指向的是内核栈底部?),返回值是选中的proc*。

3.3 uvminit()函数和userinit()函数

void
uvminit(pagetable_t pagetable, uchar *src, uint sz)
{
  char *mem;

  if(sz >= PGSIZE)
    panic("inituvm: more than a page");
  mem = kalloc();
  memset(mem, 0, PGSIZE);
  mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
  memmove(mem, src, sz);
  //注意这里是mem而不是0,因为这个pagetable不一定是目前正在用的pagetable
  //而一开始建立的恒等映射保证了最后会写到物理地址mem处。
}

所以作用是分配一个物理页,为pagetable建立0到pgsize的虚拟地址到该物理页映射的条目。然后从src指向的地址中复制size个字节到虚拟地址0处。

最后终于可以回来看userinit函数了

void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}

其中initcode对应的汇编代码如下,就是调用exec,使其调用init函数

# exec(init, argv)   // user/initcode.c
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

userinit函数在设置好初始进程后返回(建立了三个页面的映射,0到pgsize,以及虚拟地址顶部的两个页面)。

4. 初始进程的调度

userinit函数返回后,main函数继续调用scheduler函数。

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();
  
  c->proc = 0;
  for(;;){
    // Avoid deadlock by ensuring that devices can interrupt.
    intr_on();

    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->context, &p->context);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;
      }
      release(&p->lock);
    }
  }
}

scheduler函数依次扫描进程队列,由于此时只有初始进程的可运行,所以必会选到该进程然后调用swtch函数进行上下文切换。
再来看swtch函数干了什么:

.globl swtch
swtch:
        sd ra, 0(a0)  //这里保存ra很重要,ra是进程保存调用swtch后返回地址
        sd sp, 8(a0) //的寄存器,下次再调用swtch将控制权返回给scheduler
        sd s0, 16(a0) //时,由于保存了ra,所以会正确地返回到调用swtch后
        sd s1, 24(a0) //需要返回的地址,同时又保存了s0到s11所有的riscv规
        sd s2, 32(a0) //定的被调用者保存的寄存器的值,就给scheduler创造了
        sd s3, 40(a0)//幻象,好像自己只是单纯地调用了这个函数,而没有任何
        sd s4, 48(a0) //地上下文切换,调度这些复杂的东西。
        sd s5, 56(a0)
        sd s6, 64(a0)
        sd s7, 72(a0)
        sd s8, 80(a0)
        sd s9, 88(a0)
        sd s10, 96(a0)
        sd s11, 104(a0)

        ld ra, 0(a1)
        ld sp, 8(a1)
        ld s0, 16(a1)
        ld s1, 24(a1)
        ld s2, 32(a1)
        ld s3, 40(a1)
        ld s4, 48(a1)
        ld s5, 56(a1)
        ld s6, 64(a1)
        ld s7, 72(a1)
        ld s8, 80(a1)
        ld s9, 88(a1)
        ld s10, 96(a1)
        ld s11, 104(a1)
        
        ret

这里把所有调用者保存寄存器的值和ra,sp存到了cpu->context里面。然后把初始进程的context中的内容读入到寄存器中。注意在allocproc中设置了进程的ra为forkret,而sp=p->kstack + pgsize,因此调用ret后,程序将跳转到forkret的位置,且sp为该进程内核栈的栈顶。forkret函数在第一次调用时会做一些初始化工作,否则会直接调用usertrapret函数

4.1 usertrapret()函数

void
usertrapret(void)
{
  struct proc *p = myproc();

  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(), so turn off interrupts until
  // we're back in user space, where usertrap() is correct.
  intr_off();

  // send syscalls, interrupts, and exceptions to trampoline.S
  w_stvec(TRAMPOLINE + (uservec - trampoline));
  //uservec == trampoline ? 所以就是装入 TRAMPOLINE

  // set up trapframe values that uservec will need when
  // the process next re-enters the kernel.
  p->trapframe->kernel_satp = r_satp();         // kernel page table
  p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();         // hartid for cpuid()

  // set up the registers that trampoline.S's sret will use
  // to get to user space.
  
  // set S Previous Privilege mode to User.
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x); //这使得调用sret指令后将模式变为user,且开启中断

  // set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

  // tell trampoline.S the user page table to switch to.
  uint64 satp = MAKE_SATP(p->pagetable);

  // jump to trampoline.S at the top of memory, which 
  // switches to the user page table, restores user registers,
  // and switches to user mode with sret.
  uint64 fn = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}

注意:

  • 在初始化时,在stvec中装入的是kernelvec的地址,现在使用intr_off()关闭中断,然后重新装入TRAMPOLINE的地址。它作为以后发生异常的跳转点。

  • 在建立内核的虚拟地址时,trampoline有两份映像,一个是恒等映射,一个是在地址最高的虚拟页上 ,而trampoline对应的一页有两个函数,一个是uservec,位于页开头,即和trampoline同一地址。紧跟在uservec后的是userret函数,因此最后这个fn指针指向的是位于高处虚拟地址的userret函数(因为只有该处的userret在进程页表中有映射)

4.2 userret()函数

在usertrapret最后,调用了userret函数。

.globl userret
userret:
        # userret(TRAPFRAME, pagetable)
        # switch from kernel to user.
        # usertrapret() calls here.
        # a0: TRAPFRAME, in user page table.
        # a1: user page table, for satp.

        # switch to the user page table.
        csrw satp, a1
        sfence.vma zero, zero  //替换页表后需要清空地址翻译缓存

        # put the saved user a0 in sscratch, so we
        # can swap it with our a0 (TRAPFRAME) in the last step.
        ld t0, 112(a0)
        csrw sscratch, t0

        # restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
        ld tp, 64(a0)
        ld t0, 72(a0)
        ld t1, 80(a0)
        ld t2, 88(a0)
        ld s0, 96(a0)
        ld s1, 104(a0)
        ld a1, 120(a0)
        ld a2, 128(a0)
        ld a3, 136(a0)
        ld a4, 144(a0)
        ld a5, 152(a0)
        ld a6, 160(a0)
        ld a7, 168(a0)
        ld s2, 176(a0)
        ld s3, 184(a0)
        ld s4, 192(a0)
        ld s5, 200(a0)
        ld s6, 208(a0)
        ld s7, 216(a0)
        ld s8, 224(a0)
        ld s9, 232(a0)
        ld s10, 240(a0)
        ld s11, 248(a0)
        ld t3, 256(a0)
        ld t4, 264(a0)
        ld t5, 272(a0)
        ld t6, 280(a0)

	# restore user a0, and save TRAPFRAME in sscratch
        csrrw a0, sscratch, a0
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

上面的汇编很直观,就是放入用户进程的页表,然后把用户进程trapframe中保存的寄存器的值都恢复回去(解释一下为什么不直接用 ld a0, 112(a0) 。这样做使得sscratch保存了指向当前进程trapframe的虚拟地址的指针)

也就是说,从userinit设置好了该进程的运行环境,然后经过scheduler等等一系列的调度之后,从执行这条sret指令开始,xv6终于进入了用户模式,开始运行第一个进程

5. 小节

以后补充。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值