操作系統(tǒng)通過系統(tǒng)調(diào)用為運(yùn)行于其上的進(jìn)程提供服務(wù)。
當(dāng)用戶態(tài)進(jìn)程發(fā)起一個(gè)系統(tǒng)調(diào)用, CPU 將切換到 內(nèi)核態(tài) 并開始執(zhí)行一個(gè) 內(nèi)核函數(shù) 。 內(nèi)核函數(shù)負(fù)責(zé)響應(yīng)應(yīng)用程序的要求,例如操作文件、進(jìn)行網(wǎng)絡(luò)通訊或者申請(qǐng)內(nèi)存資源等。
舉一個(gè)最簡(jiǎn)單的例子,應(yīng)用進(jìn)程需要輸出一行文字,需要調(diào)用 write 這個(gè)系統(tǒng)調(diào)用:
hello_world.c
#include <string.h>#include <unistd.h>int main(int argc, char *argv[]){ char *msg = "Hello, world!/n"; write(1, msg, strlen(msg)); return 0;}注解
讀者可能會(huì)有些疑問——輸出文本不是用 printf 等函數(shù)嗎?
確實(shí)是。 printf 是更高層次的庫(kù)函數(shù),建立在系統(tǒng)調(diào)用之上,實(shí)現(xiàn)數(shù)據(jù)格式化等功能。 因此,本質(zhì)上還是系統(tǒng)調(diào)用起決定性作用。
調(diào)用流程
那么,在應(yīng)用程序內(nèi),調(diào)用一個(gè)系統(tǒng)調(diào)用的流程是怎樣的呢?
我們以一個(gè)假設(shè)的系統(tǒng)調(diào)用 xyz 為例,介紹一次系統(tǒng)調(diào)用的所有環(huán)節(jié)。
如上圖,系統(tǒng)調(diào)用執(zhí)行的流程如下:
執(zhí)行態(tài)切換
應(yīng)用程序 ( application program )與 庫(kù)函數(shù) ( libc )之間, 系統(tǒng)調(diào)用處理函數(shù) ( system call handler )與 系統(tǒng)調(diào)用服務(wù)例程 ( system call service routine )之間, 均是普通函數(shù)調(diào)用,應(yīng)該不難理解。 而 庫(kù)函數(shù) 與 系統(tǒng)調(diào)用處理函數(shù) 之間,由于涉及用戶態(tài)與內(nèi)核態(tài)的切換,要復(fù)雜一些。
Linux 通過 軟中斷 實(shí)現(xiàn)從 用戶態(tài) 到 內(nèi)核態(tài) 的切換。 用戶態(tài) 與 內(nèi)核態(tài) 是獨(dú)立的執(zhí)行流,因此在切換時(shí),需要準(zhǔn)備 執(zhí)行棧 并保存 寄存器 。
內(nèi)核實(shí)現(xiàn)了很多不同的系統(tǒng)調(diào)用(提供不同功能),而 系統(tǒng)調(diào)用處理函數(shù) 只有一個(gè)。 因此,用戶進(jìn)程必須傳遞一個(gè)參數(shù)用于區(qū)分,這便是 系統(tǒng)調(diào)用號(hào) ( system call number )。 在 Linux 中, 系統(tǒng)調(diào)用號(hào) 一般通過 eax 寄存器 來傳遞。
總結(jié)起來, 執(zhí)行態(tài)切換 過程如下:
編程實(shí)踐
下面,通過一個(gè)簡(jiǎn)單的程序,看看應(yīng)用程序如何在 用戶態(tài) 準(zhǔn)備參數(shù)并通過 int 指令觸發(fā) 軟中斷 以陷入 內(nèi)核態(tài) 執(zhí)行 系統(tǒng)調(diào)用 :
hello_world-int.S
.section .rodatamsg: .ascii "Hello, world!/n".section .text.global _start_start: # call SYS_WRITE movl $4, %eax # push arguments movl $1, %ebx movl $msg, %ecx movl $14, %edx int $0x80 # Call SYS_EXIT movl $1, %eax # push arguments movl $0, %ebx # initiate int $0x80
這是一個(gè)匯編語言程序,程序入口在 _start 標(biāo)簽之后。
第 12 行,準(zhǔn)備 系統(tǒng)調(diào)用號(hào) :將常數(shù) 4 放進(jìn) 寄存器 eax 。 系統(tǒng)調(diào)用號(hào) 4 代表 系統(tǒng)調(diào)用 SYS_write , 我們將通過該系統(tǒng)調(diào)用向標(biāo)準(zhǔn)輸出寫入一個(gè)字符串。
第 14-16 行, 準(zhǔn)備系統(tǒng)調(diào)用參數(shù):第一個(gè)參數(shù)放進(jìn) 寄存器 ebx ,第二個(gè)參數(shù)放進(jìn) ecx , 以此類推。
write 系統(tǒng)調(diào)用需要 3 個(gè)參數(shù):
第 17 行,執(zhí)行 int 指令觸發(fā)軟中斷 0x80 ,程序?qū)⑾萑雰?nèi)核態(tài)并由內(nèi)核執(zhí)行系統(tǒng)調(diào)用。 系統(tǒng)調(diào)用執(zhí)行完畢后,內(nèi)核將負(fù)責(zé)切換回用戶態(tài),應(yīng)用程序繼續(xù)執(zhí)行之后的指令( 從 20 行開始 )。
第 20-24 行,調(diào)用 exit 系統(tǒng)調(diào)用,以便退出程序。
注解
注意到,這里必須顯式調(diào)用 exit 系統(tǒng)調(diào)用退出程序。 否則,程序?qū)⒗^續(xù)往下執(zhí)行,最終遇到段錯(cuò)誤( segmentation fault )!讀者可能很好奇——我在寫 C 語言或者其他程序時(shí),這個(gè)調(diào)用并不是必須的!
這是因?yàn)?C 庫(kù)( libc )已經(jīng)幫你把臟活累活都干了。
接下來,我們編譯并執(zhí)行這個(gè)匯編語言程序:
$ lshello_world-int.S$ as -o hello_world-int.o hello_world-int.S$ lshello_world-int.o hello_world-int.S$ ld -o hello_world-int hello_world-int.o$ lshello_world-int hello_world-int.o hello_world-int.S$ ./hello_world-intHello, world!
其實(shí),將 系統(tǒng)調(diào)用號(hào) 和 調(diào)用參數(shù) 放進(jìn)正確的 寄存器 并觸發(fā)正確的 軟中斷 是個(gè)重復(fù)的麻煩事。 C 庫(kù)已經(jīng)把這臟累活給干了——試試 syscall 函數(shù)吧!
hello_world-syscall.c
#include <string.h>#include <sys/syscall.h>#include <unistd.h>int main(int argc, char *argv[]){ char *msg = "Hello, world!/n"; syscall(SYS_write, 1, msg, strlen(msg)); return 0;} 新聞熱點(diǎn)
疑難解答
圖片精選