这篇文章介绍riscv-xv6的操作系统接口(operating system interface)的实现,系统调用实现了用户态和内核态的隔离,也为实现了不同进程之间的隔离提供了条件(还有其他的机制如虚拟内存和页表等机制一起为实现不同进程的隔离提供了完整解决方案)。在riscv-xv6的user目录下,一般是用户程序的实现源码,如cat,echo, sh等,在这些程序的实现过程中,会调用内核态执行的代码逻辑,如文件读写,fork,exec等系统调用,下面就简要介绍一下添加一个系统调用的实现方法以及系统调用等trap发生时的处理过程逻辑分析。 1、在user/user.h中定义系统调用的prototype(signature),在usys.pl定义添加的系统调用的entry(通过编译过程会生成usys.S,用于生成系统调用的stubs,桩函数作为占位符,通常用于测试、RPC 代理,或者未完成的函数。)sutb里定义了通过指令ecall以及系统调用的参数和系统调用函数指针数组index等信息传递到内核去调用系统调用函数的实现(将系统调用的函数序号索引放入a7寄存器,参考引文[2])。 2、用户程序调用系统调用函数时触发stub函数,会执行ecall指令, 触发异常(trap),进入 S-mode(Supervisor Mode)。ecall 不会自动保存寄存器状态,需要软件(操作系统)来管理寄存器的保存与恢复。当 ecall 发生时,CPU 会保存当前 sepc(保存陷入前的 PC);设置 scause(记录 trap 原因,ecall 在 S-mode 下的 scause=8);切换到 stvec 处理 trap(uservec代码段);清除 SIE 位(即 sstatus.SIE = 0,禁用中断);更新 sstatus.SPP(记录陷入前的特权级)。 3、uservec代码段的功能为:(1)、保存当前用户态执行的cpu的寄存器到trapframe结构中;(2)并将trapframe中保存的内核的栈指针,页表地址等进行加载,并最后调用usertrap函数,具体实现参看引文[3]。usertrap最后调用syscall实现系统函数在内核中的真正执行,usertrap的代码实现根据不同的trap发生的条件(系统调用,硬件中断,时钟定时器中断)做对应的处理,具体可以参考引文[4]。几个关键的寄存器:(1)、stvec(// Supervisor Trap-Vector Base Address)存放发生trap时的代码起始地址,在用户态时应指向uservec[5],在内核态时应为kernelvec[6];kerneltrap发生的情形有外部硬件按中断和时钟中断以及一些运行时错误等(由于已经在内核态,不存在系统调用的的情况。进入 kerneltrap() 前,RISC-V 硬件自动清除 sie,确保不会在处理中断时发生新的中断抢占。sie 负责控制中断使能,如果 sie=0,新的中断不会打断当前的 kerneltrap 处理流程[8]。kerneltrap的处理过程代码及分析参考[12]。 4、usertrapret[7]为从usertrap执行完准备返回用户态时需要执行的代码,主要的逻辑有:关闭硬件中断;设置stvec为uservec;设置寄存器为用户态权限;执行寄存器状态恢复等操作(userret代码段[9])。 5、在创建用户进程的时候相关的函数调用路径有: allocproc([10]) -> forkret([11])-> usertrapret-> w_stvec(trampoline_uservec)([7]),可以看到创建用户进程时子进程被调度开始运行时将uservec代码段起始地址设置到stvec寄存器的问题。关于设置stvec为uservec代码段的流程的更多的分析:(1)、fork() 创建子进程,并设置其 state =…