周冰心 发表于 2025-7-15 14:50:38

Lab4 trap

目录

[*]预备知识

[*]trap机制

[*]RISC-V CPU硬件操作
[*]来自用户空间的trap
[*]来自内核空间的trap

[*]tramponline.S

[*]uservec函数
[*]userret函数


[*]backtrace

[*]获取当前的调用函数栈帧
[*]实现backtrace()

[*]Alarm

[*]系统调用准备
[*]实现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
0x00000000800028a0Alarm

实现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_sigreturn,实现alarm

//修改进程结构体,添加必要的属性
//kernel/proc.h:proc
char name;               // 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
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: Lab4 trap