我們簡單的回顧來自於 user mode 的 trap 的流程,整個流程為 uservec 到 usertrap()
再到 usertrapret()
最後 userret()
,而在我們介紹 trap 之前我們先看到 ecall,ecall 會設置 stvec
CSR 的內容,接著跳轉到 stvec
儲存的內容,在來自於 user mode 的 trap,這裡面放的是 trapoline.S
中的 uservec,而在 supervisor mode 底下,stvec
會指向到 kernel/kernelvec.S
底下的某一段程式碼,我們可以從這裡開始,去追蹤整個 supervisor mode 底下的 trap。
後面我們也將看到 machine mode 底下的 trap,最後在回顧整個 xv6 對於 trap 的處理。
在 supervisor mode 底下發生的 trap,我們可以直接使用 satp
CSR 去設置 kernel page table,回顧我們在 user mode 底下發生的 trap,我們在 uservec 底下還會重置暫存器的內容,避免來自於 user mode 的內容汙染了 supervisor mode 底下的內容,造成 user mode 和 supervisor mode 之間的隔離性消失,下面我們可以看到 kernelvec
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
# make room to save registers.
addi sp, sp, -256
# save the registers.
sd ra, 0(sp)
sd sp, 8(sp)
sd gp, 16(sp)
sd tp, 24(sp)
sd t0, 32(sp)
sd t1, 40(sp)
sd t2, 48(sp)
sd s0, 56(sp)
sd s1, 64(sp)
sd a0, 72(sp)
sd a1, 80(sp)
sd a2, 88(sp)
sd a3, 96(sp)
sd a4, 104(sp)
sd a5, 112(sp)
sd a6, 120(sp)
sd a7, 128(sp)
sd s2, 136(sp)
sd s3, 144(sp)
sd s4, 152(sp)
sd s5, 160(sp)
sd s6, 168(sp)
sd s7, 176(sp)
sd s8, 184(sp)
sd s9, 192(sp)
sd s10, 200(sp)
sd s11, 208(sp)
sd t3, 216(sp)
sd t4, 224(sp)
sd t5, 232(sp)
sd t6, 240(sp)
# call the C trap handler in trap.c
call kerneltrap
# restore registers.
...
相比於 uservec,可以發現我們少了 sp
和 satp
的設置,原因即為我們位於 supervisor mode 底下,可以直接依靠 supervisor mode 底下的 sp
和 satp
,我們在 kernelvec 前面部分就是保存所有的暫存器內容,接著準備跳轉到 kerneltrap()
中。
我們可以回想一下我們是怎麼進到 kernelvec 中,kernelvec 是為了處理發生在 supervisor mode 底下的 trap,而我們是怎麼進到 supervisor mode 的? 答案我們在前面看到了,就是 user mode 發生了 trap,接著我們便進到 supervisor mode 了,所以我們可以在 user mode 到 supervisor mode 的過程中,看到我們將 stvec
設置成了 kernelvec 的地址,以下為程式碼
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();
...
我們可以看到在 usertrap()
中我們將 stvec
設置成了 kernelvec 所在的記憶體地址。usertrap()
在此時已經處於 supervisor mode 底下了。如果這時候發生了 trap,就是在 supervisor mode 底下發生的 trap 了,trap 發生會執行 trap handler,我們通過 stvec
找到了 trap handler,也就是 kernelvec,且我們處於 supervisor mode 底下,這也就是上面所提及的,可以直接依靠 sp
和 satp
的值。
kerneltrap()
kerneltrap()
位於 kernel\trap.c
中,以下為其程式碼
void
kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();
if((sstatus & SSTATUS_SPP) == 0)
panic("kerneltrap: not from supervisor mode");
if(intr_get() != 0)
panic("kerneltrap: interrupts enabled");
if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// give up the CPU if this is a timer interrupt.
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();
// the yield() may have caused some traps to occur,
// so restore trap registers for use by kernelvec.S's sepc instruction.
w_sepc(sepc);
w_sstatus(sstatus);
}
首先會保存目前 sepc
,sstatus
,scause
CSR 的值,這部分是因為在下方如果呼叫到 yield()
,在 yield()
可能會發生其他的 trap,導致 sepc
等等 CSR 的值遭到改變,我們需要在 yield()
結束時恢復這一些 CSR 的值,因此我們在一開始時會先保存這一些值。
下面我們會先通過 sstatus
去判斷 trap 是否來自於 supervisor mode (在 usertrap()
也會使用同樣的方式去判斷 trap 是否來自於 user mode)。
接著會去判斷這是什麼類型的 trap,這邊可以看到種共有兩種 trap 的類型,分別為 device interrupt 和 timer interrupt,在 device interrupt 中會進行判斷,如果不屬於 device interrupt,則這是一個 Exception,也就是 CPU 執行指令時遇到了非預期情況,且這個情況是發生在 supervisor mode 底下,也就是 kernel space 中,這時候會產生 panic()
。
接下來是 timer interrupt 的部分,timer interrupt 產生時會去呼叫 kernelvec()
,接著會進行判斷,判斷目前的 process 處於的狀態是 RUNNING,也就是 process 目前持有 CPU 的資源並且執行中,更加精確地說,是 process 當中的 thread 持有 CPU 的資源 (持有某一個 timeslice,這一個部分可以再看完 thread 以及 scheduling 後再來觀看此篇章,這裡的 thread 是處於 kernel 底下的 thread ),接著會呼叫 yield()
讓 thread 放棄目前的 timeslice,也就是讓出 CPU 的資源讓其他的 thread 能夠獲取 CPU 的資源並且進入到 RUNNING 的狀態。
在yield()
中可能會發生 trap 導致 sepc
,sstatus
,scause
的值發生改變,因此下面可以看到在 yield()
結束後,我們將最一開始開頭保存的值重新寫回到這一些 CSR 中,到這邊,整個 kerneltrap()
就結束了。接著 kerneltrap()
從 stack 彈出,回到 kernelvec 當中,我們看回 kernelvec 下半部的部分。
call kerneltrap
# restore registers.
ld ra, 0(sp)
ld sp, 8(sp)
ld gp, 16(sp)
# not tp (contains hartid), in case we moved CPUs
ld t0, 32(sp)
ld t1, 40(sp)
ld t2, 48(sp)
ld s0, 56(sp)
ld s1, 64(sp)
ld a0, 72(sp)
ld a1, 80(sp)
ld a2, 88(sp)
ld a3, 96(sp)
ld a4, 104(sp)
ld a5, 112(sp)
ld a6, 120(sp)
ld a7, 128(sp)
ld s2, 136(sp)
ld s3, 144(sp)
ld s4, 152(sp)
ld s5, 160(sp)
ld s6, 168(sp)
ld s7, 176(sp)
ld s8, 184(sp)
ld s9, 192(sp)
ld s10, 200(sp)
ld s11, 208(sp)
ld t3, 216(sp)
ld t4, 224(sp)
ld t5, 232(sp)
ld t6, 240(sp)
addi sp, sp, 256
# return to whatever we were doing in the kernel.
sret
在呼叫完 kerneltrap()
之後,我們便回到了 kernelvec 當中,在下半部我們會恢復先前在上半部儲存的暫存器的值,接著去呼叫 sret,sret 會將 sepc
的值複製到 program counter 中,將sstatus
的 SPIE
寫入到 sstatus
的 SIE
,通過 sstatus
的 SPP
得到先前的特權模式,接著我們就會回到發生 trap 的記憶體地址了,到這裡整個 supervisor mode 底下的 trap 便處理完成了。
首先我們先看到 mstatus
CSR
MIE
(machine interrupt enable): 0 表示 disable,1 表示 enable。MPIE
(previous interrupt enable): 0 表示 diable,1 表示 enable。MPP
(previous privilege level): 00 表示 user mode,01 表示 supervisor mode,11 表示 machine mode。在 machine mode 底下的 trap 只處理 timer interrupt (其他 interrupt 或 exception 所造成的 trap 都在 supervisor mode 底下執行),下面我們看到整個在 machine mode 底下的 trap 流程
回到 xv6 的啟動與架構中,我們說到在最一開始 start.c
的部分,會在 machine mode 中進行一些設置,接下來就到 supervisor mode 了,我們可以通過檢視 start.c
來得知 machine mode 底下的 trap 相關行為
void
start()
{
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// set M Exception Program Counter to main, for mret.
// requires gcc -mcmodel=medany
w_mepc((uint64)main);
// disable paging for now.
w_satp(0);
// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// configure Physical Memory Protection to give supervisor mode
// access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
// ask for clock interrupts.
timerinit();
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
// switch to supervisor mode and jump to main().
asm volatile("mret");
}
第 5 行: 最一開始會設置 mstatus
的 MPP
域,得到上一個特權模式,用於 mret 使用 (這裡的邏輯和前面看到的 sret 邏輯是相同的)。
第 12 行: 接著會設置 mepc
CSR,表示發生 trap 的記憶體地址,在處理完 trap 之後要回到的記憶體地址,這裡發生 trap 處理結束後需要回到 main()
中 (在 sret 中也有相同的操作,通過設置 mstatus
得到要回的特權模式,通過 mepc
得到 trap 處理完成後,下一個要執行的指令位置,這個位置會在 program counter 中)。
第 18, 19 行: 可以看到 medeleg
和 mideleg
這兩個 CSR,這兩行表現了一個 xv6 中十分重要的特徵,就是除了 timer interrupt 以外,其他 trap 的處理皆在 supervisor mode 底下進行處理。
在 supervisor mode 底下的 trap,我們會去設置 scasue
CSR,因為會有許多造成 trap 的原因,包含 Exception,Device Interrupt,System call, Timer Interrupt,以及 stval
CSR,但是在 machine mode 底下的 trap,只會由 timer interrupt 所觸發,因此在這邊我們就不用通過 scause
去得知發生 trap 的原因了,因為只會有一種造成 machine mode 下發生 trap 的情況,以及設置 stval
。
第 28 行: 接著我們看到 timerinit()
的部分
void
timerinit()
{
// each CPU has a separate source of timer interrupts.
int id = r_mhartid();
// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
// prepare information in scratch[] for timervec.
// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id);
scratch[4] = interval;
w_mscratch((uint64)scratch);
// set the machine-mode trap handler.
w_mtvec((uint64)timervec);
// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);
// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
}
首先我們會讀取這是在哪一個 hart 底下的 timer,這裡也反映出每一個 hart 都有產生 timer interrupt 的硬體 (由 CLINT 產生 timer interrupt),接著我們會設置 scratch 提供等一下的 timer interrupt 的 trap handler,timervec 使用,包含儲存暫存器資訊等等,可以看到 timer_scratch 為一個陣列,每一個 hart 都有一個 timer,以及用於儲存 timer interrupt 的陣列,timer_scratch 的長度即為 xv6 中 hart 的數量,也就是 8,接著 mscartch
會指向到指令 hart 的 timer scratch 的區域。
下面會設置 mtvec
CSR 會被設置成 trap handler 的地址,上面說道 machine mode 底下的 trap 只會由 timer interrupt 所造成,因此 mtvec
CSR 中的內容為 kernelvec.S
中的 timervec。
接著啟用 machine mode 底下的 interrupt (通過 mie
CSR 設置,底下有一個 MTIE
域,表示 timer interrupt enable) ,以及 timer interrupt 便回到 start()
。
執行完 timerinit()
,回到 start()
,這時候隨時都可以發起 timer interrupt,如果這時候 timer interrupt 發起,則會進入到 mtvec
CSR 指向的 trap handler,也就是 timervec。
timervec:
# start.c has set up the memory that mscratch points to:
# scratch[0,8,16] : register save area.
# scratch[24] : address of CLINT's MTIMECMP register.
# scratch[32] : desired interval between interrupts.
csrrw a0, mscratch, a0
sd a1, 0(a0)
sd a2, 8(a0)
sd a3, 16(a0)
# schedule the next timer interrupt
# by adding interval to mtimecmp.
ld a1, 24(a0) # CLINT_MTIMECMP(hart)
ld a2, 32(a0) # interval
ld a3, 0(a1)
add a3, a3, a2
sd a3, 0(a1)
# arrange for a supervisor software interrupt
# after this handler returns.
li a1, 2
csrw sip, a1
ld a3, 16(a0)
ld a2, 8(a0)
ld a1, 0(a0)
csrrw a0, mscratch, a0
mret
mscratch
我們在前面看到,會指向到 trapframe,回顧下面這一張圖
這裡同樣的,(前面我們看到了 timerinit()
設置了 mscratch
指向到的 timer_scratch,便是在這裡使用) 將 mscratch
的值寫入到 a0
暫存器中,接著將 a0
暫存器對應到的不同域的記憶體地址寫入到 a1
,a2
,a3
中,這裡可以注意到,在 timervec 這個 timer interrupt 的 trap handler,會設置一個 software interrupt,在這個 trap handler 執行 mret 並結束後,這個 software interrupt 會根據當前是否允許 interrupt 而決定是否執行。
在 mret 執行完畢後,接著要處理剛剛發起的 software interrupt,如果目前不允許 interrupt,則這個 interrupt 會處於 pending,如果允許,則目前我們是從 machine mode 回到了 supervisor mode 中 (通過 mret),這個 interrupt 會被立即處理 (interrupt 是否允許我們在 mscause
和 sscause
中可以看到其中的 MIE
, SIE
域中,0 表示禁用 interrupt,1 表示允許 interrupt)。
usertrap()
中可以看到,分別為 Exception,Device Interrupt,System call, Timer Interrupt。trap 定義 : 處理 Interrupt (有三種 Interrupt,分別為 Externel Interrupt, Timer Interrupt, Software Interrupt),Exception 的一套流程,不同原因造成的 Interrupt 和 Exception 對應到不同的處理流程,這一些處理流程稱為 trap handler,在 trap 的過程中,可能會伴隨特權模式的切換。
Interrupt:
為什麼要有 interrupt? 為了使處理器具備多工處理的能力,能夠在多個工作之間進行切換,讓 OS 可以指派不同的任務在不同的處理器 (hart) 上面執行。
CSR 回顧
x 為 m, s,也就是 machinde mode 或 supervisor mode
xtvec
(trap vector): 當 exception 或 interrupt 發生時,program counter 會進入 xtvec
指向的記憶體地址並繼續執行,就像是主動跳入 xtvec
,因此稱作為 trap (陷阱,十分生動的比喻)。xcasue
(trap casue): 記錄發生 trap 的原因。xtval
: 記錄 exception 或 interrupt 的相關訊息。xepc
: 進入到 trap 之前的 program counter 的值,讓我們在 trap 結束後能夠回到程式中斷的地方並繼續執行。xstatus
: 記錄一些幫助 trap 處理的資訊,像是先前的特權模式,先前是否允許 interrupt 發生等等。xie
(inerrrupt enable): 決定 interrupt 是否允許發生。xip
(interrupt pending): interrupt 的等待 (pendding) 狀態。RISC V 的中斷與異常處理
The RISC-V Instruction Set Manual
An Embedded RISC-V Blog
SiFive FU540-C000 Manual v1p0
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book
Operating System: IIT Lectures / Tutorials for GATE
xv6 Kernel-14: Trap Handling