6.s081 lab4 page

Lab4: traps

基本概念

satp:保存页表地址(用户态和内核态)

sepc:保存用户态的下一条指令的地址(用来内核到用户的恢复)

sscratch:保存 trapframe页的虚拟地址

stvec:保存进入内核trampoline代码的pc地址(trampoline代码起始地址0x3ffffff000)

如何保存用户寄存器

1 sscratch寄存器,启动是进入user空间之前,kernel会把trapframe页地址保存在这里面

2 可以在trapframe上面用来保存32个用户寄存器(系统调用参数在寄存器上,所以参数也保存在trapframe上面)

trap的过程

用户空间调用write(),设置系统调用编号,执行ecall-> trampoline(每个进程指向同一个物理内存代码) ->内核

1-1用户态代码 user->kernel

ecall(硬件操作):

  1. usermode 到supervisormode,
  2. 把usermode代码的pc地址保存在sepc(为了返回usermode 继续执行)
  3. 跳转到stvec的内核代码(让pc=3ffffff000,trampoline.S的代码位置)

1-2trampoline代码(uservec部分进入内核前准备)

注: trampoline物理地址和虚拟地址直接映射的

csrrw a0,sscratch,a0,(把系统调用的参数从a0交换到sscratch,sscratch是内核trapframe栈的地址,交换之后a0是内核trapframe栈地址,sscratch是系统调用参数)

1.先sd

保存32个寄存器到trapframe(从0x3fffffe000往后)

保存sscratch(系统调用参数)

2.然后ld

读取内核的栈指针到sp

读取usertrap()地址到t0

读取kernel page table地址到satp(切换成了kernel页表)

3.jr t0

跳转到t0的usertrap()内核代码

1-3内核trap.c的 usertrap()

stvec设置为内核地址

保存sepc(用户代码位置)到当前内核的的epc,防止进程切换修改之前的sepc

sepc = sepc+4,表示用户空间的ecall后的下一条指令

根据case的值if else判断是那种trap,如果是8表示系统调用,执行syscall()

系统调用完成之后usertrapret();

2-1usertrapret() kernel->user

关中断(如果这时候发生中断,会走向用户的trap代码,会导致内核出错)

更新stvec为trampoline代码位置,方便用户下次进入内核使用

保存内核寄存器指针(页表,内核栈,usertrap()地址)

pc设置为sepc的值

satp设置为user page table (切换成了user页表)

跳到trampoline代码,恢复用户寄存器的地方

2-2trampoline代码(userret部分进入用户空间前准备)

切换页表

sscratch保存系统调用返回值

恢复32个寄存器

csrrw a0,sscratch,a0,(把系统调用的返回值sscratch交换到a0,sscratch是内核统调用的返回值,交换之后a0是系统调用返回值,sscratch是内核trapframe栈地址)

sret

切换用户模式,

复制sepc到pc

跳转到stvec的用户代码

Lab: traps

git checkout traps

phase2 Backtrace (moderate)

要求:打印出方法的调用栈

xv6栈调用图

image-20240427143035383

如上图每个栈帧中的特定位置的是返回地址,打印出来就好

fp 寄存器指向当前栈帧的开始地址

sp寄存器指向栈帧的结束地址

return地址在 fp-8的地址处 处

上一个栈帧在fp-16的地址所指向的地址处

void backtrace(){
  //uint64 ksp =(myproc()->trapframe->kernel_sp); 0x0000003fffffa000 表示栈顶指针
printf("backtrace: \n");
  //获取当前栈帧指针fp,保存在寄存器s0之中 s0=0x0000003fffff9f80 
  uint64 cfpV = r_fp();
  uint64 curPage = PGROUNDUP(cfpV);
  while(curPage == PGROUNDUP(cfpV)){
    uint64* returnAddrP = (uint64 *)(cfpV-8);
    printf("%p\n", *returnAddrP);
    cfpV = *(uint64 *)(cfpV - 16);
  }
}

Alarm (hard)

要求:新增 sigalarm(int tick, void (*handler)()) 和 sigreturn(void) 系统调用,当进程调用 sigalarm ,每经过tick个时钟周期,从内核态执行用户态的 handler 函数;sigreturn 用来从用户态正确恢复到内核态(执行 handler 之后应该回到内核态)。

