目录
- 预备知识
- trap机制
- RISC-V CPU硬件操作
- 来自用户空间的trap
- 来自内核空间的trap
- tramponline.S
- backtrace
- 获取当前的调用函数栈帧
- 实现backtrace()
- Alarm
预备知识
trap机制
导致CPU将指令的正常执行转到处理某些类型事件的特殊代码,这些事件统称为traps。
- 系统调用,执行ecall指令
- 异常:程序执行了非法操作,比如除以0或者使用无效的虚拟地址
- 设备中断
对于Xv6来说,异常将由内核来进行处理,通常的顺序是trap强制将控制转移到内核;内核保存寄存器和其它状态,使得可以恢复执行;内核执行适当的处理程序代码(例如,系统调用实现或设备驱动程序);内核恢复所保存的状态并从陷阱返回;并且原始代码在其停止的地方恢复。
Xv6的trap处理分为四个阶段:RISC-V CPU执行的硬件操作,为内核C代码准备的汇编向量,决定如何处理trap的C处理程序,以及系统调用或设备驱动程序服务例程。
Xv6为三种不同的trap准备了单独的汇编向量和trap处理程序:来自用户空间的trap,来自内核空间的trap和定时器中断。
RISC-V CPU硬件操作
由于trap处理程序是不同于之前执行的代码,因此需要切换硬件的状态。用户应用程序可以使用全部的32个寄存器,下面是一些重要的寄存器的作用:
- 程序计数寄存器(PC):指向CPU将要执行的下一条代码的地址
- MODE标志位:表明当前是supervisor mode 还是 user mode
- SATP寄存器:低44位存放了当前使用的根页表所在的PPN
- STVEC寄存器:保存内核中处理trap的指令地址
- SEPC寄存器:在trap过程中保存PC的内容,用于恢复
- SCAUSE寄存器:保存trap的原因
- SSCRATCH寄存器:保存了指向进程trapframe的指针
- SSTATUS寄存器:SIE位控制是否启用设备中断。如果SIE为0,RISC-V将延迟设备中断,直到内核设置SIE。SPP位指示trap是来自user模式(0)还是其他模式(1);而当执行sret从trap返回时,如果SPP为0,返回到U-mode,为1返回到S-mode
具体来说,RISC-V 硬件对除了定时器中断外的所有trap执行以下操作:
- 如果造成trap的原因是硬件中断,并且SSTATUS中的SIE位清0,然后跳过以下步骤
- 将SIE位清0,关闭设备中断
- 将PC的值复制到SEPC中
- 在SSTATUS的SPP位保存当前模式
- 设置SCAUSE以保存trap的原因
- 设置模式为supervisor
- 将STVEC中的值复制到PC中
- 从新的PC值开始执行
CPU不负责切换内核页表、不切换内核栈、不保存PC以为的任何寄存器,这些将由内核来进行处理。
来自用户空间的trap
这类trap的高级路径是:uservec(kernel/tramponline.S:16) -> usertrap(kernel/trap.c:37) -> usertapret(kernel/trap.c:90) -> userret(kernel/tramponline.S:16). 由于此时SATP中指向的是用户页表,而RISC-V硬件不负责切换页表,因此用户页表必须包含uservec的映射,然后由uservec来切换SATP以使用内核页表. 而为了在切换后不影响uservec的执行,uservec在用户页表和内核页表中必须使用相同的地址.Xv6使用tramponline page来实现这一功能, tramponlie page被映射到内核页表和所有用户页表的同一虚拟地址, 具体内容也就是tramponline.S.当执行用户代码时,STVEC设置为uservec。执行完uservec(具体看tramponline部分)后,接下就是usertrap,主要功能是确定trap原因,并处理返回。- //kernel/trap.c
- void
- usertrap(void)
- {
- int which_dev = 0;
-
- //SSP保存了发生trap的模式,不为0说明不是来自用户空间的trap
- if((r_sstatus() & SSTATUS_SPP) != 0)
- panic("usertrap: not from user mode");
- //更改STVEC寄存器,使其指向kernelvec
- //这是内核空间中处理trap代码的地址
- //因为此时处于内核态
- w_stvec((uint64)kernelvec);
- struct proc *p = myproc();
-
- //保存SPEC中的用户程序计数器
- //这是为了防止发生了进程切换导致覆盖了SEPC
- //因此将其保存到与进程关联的trapframe中
- p->trapframe->epc = r_sepc();
-
- //接下来根据trap的原因进行处理
- //系统调用
- if(r_scause() == 8){
- if(p->killed)
- exit(-1);
- //应该恢复在下一条指令,因为当前触发trap的指令已经执行了
- //也就是ecall的下一条,因此+4
- p->trapframe->epc += 4;
- //此时已经不会改变寄存器的状态了
- //修改SSTATUS的SIE位,开中断
- intr_on();
- //转移给系统调用
- syscall();
- }
- //设备中断
- else if((which_dev = devintr()) != 0){
- // ok
- } else {
- printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
- printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
- p->killed = 1;
- }
- if(p->killed)
- exit(-1);
- //放弃CPU时间
- if(which_dev == 2)
- yield();
- //跳转入usertrapret
- usertrapret();
- }
复制代码 usertrapret的作用是准备内核态到用户态切换。这个函数还在第一次进入用户态时被调用,具体路径:userinit()->alloccproc()->forkret()->usertrapret()- //kernel/usertrapret
- void
- usertrapret(void)
- {
- struct proc *p = myproc();
- //关中断,防止出错
- intr_off();
-
- //这里是返回用户态
- //设置STVEC寄存器指向tramponline中的代码,这里就是uservec
- w_stvec(TRAMPOLINE + (uservec - trampoline));
- //设置trapframe的数据,下一次从用户空间转换到内核空间可能会使用
- 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()
- //设置SSTATUS寄存器
- unsigned long x = r_sstatus();
- x &= ~SSTATUS_SPP; // 置SPP位为0控制sret返回到用户模式,实际上的切换在tramponline.S中
- x |= SSTATUS_SPIE; // 开中断
- w_sstatus(x);
- //设置SEPC寄存器,确认返回后执行地址
- w_sepc(p->trapframe->epc);
- //准备好用户页表的satp值,实际山的切换在tramponline.S中
- uint64 satp = MAKE_SATP(p->pagetable);
- //跳转进入userret
- uint64 fn = TRAMPOLINE + (userret - trampoline);//计算跳转地址
- ((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);//传入fn,satp
- }
复制代码 来自内核空间的trap
这类trap的路径:kernelvec(kernel/kernelvec.S:10)->kerneltrap(kernel/trap.c:134)->kernelvec(kernel/kernelvec.S:48)
这时
- 当内核在CPU上执行时,不同于在用户空间,直接使用内核页表和内核栈指针即可。因kernelvec会在被中断的内核进程的内核栈上分配空间直接保存寄存器。
- 接下来kernelvec会调用kerneltrap
- kerneltrap
- //从内核代码的中断和异常都会经由kernelvec到达这里
- void
- kerneltrap()
- {
- // 保存当前CPU的一些寄存器
- // 因为有可能当前处理的是一个时钟中断,进而会导致CPU的调度
- // 导致这些寄存器被覆盖
- // 所以必须保留下来以备将来恢复
- int which_dev = 0;
- uint64 sepc = r_sepc();
- uint64 sstatus = r_sstatus();
- uint64 scause = r_scause();
-
- //检查是否是来自内核态的trap
- if((sstatus & SSTATUS_SPP) == 0)
- panic("kerneltrap: not from supervisor mode");
- //检查中断是否关闭
- if(intr_get() != 0)
- panic("kerneltrap: interrupts enabled");
- //使用devintr来确定中断的类型,它会去检查SCAUSE寄存器的值并进行处理
- //返回值表明中断的类型,0表明是异常
- if((which_dev = devintr()) == 0){
- printf("scause %p\n", scause);
- printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
- panic("kerneltrap");
- }
- //时钟中断则放弃CPU
- if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
- yield();
- //恢复寄存器
- w_sepc(sepc);
- w_sstatus(sstatus);
- }
- //返回kernelvec
复制代码
- 恢复寄存器,销毁新增的栈内存
- 执行sret:复制SEPC的值到PC,开中断
tramponline.S
uservec函数
uservec是tramponline.S的第一部分。由于主要是汇编,这里就不贴源码了
- 首先执行csrrw指令,这会交换a0和SSCRATCH两个寄存器寄存器的内容。交换后,a0寄存器中保存的就是tranpframe(kernel/proc.c)的地址,然后通过a0寄存器保存其他用户寄存器的数据。每个进程在创建时都会为trapframe分配一个页,并始终映射到TRAPFRAMKE虚拟地址,内核可以通过p->trapframe获得其物理地址来使用,这样trapframe page中就备份好了所有寄存器的数据。
- 保存原来a0的值(现在是在SSCRATCH中),同样是保存到trapframe page中
- 切换内核栈。trapframe中的kernel_sp保存了这个进程的kernel stack的栈顶,只需要将这里的值读入Stack Pointer寄存器(SP寄存器)即可
- 写入CUP的hartid到tp寄存器,这是因为每个核都有独立的寄存器
- 向t0寄存器写入将要执行的函数指针
- 向SATP寄存器写入内核页表地址(实际上是先写入t1,然后交换t1和SATP),清空TLB。
- 最后跳入t0(一般就是usertrap)。
userret函数
userret是tramponline.S的最后部分,也就是负责切换回用户空间。
- 设置SATP寄存器,切换为用户页表,并清空TLB
- 恢复SSCRATCH寄存器为traponline page中a0寄存器的值,这个值实际上已经被系统调用的返回值覆盖;
- 恢复寄存器。此时还剩下a0寄存器:trapframe的地址;SSCRACH寄存器:返回值
- 交换a0和SSCRATCH
- 调用sret:切换为user mode,将SEPC寄存器的值拷贝回PC,重新打开中断
这样就回到了用户空间。
backtrace
在kernel/printfc.中实现一个backtrace()函数,要求能够打印调用过的函数地址。
没想到在Lab3debug的时候写的代码在这里也有。
获取当前的调用函数栈帧
如图所示,每次函数调用的时候都会在栈上创建一个栈帧。fp寄存器指向当前栈帧的开始地址,sp寄存器指向当前栈帧的结束地址,注意栈是从高地址往低地址增长的,因此fp的地址是比sp高的。fp(-8字节)处是当前栈帧的返回地址,fp(-16字节)是上一个栈帧的起始地址。根据提示通过如下代码获取当前栈帧的fp。- //kernel/riscv.h
- static inline uint64
- r_fp()
- {
- uint64 x;
- asm volatile("mv %0,s0" : "=r" (x));
- return x;
- }
复制代码 实现backtrace()
- //kernel/printf.c
- void
- backtrace()
- {
- uint64 fp = r_fp();
- //只会为栈分配一页
- uint64 top = PGROUNDUP(fp);
- uint64 bottom = PGROUNDDOWN(fp);
- printf("backtrace:\n");
- while( fp < top && fp > bottom ){
- uint64 ra = *(uint64*)(fp-8);
- printf("%p\n",ra);
- fp = *(uint64*)(fp-16);
- }
- }
- //声明
- //kernel/defs.h
- void backtrace();
- //最后在kernel/sysproc.c的sys_sleep中调用即可
- uint ticks0;
- backtrace();
复制代码 运行qemu,然后运行bttest。验证后确实是对应的函数。- 0x0000000080002cdc
- 0x0000000080002bb6
- 0x00000000800028a0
复制代码 Alarm
实现sigalarm(interval,handler)和sigreturn系统调用,为使用CPU的用户进程实现定期通知功能。sigalarm的功能是当进程使用interval个tick后,调用一次handler。
sigreturn()的功能是从handler返回原来中断的位置。
系统调用准备
模仿Lab2的步骤,准备系统调用的相关代码。- //user/user.h
- //syscall.c
- int sigalarm(int ticks, void(*handler)() );
- int sigreturn(void);
- //系统调用入口
- //user/usys.pl
- entry("sigalarm");
- entry("sigreturn");
- //系统调用号
- //kernel/syscall.h
- #define SYS_sigalarm 22
- #define SYS_sigreturn 23
- //声明处理函数,并与系统调用号关联
- //kernel/syscall.c
- extern uint64 sys_sigalarm(void);
- extern uint64 sys_sigreturn(void);
- [SYS_sigalarm] sys_sigalarm,
- [SYS_sigreturn] sys_sigreturn,
复制代码 实现alarm
- //修改进程结构体,添加必要的属性
- //kernel/proc.h:proc
- char name[16]; // Process name (debugging)
- //新增
- int alarm_interval; //报警间隔
- void (*alarm_handeler)(); //相应的处理函数
- int alarm_ticks_count; //自从上次调用经过的ticks
- struct trapframe* alarm_trapframe; //保存中断之前的trapframe,用于恢复
- int alarm_on; //是否已有alarm在处理中
复制代码- //初始化
- //kernel/proc.c:allocproc()
- // Allocate a trapframe page.
- if((p->trapframe = (struct trapframe *)kalloc()) == 0){
- release(&p->lock);
- return 0;
- }
- //新增
- if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
- release(&p->lock);
- return 0;
- }
- p->alarm_interval = 0;
- p->alarm_handeler = 0;
- p->alarm_ticks_count = 0;
- p->alarm_on = 0;
复制代码- //释放
- //kernel/proc.c:freeproc()
- if(p->trapframe)
- kfree((void*)p->trapframe);
- p->trapframe = 0;
- //新增
- if(p->alarm_trapframe)
- kfree((void*)p->alarm_trapframe);
- p->alarm_trapframe = 0;
-
- p->xstate = 0;
- //新增
- p->alarm_interval = 0;
- p->alarm_interval = 0;
- p->alarm_ticks_count = 0;
- p->alarm_on = 0;
复制代码 实现sigalarm和sigreturn- //kernel/sysproc.c
- uint64
- sys_sigalarm(void)
- {
- int interval; //报警间隔
- uint64 handler; //处理函数地址
- if( argint(0, &interval) < 0)
- return -1;
- if( argaddr(1, &handler) < 0)
- return -1;
- struct proc* p = myproc();
- p->alarm_interval = interval;
- p->alarm_handeler = (void(*)())handler;
- p->alarm_ticks_count = 0;
- p->alarm_on = 0;
- return 0;
-
- }
- uint64
- sys_sigreturn(void)
- {
- struct proc* p = myproc();
- *p->trapframe = *p->alarm_trapframe;
- p->alarm_on = 0;
- return 0;
- }
复制代码 在usertrap中实现该时钟中断的代码- // give up the CPU if this is a timer interrupt.
- if(which_dev == 2){
- struct proc* p = myproc();
- //如果设置了时钟
- if( p->alarm_interval > 0){
- p->alarm_ticks_count++;
- //到达设定好的时钟计时,并且没有其他时钟在运行
- //如果有则重置计时,延迟触发
- if(p->alarm_ticks_count >= p->alarm_interval && p->alarm_on == 0){
- p->alarm_ticks_count = 0;
- p->alarm_on = 1;
- *p->alarm_trapframe = *p->trapframe;
- p->trapframe->epc = (uint64)p->alarm_handeler;
- }
- }
- yield();
- }
复制代码 最后修改Makefile,添加$U/_alarmtest
测试结果:- $ alarmtest
- test0 start
- .................................................alarm!
- test0 passed
- test1 start
- ......alarm!
- ......alarm!
- ......alarm!
- .......alarm!
- .......alarm!
- ......alarm!
- ......alarm!
- ......alarm!
- .......alarm!
- .......alarm!
- test1 passed
- test2 start
- .............................................................alarm!
- test2 passed
复制代码 来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |