iT邦幫忙

2022 iThome 鐵人賽

DAY 16
0

前言

前面看完了 ecall 的前後變化,接下來我們將繼續深入追蹤 trap 的流程,查看是如何進行整個 user mode 到 supervisor mode 的切換。

Overview

我們前面提到了當 user space 底下的 process 發生了 Timer Interrupt, Device Interrupt, System call, Expection 等等,皆會發生 trap,trap 發生後會先執行 uservec

名詞: trapframe 表示 process->trapframe,TRAPFRAME 表示 user address space,第二高的 page。

  • uservec() : 這是一段由組合語言寫成的程式碼(嚴格來說,uservec 並不是函式,因為他不會有 return 的行為),位於trampoline.s中,而這段程式碼會在 trampoline page 中,且會映射到 user address space 和 kernel address space,而uservec會進行以下工作:
    • 將暫存器以及 program counter 儲存在 TRAPFRAME 中,每一個 process 都有自己的 trapframe page,
    • tp, sp 暫存器重置,由於這一些暫存器在 user mode 時有被使用到,為了避免在 kernel 中讀取這一些來自 user mode 的資料,造成安全性的風險 (不信任來自於 user mode的資料)。
    • satp 指向 kernel page table
    • 跳轉到 usertrap()
  • usertrap()
    • 存放 kernel 的 trap vector 到 stvec
    • 存取 scause 暫存器,根據不同的 trap 原因對應到不同的行為
      • Exception: 印出錯誤訊息並結束 (exit) process
      • Device: 呼叫 deviceintr() 處理裝置的中斷
      • System call: 允許中斷,接著呼叫syscall(),如果 process 處於 died,呼叫exit()
      • Timer interrupt: 結束 time slice,如果 process 沒有被 kill,則呼叫 yield()
    • 呼叫 usertrapret()
  • usertrapret()
    • 禁用中斷
    • stvec 的內容設置成 uservec
    • 儲存 sp, tp 的內容
    • 將 program counter 的值放到 sepc
    • 跳轉到 userret()
  • userret() : 位於 trampoline page 中
    • satp 指向 user page table
    • 回復 user space 底下的暫存器內容
    • 設置 sstatus 暫存器的 bit,包含 spp,設置成 user mode,SPIE (previous interrupt flag) 設置成允許中斷
    • 而執行了 sret 後,中斷將被允許,且回到 user mode 中 (sret 會讀取 sstatus 暫存器的內容)

Trapframe (user address space 的 Trapframe)


當我們在 user mode 底下,sscratch CSR 會指向 trapframe,而我們試著從下到上分析 trapframe 這一個結構

  • Register Save Area : 儲存在 user mode 中 31 個通用的暫存器 (第32個永遠為0,不需要儲存)
  • epc: 儲存前一狀態的 program counter
  • kernel_harid: 當 trap 發生時,我們需要載入 hart id。
  • kernel_trap: usertrap()所在的記憶體地址
  • kernel_sp: 儲存 stack pointer,指向到某一個 process 所使用的 stack。
  • kernel_satp: 指向 kernel page table

而到這裡我們可以發現到,trap 的過程中會涉及 process 的暫存器儲存,以及 page table 的操作,而以下我們將看到 process 的結構

proc.h

proc.h中,有許多結構 (struct),分別為 cpu, proc 等等,以下將介紹。

// Per-CPU state.
struct cpu {
  struct proc *proc;          // The process running on this cpu, or null.
  struct context context;     // swtch() here to enter scheduler().
  int noff;                   // Depth of push_off() nesting.
  int intena;                 // Were interrupts enabled before push_off()?
};

extern struct cpu cpus[NCPU];

cpus 為一陣列,元素為struct cpu,而struct cpu中包含了 cpu 的狀態,這裡的 cpu 為核心 (hart),在 xv6 中定義這一台有NCPU核心的電腦,也就是8核心的電腦。

  • proc : 指向到目前該 hart 所執行的 process
  • context : 指向到目前該 hart 的暫存器狀態,也是一個結構 (struct)

而下面我們將開始 trace 整個 trap,在 ecall 完成後的動作。

uservec 儲存 user mode 底下暫存器資訊

uservec 為 trampoline.s 的一部分,以下為trampoline.s中的 uservec