1.添加系统调用

1.1 user

user.h (系统调用入口)

int sigalarm(int, void (*handler)());
int sigreturn(void);

usys.pl

entry("sigalarm");
entry("sigreturn");

usys.S

.global sigalarm
sigalarm:
 li a7, SYS_sigalarm
 ecall
 ret
.global sigreturn
sigreturn:
 li a7, SYS_sigreturn
 ecall
 ret

1.2 kernel

syscall.h (系统调用定义)

#define SYS_sigalarm  22
#define SYS_sigreturn  23

syscall.c

extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm]   sys_sigalarm,
[SYS_sigreturn]   sys_sigreturn,
}

sysproc.c (系统调用实现)

uint64
sys_sigalarm(void)
{
  return 0;
}
uint64
sys_sigreturn(void)
{
return 0;
}

2.实现系统调用

思路:

  1. 在执行普通用户态代码的时候,当每次发生中断(用户态->内核态->trap.c->usertrap()),根据if条件找到时钟中断的代码处,更新进程的状态,直到满足tick条件,修改pc寄存器到用户态的 handler 地址(需实现),返回用户态(系统已实现)
  2. 当执行handler函数之后,应该继续执行之前普通的用户态代码,所以需要使用 sigreturn 系统调用,让trapframe的寄存器值应该恢复到 handler 执行之前的样子,此时 pc 就指向普通代码的下一条指令地址,再返回用户态,进程正常流程。

proc.h 为进程添加字段

struct proc {
  ....
  uint64 period; //频率,sigalarm第一个参数
  uint64 handlerAddr;   //sigalarm的handler函数,sigalarm第第二个参数
  uint64 trickCount; //进程已经trick的次数
    //!! 需要为prevTrapframe分配和回收内存,在proc.c的allocproc()函数和freeproc()中添加代码,参考trapframe一样
  struct trapframe *prevTrapframe; //时钟中断到用户态时,保存当前进程的trapframe
  uint64 rerntrantCount; //进入sigalarm的次数
};

trap.c : 出现时钟中断的代码

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
//
void
usertrap(void){
......
    
 // give up the CPU if this is a timer interrupt.
 //计算机时钟中断入口
if(which_dev == 2){
    struct proc* p = myproc();
    if (p->period>0)
    {
      p->trickCount++;
       //指定时钟周期之后报警
      if(p->trickCount>=p->period && p->rerntrantCount==0){
        p->trickCount=0;
          
        //保存当前的trapframe,用来执行完成handler再恢复到现在的样子,让程序正常执行
        p->prevTrapframe->kernel_satp=p->trapframe->kernel_satp;
        p->prevTrapframe->kernel_sp=p->trapframe->kernel_sp;
        p->prevTrapframe->kernel_trap=p->trapframe->kernel_trap;
        p->prevTrapframe->epc=p->trapframe->epc;
        p->prevTrapframe->kernel_hartid=p->trapframe->kernel_hartid;
        p->prevTrapframe->ra=p->trapframe->ra;
        p->prevTrapframe->sp=p->trapframe->sp;
        p->prevTrapframe->gp=p->trapframe->gp;
        p->prevTrapframe->tp=p->trapframe->tp;
        p->prevTrapframe->t0=p->trapframe->t0;
        p->prevTrapframe->t1=p->trapframe->t1;
        p->prevTrapframe->t2=p->trapframe->t2;
        p->prevTrapframe->s0=p->trapframe->s0;
        p->prevTrapframe->s1=p->trapframe->s1;
        p->prevTrapframe->a0=p->trapframe->a0;
        p->prevTrapframe->a1=p->trapframe->a1;
        p->prevTrapframe->a2=p->trapframe->a2;
        p->prevTrapframe->a3=p->trapframe->a3;
        p->prevTrapframe->a4=p->trapframe->a4;
        p->prevTrapframe->a5=p->trapframe->a5;
        p->prevTrapframe->a6=p->trapframe->a6;
        p->prevTrapframe->s2=p->trapframe->s2;
        p->prevTrapframe->s3=p->trapframe->s3;
        p->prevTrapframe->s5=p->trapframe->s5;
        p->prevTrapframe->s6=p->trapframe->s6;
        p->prevTrapframe->s7=p->trapframe->s7;
        p->prevTrapframe->s8=p->trapframe->s8;
        p->prevTrapframe->s9=p->trapframe->s9;
        p->prevTrapframe->s10=p->trapframe->s10;
        p->prevTrapframe->s11=p->trapframe->s11;
        p->prevTrapframe->t3=p->trapframe->t3;
        p->prevTrapframe->t4=p->trapframe->t4;
        p->prevTrapframe->t5=p->trapframe->t5;
        p->prevTrapframe->t6=p->trapframe->t6;
        //简单代码可以这样 *p->prevTrapframe = *p->trapframe; 
          
        //pc切换到用户空间的handler函数
        p->trapframe->epc=p->handlerAddr;
          
         //防止进程重入
        p->rerntrantCount=1; 
      }

    }
    yield();
  }
  usertrapret();

}


