系統調用接口的主要任務是把進程從用戶態切換到內核態。在具有保護機制的計算機系 統中,用戶必須通過
軟件中斷
或陷阱
,才能使進程從用戶態切換為內核態。系統調用通過軟中斷0x80陷入內核,跳轉到系統調用處理程序system_call函數,并執行相應的服務例程(內核函數)。
用戶態——內核態
用戶態——內核態——底層硬件
《Linux內核修煉之道》第5章講解系統調用,它是應用程序和內核間的橋梁,學習并理解它是我們走向內核的一個很好的過渡。
1、系統調用
一個穩定運行的Linux操作系統需要內核和用戶應用程序之間的完美配合,
內核提供各種各樣的服務
,然后用戶應用程序通過某種途徑使用這些服務
,進而契合用戶的不同需求。用戶應用程序訪問并使用內核所提供的各種服務的途徑即是
系統調用
。在內核和用戶應用程序相交界的地方
,內核提供了一組系統調用接口
,通過這組接口,應用程序可以訪問系統硬件和各種操作系統資源。
比如用戶可以通過文件系統相關的系統調用,請求系統打開文件、關閉文件或讀寫文件;可以通過時鐘相關的系統調用,獲得系統時間或設置定時器等。內核提供的這組系統調用通常也被稱之為
系統調用接口層
。系統調用接口層作為內核和用戶應用程序之間的中間層,扮演了一個橋梁
,或者說中間人的角色
。系統調用把應用程序的請求傳達給內核,待內核處理完請求后再將處理結果返回給應用程序。
1.1、用戶程序如何進行系統調用
- 方式一:通過C庫函數,C庫函數封裝了所有的系統調用。
- 方式二:2.6.18版本之前的內核可以使用_syscall宏。但是自2.6.19版本開始,_syscall宏被廢除,我們需要使用syscall函數,通過指定
系統調用號
和一組參數來調用系統調用。
syscall函數原型為:
int syscall(int number, ...);
其中number是系統調用號
,number后面應順序接上該系統調用的所有參數。
下面是gettid系統調用的調用實例。
00 #include <unistd.h>
01 #include <sys/syscall.h>
02 #include <sys/types.h>
03
04 #define __NR_gettid 224
05
06 int main(int argc, char *argv[])
07 {
08 pid_t tid;
09
10 tid = syscall(__NR_gettid); //括號內參數直接寫224也可以
11 }
大部分系統調用都包括了一個SYS_符號常量來指定自己到系統調用號的映射,因此上面第10行可重寫為:
tid = syscall(SYS_gettid);
1.2、為什么需要系統調用
- 為什么需要系統調用?主要有以下
兩方面原因。
(1)系統調用可以為用戶空間提供訪問硬件資源的統一接口
,以至于應用程序不必去關注具體的硬件訪問操作。比如,讀寫文件時,應用程序不用去管磁盤類型,甚至于不用關心是哪種文件系統。
(2)系統調用可以對系統進行保護,保證系統的穩定和安全
。系統調用的存在規定了用戶進程進入內核的具體方式,換句話說,用戶訪問內核的路徑是事先規定好
的,只能從規定位置進入內核,而不準許肆意跳入內核。有了這樣的進入內核的統一訪問路徑限制才能保證內核的安全。
我們可以形象地描述這種機制:作為一個游客,你可以買票要求進入野生動物園,但你必須老老實實地坐在觀光車上,按照規定的路線觀光游覽。當然,不準下車,因為那樣太危險,不是讓你丟掉小命,就是讓你嚇壞了野生動物。
1.3、系統調用執行過程——把進程從用戶態切換到內核態
- 系統調用通過軟中斷0x80陷入內核,跳轉到系統調用處理程序system_call函數,并執行相應的服務例程。
- 主要分為兩個階段:
1)通過軟中斷使進程從用戶空間轉換到內核空間。
用戶空間到內核空間的轉換階段
如圖所示,系統調用的執行需要一個用戶空間到內核空間的狀態轉換,不同的平臺具有不同的指令可以完成這種轉換,這種指令也被稱作操作系統陷入
(operating system trap)指令。
Linux通過軟中斷來實現這種陷入
,具體對于X86架構來說,是軟中斷0x80,也即int $0x80匯編指令
。軟中斷和我們常說的中斷(硬件中斷)不同之處在于-它由軟件指令觸發而并非由硬件外設引發。
int 0x80指令被封裝在C庫中,對于用戶應用來說,基于可移植性的考慮,不應該直接調用int $0x80指令。陷入指令的平臺依賴性,也正是系統調用需要在C庫進行封裝的原因之一。
通過軟中斷0x80,系統會跳轉到一個預設的內核空間地址
,它指向了系統調用處理程序
(不要和系統調用服務例程相混淆),即在arch/i386/kernel/entry.S文件中使用匯編語言編寫的system_call函數
。
2)system_call函數到系統調用服務例程。
所有的系統調用都會統一跳轉到這個地址進而執行system_call函數
1、進入system_call函數前,用戶應用將參數存放到對應寄存器中,system_call函數執行時會首先將這些寄存器壓入堆棧。
2、軟中斷指令int 0x80執行時,系統調用號
會被放入eax寄存器,同時,系統調用表
sys_call_table每一項占用4個字節。這樣,如上圖所示,system_call函數可以讀取eax寄存器獲得當前系統調用的系統調用號,偏移地址=4x%eax
,然后以sys_call_table為基址,基址加上偏移地址
所指向的內容即是應該執行的系統調用服務例程(內核函數)的地址
。
3、對于系統調用服務例程,可以直接從system_call函數壓入的堆棧中獲得參數,對參數的修改也可以一直在堆棧中進行。在system_call函數退出后,用戶應用可以直接從寄存器中獲得被修改過的參數。
2、C庫提供操作系統應用編程接口(API)
- 用戶應用程序在
某些時候
可以直接通過系統調用來訪問內核;但更多時候
, 應用程序是通過操作系統提供的應用編程接口(API——C庫的函數)而不是直接通過系統調用來編程。 - 在UNIX世界里,最通用的操作系統API基于POSIX(Portable Operating System Interface of UNIX,可移植操作系統接口)標準。
- 即POSIX就是一種統一的標準的API編寫規范。便于用戶程序在各種不同的UNIX和LINUX操作系統下調用的API函數都能正常運行。
- 操作系統API的主要作用是把操作系統的功能完全展示出來,提供給應用程序,基于該操作系統,與文件、內存、時鐘、網絡、圖形、各種外設等互操作的能力。此外,操作系統API通常還提供許多工具類的功能,比如操縱字符串、各種數據類型、時間日期等。
- 各種操作系統都會提供類似的C庫,C庫中的那些函數接口就是API。
3、C庫函數(API)內部封裝系統調用(函數)
- UNIX和LINUX操作系統API通常都以C庫的方式提供。C庫提供了POSIX的絕大部分API。
- 內核提供的每個系統調用在C庫中都具有相應的
封裝函數
。系統調用與其C庫封裝函數的名稱常常相同,比如,read系統調用在C庫中的封裝函數即為read函數。當然,也會有挺多C庫封裝函數和系統調用名稱不同,特別是存在多個C庫封裝函數內部封裝了相同的系統調用的時候。 - 系統調用和C庫函數之間并不是一一對應的關系。可能幾個不同的函數會調用到同一個系統調用,即
多對一關系
,比如C庫函數malloc和free都是通過brk系統調用來擴大或縮小進程的堆棧,execl、execlp、execle、execv、execvp和execve這些C庫函數都是通過execve系統調用來執行一個可執行文件。也有可能一個函數調用多個系統調用,即一對多關系
。 - 更有些C庫API函數并不依賴于任何系統調用,比如strcpy函數(復制字符串)和atoi函數(轉換ASCII為整數),因為它們并不需要向內核請求任何服務。
- 實際上,從用戶的角度看,系統調用和C庫之間的區別并不重要,他們只需通過C庫函數完成所需功能。相反,從內核的角度看,需要考慮的則是提供哪些針對確定目的的系統調用,并不需要關注它們如何被使用。
4、系統命令
- 系統命令利用C庫實現的可執行程序,比如最為常用的ls、cd等命令。
- strace工具可以跟蹤命令的執行,使用希望跟蹤的命令為參數,并顯示出該命令執行過程中所使用到的所有系統調用。比如,如果希望了解在執行pwd命令時都調用了哪些系統調用,可以使用下面的命令:
$strace pwd
結果會產生大量的信息,顯示出pwd命令執行過程中所調用到的各個系統調用:
……
write(1, "/usr/src/linux-2.6.23\n", 22/usr/src/linux-2.6.23) = 22
close(1) = 0
munmap(0xb7f5a000, 4096) = 0
exit_group(0)
5、系統調用——>內核函數
- 內核函數與C庫函數的區別僅僅是內核函數在內核實現,因此必須遵守內核編程的規則。
- 系統調用最終必須具有明確的操作。用戶應用程序通過系統調用進入內核后,會執行各個系統調用對應的
內核函數,即系統調用服務例程
,比如系統調用getpid的服務例程是內核函數sys_getpid。 - 系統調用服務例程之外,內核中存在著大量的內核函數。有些局限于某個內核文件自己使用,有些則是export出來供內核其他部分共同使用。對于export出來的內核函數,可以使用ksyms命令或通過/proc/ksyms文件查看。
6、系統調用號——>系統調用表——>系統調用服務例程(內核函數)
6.1系統調用號
每個系統調用都擁有一個獨一無二的系統調用號,用戶應用程序通過它,而不是系統調用的名稱,來指明要執行哪個系統調用。
系統調用號的定義在include/asm-i386/unistd.h文件。
008 #define __NR_restart_syscall 0
007 #define __NR_exit 1
009 #define __NR_fork 2
010 #define __NR_read 3
011 #define __NR_write 4
012 #define __NR_open 5 ——系統調用號
……
326 #define __NR_getcpu 318
327 #define __NR_epoll_pwait 319
328 #define __NR_utimensat 320 ——系統調用號
329 #define __NR_signalfd 321
330 #define __NR_timerfd 322
331 #define __NR_eventfd 323
332 #define __NR_fallocate 324
將其與系統調用表的定義相比較可以發現,每個系統調用號都依次對應了系統調用表中的某一項。內核正是將系統調用號作為下標去獲取系統調用表中的服務例程函數地址。
系統調用號與系統調用為相依相生的關系,一旦分配就不能再有任何變更,即使該系統調用被刪除,它所擁有的系統調用號也不能被回收利用。
6.2系統調用表sys_call_table
- 系統調用表集中存放了所有系統調用服務例程(內核函數)的地址,那么系統調用在內核中的執行就可以轉化為從該表獲取對應的服務例程并執行的過程。
- 系統調用表存儲了所有系統調用對應的服務例程的函數地址,在arch/i386/kernel/syscall_table.S文件中被定義:
001 ENTRY(sys_call_table)
002 .long sys_restart_syscall /* 0 - old
"setup()" system call, used for restarting */
003 .long sys_exit
004 .long sys_fork
005 .long sys_read
006 .long sys_write
007 .long sys_open /* 5 ——系統調用號*/
……
320 .long sys_getcpu
321 .long sys_epoll_pwait
322 .long sys_utimensat /* 320 ——系統調用號*/
323 .long sys_signalfd
324 .long sys_timerfd
325 .long sys_eventfd
326 .long sys_fallocate
- 從中可發現兩個特別之處。
首先,
所有系統調用服務例程的命名均遵守一定的規則,即在系統調用名稱之前增加"sys_"前綴,比如open系統調用對應sys_open函數。
其次,
內核提供的系統調用數目非常有限,到2.6.23版本的內核也不過才達到僅僅325個,使用"man 2 syscalls"命令即可以瀏覽到所有系統調用的添加歷史。 - 這也是系統調用與C庫函數的區別之一:系統調用通常只提供最小的接口,C庫函數則在此基礎之上提供更多復雜的功能。
6.3系統調用服務例程
- 系統調用最終由系統調用服務例程完成明確的操作。所有的系統調用服務例程集中
聲明函數原型在include/linux/syscalls.h文件
,但分散定義在很多不同的文件。比如getpid系統調用用于獲取當前進程的PID,它的服務例程sys_getpid在kernel/timer.c文件中定義為:
954 asmlinkage long sys_getpid(void)
955 {
956 return current->tgid;
957 }
除了都具有
"sys_"前綴
之外,所有的系統調用服務例程命名與定義還必須遵守其他的一些規則。首先,函數定義中必須添加asmlinkage標記
,通知編譯器僅從堆棧中獲取該函數的參數。其次,必須返回一個long類型的返回值表示成功或錯誤
,通常返回0表示成功,返回負值表示錯誤。當然,getpid系統調用非常簡單,不可能會失敗,通過命令"man 2 getpid"可以查看它的手冊,里面也明確指出了這一點。每個系統調用的系統調用號、命名以及操作目的都是固定的,但內核如何去實現并沒有明確規定,不同版本、不同架構的內核實現都有可能會有所變化。