深入理解Linux系統調用
?大家好,我是小風哥。
在前兩篇文章《為什么計算機需要操作系統?》《??系統調用與函數調用有什么區別??》中我們了解了什么是系統調用、為什么需要系統調用、系統調用與函數調用有什么區別,那么在今天的文章中我們從理論來到現實,看看Linux中的系統調用是怎樣實現的。
首先我們先來簡單復習下之前講解過的知識。
系統調用和普通的函數調用沒有本質區別,普通的函數調用一般調用的是我們自己編寫的函數或者其它庫函數,而系統調用調用的則是內核中的函數,更學術一點的說法是這樣的,所謂系統調用是指用戶態程序請求操作系統提供的服務。
一提到服務,大家最先想到的一定是服務器,假設客戶端是瀏覽器,瀏覽器發送http請求,服務器接收到請求后進行解析然后調用相應的hander,從本質上講就是客戶端觸發了服務器端的某個函數的運行,這時我們說客戶端請求了服務器端上的服務。
而系統調用與此類似,只不過用戶態程序并不是通過http觸發了操作系統中某個函數的運行,而是通過機器指令來觸發的,因為用戶態的App和操作系統運行在同一臺計算機系統上,而客戶端和服務器端運行在不同的計算機系統中(絕大部分情況下),因此客戶端只能通過網絡協議http來與服務器進行通信。
更通俗的說法就是所謂系統調用是指用戶態的某個函數調用內核中的某個函數。
接下來我們用一段簡單的hello world程序看下系統調用,這段程序需要運行在x86_64下:
使用以下命令編譯:
然后執行:
這段匯編代碼成功的打印出了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中:
其中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號系統調用表示的是內核中的read函數,1號系統調用表示的內核中的write函數,在進行系統調用時會將表示系統調用類別的序號寫入通用寄存器中。
從上面這個表格中可以看到write系統調用的序號是1,因此在hello world程序中我們將1寫入寄存器rax中:
這條指令就表示我們將要調用第1號系統調用,也就是sys_write,hello world程序中后續三條機器指令的函數是:
實際上這四條機器指令都是為執行syscall進行的鋪墊,也就是執行syscall所需要的參數,可以看到我們進行系統調用傳遞參數時都是通過寄存器來完成的。
這樣當CPU執行syscall執行時就會跳轉到Linux內核中的write函數,同時在執行該函數時也能知道write函數所需要的參數是什么。?