3.系統呼叫跳轉流程

INnoVation-V2發表於2024-04-21

系統呼叫跳轉流程

以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

系統呼叫執行結束

相關文章