LEC 6 (ab) Isolation system call entry/exit

LEC 6 (ab) Isolation  system call entry/exit RISC-V trap machinery相关的寄存器stvec内核将其陷阱处理程序的地址写入此寄存器RISC-V 会跳转到stvec中的地址来处理陷阱这个寄存器在内核态和用户态会有两个不同的值分别是uservec和kernelvececall指令执行的时候会将pc置为stvec中保存的值并将当前pc值保存为sepc。sepc当陷阱发生时RISC-V 会将程序计数器pc保存在这里因为 pc 随后会被stvec中的值覆盖。sret从陷阱返回指令会将sepc的值复制到 pc 中。内核可以写入sepc来控制sret返回到何处。scauseRISC-V 在此存放一个数字用于描述陷阱的原因。sscratch内核将一个在陷阱处理程序刚开始时非常有用的值放在这里。sstatussstatus中的 SIE 位控制设备中断是否使能。如果内核清除 SIE 位RISC-V 将推迟设备中断直到内核重新设置 SIE 位。SPP 位指示陷阱来自用户模式还是监管模式并控制sret返回到哪种模式。上述不能在用户模式下读取或写入在机器模式下处理陷阱有一组类似的控制寄存器但是xv6 仅将它们用于定时器中断。多核芯片上的每个 CPU 均有自己的一组这类寄存器且在任一给定时刻可能有不止一个 CPU 正在处理陷阱。执行流程当需要强制执行一次陷阱时RISC-V硬件会针对所有陷阱类型定时器中断除外执行以下步骤若陷阱是设备中断且sstatus的 SIE 位为 0则不做后续任何操作。通过清除sstatus中的 SIE 位来禁用中断。将pc复制到sepc。将当前模式用户模式或监管模式保存到sstatus的 SPP 位中。设置scause以反映陷阱的原因。将模式设置为 S 监管模式。将stvec复制到pc。从新的pc处开始执行。请注意CPU 并不会切换到内核页表不会切换到内核中的栈也不会保存除pc以外的任何寄存器。这些任务必须由内核软件完成。CPU 在陷阱期间只做最少工作的一个原因是为了给软件提供灵活性例如某些操作系统在某些情况下会省略页表切换以提升陷阱处理性能。usertrapvskerneltrap场景陷阱来自stvec指向跳转到 C 函数用户态陷阱User 模式uservectrampoline.Susertrap()内核态陷阱Supervisor 模式kernelveckernelvec.Skerneltrap()在uservec中处理的是从用户态来的陷阱所以它应该跳转到usertrap()而不是kerneltrap()p-trapframe在内核空间和用户空间中的两次映射p-trapframe在内核空间和用户空间中映射的位置是不同的在内核空间中进行了一次直接映射内核页表下访问p-trapframe直接就是对应的物理地址而在用户页表下访问p-trapframe获得的是Trampoline页下面那一页。Calling system callsTraps from kernel space中断触发定时器中断或者PLIC管理的外部中断。异常触发常见原因kernel page fault内核访问了没有映射的虚拟地址。illegal instruction内核执行了非法指令。instruction page fault内核跳转到了一个不可执行或没有映射的地址。load page fault内核从非法地址读数据。store page fault内核向非法地址写数据。S-mode ecall如果内核态代码执行 ecall会触发 environment call from S-mode。但这不是正常系统调用xv6 会把它当成内核异常处理。breakpoint执行断点指令可能触发异常,xv6中没有。假设内核正在执行某条指令结果这条指令触发异常例如访问非法地址。RISC-V 硬件会做几件事把出错指令的地址保存到 sepc把异常原因保存到 scause把附加信息保存到 stval例如 page fault 时stval 通常保存出错的虚拟地址修改 sstatus记录 trap 前的特权级并关闭中断跳转到 stvec 指向的地址添加一个定时器中断函数以Alarm (hard)为例注册中断首先添加一个新的sigalarm(interval, handler)系统调用interval是时钟间隔handler是一个函数指针指针类型为void (*handler)().在usys.pl文件中添加entry(sigalarm)使其生成如下汇编代码.global sigalarm sigalarm:li a7,SYS_sigalarm ecall ret并且在user.h头文件中添加函数定义int sigalarm(int ticks, void (*handler)());使其能被其他的文件引用到。在syscall.h头文件中添加#define SYS_sigalarm 22在syscall.c头文件中的系统调用列表中添加staticuint64(*syscalls[])(void){.....[SYS_sigalarm]sys_sigalarm,//添加这一行...};这样能直接在void syscall(void)函数中找到对应的sys_sigalarm然后定义sys_sigalarm函数这里我把它定义在sysproc.c中也可以定义在其他能找到的位置//for Alarm (hard)uint64sys_sigalarm(void){structproc*pmyproc();argint(0,p-alarm_interval);p-alarm_remainedp-alarm_interval;argaddr(1,(p-alarm_handler));return0;}再改动一下usertrap.c中的相关代码就注册完成了...}elseif((which_devdevintr())!0){// ok/*for Alarm (hard) 每个时钟中断到来之后首先检查一下alarm是否开启如果开启,执行后续操作 alarm_remaning--,然后在这里检查一下alarm_remaining是否为 0 如果为0就重新装填一下alarm_remaining的值然后回到用户态调用p-alarm_handler处理函数 这里还需要检查一个标记位用于查看是否在一个alarm_handler处理程序中 */if(which_dev2p-alarm_interval!0p-is_alarm_handing0){p-alarm_remained--;if(p-alarm_remained0){//保存一下当前用户寄存器的值然后在sigreturn中重置回去memmove(p-trapframe_for_interrupt,p-trapframe,PGSIZE);p-trapframe-epcp-alarm_handler;p-alarm_remainedp-alarm_interval;//这里再标记一下p-is_alarm_handing 1,代表它在一个中断的处理过程中p-is_alarm_handing1;}}...这里的逻辑就是由于CPU 在每次执行下一条指令的时候都会检查一下中断位因此在每个时钟中断到来时CPU会进入trap检查一下当前的时钟剩余时间是否为0如果为0就将当前用户trampframe中的值复制到程序结构体中专门用于存储中断的trapframe_for_interrupt中去然后将epc改为alarm_handler这样回去就会直接跳转到alarm_handler中去。中断执行中断执行的逻辑就是上面那段代码叙述的那样同时在开始处理中断程序的时候需要加上p-is_alarm_handing 1;这样在避免了处理函数重入的问题。中断返回这个任务中任何一个中断处理函数都是以sigreturn();结尾的因此需要在sigreturn()中返回到中断前的状态voidperiodic(){countcount1;printf(alarm!\n);sigreturn();}我们使用类似的方法将sigreturn也注册为一个系统调用具体实现函数如下uint64sys_sigreturn(void){//将用户页表重置回去structproc*pmyproc();memmove(p-trapframe,p-trapframe_for_interrupt,PGSIZE);return0;}这里也需要配套trap.c中的代码使用,需改一下usertrap函数中关于系统调用的部分....if(r_scause()8){// system callif(p-killed)exit(-1);// sepc points to the ecall instruction,// but we want to return to the next instruction.p-trapframe-epc4;// an interrupt will change sstatus c registers,// so dont enable until done with those registers.intr_on();if(p-trapframe-a7SYS_sigreturn){syscall();//由于syscall()更改了p-trapframe-a0作为系统调用的返回值因此这里需要将其设置为原来的值p-trapframe-a0p-trapframe_for_interrupt-a0;//将p-is_alarm_handing 0代表已经从这个时钟中断处理程序中离开p-is_alarm_handing0;}else{syscall();}}....这里的主要思想就是前面正常进入系统调用从系统调用回来之后需要将p-trapframe-a0重新设置为p-trapframe_for_interrupt-a0原因是syscall()函数中将p-trapframe-a0设置为了具体syscall函数的返回值区分同步异常和异步中断RISC-V 中断的精确时序CPU 的工作模型是这样的概念上完成指令 0x0FFC↓ ────── 指令边界 ────── ← 在这里检查中断 ↓[如果有中断pending且sstatus.SIE1]→ 触发 trapsepc0x1000[否则]→ 执行指令 0x1000所以准确的描述是CPU 在指令边界上一条指令已提交、下一条还未开始检查中断标志位。sepc保存的是下一条本该执行但还没执行的指令地址。对比项中断interrupt异常exception如 ecallsepc指向尚未执行的下一条指令引发异常的指令本身返回处理直接sret即可需要先sepc 4再sretecall是同步异常——CPU 执行到ecall这条指令执行它的效果就是触发 trap所以 sepc 指向 ecall 自身。返回用户态时需要4跳过它否则会无限循环执行 ecall。时钟中断是异步的——它不是某条指令造成的而是外部信号在两条指令之间被采样到的。sepc 指向的指令完全没被执行过恢复时应该原样执行它。从进入trap到返回用户态中断的开关情况可以把整条路径记成用户态开中断 - trap 硬件自动关中断 - usertrap 前半段保持关中断 - syscall 分支可以开中断 - 回用户态前 usertrapret 再关中断 - sret 回用户态后重新开中断1. 用户态运行时用户程序正常跑时中断通常是开的。这个状态来自usertrapret()里设置的x|SSTATUS_SPIE;SPIE1的意思是下一次sret回到用户态后把中断打开。2. trap 发生的一瞬间无论是ecall、时钟中断、还是 page faultCPU 进入 supervisor trap 时会自动做几件事sepc 当前恢复地址 SPP 之前的特权级 SPIE SIE SIE 0 pc stvec重点是SIE 被硬件清 0所以刚进入 trap 时中断已经关了。3. 进入usertrap()时此时中断还是关的。这里先做关键保存w_stvec((uint64)kernelvec);p-trapframe-epcr_sepc();这段不能被打断。尤其stvec还在切换epc还没保存完不能提前开中断。4. syscall 分支会开中断p-trapframe-epc4;intr_on();syscall();这里可以开中断因为 syscall 可能运行较久可能 sleep。如果 syscall 期间一直关中断时钟、磁盘、UART 都会受影响。但必须先做p-trapframe-epc4;因为ecall已经执行过返回时要跳过它。5. 时钟中断 / 设备中断分支一般不开中断}elseif((which_devdevintr())!0){这个分支进入时本来就是关中断状态。devintr()、alarm 现场保存、设置 handler一般都不需要你额外intr_off()/intr_on()。尤其不要在这里手动intr_on();因为后面还可能yield();usertrapret();trap 流程还没收尾提前开中断会制造不必要的嵌套风险。6. 回用户态前必须关中断intr_off();这是必须的因为 syscall 分支可能已经intr_on()了。接下来要做一组敏感操作w_stvec(TRAMPOLINE(uservec-trampoline));w_sepc(p-trapframe-epc);w_sstatus(x);这里正在把 trap 入口切回用户态 trampoline并准备sret。中间不能被内核中断打断。7.sret之后用户态重新开中断usertrapret()里设置x~SSTATUS_SPP;x|SSTATUS_SPIE;w_sstatus(x);然后 trampoline 执行sret。sret会根据SPIE恢复中断状态。因为SPIE1所以回到用户态后SIE 1也就是用户态中断重新打开。