深入 Linux 內(nèi)核:系統(tǒng)調(diào)用機(jī)制完全解析
操作系統(tǒng)的誕生本質(zhì)就是:
- 計(jì)算機(jī)資源管理:透明后臺(tái)自動(dòng)完成
- 應(yīng)用程序提供抽象:針對(duì)系統(tǒng)文件進(jìn)行創(chuàng)建、修改、寫入、刪除
這其中關(guān)于進(jìn)程的內(nèi)核調(diào)用在安全管理層面也有著非常出色的設(shè)計(jì),了解內(nèi)核態(tài)調(diào)用可以更好地輔助開發(fā)人員理解操作系統(tǒng)工作機(jī)制,更好地完成一些生產(chǎn)問題的排查,所以本文將針對(duì)此話題展開更進(jìn)一步的分析和探討,希望對(duì)你有所幫助。

一、基于open函數(shù)了解進(jìn)程內(nèi)核態(tài)調(diào)用執(zhí)行步驟
1. 操作系統(tǒng)的本職工作
現(xiàn)代操作系統(tǒng)本質(zhì)上就是對(duì)于多道處理程序(解決CPU單位時(shí)間內(nèi)只能執(zhí)行一條指令時(shí)阻塞帶來的空轉(zhuǎn)問題)的一種抽象,一種對(duì)于物理層面的一種封裝,幫助人類完成計(jì)算機(jī)資源管理和應(yīng)用程序的統(tǒng)一抽象管理。以本文要講的內(nèi)容為例,當(dāng)要讀取系統(tǒng)文件時(shí),對(duì)應(yīng)的程序需要發(fā)起一次系統(tǒng)調(diào)用,即通過syscall指令進(jìn)入操作系統(tǒng)內(nèi)核。 隨后,內(nèi)核代碼針對(duì)該指令進(jìn)行必要的參數(shù)檢查后,執(zhí)行文件讀取調(diào)用,并將控制返回給系統(tǒng)調(diào)用后的指令,而這個(gè)過程也就是我們常說的內(nèi)核調(diào)用。這種調(diào)用方式在系統(tǒng)調(diào)用和中斷處理時(shí)都會(huì)觸發(fā),對(duì)于一般的用戶態(tài)調(diào)用則不會(huì)進(jìn)行上下文切換:

2. 詳解操作系統(tǒng)中的I/O調(diào)用
針對(duì)操作系統(tǒng)的內(nèi)核態(tài)調(diào)用,我們還是以最經(jīng)典的I/O文件讀取展開探討,以下以筆者的read.c代碼為例,可以看到針對(duì)Linux服務(wù)器下的txt文件讀取,大體分為如下幾步:
- 要通過open函數(shù)執(zhí)行調(diào)用獲取文件描述符
- 調(diào)用read函數(shù)讀取文件中的數(shù)據(jù),并返回讀取到的字節(jié)數(shù)
- 輸出打印到系統(tǒng)終端
- 關(guān)閉文件描述符資源
這其中open和read調(diào)用都涉及進(jìn)程上下文切換調(diào)用阻塞等待結(jié)果并返回:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
int main() {
int fd = open("example.txt", O_RDONLY); // 打開文件
if (fd == -1) {
perror("Error opening file");
return 1;
}
char buffer[128];
ssize_t bytesRead;
// 讀取文件內(nèi)容
while ((bytesRead = read(fd, buffer, sizeof(buffer) - 1)) > 0) {
buffer[bytesRead] = '\0'; // 確保字符串以 '\0' 結(jié)尾
printf("bytesRead: %zd\n", bytesRead);
printf("%s", buffer);
}
if (bytesRead == -1) {
perror("Error reading file");
}
close(fd); // 關(guān)閉文件
return 0;
}考慮到代碼示例的完整性,筆者也給出編譯和執(zhí)行的指令:
# 編譯c程序
gcc read.c -o read
# 執(zhí)行程序
./read最終輸出結(jié)果如下,可以看到對(duì)于hello c lang讀取結(jié)果為13字節(jié)(c語(yǔ)言字符串末尾默認(rèn)添加一個(gè)\0結(jié)束符),同時(shí)輸出的字符也很預(yù)期結(jié)果一致:
bytesRead: 13
hello c lang3. 深入剖析read調(diào)用的工作機(jī)制
上面這個(gè)程序?qū)?yīng)的open和read都涉及內(nèi)核態(tài)調(diào)用,以read為例,該調(diào)用返回的是文件讀取的字節(jié)數(shù)。在執(zhí)行該函數(shù)調(diào)用時(shí),調(diào)用程序會(huì)先將函數(shù)參數(shù)存到RDI、RSI、RDX、R10、R8、R9這組寄存器上,一旦超過6個(gè)參數(shù)就會(huì)將多出來的參數(shù)直接壓入堆棧。以我們的read調(diào)用為例,因?yàn)橹挥?個(gè)參數(shù),所以只會(huì)用到RDI、RSI、RDX這幾個(gè)寄存器。
一旦發(fā)起read系統(tǒng)調(diào)用讀取磁盤數(shù)據(jù)時(shí),read庫(kù)函數(shù)就會(huì)將系統(tǒng)調(diào)用號(hào)存放到RAX寄存器上,因?yàn)榭紤]到應(yīng)用程序安全層面的不穩(wěn)定因素,操作系統(tǒng)會(huì)通過一個(gè)陷阱指令,對(duì)應(yīng)我們的X86-64的CPU也就是syscall調(diào)用,指令讓read調(diào)用的線程切換到內(nèi)核態(tài)中,內(nèi)核檢查RAX寄存器中的系統(tǒng)調(diào)用號(hào)將請(qǐng)求分發(fā)給對(duì)應(yīng)的內(nèi)核函數(shù)。 在此期間該線程會(huì)因?yàn)镮O調(diào)用而發(fā)起阻塞,所以在多道程序設(shè)計(jì)的理念上,為避免CPU因?yàn)镮O阻塞而未能處理其它進(jìn)程的程序的指令,操作系統(tǒng)在此時(shí)會(huì)將進(jìn)程的上下文保存到其結(jié)構(gòu)體中(本質(zhì)就是內(nèi)存),然后去執(zhí)行其它程序的指令,將其它指令需要的數(shù)據(jù)存放到寄存器上執(zhí)行。
我們的線程在內(nèi)核完成文件讀取后,內(nèi)核將結(jié)果返回給用戶程序,繼續(xù)執(zhí)行用戶程序的下一條指令。 對(duì)應(yīng)我們也給出read系統(tǒng)調(diào)用返回的完整流程圖,可以看到我們會(huì)將read參數(shù)分別放到3個(gè)寄存器上,并將系統(tǒng)調(diào)用號(hào)存到RAX寄存器上,在隨后進(jìn)行的syscall指令的上下文切換中,內(nèi)核代碼會(huì)根據(jù)RAX中的系統(tǒng)調(diào)用號(hào)定位到對(duì)應(yīng)的內(nèi)核函數(shù)完成數(shù)據(jù)讀取,并將結(jié)果以函數(shù)返回值的方式返回給用戶程序:

二、內(nèi)核態(tài)調(diào)用常見問題
1. 為什么調(diào)用要進(jìn)入內(nèi)核態(tài)調(diào)用
應(yīng)用程序不受操作系統(tǒng)的完全控制,考慮到安全性,所有涉及系統(tǒng)資源的操作都封裝成內(nèi)核函數(shù)
2. 為什么不直接在open函數(shù)上配置指令地址
為避免應(yīng)用程序識(shí)別內(nèi)核函數(shù)地址直接違法調(diào)用,系統(tǒng)會(huì)變化指令地址,所以就需要通過映射表進(jìn)行管理
三、小結(jié)
操作系統(tǒng)本質(zhì)上是針對(duì)用戶程序的抽象和系統(tǒng)資源的統(tǒng)一管理,對(duì)于日常的I/O調(diào)用,應(yīng)用程序執(zhí)行時(shí)會(huì)將參數(shù)存放到有限的寄存器上或堆棧上,然后陷阱指令(對(duì)應(yīng)x86-64 CPU也就是syscall指令)進(jìn)入Linux內(nèi)核,此時(shí)內(nèi)核代碼就會(huì)根據(jù)RAX寄存器中的系統(tǒng)調(diào)用號(hào)定位到對(duì)應(yīng)的內(nèi)核函數(shù)執(zhí)行系統(tǒng)調(diào)用,并將結(jié)果以函數(shù)返回值的方式返回給應(yīng)用程序。


























