Linux系統(tǒng)調(diào)用原理
一、什么是系統(tǒng)調(diào)用
系統(tǒng)調(diào)用?跟用戶自定義函數(shù)一樣也是一個函數(shù),不同的是?系統(tǒng)調(diào)用?運行在內(nèi)核態(tài),而用戶自定義函數(shù)運行在用戶態(tài)。由于某些指令(如設置時鐘、關(guān)閉/打開中斷和I/O操作等)只能運行在內(nèi)核態(tài),所以操作系統(tǒng)必須提供一種能夠進入內(nèi)核態(tài)的方式,系統(tǒng)調(diào)用?就是這樣的一種機制。
系統(tǒng)調(diào)用?是 Linux 內(nèi)核提供的一段代碼(函數(shù)),其實現(xiàn)了一些特定的功能,用戶可以通過?int 0x80?中斷(x86 CPU)或者?syscall?指令(x64 CPU)來調(diào)用?系統(tǒng)調(diào)用。
二、進入系統(tǒng)調(diào)用
本文主要介紹的是 x86 CPU 進入系統(tǒng)調(diào)用的方式
Linux 提供了?int 0x80?中斷來讓用戶程序進入?系統(tǒng)調(diào)用,我們來看看 Linux 對?int 0x80?中斷的處理初始化過程:
void __init trap_init(void)
{
...
set_system_gate(SYSCALL_VECTOR, &system_call);
...
}
系統(tǒng)初始化時,會在?trap_init()?函數(shù)中對?int 0x80?中斷處理進行初始化,設置其中斷處理過程入口為?system_call。system_call?是一段由匯編語言編寫的代碼,我們看看關(guān)鍵部分,如下:
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?變量是一個數(shù)組,數(shù)組的每一個元素代表一個?系統(tǒng)調(diào)用?的入口,其定義如下(在文件 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,
...
};
用戶調(diào)用?系統(tǒng)調(diào)用?時,通過向?eax?寄存器寫入要調(diào)用的?系統(tǒng)調(diào)用?編號,這個編號就是?sys_call_table?數(shù)組的下標。?system_call?過程獲取?eax?寄存器的值,然后通過?eax?寄存器的值找到要調(diào)用的?系統(tǒng)調(diào)用?入口,并且進行調(diào)用。調(diào)用完成后,系統(tǒng)調(diào)用?會把返回值保存到?eax?寄存器中。
原理如下圖(圖片來源?https://developer.ibm.com/zh/technologies/linux/tutorials/l-system-calls/?):

三、系統(tǒng)調(diào)用實現(xiàn)
當用戶要調(diào)用?系統(tǒng)調(diào)用?時,需要通過向?eax?寄存器寫入要調(diào)用的?系統(tǒng)調(diào)用?編號。因為?用戶態(tài)?和?內(nèi)核態(tài)?使用的棧不同,而調(diào)用?系統(tǒng)調(diào)用?是在用戶態(tài)調(diào)用的,而進入?系統(tǒng)調(diào)用?后會變成內(nèi)核態(tài),所以參數(shù)就不能通過棧來傳遞。Linux 使用寄存器來傳遞參數(shù),參數(shù)與寄存器的關(guān)系如下:
第1個參數(shù)放置在?
ebx?寄存器。第2個參數(shù)放置在?
ecx?寄存器。第3個參數(shù)放置在?
edx?寄存器。第4個參數(shù)放置在?
esi?寄存器。第5個參數(shù)放置在?
edi?寄存器。第6個參數(shù)放置在?
ebp?寄存器。
而 Linux 進入中斷處理程序時,會把這些寄存器的值保存到內(nèi)核棧中,這樣?系統(tǒng)調(diào)用?就能通過內(nèi)核棧來獲取到參數(shù)。
下面我們通過?sys_open()?系統(tǒng)調(diào)用來說明一下?系統(tǒng)調(diào)用?的運作方式,sys_open()?實現(xiàn)如下:
asmlinkage long sys_open(const char *filename, int flags, int mode)
{
...
}
一般?系統(tǒng)調(diào)用?都需要使用?asmlinkage?編譯選項,asmlinkage?編譯選項是告訴編譯器從棧中讀取參數(shù),其實際是封裝了 GCC 的編譯選項,如下:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
__attribute__((regparm(0)))?就是告訴 GCC 所有參數(shù)都從棧中讀取,而 Linux 進入中斷處理上下文時,會把?ebx、ecx、edx、esi、edi、ebp?寄存器的值保存到內(nèi)核棧中,那么?系統(tǒng)調(diào)用?就可以從內(nèi)核棧獲取到參數(shù)的值。
但由于寄存器只能傳遞 32 位的整型值(x86 CPU),所以參數(shù)一般只能傳遞指針或者整型的數(shù)值,如果要獲取指針對應結(jié)構(gòu)的數(shù)據(jù),就必須通過從用戶空間復制到內(nèi)核空間,如?sys_open()?系統(tǒng)調(diào)用獲取要打開的文件路徑:
asmlinkage long sys_open(const char *filename, int flags, int mode)
{
char * tmp;
...
tmp = getname(filename);
...
}
getname()?函數(shù)就是用于從用戶空間復制數(shù)據(jù)到內(nèi)核空間。
