linux服務器開發相關視頻解析:
linux多線程之epoll原理剖析與reactor原理及應用
手把手帶你實現一個Linux內核文件系統
什麼技術水平,才能拿到騰訊T9(原T3.1)offer?
一、什麼是系統調用
系統調用 跟用戶自定義函數一樣也是一個函數,不同的是 系統調用 運行在內核態,而用戶自定義函數運行在用戶態。由於某些指令(如設置時鐘、關閉/打開中斷和I/O操作等)只能運行在內核態,所以操作系統必須提供一種能夠進入內核態的方式,系統調用 就是這樣的一種機制。
系統調用 是 Linux 內核提供的一段代碼(函數),其實現了一些特定的功能,用戶可以通過 int 0x80 中斷(x86 CPU)或者 syscall 指令(x64 CPU)來調用 系統調用。
二、進入系統調用
本文主要介紹的是 x86 CPU 進入系統調用的方式
Linux 提供了 int 0x80 中斷來讓用戶程序進入 系統調用,我們來看看 Linux 對 int 0x80 中斷的處理初始化過程:
void __init trap_init(void)
{
...
set_system_gate(SYSCALL_VECTOR, &system_call);
...
}
系統初始化時,會在 trap_init() 函數中對 int 0x80 中斷處理進行初始化,設置其中斷處理過程入口爲 system_call。system_call 是一段由彙編語言編寫的代碼,我們看看關鍵部分,如下:
ENTRY(system_call)
...
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
movl %eax,EAX(%esp) # save the return value
...
我們把上面的彙編改寫成 C 代碼如下:
void system_call()
{
...
// 變量 eax 代表 eax 寄存器的值
syscall = sys_call_table[eax];
eax = syscall();
...
}
sys_call_table 變量是一個數組,數組的每一個元素代表一個 系統調用 的入口,其定義如下(在文件 arch/i386/kernel/entry.S 中):
.data
ENTRY(sys_call_table)
.long SYMBOL_NAME(sys_ni_syscall)
.long SYMBOL_NAME(sys_exit)
.long SYMBOL_NAME(sys_fork)
.long SYMBOL_NAME(sys_read)
.long SYMBOL_NAME(sys_write)
.long SYMBOL_NAME(sys_open)
.long SYMBOL_NAME(sys_close)
...
翻譯成 C 代碼如下:
long sys_call_table[] = {
sys_ni_syscall,
sys_exit,
sys_fork,
sys_read,
sys_write,
sys_open,
sys_close,
...
};
用戶調用 系統調用 時,通過向 eax 寄存器寫入要調用的 系統調用 編號,這個編號就是 sys_call_table 數組的下標。 system_call 過程獲取 eax 寄存器的值,然後通過 eax 寄存器的值找到要調用的 系統調用 入口,並且進行調用。調用完成後,系統調用 會把返回值保存到 eax 寄存器中。
原理如下圖:
【文章福利】需要C/C++ Linux服務器架構師學習資料加羣812855908(資料包括C/C++,Linux,golang技術,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK,ffmpeg等)
三、系統調用實現
當用戶要調用 系統調用 時,需要通過向 eax 寄存器寫入要調用的 系統調用 編號。因爲 用戶態 和 內核態 使用的棧不同,而調用 系統調用 是在用戶態調用的,而進入 系統調用 後會變成內核態,所以參數就不能通過棧來傳遞。Linux 使用寄存器來傳遞參數,參數與寄存器的關係如下:
- 第1個參數放置在 ebx 寄存器。
- 第2個參數放置在 ecx 寄存器。
- 第3個參數放置在 edx 寄存器。
- 第4個參數放置在 esi 寄存器。
- 第5個參數放置在 edi 寄存器。
- 第6個參數放置在 ebp 寄存器。
而 Linux 進入中斷處理程序時,會把這些寄存器的值保存到內核棧中,這樣 系統調用 就能通過內核棧來獲取到參數。
下面我們通過 sys_open() 系統調用來說明一下 系統調用 的運作方式,sys_open() 實現如下:
asmlinkage long sys_open(const char *filename, int flags, int mode)
{
...
}
一般 系統調用 都需要使用 asmlinkage 編譯選項,asmlinkage 編譯選項是告訴編譯器從棧中讀取參數,其實際是封裝了 GCC 的編譯選項,如下:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
attribute((regparm(0))) 就是告訴 GCC 所有參數都從棧中讀取,而 Linux 進入中斷處理上下文時,會把 ebx、ecx、edx、esi、edi、ebp 寄存器的值保存到內核棧中,那麼 系統調用 就可以從內核棧獲取到參數的值。
但由於寄存器只能傳遞 32 位的整型值(x86 CPU),所以參數一般只能傳遞指針或者整型的數值,如果要獲取指針對應結構的數據,就必須通過從用戶空間複製到內核空間,如 sys_open() 系統調用獲取要打開的文件路徑:
asmlinkage long sys_open(const char *filename, int flags, int mode)
{
char * tmp;
...
tmp = getname(filename);
...
}
getname() 函數就是用於從用戶空間複製數據到內核空間。