traps源码阅读


这部分的文件主要是trampoline.Skernelvec.Strap.c


trampoline.S

uservec

这是一个汇编代码文件,在xv6中负责处理用户态和内核态的切换。

.section trampsec  // 声明代码位于trampsec段
.globl trampoline  // 声明trampoline代码起始地址
.globl usertrap    // 声明trap处理函数
trampoline:        // trampoline代码起始标签
.align 4           // 对齐

首先是一些声明。

.globl uservec     // 声明用户态trap入口

然后是uservec代码,它在用户态到内核态切换时保存寄存器状态并设置内核环境。trap.c会设置STVEC寄存器指向这里,因此用户空间中的陷阱触发后从这里开始,使用用户页表,但是在supervisor模式下。

csrw sscratch, a0  // 将用户a0寄存器值暂存到sscratch寄存器
li a0, TRAPFRAME   // 加载TRAPFRAME虚拟地址到a0

// 下面这些里面只少了112,因为它是专门用来存原始a0值
sd ..., ...(a0)    // 保存用户寄存器到trapframe

csrr t0, sscratch  // 恢复用户原始a0值
sd t0, 112(a0)     // 存入p->trapframe->a0

ld sp, 8(a0)       // 加载内核栈指针,从p->trapframe->kernel_sp
ld tp, 32(a0)      // 设置当前hartid,从p->trapframe->kernel_hartid
ld t0, 16(a0)      // 加载usertrap函数地址,从trapframe->kernel_trap
ld t1, 0(a0)       // 获取内核页表地址,从trapframe->kernel_satp

a0前面的数字是trapframe结构体中各字段的内存偏移量,trapframe的定义在proc.h中。

然后sfence.vma zero, zero是内存屏障指令,会保证之前的内存操作完成。

csrw satp, t1      // 切换内核页表

这时sfence.vma zero zero会刷新TLB,清除旧页表缓存。

然后jr t0跳转到usertrap函数。

userret

trap.c中的usertrapret调用,负责从内核态回到用户态。

sfence.vma zero, zero
csrw satp, a0
sfence.vma zero, zero

先切换回用户页表。

这里的a0是内核a0而非前面的用户a0,用户a0在先前被修改,但这里的内核a0是内核准备的参数值。

然后加载TRAPFRAME虚拟地址,保存寄存器等。

li a0, TRAPFRAME

最后会把重新存回用户a0寄存器的值,并返回用户空间。

ld a0, 112(a0)
sret

kernelvec.S

kernelvec是内核陷阱的入口,处理来自内核代码中的中断或异常。

addi sp, sp, -256

它先分配256字节栈空间来保存寄存器的值。然后通过call kerneltrap调用C处理函数,处理完成后再恢复寄存器状态。


trap.c

trapinithart

trapinithart是内核陷阱处理的核心初始化函数。

// set up to take exceptions and traps while in the kernel.
void
trapinithart(void)
{
  w_stvec((uint64)kernelvec);
}

它通过w_stvec设置陷阱向量基址寄存器,将内核陷阱处理程序入口地址设置为kernelvec

kerneltrap

这是处理内核空间陷阱的核心函数。

void 
kerneltrap()
{
  int which_dev = 0;
  uint64 sepc = r_sepc();
  uint64 sstatus = r_sstatus();
  uint64 scause = r_scause();
  
  if((sstatus & SSTATUS_SPP) == 0)
    panic("kerneltrap: not from supervisor mode");
  if(intr_get() != 0)
    panic("kerneltrap: interrupts enabled");

  if((which_dev = devintr()) == 0){
    // interrupt or trap from an unknown source
    printf("scause=0x%lx sepc=0x%lx stval=0x%lx\n", scause, r_sepc(), r_stval());
    panic("kerneltrap");
  }

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2 && myproc() != 0)
    yield();

  // the yield() may have caused some traps to occur,
  // so restore trap registers for use by kernelvec.S's sepc instruction.
  w_sepc(sepc);
  w_sstatus(sstatus);
}

首先保存关键寄存器的值,然后验证是否来自supervisor mode并检查中断是否已经禁用,再尝试用devintr处理中断。若是定时器中断且进程存在,则调用yield触发进程调度。最后恢复寄存器的值。

usertrap

这是处理用户空间陷阱的核心函数。

if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

首先检查确认来自用户模式,然后设置入口地址,后续若触发中断将交由内核陷阱处理程序来处理。

w_stvec((uint64)kernelvec);

然后保存用户程序计数器:

struct proc *p = myproc();
  
// save user program counter.
p->trapframe->epc = r_sepc();

再根据scause寄存器的值来确定不同的处理方法:

if(r_scause() == 8){
    // system call

    if(killed(p))
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();

    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok
  } else if (r_scause() == 15){
    uint64 va = r_stval();
    printf("page fault %p\n", (void *)va);
    uint64 ka = (uint64) kalloc();
    if (ka == 0){
      p->killed = 1;
    } else {
      memset((char*)ka, 0, PGSIZE);
      va = PGROUNDDOWN(va);
      if (mappages(p->pagetable, va, PGSIZE, ka, PTE_W | PTE_U | PTE_R) != 0){
        kfree((void*)ka);
        p->killed = 1;
      }
    }
  } else {
    printf("usertrap(): unexpected scause 0x%lx pid=%d\n", r_scause(), p->pid);
    printf("            sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());
    setkilled(p);
  }

当定时器中断则触发yield让出CPU:

if(which_dev == 2)
    yield();

最后调用usertrapret返回用户空间。

usertrapret

这个函数用于返回用户空间。

首先intr_off关闭中断,然后重新设置用户陷阱处理入口:

uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);

并重新配置trapframesstatus寄存器:

  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()
  
  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);

返回地址使用之前保存的:

w_sepc(p->trapframe->epc);

然后切换页表,重新跳转到用户空间。

uint64 satp = MAKE_SATP(p->pagetable);
uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64))trampoline_userret)(satp);  // 执行userret汇编代码

其他

devintrclockintr涉及中断,后面再补充。