iT邦幫忙

0

[6.1810][code] xv6 的 traps (一)

  • 分享至 

  • xImage
  •  

系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念

大綱

  • ch4 Traps and system calls
  • ch4.1 RISC-V trap machinery
  • kernel/trampoline.S/uservec
  • kernel/trap.c/usertrap
  • kernel/trap.c/prepare_return
  • kernel/trampoline.S/userret
  • kernel/trap.c/trapinit
  • kernel/trap.c/trapinithart
  • ch4.2 Traps from user space

ch4 Traps and system calls

有三種事件會讓 CPU 擱置原本的 instruction,強制將控制權轉交給負責該事件的特別的 kernel code。

  • system call
    • a user program executes the ecall instruction to ask the kernel to do something
  • exception
    • an instruction (user or kernel) does something illegal, such as load from an invalid virtual address.
  • device interrupt
    • when a device signals that it needs attention, for example when the disk hardware finishes a read or write request

這三種事件在這裡都統稱為 trap
通常發生 trap 時正在被執行的程式碼,在之後會希望可以恢復執行,並且不需要意識到發生了什麼事。

我們通常會希望 trap透明的(transparent) ( 不會被 trap 的 process 所察覺。例如一個正在計算某個東西的程式,發生了 timer interrupt 的時候,這個程式不需要為這個 timer interrupt 特別處理,大部分的時候也不會知道,不會看到,不會觀察到 timer interrupt 的發生,就好比 timer interrupt 對於被 trap 的 process 來說,是透明的 )

一個 trap 會強制將控制權交給 kernel ( from U-mode to S-mode ),xv6 kernel 會保存 register 的值或是其他狀態,讓我們可以返回 trap 前正在執行的程式碼。

kernel 會執行適當的 handler ( system call implementation 或是 device driver ),kernel 會把 trap 前的狀態恢復,並從 trap 回到原本的執行流,從原本發生 trap 的地方恢復執行。

xv6-riscv 會在 kernel ( S-mode ) 去處理所有的 trap ( 包含 system call, exception, interrupt ),並不會在 U-mode 去處理 trap。

xv6 的 trap handling 有四個步驟

  • hardware actions taken by the RISC-V CPU
  • some assembly instructions that prepare the way for kernel C code
  • a C function that decides what to do with the trap
  • the system call or device-driver service routine

雖然 kernel 可以用單一個 code path 去處理三種 trap,但把兩個情況分開來處理會更加方便

  • 來自 user space ( U-mode ) 的 trap
  • 來自 kernel space ( S-mode ) 的 trap

處理 trap 的 kernel code 通常被稱為 handler
handler 的第一道指令通常會以組合語言撰寫,並且有時被稱為 vector



ch4.1 RISC-V trap machinery

每個 RISC-V CPU 都有一組 CSR, kernel 透過寫入這些暫存器來告知 CPU 如何處理 trap,並透過讀取這些 CSR 來了解發生了什麼樣的 trap

這邊整理一下重要的 registers

  • stvec
    • The kernel writes the address of its trap handler code here; the RISC-V jumps to the address in stvec to handle a trap.
  • sepc
    • 當一個 trap 發生的時候,RISC-V 會把 pc 值存進 sepc ( 因為 pc 已經被 stvec 覆蓋了 ),之後sret ( return from trap ) 會把 sepc 存回 pc。
    • kernel 可以藉由設定 sepc,來控制 sret 要跳去哪個地方。
  • scause
    • RISC-V puts a number here that describes the reason for the trap.
  • sscratch
    • The kernel trap handler code uses sscratch to help it avoid overwriting user registers before saving them.
  • sstatus
    • sstatus.SIE bit 決定了 device interrupt 能不能 trap CPU。假如 SIE == 0,RISC-V 會推遲 device interrupt ( 但不會推遲 system call, exception ) 直到 kernel 設定 SIE = 1。
    • sstatus.SPP bit 可以讓我們知道,這個 trap 是從 U-mode 來的,還是 S-mode 來的,並且也控制了 sret 會回到哪個 mode。

