此时此刻

xv6的trap机制

xv6的trap机制

syscall相关的riscv寄存器:

  • stvec:ecall 指令跳转到这儿执行,这个寄存器存放的是trampoline的地址;trampline汇编代码是用户空间进如内核空间的代码
  • sepc:ecall指令把用户空间的pc指针保存在此
  • scause:ecall将其设置为8,表示一个系统调用
  • sscratch: trapframe的地址
  • sapt(S-mode address translation and protection):寄存器保存当前的页表基址
  • a0-a7:系统调用的参数
  • ra:返回地址
  • a0:返回值
  • a7:syscall的号码
  • tp:当前cpu线程号

Sv39地址翻译

可以看到,虚拟地址用到了64位中的低39位,即最大的虚拟地址空间0x4000000000

以write为例,说明系统调用的过程:

  • write()
    • trampoline / uservec
      • usertrap()
        • syscall()
          • sys_write()
      • usertrapret()
    • trampoline / userret
  • write()返回

以sh.c中的write系统调用为例子说明:

  1. 首先查看sh.c的objdump代码sh.asm,查看到write函数直接调用的是user/usys.S的write函数,并且地址是0000000000000d6a,然后进入gdb调试。
  2. 在执行ecall指令之前,查看各个寄存器的值,包括pc、sapt、sepc、stvec、scause、sscratch以及a0
  3. 然后si单步执行ecall,进入到kernel/trampoline.S执行,
    1. 首先,把sscratch寄存器与a0寄存器的值交换,然后此时a0寄存器就是trapframe的地址,即要把当前进程的寄存器保存到a0寄存器所存的地址对应的页表中。
    2. 最后跳转到进程结构体p->tf->kernel_trap处执行,(这个在kernel/trap.cusertrapret中设置进程的内核trap处理函数为usertrap)
  4. 然后进入kernel/trap.cusertrap执,因为已经进入内核,首先在函数中切换中断处理为kernelvec
  5. 判断sscause寄存器的值是否为8,如果是8,就说明是来自用户空间的syscall。然后把寄存器epc的值保存到当前进程结构体中,并且epc+4,即反悔的时候直接执行ecall的下一条指令。
  6. 最后调用syscall(),根据寄存器a7选择相应的系统调用处理函数,最后把返回值置给寄存器a0,最后返回。
  7. 首先回到usertrapret函数中,设置进程关于内核的一些参数,如内核satp、kernel_sp进程的内核栈地址,进程trap的处理函数等,最后跳转到trampoline的userret处执行,重新从trapframe中把各个寄存器数据回复到物理的寄存器上,然后返回用户进程。

以initcode的系统调用为例:

执行ecall:

进入usertrap

  • 设置stvec寄存器为kernelvec,表示已经进入内核,相应的中断处理要由内核完成。
  • 判断scause寄存器的值,如果是8,表示syscall,先把epc+4,即返回的时候直接执行ecall的下一条指令
  • 执行syscall函数,根据a7寄存器执行sys_xxx,返回值放在a0寄存器
  • 进入usertrapret
    • 关中断,写stvec寄存器为trampoline的userret。当当前不处理其他的中断,唯一处理的是进程从内核返回用户态,使用的代码是trampoline中的userret。
    • 设置好各种参数,包括设置epc为进程保存的epc、然后把sapt寄存器设置为当前进程的页表基址
    • 最后执行trampoline中的userret,回复进程现场,返回到用户态,继续执行ecall的下一条指令

参考

  • gdb调试:gdb调试有一个.gdbinit ,可以吧进入gdb之后的固定配置写到里面。执行命令gdb。首先执行这些命令。
  • 进程优先级:通过nice 调整,nice的取值-20-19,数字越大,优先级越低
  • taskset:绑定进程到某个CPU执行