sys_sigalarm:从trapframe取出两个参数,放在进程变量中

uint64
sys_sigalarm(void)
{
  struct proc* p = myproc();
  int ticks;
   if(argint(0, &ticks) < 0)
    return -1;
  uint64 handlerP;
  if(argaddr(1, &handlerP) < 0)
    return -1;

  p->period = ticks;

  p->handlerAddr = handlerP;
  p->trickCount = 0;

  return 0;
}

sys_sigreturn:让执行完handler之后的,进程恢复到正常代码流程,所以要恢复之前的trapframe(保存用户态的所有寄存器)

uint64
sys_sigreturn(void)
{
  struct proc* p = myproc();

  p->trapframe->kernel_satp=p->prevTrapframe->kernel_satp;
  p->trapframe->kernel_sp=p->prevTrapframe->kernel_sp;
  p->trapframe->kernel_trap=p->prevTrapframe->kernel_trap;
  p->trapframe->epc=p->prevTrapframe->epc;
  p->trapframe->kernel_hartid=p->prevTrapframe->kernel_hartid;
  p->trapframe->ra=p->prevTrapframe->ra;
  p->trapframe->sp=p->prevTrapframe->sp;
  p->trapframe->gp=p->prevTrapframe->gp;
  p->trapframe->tp=p->prevTrapframe->tp;
  p->trapframe->t0=p->prevTrapframe->t0;
  p->trapframe->t1=p->prevTrapframe->t1;
  p->trapframe->t2=p->prevTrapframe->t2;
  p->trapframe->s0=p->prevTrapframe->s0;
  p->trapframe->s1=p->prevTrapframe->s1;
  p->trapframe->a0=p->prevTrapframe->a0;
  p->trapframe->a1=p->prevTrapframe->a1;
  p->trapframe->a2=p->prevTrapframe->a2;
  p->trapframe->a3=p->prevTrapframe->a3;
  p->trapframe->a4=p->prevTrapframe->a4;
  p->trapframe->a5=p->prevTrapframe->a5;
  p->trapframe->a6=p->prevTrapframe->a6;
  p->trapframe->s2=p->prevTrapframe->s2;
  p->trapframe->s3=p->prevTrapframe->s3;
  p->trapframe->s5=p->prevTrapframe->s5;
  p->trapframe->s6=p->prevTrapframe->s6;
  p->trapframe->s7=p->prevTrapframe->s7;
  p->trapframe->s8=p->prevTrapframe->s8;
  p->trapframe->s9=p->prevTrapframe->s9;
  p->trapframe->s10=p->prevTrapframe->s10;
  p->trapframe->s11=p->prevTrapframe->s11;
  p->trapframe->t3=p->prevTrapframe->t3;
  p->trapframe->t4=p->prevTrapframe->t4;
  p->trapframe->t5=p->prevTrapframe->t5;
  p->trapframe->t6=p->prevTrapframe->t6;
  //*p->trapframe = *p->prevTrapframe; 
  p->rerntrantCount=0;
  return 0;
}

实验结果

make grade

image-20240427143035383

收获

  • 复习了系统调用的实现过程
  • 理解了 risc-v 的调用栈的结构
  • 理解了xv6处理中断,异常,系统调用的过程,用户态到内核态切换的原理