xv6系統(tǒng)調(diào)用流程——MIT6.S081操作系統(tǒng)
這篇文章通過gdb跟蹤基于risc-v架構(gòu)的xv6系統(tǒng)中write
系統(tǒng)調(diào)用的處理流程。
系統(tǒng)調(diào)用是操作系統(tǒng)給應(yīng)用程序提供的操作底層硬件資源的簡單清晰的接口,隱藏底層資源的復(fù)雜性,比如UNIX會把網(wǎng)絡(luò)、磁盤等一系列東西都抽象成文件,然后你可以簡單的使用write
對它們進(jìn)行讀寫,你無需關(guān)心磁道、扇區(qū)等概念。
同時,由于系統(tǒng)調(diào)用會與底層資源通信,所以一定要在內(nèi)核態(tài)執(zhí)行,在xv6中稱為supervisor mode
,這里一定涉及到用戶內(nèi)核態(tài)的轉(zhuǎn)換。
在轉(zhuǎn)換過程中需要保存用戶程序執(zhí)行的現(xiàn)場,安全的陷入內(nèi)核,執(zhí)行實(shí)際系統(tǒng)調(diào)用,并恢復(fù)程序執(zhí)行現(xiàn)場。
C中的包裝方法#
C庫中的write
調(diào)用,也就是我們程序中進(jìn)行系統(tǒng)調(diào)用的函數(shù):
int write(int, const void*, int);
它實(shí)際上是實(shí)際系統(tǒng)調(diào)用的一個包裝,它(以及所有其它的系統(tǒng)調(diào)用)定義在user/user.h
中,只有一個函數(shù)描述,實(shí)際的函數(shù)體是由三條匯編指令完成的。
通過查看編譯后的程序的asm
,我們可以看到write
函數(shù)實(shí)際的指令。
將
SYS_write
常量保存到a7
寄存器中執(zhí)行
ecall
進(jìn)入內(nèi)核ret
返回
SYS_write
是實(shí)際的write
系統(tǒng)調(diào)用的系統(tǒng)調(diào)用號,實(shí)際的值為16,定義在kernel/syscall.h
中,所有的系統(tǒng)調(diào)用號都定義在這。

ecall#
ecall
是進(jìn)行實(shí)際系統(tǒng)調(diào)用的入口。它是由risc-v提供的一個用于實(shí)現(xiàn)系統(tǒng)調(diào)用的指令,通常由低特權(quán)的代碼發(fā)起,用來執(zhí)行高特權(quán)代碼,比如UserMode到SupervisorMode、SupervisorMode到MachineMode。
若你在UMode,ecall
指令會做三件事:
將
pc
保存到sepc
(S態(tài)異常程序計數(shù)器)將權(quán)限提升至
SMode
跳轉(zhuǎn)到
STVEC
(S態(tài)陷阱向量基地址寄存器)
通過GDB查看write
中這幾條匯編指令,第一個就是熟悉的將16
(SYS_write)存到a7
中:

然后,我們執(zhí)行stepi
執(zhí)行ecall
,注意在執(zhí)行前,pc
指向的位置是0x2d2
,這是一個用戶地址空間中很小的地址,指向的就是用戶空間的write
包裝函數(shù)中的li
指令地址。
當(dāng)我們stepi
執(zhí)行ecall
后,按照risc-v中的約定,現(xiàn)在sepc
中保存了原來的pc
,也就是0x2d4
(用戶代碼中ecall
的地址),pc
應(yīng)該跳轉(zhuǎn)到stvec
寄存器指向的位置。

trampoline初識#
實(shí)際上,stvec
寄存器指向了0x3ffffff000
這個位置,無論是用戶虛擬地址空間還是內(nèi)核虛擬地址空間,其最頂部,也就是0x3ffffff000
的位置都被映射成了相同的東西,也就是下圖中的這個trampoline
。

