用戶空間和內核空間之間的切換通常稱為trap
trap的三種形式
- 系統調用引發
- 異常發生
- 設備中斷 (時間中斷、IO中斷、網絡中斷等)
supervise mode的權限
用戶態和內核態之間的到底有什么區別?其實區別很小:
其中的一件事情是,你現在可以讀寫控制寄存器了。比如說,當你在supervisor mode時,你可以:讀寫SATP寄存器,也就是page table的指針;STVEC,也就是處理trap的內核指令地址;SEPC,保存當發生trap時的程序計數器;SSCRATCH等等。在supervisor mode你可以讀寫這些寄存器,而用戶代碼不能做這樣的操作。
另一件事情supervisor mode可以做的是,它可以使用PTE_U標志位為0的PTE(頁表項 詳見頁表)。當PTE_U標志位為1的時候,表明用戶代碼可以使用這個頁表;如果這個標志位為0,則只有supervisor mode可以使用這個頁表。
這也是supervise mode唯二可以做的事情了。
ecall指令
系統使用ecall指令來執行系統調用
第一,ecall將代碼從user mode改到supervisor mode
第二,ecall將程序計數器的值保存在了SEPC寄存器。
第三,ecall會跳轉到STVEC寄存器指向的指令 即trampoline.S
trap的全過程
以exec系統調用為例。
首先,用戶程序中調用exec函數,編譯器解析exec函數時,並不會調用其源碼,而是將exec系統調用的編號寫入A7寄存器中,執行ecall指令,ecall指令將跳轉到trampoline.S。
系統從用戶態進入了內核態,但目前頁表依舊是user page table。trampoline.S主要是使用trapFrame(附錄部分)保存了系統的狀態以及用戶寄存器,用於恢復用戶進程,通過這個操作也實現用戶進程對於進程切換的無感知。在將需要存儲的狀態存入user page table后,系統將切換內核頁表,執行相應的內核代碼,即usertrap函數。
usertrap根據trap的不同情況(scause寄存器中存儲着中斷原因),調用相應的處理函數。
代碼解析
下面的代碼是操作系統的源代碼,里面包含了很多的細節,你需要里了解的是整個流程、體系,不要過多的關注細節
trap的保存流程
- 首先,我們需要保存32個用戶寄存器。因為很顯然我們需要恢復用戶應用程序的執行,尤其是當用戶程序隨機的被設備中斷所打斷時。(存儲用戶寄存器)
- 程序計數器也需要在某個地方保存,它幾乎跟一個用戶寄存器的地位是一樣的,我們需要能夠在用戶程序運行中斷的位置繼續執行用戶程序。(存儲一些系統寄存器 如PC)
- 我們需要將mode改成supervisor mode,因為我們想要使用內核中的各種各樣的特權指令。(修改運行模式)
- SATP(Supervisor Address Translation and Protection,存儲了根頁表的地址)寄存器現在正指向user page table,而user page table只包含了用戶程序所需要的內存映射和一兩個其他的映射,它並沒有包含整個內核數據的內存映射。所以在運行內核代碼之前,我們需要將SATP指向kernel page table。(修改SATP,獲得內核的頁表)
- 我們需要將堆棧寄存器指向位於內核的一個地址,因為我們需要一個堆棧來調用內核的C函數。(轉換到內核堆棧)
- 一旦我們設置好了,並且所有的硬件狀態都適合在內核中使用, 我們需要跳入內核的C代碼usertrap函數。
trampoline.S
#
# code to switch between user and kernel space.
#
# this code is mapped 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 to be aligned
# to a page boundary.
#
.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
//trap狀態保存
uservec:
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->trapframe is
# mapped into user space, at TRAPFRAME.
#
# swap a0 and sscratch
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0
# 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
//trap狀態恢復
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.
# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero
# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0
# 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, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0
# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret
用戶trap處理程序
void
usertrap(void)
{
int which_dev = 0;
//判斷系統mode狀態
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.
// 將中斷處理程序設置為kernelvec
// 即將kernelvec函數地址寫入到stvec寄存器
// 和user model下的中斷對應,kernelvec用來處理內核態下的中斷
// 兩個流程很相似,kernelvec更簡單一些,就不做解釋了
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
//保存用戶的PC值
// save user program counter.
p->trapframe->epc = r_sepc();
//根據scause寄存器的值判斷異常原因
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();
//trap處理完畢,恢復用戶進程狀態
usertrapret();
}
附:
系統調用函數 (根據A7寄存器存儲的內容,調用不同的函數指針)
extern uint64 sys_chdir(void);
extern uint64 sys_close(void);
extern uint64 sys_dup(void);
extern uint64 sys_exec(void);
extern uint64 sys_exit(void);
extern uint64 sys_fork(void);
extern uint64 sys_fstat(void);
extern uint64 sys_getpid(void);
extern uint64 sys_kill(void);
extern uint64 sys_link(void);
extern uint64 sys_mkdir(void);
extern uint64 sys_mknod(void);
extern uint64 sys_open(void);
extern uint64 sys_pipe(void);
extern uint64 sys_read(void);
extern uint64 sys_sbrk(void);
extern uint64 sys_sleep(void);
extern uint64 sys_unlink(void);
extern uint64 sys_wait(void);
extern uint64 sys_write(void);
extern uint64 sys_uptime(void);
static uint64 (*syscalls[])(void) = {
[SYS_fork] sys_fork,
[SYS_exit] sys_exit,
[SYS_wait] sys_wait,
[SYS_pipe] sys_pipe,
[SYS_read] sys_read,
[SYS_kill] sys_kill,
[SYS_exec] sys_exec,
[SYS_fstat] sys_fstat,
[SYS_chdir] sys_chdir,
[SYS_dup] sys_dup,
[SYS_getpid] sys_getpid,
[SYS_sbrk] sys_sbrk,
[SYS_sleep] sys_sleep,
[SYS_uptime] sys_uptime,
[SYS_open] sys_open,
[SYS_write] sys_write,
[SYS_mknod] sys_mknod,
[SYS_unlink] sys_unlink,
[SYS_link] sys_link,
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
};
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;
}
}