#include "riscv.h"
#include "memlayout.h"

	.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:    
	#
        # trap.c sets stvec to point here, so
        # traps from user space start here,
        # in supervisor mode, but with a
        # user page table.
        #

        # save user a0 in sscratch so
        # a0 can be used to get at TRAPFRAME.
        csrw sscratch, a0

        # each process has a separate p->trapframe memory area,
        # but it's mapped to the same virtual address
        # (TRAPFRAME) in every process.
        li a0, TRAPFRAME
        
        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)
        sd tp, 64(a0)
        sd t0, 72(a0)
        sd t1, 80(a0)
        sd t2, 88(a0)
        sd s0, 96(a0)
        sd s1, 104(a0)
        sd a1, 120(a0)
        sd a2, 128(a0)
        sd a3, 136(a0)
        sd a4, 144(a0)
        sd a5, 152(a0)
        sd a6, 160(a0)
        sd a7, 168(a0)
        sd s2, 176(a0)
        sd s3, 184(a0)
        sd s4, 192(a0)
        sd s5, 200(a0)
        sd s6, 208(a0)
        sd s7, 216(a0)
        sd s8, 224(a0)
        sd s9, 232(a0)
        sd s10, 240(a0)
        sd s11, 248(a0)
        sd t3, 256(a0)
        sd t4, 264(a0)
        sd t5, 272(a0)
        sd t6, 280(a0)

	# save the user a0 in p->trapframe->a0
        csrr t0, sscratch
        sd t0, 112(a0)

        # restore kernel stack pointer from p->trapframe->kernel_sp
        ld sp, 8(a0)

        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        ld tp, 32(a0)

        # load the address of usertrap(), p->trapframe->kernel_trap
        ld t0, 16(a0)

        # restore kernel page table from p->trapframe->kernel_satp
        ld t1, 0(a0)
        csrw satp, t1
        sfence.vma zero, zero

        # a0 is no longer valid, since the kernel page
        # table does not specially map p->tf.

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

在一開始的時候,執行了csrw sscratch, a0,將暫存器 a0 的內容寫入到 sscratch CSR 中,也就是先將 a0 的值儲存下來,接著使用li a0, TRAPFRAME將 TRAPFRAME 的記憶體地址寫入到 a0 暫存器,這樣 a0 便指向到 TRAPFRAME,可以通過 a0 暫存器去對 TRAPFRAME 進行操作。而下面可以看到許多將暫存器內容寫入到 a0 指向的 TRAPFRAME 操作,TRAPFRAME 用來儲存 user space 的暫存器。(在上方我們展示了 trapframe 的結構,TRAPFRAME為物理記憶體,trapframe 為 process->trapframe,trapframe 映射到TRAPFRAME,映射的地址相同,物理地址和虛擬記憶體地址相等)。

接著執行csrr t0, sscratch 讀取 sscratch CSR,sscratch 裡面儲存 user space 的 a0 暫存器的值,接著儲存到 t0 暫存器中,接著sd t0, 112(a0)t0 暫存器的值儲存到 a0 (指向到 TRAPFRAME) offset 112 的地方。

ld sp, 8(a0)ld tp, 32(a0)會從 TRAPFRAME 中讀取stack pointer 以及 hart id,儲存到 sp 暫存器和 tp 暫存器。

ld t0, 16(a0) 這裡將 usertrap()的記憶體地址載入到 t0 暫存器中。

下面的指令是為了切換到 kernel page table,ld t1, 0(a0) 將 TRAPFRAME 中 offset 0,也就是存放 kernel page table 的記憶體地址,存放到 t1 暫存器中,csrw satp, t1t1 暫存器的值寫入到 satp CSR,目前 satp CSR 便指向到 kernel page table。

可以看到在執行完 uservec 後,會跳轉到 t0 暫存器所存放的記憶體地址,而 t0 記憶體地址指向的地方為usertrap(),因此在執行完 uservec 後會跳轉到usertrap()中,usertrap()位於kernel/trap.c中。可以發現我們沒有將 return address 給存下來,這邊直接進行跳轉,也就是 usertrap()執行結束後,不會回到 uservec 中。

