3.系统调用跳转流程

系统调用跳转流程

以write()系统调用为例

1. 用户态

  1. 调用write()

  2. 跳转到usys.s\write

    #include "kernel/syscall.h"
    write:
     li a7, SYS_write
     ecall
     ret
    

    SYS_write的定义在kernel/syscall.h

    #define SYS_write  16
    

    SYS_write的索引16放到a7寄存器

    执行ecall,跳转到STVEC指向的地址

    !!!STVEC涉及到中断相关知识,后续补齐!!!

    ???STVEC中的地址在哪里设置的

    此处STVEC指向trampoline.S\uservec()

Q1.ECALL功能?

  1. user mode切换到supervisor mode
  2. pc的值保存到SEPC寄存器
  3. 跳转到STVEC指向的地址

ecall的功能十分简单,主要是为了提升软件设计的灵活性,但此时ecall做的事远远不够,对于xv6,我们还需要完成

  1. 保存31个用户寄存器的内容,之后用来恢复代码运行状态
  2. ecall不切换page table,因此此时还在使用user page table,需要切换到kernel page table
  3. 需要创建或者找到一个kernel stack,并将Stack Pointer寄存器的内容指向这个kernel stack。代码需要使用栈执行程序。
  4. 需要跳转到内核代码的某些合理的位置。
使用ECALL指令时,将系统调用类型存在a7寄存器,参数存在a0-a5寄存器

2 trampoline.s\uservec()

保存32个通用寄存器

uservec:
	# 需要保存所有寄存器的值,之后需要恢复执行
	# 此时所有通用寄存器都不可修改
  
  # 将`a0`存储到`sscratch`,这样`a0`就可以修改了
  csrw sscratch, a0
  
  # 将`TRAPFRAME`放到`a0`中,给后续存储提供基地址
  li a0, TRAPFRAME
  
  # 保存其他通用寄存器
  sd ra, 40(a0)
  sd sp, 48(a0)
  sd gp, 56(a0)
  sd tp, 64(a0)
  ....

	# 将之前保存在`sscratch`中的`a0`取出并保存
  csrr t0, sscratch
  sd t0, 112(a0)

  # 加载`TRAPFRAME`中的`kernel_sp`到`stack pointer`寄存器中
  ld sp, 8(a0)

  # 加载`kernel_hartid`(当前CPU编号)到`tp`寄存器
  ld tp, 32(a0)

  # 加载`usertrap()`地址到`t0`寄存器
  ld t0, 16(a0)

  # 加载`kernel page table`到`t1`寄存器中
  ld t1, 0(a0)

  # 等待之前内存访问完成,清空tle
  sfence.vma zero, zero

  # 加载`t1`到`satp`,页表转换为内核页表
  csrw satp, t1
  
  sfence.vma zero, zero

	# jump to usertrap(), which does not return
	jr t0

??? t0在哪里设置的

3 trap.c\usertrap()

确定trap类型并进行处理

void usertrap(void) {
  int which_dev = 0;
	
  // 获取`sstatus`寄存器的值,`SPP`位指示`trap`是来自
  // 用户模式还是管理模式
  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // 令`stvec`指向`kernelvec`而不是之前的`uservec`
  // 在kernel中,trap处理不必使用user模式的逻辑
  w_stvec((uint64)kernelvec);
	
  // myproc()根据之前存储的hart_id值,查询进程数组
  // 获取当前进程信息
  struct proc *p = myproc();
  
  // 将`ecall`指令存储在`sepc`中的`pc`值保存到`trapframe`中
  p->trapframe->epc = r_sepc();
  
  //获取`scause`的值,判断trap类型
  if(r_scause() == 8){
    // system call

    // 判断当前进程是否被杀掉
    if(killed(p))
      exit(-1);

    // pc存储的是系统调用的地址,返回时需指向下一条指令
    p->trapframe->epc += 4;

    // 修改`sstatus`的SIE位,打开中断,trap执行过程中可响应中断
    intr_on();

		// 根据之前存储在`p->trapframe->a7`中的值找到系统调用并执行
    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());
    setkilled(p);
  }
	
  // 如果进程已关闭,就不必恢复了
  if(killed(p))
    exit(-1);
  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

Q1 为什么需要存储sepc中的pc值?

当前程序运行时,CPU可能切换到别的进程,而别的进程如果也进行系统调用,sepc寄存器就会被修改,这个操作可以放到之前的trampoline.s\uservec()中,逻辑更统一。

?scause是何时被设置的

ecall指令用于向运行时环境发出请求,如系统调用,因此应该是ecall设置的

4.1.4 trap.c\usertrapret()

void usertrapret(void)
{
  struct proc *p = myproc();

  // 关闭中断,因为接下来会修改`stvec`指向uservec,此时如果发生中断,程序会跳转到用户trap处理代码,容易出错
  intr_off();

  // 设置STVEC指向uservec
  uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
  w_stvec(trampoline_uservec);

  // 将一些内核信息存储到trapframe中方便下次使用
  p->trapframe->kernel_satp = r_satp();
  p->trapframe->kernel_sp = p->kstack + PGSIZE;
  p->trapframe->kernel_trap = (uint64)usertrap;
  p->trapframe->kernel_hartid = r_tp();

  unsigned long x = r_sstatus();
  
  //SPP位和SPIE位都会影响sret指令的行为
  
  // SPP为0表示下次执行sret的时候,返回user mode
  // 而不是supervisor mode
  x &= ~SSTATUS_SPP;
  
  //SPIE位控制在执行完sret之后,是否打开中断。因为我们希望打开中断
  //所以这里将SPIE bit位设置为1
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);

  // 将之前存储的sepc值写回
  w_sepc(p->trapframe->epc);

  // 根据user page table地址生成相应的SATP值
  uint64 satp = MAKE_SATP(p->pagetable);

  // 我们会在汇编代码trampoline.S\userret()中完成page table
  // 的切换,切换只能在trampoline中完成,因为只有trampoline中
  // 的代码是同时在用户和内核空间中映射的
  
  // 求出trampoline.S\userret()的地址
  uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
  // satp会作为第二个参数传给userret()
  ((void (*)(uint64))trampoline_userret)(satp);
}

4.1.5 trampoline.S\userret()

userret:
	# a0: user page table, for satp.
	
  # switch to the user page table.
  sfence.vma zero, zero
  csrw satp, a0
  sfence.vma zero, zero

	# a0指向TRAPFRAME作为基地址
  li a0, TRAPFRAME

  # restore all but a0 from TRAPFRAME
  ld ra, 40(a0)
  ld sp, 48(a0)
  ld gp, 56(a0)
  ld tp, 64(a0)
  ...

	# restore user a0
	ld a0, 112(a0)
	# sret功能:
	# 1.程序切换回user mode
	# 2.SEPC寄存器的数值会被拷贝到PC寄存器
	# 3.重新打开中断
	sret

系统调用执行结束

posted @ 2024-04-21 00:18  INnoVation-V2  阅读(8)  评论(0编辑  收藏  举报