trampoline
直譯過來是蹦床的意思,你可以理解為它是一個U態(tài)到S態(tài)的蹦床。不過注意,執(zhí)行ecall
后我們已經(jīng)處于S態(tài),但我們還不能貿(mào)然的執(zhí)行內(nèi)核代碼,因?yàn)槲覀冞€要保存原來執(zhí)行系統(tǒng)調(diào)用的用戶進(jìn)程的數(shù)據(jù)。
trampoline
中的實(shí)際代碼在kernel/trampoline.S
中可以看到,實(shí)際上,也就是上面那張截圖中的匯編代碼。
trampoline是一個神奇的東西,由于我們需要從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài),執(zhí)行內(nèi)核代碼,所以我們肯定要切換用戶頁表到內(nèi)核頁表,而無論用戶頁表還是內(nèi)核頁表中都有trampoline這個東西,并映射到了相同的位置,所以,在trampoline代碼中切換頁表是安全的,切換頁表后程序不會崩潰,對于trampoline中的下一條指令,在切換后的頁表中仍然存在相同的虛擬地址。
trampoline流程#
到了trampoline中,實(shí)際上我們已經(jīng)在SMode了,只不過當(dāng)前的用戶寄存器保存的還是用戶進(jìn)程的數(shù)據(jù),頁表、sp指針等都是用戶進(jìn)程的。
sscratch寄存器和進(jìn)程的trapframe#
在進(jìn)入用戶空間之前(無論是由于進(jìn)程啟動還是從中斷中恢復(fù)),內(nèi)核會先設(shè)置sscratch
寄存器指向trapframe,它是每個進(jìn)程都有的一個用于存儲所有用戶寄存器的結(jié)構(gòu)體,并且,它也被映射到用戶頁表的TRAPFRAME部分,位于TRAMPOLINE的下面,所以也可以通過用戶頁表訪問。
從源碼上看,trapframe實(shí)際上是在陷入內(nèi)核時用于保存進(jìn)程的寄存器啥的亂七八糟的東西的一個結(jié)構(gòu)體,它在kernel/proc.h
中被定義,trampoline代碼實(shí)際上會將用戶寄存器保存到該結(jié)構(gòu)體中。

保存用戶寄存器#
現(xiàn)在,所有用戶寄存器都被正確保存到trapframe中了,除了a0
,現(xiàn)在需要處理它。
需要注意的是,一旦一個用戶寄存器被保存到用戶進(jìn)程的trapframe中,內(nèi)核代碼就能隨意操作它,不用擔(dān)心用戶數(shù)據(jù)丟失,就像上面使用t0
來做中間寄存器一樣。
切換到內(nèi)核執(zhí)行環(huán)境#
用戶寄存器已經(jīng)保存好了,在執(zhí)行內(nèi)核代碼之前,還需要加載內(nèi)核的執(zhí)行環(huán)境,比如將sp
寄存器換成內(nèi)核棧,將satp
寄存器切換成內(nèi)核頁表。
可以看到這里,從進(jìn)程的trapframe
中讀出了內(nèi)核棧指針、hartid、陷阱處理程序usertrap
的地址以及內(nèi)核頁表,并設(shè)置到對應(yīng)的寄存器上。
進(jìn)程中的內(nèi)核數(shù)據(jù)從哪里來?
無論是進(jìn)程最初啟動,還是從中斷、陷阱中恢復(fù),都會先執(zhí)行對這些內(nèi)核數(shù)據(jù)的設(shè)置。
一旦我們執(zhí)行了csrw satp, t1
這一行代碼,用戶頁表就被切換到內(nèi)核頁表,此時,任何位于用戶頁表中的內(nèi)容都無法訪問了。
內(nèi)存屏障的執(zhí)行也很有必要,當(dāng)我們還在用戶頁表下工作時,TLB中有很多基于用戶頁表的虛擬地址到物理地址的映射,一旦頁表切換,這些映射就是錯的了,所以,我們需要執(zhí)行內(nèi)存屏障將它們清空掉。
執(zhí)行usertrap——陷阱處理程序#
上面的ld t0, 16(a0)
中將內(nèi)核中的陷阱處理程序地址寫到t0
寄存器了,所以trampoline中的下一行代碼就是jr t0
,跳轉(zhuǎn)到陷阱處理程序。注意下圖執(zhí)行si
后ASM窗口和REG窗口中pc寄存器的變化,它從trampoline跳轉(zhuǎn)到了陷阱處理程序——usertrap
。

如果你的GDB加載的文件不是kernel/kernel
,你沒法跟蹤它的源碼,可以使用file kernel/kernel
加載內(nèi)核文件然后再用layout src
跟蹤源碼。


