深入理解 Linux 系統調用

大家好,我是小風哥。

在前兩篇文章《爲什麼計算機需要操作系統》《系統調用與函數調用有什麼區別》中我們瞭解了什麼是系統調用、爲什麼需要系統調用、系統調用與函數調用有什麼區別,那麼在今天的文章中我們從理論來到現實,看看 Linux 中的系統調用是怎樣實現的。

首先我們先來簡單複習下之前講解過的知識。

系統調用和普通的函數調用沒有本質區別,普通的函數調用一般調用的是我們自己編寫的函數或者其它庫函數,而系統調用調用的則是內核中的函數,更學術一點的說法是這樣的,所謂系統調用是指用戶態程序請求操作系統提供的服務。

一提到服務,大家最先想到的一定是服務器,假設客戶端是瀏覽器,瀏覽器發送 http 請求,服務器接收到請求後進行解析然後調用相應的 hander,從本質上講就是客戶端觸發了服務器端的某個函數的運行,這時我們說客戶端請求了服務器端上的服務。

而系統調用與此類似,只不過用戶態程序並不是通過 http 觸發了操作系統中某個函數的運行,而是通過機器指令來觸發的,因爲用戶態的 App 和操作系統運行在同一臺計算機系統上,而客戶端和服務器端運行在不同的計算機系統中 (絕大部分情況下),因此客戶端只能通過網絡協議 http 來與服務器進行通信。

更通俗的說法就是所謂系統調用是指用戶態的某個函數調用內核中的某個函數。

接下來我們用一段簡單的 hello world 程序看下系統調用,這段程序需要運行在 x86_64 下:

.datamsg:    .ascii "Hello, world!\n"    len = . - msg.text    .global _start_start:    movq  $1, %rax    movq  $1, %rdi    movq  $msg, %rsi    movq  $len, %rdxsyscall    movq  $60, %rax    xorq  %rdi, %rdisyscall

使用以下命令編譯:

$ gcc -c test.S
$ ld -o test test.o

然後執行:

./test
Hello, world!

這段彙編代碼成功的打印出了 hello world,這段代碼是什麼意思呢?

注意看. data 這一段,這裏說的是程序定義了哪些數據,.text 段是說程序中包含了哪些執行,我們之前提到進程的內存佈局時總是說數據段以及代碼段,這裏的數據段指的就是彙編中的. data 段、代碼段指的就是彙編中的. text 段,現在你應該明白了吧。

在. text 段我們看到了一條略顯奇怪的指令,syscall,這條指令是什麼意思呢?

我們來翻看一下 intel 的開發手冊:

SYSCALL invokes an OS system-call handler at privilege level 0. It does so by loading RIP from the IA32_LSTAR MSR (after saving the address of the instruction following SYSCALL into RCX). (The WRMSR instruction ensures that the IA32_LSTAR MSR always contain a canonical address.)

這段話告訴我們 intel 處理器在執行 syscall 指令時會在內核態調用操作系統的某個函數,即 syscall-call handler,這個過程就是所謂的系統調用,我們知道 CPU 執行某個函數時必須知道某個函數在內存中的地址,那麼 CPU 是怎麼知道某個 syscall-call handler 的內存地址呢?

原來 syscall-call handler 所在的內存地址存儲在寄存器 MSR 中,那麼又是誰將這個地址存儲在了寄存器 MSR 中呢?很顯然是操作系統,接下來以 Linux 爲例來講解。

Linux 內核初始化時將 syscall-call handler 也就是 Linux 內核中 entry_SYSCALL_64 函數的地址寫入寄存器 MSR 中:

wrmsrl(MSR_LSTAR, entry_SYSCALL_64);

其中 syscall-call handler 也就是 entry_SYSCALL_64 定義在了 Linux 源碼中的 arch/x86/entry/entry_64.S,上述初始化寄存器 MSR 的代碼定義在了 arch/x86/kernel/cpu/common.c。

現在我們知道了,當 CPU 執行 syscall 時會無腦跳轉到寄存器 MSR 中保存的函數地址,也就是 entry_SYSCALL_64 函數,那麼很顯然的,所有系統調用的入口都是 entry_SYSCALL_64 函數,那麼操作系統該怎麼區分到底是調用的 read 系統調用還是 write 等系統調用?

原來,操作系統中給每種系統調用分配了一個序號,就像 Linux 中這樣:

0  common  read      sys_read
1  common  write      sys_write
2  common  open      sys_open
3  common  close      sys_close
4  common  stat      sys_newstat
5  common  fstat      sys_newfstat
6  common  lstat      sys_newlstat
7  common  poll      sys_poll
8  common  lseek      sys_lseek
9  common  mmap      sys_mmap
...

可以看到,0 號系統調用表示的是內核中的 read 函數,1 號系統調用表示的內核中的 write 函數,在進行系統調用時會將表示系統調用類別的序號寫入通用寄存器中。

從上面這個表格中可以看到 write 系統調用的序號是 1,因此在 hello world 程序中我們將 1 寫入寄存器 rax 中:

movq  $1, %rax

這條指令就表示我們將要調用第 1 號系統調用,也就是 sys_write,hello world 程序中後續三條機器指令的函數是:

# 寫入文件描述符1
movq  $1, %rdi
# 保存指向字符串的指針
movq  $msg, %rsi
# 寫入數據的大小
movq  $len, %rdx

實際上這四條機器指令都是爲執行 syscall 進行的鋪墊,也就是執行 syscall 所需要的參數,可以看到我們進行系統調用傳遞參數時都是通過寄存器來完成的。

這樣當 CPU 執行 syscall 執行時就會跳轉到 Linux 內核中的 write 函數,同時在執行該函數時也能知道 write 函數所需要的參數是什麼。

好啦,這篇就到這裏,最後,我準備開通知識星球啦,我會把所有文章中留下的問題總結在這裏,同時也鼓勵大家在這裏輸出自己的深度、系統性的思考,沉澱出屬於自己的知識。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/xzcKSQ-tfgTsgHc9hFoglQ