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