現(xiàn)在,我們進(jìn)入到kernel/trap.c
的usertrap
函數(shù)中,這就是陷阱處理程序。
陷阱處理程序要處理的東西比我們想的復(fù)雜,除了系統(tǒng)調(diào)用外,它還可能是因?yàn)槌绦蜻\(yùn)行中出現(xiàn)錯誤等原因必須陷入內(nèi)核。此外,它還可能本身就是從內(nèi)核空間進(jìn)入的。這里,我們不考慮我們不需要考慮的代碼,只考慮從用戶空間通過系統(tǒng)調(diào)用進(jìn)入的情況。
解釋一下上面最后一行代碼,由于當(dāng)前進(jìn)程可能會由于時間片不足被切換到其它進(jìn)程,所以這里我們不能保證sepc是否會被其它進(jìn)程沖掉(比如它再進(jìn)行一次系統(tǒng)調(diào)用),所以這里還需要將它保存到trapframe中。這行代碼不寫在trampoline中,而是以C語言的形式寫出來,可能是因?yàn)閺母鞣N其它方面進(jìn)入的代碼也需要修改這個寄存器,所以從這里統(tǒng)一修改吧,也有可能是因?yàn)?code>sd的操作數(shù)必須是用戶寄存器。
繼續(xù)往下
syscall#
syscall
執(zhí)行實(shí)際的系統(tǒng)調(diào)用函數(shù),通過之前在C中的包裝函數(shù)里,我們將系統(tǒng)調(diào)用號SYS_write
保存在了用戶寄存器a7
中,在trampoline代碼中,我們將它保存在了進(jìn)程的trapframe中,現(xiàn)在,我們在syscall
里,通過進(jìn)程的trapframe->a7
讀取到這個系統(tǒng)調(diào)用號,執(zhí)行對應(yīng)的內(nèi)核中的系統(tǒng)調(diào)用程序。
每一個系統(tǒng)調(diào)用有一個返回值,這個返回值保存在trapframe->a0
中,如果系統(tǒng)調(diào)用號未知,就保存-1
。
回到usertrap#
下面是usertrap的完整代碼:
從陷阱中返回——usertrapret#
現(xiàn)在,系統(tǒng)調(diào)用已經(jīng)執(zhí)行完畢,是時候做必要的恢復(fù)并返回到用戶空間。usertrapret
函數(shù)就是完成這個工作的。
比較值得一提的,這個fn是一個函數(shù)指針,它指向了內(nèi)存頂部的trampoline代碼中的userret
,調(diào)用這個函數(shù)指針,并將TRAPFRAME
和satp
作為參數(shù)傳遞,它們會被存到a0
和a1
上?,F(xiàn)在,我們可以進(jìn)入trampoline的userret
。
回到trampoline——userret#
首先就將a1
和satp
做了一個交換,并執(zhí)行了一個內(nèi)存屏障。相當(dāng)于將頁表切換回用戶頁表了。?
同樣,由于trampoline在用戶頁表和進(jìn)程頁表間被映射到了相同的虛擬地址上,這個切換不會發(fā)生問題。
現(xiàn)在,a0
是函數(shù)指針那里傳入的trapframe,從trapframe中找到原始進(jìn)程中的a0
(112(a0)),與sscratch
進(jìn)行交換。這一步是為了userret
中最后一步的交換做準(zhǔn)備,先不用管,只需要知道現(xiàn)在sscratch
中保存了用戶的a0
寄存器值,而目前的a0
保存的確實(shí)是trapframe的值,這是函數(shù)指針調(diào)用處傳過來的。
下面,將所有trapframe中的東西存回用戶寄存器
總結(jié)#
在用戶模式,調(diào)用C函數(shù)庫中的系統(tǒng)調(diào)用的封裝,如
write
該封裝中會將具體的系統(tǒng)調(diào)用號加載到a7中,然后調(diào)用
ecall
指令ecall
指令會做三件事切換UMode到SMode
將用戶pc保存到satp
跳轉(zhuǎn)到stvec指定的位置,也就是trampoline
在trampoline中
將用戶寄存器保存到進(jìn)程的trapframe中
從trapframe中讀取內(nèi)核棧、內(nèi)核頁表、中斷處理程序
usertrap
的位置,當(dāng)前CPU核心id加載內(nèi)核棧到sp、切換satp位內(nèi)核頁表,跳轉(zhuǎn)到陷阱處理程序
陷阱處理程序?qū)rapframe中的epc寫成
ecall
的下一條指令,調(diào)用syscall
執(zhí)行系統(tǒng)調(diào)用syscall
調(diào)用具體的在內(nèi)核中的系統(tǒng)調(diào)用代碼,然后將返回值寫到a0usertrap
執(zhí)行usertrapret
,為返回用戶空間做準(zhǔn)備比如設(shè)置進(jìn)程trapframe中的內(nèi)核相關(guān)的信息,內(nèi)核棧、內(nèi)核頁表、中斷處理程序位置等
設(shè)置
sret
指令的控制寄存器,以在sret
執(zhí)行時順利恢復(fù)到用戶模式設(shè)置
sepc
為trapframe的epc使用函數(shù)指針,調(diào)用trampoline代碼中的userret,并將TRAPFREAM作為a0、用戶頁表位置作為a1
trampoline中的userret做的就很簡單了,切換回用戶頁表,從trapframe恢復(fù)用戶寄存器
執(zhí)行sret返回到UMode
參考#
Misunderstanding RISC-V ecalls and syscalls - Juraj's Blog
xv6--內(nèi)存管理 - CSDN
MIT6.S081 Operating System Engineering
感謝ChatGPT和newbing的大力支持