淺談特權模式與模式切換 - ianchen0119/AwesomeCS GitHub Wiki
恐龍書上的 User Mode 與 Kernel Mode
在恐龍書中有提到,作業系統一般會在 User Mode 與 Kernel Mode 之間切換,Kernel Mode 具有更高的系統控制權且掌管了多數的硬體資源。
而 User Mode 通常用於執行 User Application,如果 User Application 呼叫了 System call
,系統便會切換到 Kernel Mode 進行處理,並在處理完成後退回 User Mode。
恐龍本沒有教的事:特權指令
前面提到: Kernel Mode 具有更高的系統控制權且掌管了多數的硬體資源,到底是如何達成的呢? 這與 ISA 的特權模式有很大的關聯:
上圖為 RISC-V Spec 定義的特權模式,其權限由高到低為: Machine Mode
, Supervisor Mode
以及 User Mode
。
Machine Mode
必須執行絕對可性的程式碼以保護系統安全。
每個 Mode 都有屬於自己的中斷與異常暫存器與中斷向量表,使其可以各自維護中斷的紀錄與應對方式。
專案觀摩
由於 mini-riscv-os
僅運行在 Machine Mode
上,且尚沒有增加對 system call
的支援,所以我們選用 xv6-riscv
的原始碼來學習作業系統的模式切換。
如何支援 System Call
會讓作業系統在 User/Kernel Mode 頻繁切換的主因就是系統呼叫,所以我們必須先搞清楚系統呼叫對於硬體來說是什麼。
而 xv6-riscv
使用軟體中斷做為系統呼叫的信號,所以要進入 Kernel Mode
之前,作業系統會使用 ecall
指令來產生軟體中斷,進入中斷處理後,再讓 software_interrupt
的 Handler
處理系統呼叫的判斷即可。
xv6 使用的特權模式
搞清楚系統呼叫如何產生以後,我們來看一下 xv6-riscv
的 User/Kernel Mode 分別對應哪個特權模式:
- User Mode: User Mode
- Kernel Mode: Supervisor Mode
由此可知,當系統呼叫產生軟體中斷後,系統程式需要從 User Mode
跳進 Supervisor Mode
處理系統呼叫,待結束時再回到 User Mode
。
回到正題
搞清楚來龍去脈以後,讓我們直接閱讀原始碼學習吧!
首先看到 kernel/riscv.h
定義的狀態暫存器:
// Supervisor Status Register, sstatus
#define SSTATUS_SPP (1L << 8) // Previous mode, 1=Supervisor, 0=User
#define SSTATUS_SPIE (1L << 5) // Supervisor Previous Interrupt Enable
#define SSTATUS_UPIE (1L << 4) // User Previous Interrupt Enable
#define SSTATUS_SIE (1L << 1) // Supervisor Interrupt Enable
#define SSTATUS_UIE (1L << 0) // User Interrupt Enable
接著觀察 kernel/trap.c
當中的程式碼:
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();
}
當系統在 User Mode
的情況下發生中斷時,會進行一些安全性檢查,這邊比較值得一提的動作有:
1. w_stvec((uint64)kernelvec);
: 將 S-Mode 下的中斷向量表設定為 kernelvec,這也代表系統已經進入 Kernel Mode。
至於系統是如何從 User Mode 跳到 Supervisor Mode,可以參考以下資訊。
在 RISC-V 中,所有中斷與異常的處理預設都會由 Machine Mode 處理,為了節省效能開銷,我們可以透過設定:
- mideleg (Machine Interrupt Delegation)
- medeleg (Machine Exception Delegation)
將中斷與異常委託給其他模式處理。 這一段操作我們也可以在 xv6 的原始碼中看到:
// in start.c
void start(){
// ...
// delegate all interrupts and exceptions to supervisor mode.
w_medeleg(0xffff);
w_mideleg(0xffff);
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// ...
}
上面的程式碼將原本該由 Machine mode 處理的中斷與異常委託給 Supervisor Mode,這也是為何當 User Mode 底下產生中斷與異常時,處理器能切換至 Supervisor Mode 的原因。
注意! 中斷與異常的委託僅能委託等同或是更高權限的模式處理,像是 Machine Mode 下產生的中斷是無法委託 Supervisor/User Mode 處理的。
2. if(r_scause() == 8)
: 在這個條件成立的情況下,系統會儲存修改 epc
暫存器的資料,它記錄了產生中斷時當下 Program counter 的位址,當系統處理完中斷時會將 epc
的資料放回 PC,所以為了避免重複執行 ecall
不斷的進入中斷,我們會將 ecall
跳過,意即 p->trapframe->epc += 4;
。
3. usertrapret();
: 這邊呼叫了 usertrapret()
讓系統跳回 User Mode 執行。
觀摩完 usertrap()
,我們可以跳到它所呼叫到的 kernelvec
挖寶:
# kernel/kernelvic.S
.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.
ld ra, 0(sp)
ld sp, 8(sp)
ld gp, 16(sp)
// not this, in case we moved CPUs: ld tp, 24(sp)
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
kernelvec
做的事很簡單,就是將處理器的暫存器位置先存起來,呼叫 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);
}
kerneltrap()
所做的事情就跟 mini-riscv-os
的 Interrupt_handler()
差不多,負責處理外部中斷與時間中斷。
回到 usertrapret()
,這個函式主要就是設定中斷向量表與 Interrupt Enable 以及 User Page Table:
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 trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
// 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 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 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}
作業系統之所以能夠讓處理器在這個步驟切換回 User mode 下工作,跟這一段程式碼有很大的關聯性:
// 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 寫為 0,如此一來,等到處理器跳至 trampoline.S
執行 sret
結束整個中斷流程時,就會依據 SPP 的值決定要跳回哪一個模式。
總結
本篇文章帶讀者粗略的學習 xv6 的中斷處理與系統呼叫的機制,如果對中斷處理那邊感到困惑的話,建議先去閱讀 RISC-V 的 Spec 或是教學文章。
因為筆者自己也只有幫
mini-riscv-os
寫寫 Patch,並不是 RISC-V 的熟手,所以可能無法提供很 Hard Core 的技術文章。 不過,我想這對於想要實作系統呼叫或是 User/Kernel Mode Switching 的朋友們應該是有幫助的。