而我們可以看到,我們是將 user space 底下的 process 的 context 內容儲存到 TRAPFRAME 中,而這個操作我們也稱為 context switch 中的儲存 context。

usertrap 切換到 supervisor mode 處理各種原因的 trap

以下為usertrap()

void
usertrap(void)
{
  int which_dev = 0;

  if((r_sstatus() & SSTATUS_SPP) != 0)
    panic("usertrap: not from user mode");

  // send interrupts and exceptions to kerneltrap(),
  // since we're now in the kernel.
  w_stvec((uint64)kernelvec);

  struct proc *p = myproc();
  
  // save user program counter.
  p->trapframe->epc = r_sepc();
  
  if(r_scause() == 8){
    // system call

    if(p->killed)
      exit(-1);

    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;

    // an interrupt will change sstatus &c registers,
    // so don't enable until done with those registers.
    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);

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2)
    yield();

  usertrapret();
}

第 6 行: 我們先讀取 sstatus CSR 的內容,比對 sstatus CSR 的 SPP 域,得知在進入 supervisor mode 以前的模式,SPP 域0表示 user mode,1表示 supervisor mode,我們通過比對其是否為0得知在進入 supervisor mode 之前是否為 user mode。

第 11 行: 現在我們處於 supervisor mode 底下,在 supervisor mode 底下處理 trap 的 handler code 所在的地址,也就是 kernelvec() 寫入到 stvec CSR,用來處理 supervisor mode 底下的 trap (在 supervisor mode 底下導致 trap 有兩種原因,分別為 exceptions 和 device interrupt)。

第 13 行: 獲得 process 的結構,從 process 結構中,我們可以得到 process 的 trapframe,在 process->trapframe 中存放 trapframe 的實體物理記憶體地址(每一個 process 的 trapframe 會映射到 user address space 的 trapframe),這裡之所以我們需要物理的記憶體地址,是因為目前我們處於 supervisor mode 底下,不使用 user page table,因此我們需要知道 process 持有的 trapframe 具體的物理記憶體地址。

第 16 行 : 在 trapfram 中存放了 user space 底下的 program counter,我們需要將它儲存下來,以便 trap 處理結束後回到 trap 之前的狀態。

接著我們進入到一個巨大的判斷式,使用 scause 判斷發生 trap 的原因,在上方我們可以看到有4種 trap 原因,為 exception, device, system call, timer interrupt 。

  • 先看到 System call,首先我們會確認 process 是否被 kill 了,如果 killed 域為1,表示該 process 被 kill 了,我們便結束該 process,關於 kill 會牽涉到 sleep()wakeup(),這部分將在後續進行討論。

    接著我們會進入到 syscall()syscall() 會需要使用到 ecall,避免我們在結束 syscall() 後再度進入 ecall,我們將 program counter + 4。

    在第32行的地方,執行了 syscall(),以下為syscall()

    void
    syscall(void)
    {
      int num;
      struct proc *p = myproc();
    
      num = p->trapframe->a7;
      if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
        p->trapframe->a0 = syscalls[num]();
      } else {
        printf("%d %s: unknown sys call %d\n",
                p->pid, p->name, num);
        p->trapframe->a0 = -1;
      }
    }
    

    看到 syscall() 會根據傳入的 System call 編號在 syscalls 這一個陣列中尋找對應的 System call,以 write() 這一個函式來說,他會執行的 System call 便是 sys_write()sys_write() 會將要輸出的內容輸出到 console 上,並且結束後回到 syscall()

  • 如果不是 System call 導致 trap,我們接著會使用devintr()判斷,devintr()回傳1表示為 UART 或是 disk 所導致的 trap,2 表示 timer interrupt (software interrupt),0 表示其他或是錯誤。非錯誤的 trap 會在 devintr() 完成處理,也就是devintr() 為 UART, disk, software interrupt 造成的 trap 的 handler code。

  • 接著處理上方 devintr() 回傳 0 的錯誤,會印出一些錯誤訊息,並且將該 process kill。

第 45 行: 如果為 timer interrupt,則會呼叫 yield()進行處理。這個函式會在 thread 的討論中探討。

在執行完 usertrap() 後,接著會進入到 usertrapret()

usertrapret 準備回到 user mode