上面的這些 CSR 都只能在 supervisor mode ( S-mode ) 的時候去使用,CPU 會避免讓 U-mode 去讀寫這些 CSR。

在多核系統上的每一個 CPU 都有自己的 CSRs。
當一個 trap 發生的時候,RISC-V 的硬體或作下列的事情

  1. 假如這個 trap 是一個來自硬體的中斷 ( interrupt ), sstatus.SIE == 0,則不會繼續往下走
  2. copy sstatus.SIE to sstatus.SPIE
  3. Disable interrupts by clearing the SIE bit in sstatus.
  4. Copy the pc to sepc
  5. Save the current mode (user or supervisor) in the SPP bit in sstatus
  6. Set scause to a number indicating the trap’s cause.
  7. Set the mode to supervisor.
  8. Copy stvec to the pc.
  9. Start executing at the new pc.

CPU 不會切換到 root kernel page table,也並不會切換到 kernel 的 stack,並且不會儲存除了 pc 以外的 registers,作業系統核心的程式碼需要自己處理這些工作。CPU 會做那麼少事情的原因之一是會了提供彈性,因為某些作業系統為了提高 trap 的效能,可能會省略切換 page table 這件事情,

CPU 在 trap 發生時什麼都不做的話,可能導致資訊安全的危險。例如說 CPU 在 trap 之後沒有自動地把 pc 切換成 stvec 的話,那就表示進入 S-mode 時,還是繼續執行 user 撰寫的程式碼,這時候攻擊者可以很輕鬆地把 satp 換掉,指向一個由攻擊者構成的 root page table,取得所有 physical memory 的控制權。



kernel/trampoline.S/uservec

trampoline 是跳板 ( 或彈跳床 ) 的意思,在這裡的意思應該為 user process ( U-mode ) 跟 kernel ( S-mode ) 間的跳板。



