Linux系統調用原理及實現

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() 函數就是用於從用戶空間複製數據到內核空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章