以下為usertrapret()

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

  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(), so turn off interrupts until
  // we're back in user space, where usertrap() is correct.
  intr_off();

  // send syscalls, interrupts, and exceptions to uservec in trampoline.S
  uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
  w_stvec(trampoline_uservec);

  // set up trapframe values that uservec will need when
  // the process next re-enters the kernel.
  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()

  // set up the registers that trampoline.S's sret will use
  // to get to user space.
  
  // set S Previous Privilege mode to User.
  unsigned long x = r_sstatus();
  x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
  x |= SSTATUS_SPIE; // enable interrupts in user mode
  w_sstatus(x);

  // set S Exception Program Counter to the saved user pc.
  w_sepc(p->trapframe->epc);

  // tell trampoline.S the user page table to switch to.
  uint64 satp = MAKE_SATP(p->pagetable);

  // jump to userret in trampoline.S at the top of memory, which 
  // switches to the user page table, restores user registers,
  // and switches to user mode with sret.
  uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
  ((void (*)(uint64))trampoline_userret)(satp);
}

在一開始我們先將中斷禁用,避免我們損失一些 CSR 的資訊

第 13 行: 我們寫入 stvec CSR,先前裡面為處理 supervisor mode 底下 trap 的 handler code 的記憶體地址,現在我們將 uservec 的記憶體地址寫入,之後發生中斷,我們便會執行 uservec。

第 17 行: satp CSR 中為 kernel page table 的地址,我們將其寫到 process 的 trapframe 特定域中。

第 19 行: 將 usertrap(),也就是 trap 的 handler code 的地址儲存到 process 的 trapframe 特定域中。

第 20 行: 由於我們在 timer interrupt 通過了 yield() 進行了一些處理,而在yield()中可能會有切換 thread 的情況發生,因此我們需要 tp 暫存器得到目前的 thread。

第 26~29 行: 我們讀取 sstatus CSR,將 SPP 域清空,全部皆為 0,前面提到 0 表示user mode,為等一下回到 user mode 進行準備,接著允許中斷,然後寫回到 sstatus CSR。

接著回復 program counter 的值,以及設置 satp CSR 指向到 user space process 的 page table。

下方,我們通過 function pointer 進入到 userret,將 satp 作為參數,satp 已經指向到 user space process 的 page table。

userret 切換到 user mode

userret:
        # userret(pagetable)
        # switch from kernel to user.
        # a0: user page table, for satp.

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

        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)
        ld t0, 72(a0)
        ld t1, 80(a0)
        ld t2, 88(a0)
        ld s0, 96(a0)
        ld s1, 104(a0)
        ld a1, 120(a0)
        ld a2, 128(a0)
        ld a3, 136(a0)
        ld a4, 144(a0)
        ld a5, 152(a0)
        ld a6, 160(a0)
        ld a7, 168(a0)
        ld s2, 176(a0)
        ld s3, 184(a0)
        ld s4, 192(a0)
        ld s5, 200(a0)
        ld s6, 208(a0)
        ld s7, 216(a0)
        ld s8, 224(a0)
        ld s9, 232(a0)
        ld s10, 240(a0)
        ld s11, 248(a0)
        ld t3, 256(a0)
        ld t4, 264(a0)
        ld t5, 272(a0)
        ld t6, 280(a0)

	# restore user a0
        ld a0, 112(a0)
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

a0 暫存器的內容,寫入到 satp CSR,現在 satp 指向到 user space 的 process 的 page table,接著從 TRAPFRAME 回復暫存器的值 (TRAPFRAME 存放 user space 的暫存器資訊)。也稱為 context switch 中的恢復 context。

接著將 TRAPFRAME 的地址寫回到 a0 暫存器的特定域。

使用 sret 回到 user mode。

到這裡便完成了整個 trap 的操作,從 ecall,到暫存器資訊的儲存,trap handler 以及回到 user mode的操作。接下來我們將從前面所提及的fork()exec()等 System call 來看實際上 user space 底下的 System call 的 Trap 流程與路徑。

reference

SiFive FU540-C000 Manual v1p0
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book


上一篇
Day-14 xv6 Trap (user mode): overview, ecall
下一篇
Day-16 xv6 Trap (user mode): Trace exec()
系列文
與作業系統的第一類接觸 : 探索 xv631
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言