{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trampoline.S#L21 }

        #
        # low-level code to handle traps from user space into
        # the kernel, and returns from kernel to user.
        #
        # the kernel maps the page holding this code
        # at the same virtual address (TRAMPOLINE)
        # in user and kernel space so that it continues
        # to work when it switches page tables.
        # kernel.ld causes this code to start at 
        # a page boundary.
        #

trampoline.S 裡面的程式碼會被映射到 virtual address : TRAMPOLINE 的位置,不論是 kernel ( S-mode ) 或是 user process ( U-mode ) 都是這個樣子。

這時候有幾個問題可以思考

  • Q: 為什麼要將 trampoline 這個只有 S-mode 可以執行的程式碼,需要映射在 user process 的 root page table 呢 ?
  • Q: 為什麼 U-mode 跟 S-mode 的 trampoline 要映射在相同的 virtual address ( TRAMPOLINE ) 呢 ?

以下是原因

  • trap 發生,從 U-mode 跳到 S-mode 的時候, satp 還沒有切換,所以雖然我們已經在 S-mode,但還是使用 user process 的 root page table 而不是 root kernel page table。
  • 所以 trap 發生後的第一道指令,必須要映射在 user process 的 root page table。
  • 在進入 usertrap 這個 frunction 前,才會把 sapt 覆蓋過去,換成 root kernel page table。
  • 為了方便,讓 trampoline 程式碼可以在 U-mode 以及 S-mode 裡面順暢執行,所以乾脆讓 trampoline 程式碼在 user process 的 root pable table 以及 kernel prcocess 的 root kernel page table 映射在相同的地方。


.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.
        #

在 U-mode 的時候,stvec 會被設為 uservec。當我們發生 trap ( ecall, exception, interrupt ),就會提權到 S-mode,且 pc 就會跳到這邊來進行執行,並且仍舊使用 user process 的 page table



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

把 user-process 的 a0 暫時放在 sscratch 裡面,因為 a0 需要被用來指向 TRAPFRAME。



        # each process has a separate p->trapframe memory area,
        # but it's mapped to the same virtual address
        # (TRAPFRAME) in every process's user page table.
        li a0, TRAPFRAME

讓 a0 指向 TRAPFRAME,幫助我們把 user process 的 CPU 狀態存進 p->trapfram。
下面稍微看一下 TRAPFRAME 是怎麼被映射的 ( va ←→pa )。


{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/proc.c#L198 }

  // map the trapframe page just below the trampoline page, for
  // trampoline.S.
  if(mappages(pagetable, TRAPFRAME, PGSIZE,
              (uint64)(p->trapframe), PTE_R | PTE_W) < 0){
    uvmunmap(pagetable, TRAMPOLINE, 1, 0);
    uvmfree(pagetable, 0);
    return 0;
  }

在這裡,會把 p->trapframe ( physical address ) 映射到 TRAPFRAM ( virtual address )。
在 trap 發生時,可以將 user process 發生 trap 當下的 CPU 資訊 ( e.g. registers ) 存放在 trapframe 裡面。要從 trap 返回 user process 的時候,可以把 trapframe 裡面的資訊放回 CPU 上,使得 trap 對 user process 來說是透明的 ( transparent ),user process 大部分時候不會知道有 trap 的發生。

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/memlayout.h#L59 }
TRAPFRAM 這一段 virtual address 是放在 TRAMPOLINE 下面。跟 TRAMPOLINE 很像,雖然在 user process 的 root page table 裡面有這一段映射,但其實 user process ( U-mode ) 沒辦法使用,只有在 trap 到 S-mode 的時候,才有辦法使用。


{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/proc.h#L31C1-L81C1 }

// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
  /*   0 */ uint64 kernel_satp;   // kernel page table
  /*   8 */ uint64 kernel_sp;     // top of process's kernel stack
  /*  16 */ uint64 kernel_trap;   // usertrap()
  /*  24 */ uint64 epc;           // saved user program counter
  /*  32 */ uint64 kernel_hartid; // saved kernel tp
  /*  40 */ uint64 ra;
  /*  48 */ uint64 sp;
  /*  56 */ uint64 gp;
  /*  64 */ uint64 tp;
  /*  72 */ uint64 t0;
  /*  80 */ uint64 t1;
  /*  88 */ uint64 t2;
  /*  96 */ uint64 s0;
…

每一個 process 都有自己的 page 來存放 trapframe,並且會映射在 user process 的 root page table,並不會特別映射在 kernel root page table

雖然每一個 process 都有自己的一塊 trapframe,但他們通通都映射到相同的 virtual address TRAPFRAME 上。這讓每一個 process 在找自己的 trapframe 的時候,都可以用相同的 virtual address,相當方便。

trampoline.S/uservec 裡面,會將 user registers 都存進 trapframe 裡面,並且仰賴 trapframe 裡面的資訊去初始化一些 registers

  • kernel_sp : 裡面存放著 kernel stack 的位址,會利用這個資訊,從 user stack 切換到 kernel stack。
  • kernel_hartid : 裡面存放當前 CPU 的 hartid,會交給 tp register。
  • kernel_satp : 利用這個資訊,從 user process page table 切換到 kernel page table。
  • kernel_trap : 儲存 usertrap 這個 function 的 address,會透過這個資訊,將 pc 值跳到 usertrap 這個 function 進行後續的處理。

當我們要從 trap 返回 user process 的時候

  • 會將 trapframe 裡面 kernel_* 設定好
  • 從 trapframe 拿出 user registers
  • 切換到 user 的 page table ( 設定 satp )
  • 使用 sret 回到 user space ( U-mode )

要注意,在 trapframe 這邊除了 caller-saved register 以外,也需要儲存 callee-saved registers。



        # 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)
…

這裡 a0 會指向 p->trapframe,開始把 user process 的 registers 放進 trapframe。



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

因為 user process 的 register : t0 已經被放進 trapframe 了,所以我們可以開始使用這個 register! 我們把 user process register : a0 從 sscratch 放進 t0,然後再放進 trapframe 裡面。



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

從 user stack 切換到 kernel stack


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

讓 tp 保持有當前的 hartid,這是因為 xv6-riscv 的 cpuid 實作是仰賴 tp ( https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/proc.c#L65 )。

而為什麼 p->trapframe->kernel_hartid 會指向當前的 id 呢 ? 這是因為在 prepare_return { https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/5474d4bf72fd95a6e5c735c2d7f208f58990ceab/kernel/trap.c#L118 } 的時候,會把當前的 hart-id 存進 p->trapframe->kernel_hartid

就算這個 process 從 CPU0 換到 CPU2 去執行,因為在 context switch ,回到 user-space 才會執行 prepare_return,所以 p->trapframe->kernel_hartid 永遠會指向最新的 hartid。


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

把 usertrap() 的 function pointer 放進 t0。


        # fetch the kernel page table address, from p->trapframe->kernel_satp.
        ld t1, 0(a0)

拿到 kernel 的 root page table。



        # wait for any previous memory operations to complete, so that
        # they use the user page table.
        sfence.vma zero, zero

        # install the kernel page table.
        csrw satp, t1

        # flush now-stale user entries from the TLB.
        sfence.vma zero, zero

設定 satp,從 user process 的 page table,正式切換到 kernel root page table。
在設定 satp 的前後,記得都要加上 memory barrier。

  • satp 前的 memory barrier,是為了要讓記憶體的操作都結束後,再切換 satp,免得讓在此之前的記憶體操作誤用到新的 satp
  • satp 後的 memory barrier,是為了刷 tlb cache,避免在新的 satp 使用到舊有的 va ←→映射。


        # call usertrap()
        jalr t0

剛剛有把 usertrap() 的 function pointer 放進 t0。
所以這邊會把 pc 值跳到 usertrap 開始執行。



kernel/trap.c/usertrap

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/5474d4bf72fd95a6e5c735c2d7f208f58990ceab/kernel/trap.c#L38 }

//
// handle an interrupt, exception, or system call from user space.
// called from, and returns to, trampoline.S
// return value is user satp for trampoline.S to switch to.
//
uint64
usertrap(void)

在 usertrap 這個 function 裡面,會去處理來自 U-mode 的 interrupt, exception, 以及 system call

trampoline.S 的 uservec 會跳到這裡執行,而這邊處理完後,會跳回到 trampoline.S 的 userret

這個 function 的 return value 是 user process 的 root page table,會回傳給 trampoline.S/userret



{
  int which_dev = 0;

which_dev 用於紀錄觸發中斷的設備類型。

  • 2 代表 timer interrupt
  • 1 代表其他 device 的 interrupt
  • 0 代表無法識別,不知道什麼裝置


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

{ riscv-privileged : 12.1.1. Supervisor Status (sstatus) Register }
sstatus.SPP ( Supervisor Previous Privilege ) : 當中斷發生時,CPU ( 硬體 ) 會將當下的特權級 ( U-mode or S-mode ) 存在 sstatus.SPP 裡面

  • SPP == 0 : U-mode
  • SPP == 1 : S-mode

這邊假如檢查到,中斷不是來自於 U-mode,卻進入了 usertrap,這顯然不正常。會發 panic,把系統整個凍結住。



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

因為我們現在是在 kernel ( S-mode ),所以當中斷發生時,我們該跳去 kernelvec,而不是 uservec,這邊把 stvec 設為 kernelvec。



  struct proc *p = myproc();

取得目前正在執行的 process 的 struct proc



  // save user program counter.
  p->trapframe->epc = r_sepc();

sepc 存進該 process 的 trapframe->epc。



  if(r_scause() == 8){
    // system call

    if(killed(p))
      kexit(-1);

假如這個 process 已經標記為被砍掉的 process,就用 kexit 把這個 process 砍掉。



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

假如我們是因為執行 ecall 而發生 trap 的話,sepc 實際上會指向 ecall 這個 instruction 的位址。所以當我們想從 S-mode 回到 U-mode 的時候,實際上我們會想要回到 sepc + 4 的位置 ( ecall 的下一道指令 )。

假想一下,我們沒有 +4 的話,當我們從 trap 回到 user process,就又會再執行一次 ecall,然後又進入 trap … 會陷入無窮的 trap !!



    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    intr_on();

到此為止,已經把該存的狀態都存的差不多了,所以又可以將 interrupt 開啟。



    syscall();

開始處理 system call。



  } else if((which_dev = devintr()) != 0){
    // ok

devintr 會去查看 scause,看看這是否是硬體觸發的中斷。

  • 當中斷發生時,scause 的最高位元會被設為 1
  • 當 ecall 或是 exception 發生時,最高位元會被設為 0

接下來

  • 假如是硬體中斷,devintr 裡面會再去呼叫相對應的 driver。
  • 假如不是硬體中斷,或是無法識別的硬體,devintr 會回傳 0。


  } else if((r_scause() == 15 || r_scause() == 13) &&
            vmfault(p->pagetable, r_stval(), (r_scause() == 13)? 1 : 0) != 0) {
    // page fault on lazily-allocated page

vmfault : 假如 va 是落在合法的位址,就配置一塊記憶體。

scause :

  • 13 : Load page fault
    • 嘗試對 va 進行讀取,但該 va 在該 page table 沒有映射到任何的 leaf PTE,沒有映射到任何 physical address。
  • 15 : Store/AMO page fault
    • 嘗試對 va 進行寫入,但 va 在該 page table 沒有映射到任何 pa


  } else {
    printf("usertrap(): unexpected scause 0x%lx pid=%d\n", r_scause(), p->pid);
    printf("            sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());
    setkilled(p);
  }

假如沒辦法處理這個 trap ( e.g. 除以 0,未知的硬體中斷... ),就把這個 process 給 kill 掉。
setkilled 實際上只會進行 p->killed = 1; ,將 killed flag 舉起來,真的想殺掉這個 process,需要仰賴 kexit



  if(killed(p))
    kexit(-1);

檢查這個 process 是否已經被標記為 killed ,若是的話則呼叫 kexit 結束它。



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

假如遇到 timer interrupt,就把 CPU 讓給其他人。
呼叫 yield function 會讓這個 process 把 CPU 讓出來,讓其他 process 有機會使用這個 CPU。



  prepare_return();

在這個 function 裡,我們會將一些資訊存回 trapframe,並且設定 CPU 的 CSR。
準備回到 user space。



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

  // return to trampoline.S; satp value in a0.
  return satp;
}

MAKE_SATP 可以將 page table 的 physical address 變成可以放進 satp 的值。

在 usertrap 會 return satp 的值給 trampoline.S/userret
userret 裡面,可以透過 a0 register 取得這個值。



kernel/trap.c/prepare_return

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L100 }

//
// set up trapframe and control registers for a return to user space
//
void
prepare_return(void)

這個 function 會把一些資訊存回 p->trapframe,以及設定當前 CPU 的一些 CSRs,為返回 user space 來作準備。

trapframe

  • kernel_satp
  • kernel_sp
  • kernel_trap
  • kernel_hartid

CSRs

  • stvec
  • sstatus.SPP
  • sstatus.SPIE
  • sepc


  // we're about to switch the destination of traps from
  // kerneltrap() to usertrap(). because a trap from kernel
  // code to usertrap would be a disaster, turn off interrupts.
  intr_off();

因為要返回 user space 了,所以準備要將 stvec 從 kerneltrap 換成 usertrap
假如這時候發生 trap,以 kernel ( S-mode ) 的身分跳進 usertrap 可就慘了! 所以這邊乾脆把中斷給關起來。



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

計算出 uservec 的 virtual address 該在哪邊 ( trampoline_uservec ),並且把它放進 stvec。


Q: 為什麼會需要 uservec - trampoline ??
為什麼不直接 trampoline_uservec = TRAMPOLINE ??

A: 可能是怕 trampoline 跟 uservec 之間會插入其他程式碼。



  // set up trapframe values that uservec will need when
  // the process next traps into 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()
  • kernel_satp
    • 把 kernel root page table 的位置給起來
  • kernel_sp
    • 把 kernel stack 的位址記起來
  • kernel_trap
    • 把 usertrap 的位置記起來
  • kernel_hartid
    • 怕 user space 會把 tp 覆蓋過去,所以這邊先把 tp 存起來


  // 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);
  • sstatus.SPP
    • 將 SPP 設為 0 的話,當 sret 發生後,就會跳到 u-mode
  • sstatus.SPIE
    • 將 SPIE 設為 1 的話,當 sret 發生後,就會將 interrupt 給 enable


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

因為等等會呼叫 sret 跳回 user-mode
這邊將想要跳去的值寫入 sepc,這樣子呼叫 sret 的時候,就會跳到 p->trapframe->epc



kernel/trampoline.S/userret

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trampoline.S#L101 }

        # usertrap() returns here, with user satp in a0.
        # return from kernel to user.

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

設定 sapt,將 page table 從 kernel root page table 換回到屬於這個 process 的 user process root page table。
在切換 sapt 的前後,需要記得加上 barrier。



       li a0, TRAPFRAME

將 a0 指向 TRAPFRAME ( virtual address )。
現在我們已經使用 user process 的 root page table 了! 雖然我們仍舊身處在 kernel space ( S-mode )。



        # 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)

從 trapframe 裡面,去把 user process 的 register 放回到 CPU 上。



        # restore user a0
        ld a0, 112(a0)

a0 不用再指向 p->trapframe,終於功成身退了,可以把它恢復成 user process 的 a0 register。



        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

正式跳回 U-mode !

sret 硬體上會 :

  • 把 sepc 的值放到 pc,也就是說,CPU 會跳到 sepc 指向的位址。
  • 將目前的特權階級,設定為 SPP 的值
    • 0 : U-mode
    • 1 : S-mode
  • 將 SIE 設為 SPIE。
  • 把 SPIE 設為 1,把 SPP 設為 0


kernel/trap.c/trapinit

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L20 }

void
trapinit(void)
{
  initlock(&tickslock, "time");
}

初始化 tickslock,這個 lock 是用來保護 ticks 變數。



kernel/trap.c/trapinithart

{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L27 }

// set up to take exceptions and traps while in the kernel.
void
trapinithart(void)
{
  w_stvec((uint64)kernelvec);
}

每個 CPU 都會去呼叫 trapinithart 來初始化自己的 stvec。這邊將 kernelvec 這個 function 的位址寫入 stvec ( Supervisor Trap Vector ),代表只要在 kernel ( S-mode ) 發生中斷時, pc 就會跳到 kernelvec 去處理。



ch4.2 Traps from user space

xv6-riscv 在處理 trap 的時候,會把 (來自 U-mode 的 trap) ,以及 ( 來自 S-mode 的 trap ) 分開處理。

這邊簡單介紹一下,來自 U-mode 的 trap 大致上會怎麼樣處理。

一個來自 user space 的 trap 可能是因為 user program

  • 呼叫了 system call ( ecall instruction )
  • 做了某些非法操作 ( e.g. 除以 0 )
  • 發生中斷 ( e.g. uart 發信號給 PLIC,PLIC 再發信號到 CPU )

簡化的流程處理流程

  • 當 trap 發生時,CPU ( 硬體 ) 會自己跳到 stvec 指向的地方,也就是 uservec
    • CPU ( 硬體 ) 會將原本的 SIE 存在 SPIE,並關掉中斷 ( clear SIE bit )
    • CPU ( 硬體 ) 會將原本的權限存在 SPP,並將當前權限從 U-mode 轉成 S-mode。
    • CPU ( 硬體 ) 會將中斷前的 pc 值存在 sepc,並將 pc 設定為 stvec 的值
    • CPU ( 硬體 ) 不會 去設定 satp,不會 切換 page table。
  • uservec 做完一些處理後,會進入 usertrap
  • usertrap 處理結束之後,會用 ret 回到 trampoline.S,然後執行 userret
  • userret 會用 sret 從 S-mode 跳回到 U-mode,並回到 user process。

這邊要特別注意的是,當我們在 U-mode 發生 trap,進入 uservec 的時候,硬體並不會幫我們切換 page table,所以在剛進入 uservec 的時候,其實我們仍舊使用 user process 的 root page table !

因為硬體不會幫我們切換 page table,所以 xv6-riscv 的 trap handling 程式碼 ( 被 stvec 指向的地方 ),勢必要映射在 user process 的 root page table 裡面,儘管 user process 沒辦法觸碰這個區段。

又因為我們在 xv6-riscv 的 trap handling 需要去切換 page table ( 從 user-process root page table 切換到 kernel root page table ),所以在 kernel root page table 也必須要有 trap handling ( stvec 指向的位址 ) 這塊 address 的 va ←→pa 映射。

xv6-riscv 為了滿足這些需求,所以使用了一個 trampoline page ( 跳板頁 ),來存放 user spacekernel space 間的跳板。這個 page 包含了 uservec ( 有就是 user process 執行時,stvec 所指向的地方 )。這個 trampoline page 在每一個 process 的 page table,以及 kernel 的 root page table,都會有映射。而映射的位址會在 virtual address : 0x3ffffff000 (在程式碼內叫做 TRAMPOLINE)。

trampoline page 會位於 virtual address space 的最後一個 page
因為 trampline page 在 user process page table 跟 kernel page table 會映射到相同的位址,所以在 trap handler 裡面,就算我們設定 sapt,並從 user process page table 轉換成 kernel kernel table 的時候,我們還是可以順暢地繼續執行。

trapframe page 的性質跟 trmapoline page 有點相似,但也有不同的地方。

  • trapframe 雖然也被映射在 user process 的 page table 裡面,但 U-mode 不能使用
  • 但是 kernel space 不需要把 trapframe page 映射在 TRAPFRAME ( 0x3fffffe000 ),kernel space 在取用特定 process 的 trapframe 的時候,會直接使用 direct-mapped physical memory ( kernel space 會把大部份的 physical memory direct-mapped 在自己的 root page table 裡面。 ) ( direct-mapped : 雖然在 page table 上有 va ←→pa 的映射,但其實 va == pa )。所以可以看到,在 usertrap 這個 function 裡,會去用 myproc 去取得相對應的 process 的 trapframe。

每個 user process 都會把 trapframe 映射在 virtual address ( TRAPFRAME, 0x3fffffe000 ),但是每個 user process 的 trapframe 實際上會是在不同的 physical address ( 這也是理所當然的,每個 process 都該有自己的一份 trapframe )。

trap 會有幾種狀況

  • system call : 交給 syscall function 處理
  • device interrupt : 交給 devintr function 處理
  • page fault : 交給 vmfault 處理
  • 假如上述 function 都沒辦法處理,就把這個 process 砍掉!

要注意的是,假如是處理 system call 的時候,需要把 p->trapframe->epc + 4 ( interrupt, exception 不需要 ),因為當 U-mode 執行 ecall,並 trap 進 S-mode 的時候,硬體 在 sepc 所儲存的 pc 值是指向 ecall 這個 instruction,於是當我們處理完 system call,想從 S-mode 回到 U-mode 的時候,我們該回到的是 ecall 的下一道 instruction。

從 S-mode 回到 U-mode 大致上的步驟:

  • 呼叫 prepare_return
    • 設定 RISC-V control register
      • stvec
      • sepc
      • sstatus.SPP
      • sstatus.SPIE
      • 把一些資訊存回該 process 的 trapframe
      • 把 user processs 的 satp 放在 a0 register
  • 回到 trampoline page,並執行 userret
    • 切換回 user process 的 page table
      • 在 S-mode,且 page table 是 user process 的 page table,我們只能使用 trampoline page 的資料,以及 TRAPFRAME 這個 va 所指向的資料。
    • 透過 va : TRAPFRAME,拿到該 process 的 trapframe
    • 透過 trapframe,把 user process 發生 trap 時的 registers 存回 CPU
    • 執行 sret,回到 U-mode !

圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言