系統調用
- 1 API、POSIX和C庫
- 2 系統調用
- 系統調用號
- 3 系統調用處理程序
- 指定恰當的系統調用
- 參數傳遞
- 4 系統調用的實現
- 參數驗證
- 5 系統調用上下文
- 綁定一個系統調用的最后步驟
- 從用戶空間訪問系統調用
- 為什么不通過系統調用的方式實現
1 API、POSIX和C庫
API:應用編程接口。一個API定義了一組應用程序使用的編程接口。它們可以實現成一個系統調用,也可以通過調用多個系統調用來實現,而完全不使用任何系統調用也不存在問題。在Unix世界中,最流行的應用編程接口是基于POSIX標準的。Linux是與POSIX兼容的。
Linux的系統調用像大多數Unix一樣,作為C庫的一部分。C庫實現了Unix系統的主要API,包括標準C庫函數和系統調用。
Unix的系統調用抽象出了完成某種確定目的的函數。至于這些函數怎么用完全不需要內核去關心,提供機制(需要提供什么功能)而不是策略(怎樣實現這些功能)。
2 系統調用
系統調用通常通過函數進行調用,在Linux中常稱作syscalls。系統調用還會通過一個long類型的返回值來表示成功或者失敗,使用long類型是為了與64位的硬件體系結構保持兼容。
函數聲明中如果用asmlinkage限定詞,表示通知編譯器僅從棧中提取函數的參數,所有的系統調用都需要這個限定詞。Linux所有的系統調用都應該遵守的命名規則:系統調用getpid()在內核中被定義為sys_getpid(),要在系統調用加上sys_。
系統調用號
Linux中,每個系統調用都被賦予一個系統調用號。這樣,通過這個獨一無二的號就可以關聯系統調用。當用戶空間的進程執行一個系統調用的時候,這個系統調用號就被用來指明到底是要執行那個系統調用,進程不會提及系統調用的名稱。
系統調用號一旦分配就不能再有任何變更,否則編譯好的應用程序就會崩潰。此外,如果一個系統調用被刪除,它所占的系統調用號也不允許被回收利用,否則,以前編譯過的代碼再調用這個系統調用,實際上調用的就是另一個系統調用了。Linux有一個系統調用sys_ni_syscall(),它除了返回ENOSYS外不做任何其他的工作,這個錯誤號就是專門針對無效的系統調用而設的。
內核記錄了系統調用表中的所有已被注冊過的系統調用的列表,存儲在sys_call_table中。它與結構體系有關,需要將系統調用分別注冊到每個需要支持的體系結構去,一般定義在entry.s中。2.6.10版本的位置在arch/結構體系/kernel/entry.S。這個表中為每一個有效的系統調用指定了唯一的系統調用號。
m32r體系結構:
3 系統調用處理程序
用戶空間的程序無法直接執行內核代碼,他們不能直接調用內核空間中的函數,所以,應用程序應該以某種方式通知系統,告訴內核自己需要執行一個系統調用,希望系統切換到內核態,通知內核的機制是靠軟中斷實現的:通過一個異常來促使系統切換到內核態去執行異常處理程序。此時的異常處理程序就是系統調用程序。x86系統上的軟中斷是int 0x80指令產生的。這條指令會觸發一個異常導致系統切換到內核態并執行第128號異常處理程序,而該程序正是系統調用處理程序,叫system_call()。x86處理器增加了一條叫做sysenter的指令。與int指令相比,這條指令提供了更快、更專業的陷入內核執行系統調用的方式。
指定恰當的系統調用
因為所有的系統調用陷入內核方式都一樣,因此必須把系統調用號一并傳給內核,告訴內核去執行什么系統調用。在x86上,系統調用號是通過eax寄存器傳遞給內核的。在陷入內核之前,用戶空間就把相應系統調用所對應的號放入eax中。這樣系統調用處理程序一旦運行,就可以從eax中得到數據。
system_call()函數通過將給定的系統調用號與NR_syscalls做比較來檢查其有效性。如果大于等于NR_syscalls,該函數返回ENOSYS。否則,就執行相應的系統調用。
call *sys_call_table(,%eax,4)
由于系統調用表中的表項是32位(4字節)類型存放的,所以內核需要將給定的系統調用號乘以4,然后用所得的結構在該表中查詢其位置。
參數傳遞
除了系統調用號外,大部分系統調用都還需要一些外部的參數輸入。所以,在發送異常的時候,應該把這些參數從用戶空間傳給內核。最簡單的辦法就是像傳遞系統調用號一樣:把這些參數存放到寄存器里。在x86系統上,ebx、ecx、edx、edi和esi按照順序存放前五個參數。需要6個或6個以上參數的,應該用一個單獨的寄存器存放指向所有這些參數在用戶空間地址的指針。
用戶空間返回值也通過寄存器傳遞,在x86系統上,它存放在eax寄存器中。
4 系統調用的實現
實現一個新的系統調用的第一步是決定它的用途。它要做什么?每個系統調用都應該有一個明確的用途。在Linux中不提倡采用多用途的系統調用(一個系統調用根據參數來選擇完成不同的工作)。
新系統調用的參數、返回值和錯誤碼又該是什么?系統調用的接口應該簡潔,參數盡可能的少。
設計接口的時候要盡量為將來多做考慮。
當寫一個系統調用的時候,要時刻注意可移植性和健壯性,不但要考慮當前,還要為將來做打算。
參數驗證
系統調用必須仔細檢查它們所有的參數是否合法有效。系統調用在內核空間執行,如果任由用戶將不合法的輸入傳遞給內核,那么系統的安全和穩定將面臨巨大的考驗。
內核提供了兩個方法來完成必須的檢查和內核空間與用戶空間之間數據的來回拷貝。為了向用戶空間寫入數據,內核提供了copy_to_uesr(),它需要三個參數。第一個參數是進程空間中的目的內存地址。第二個是內核空間內的源地址。最后一個參數是需要拷貝的數據長度。為了從用戶空間讀取數據,內核提供了copy_from_user()。該函數把第二個參數指定的位置上的數據拷貝到第一個參數指定的位置上,拷貝的數據長度由第三個參數決定。
注意,內核無論何時都不能輕率地接受來自用戶空間的指針!
5 系統調用上下文
在執行系統調用的時候處于進程上下文。current指針指向當前任務,即引發系統調用的那個進程。
在進程上下文中,內核可以休眠并且可以被搶占。當系統調用返回的時候,控制權仍然在system_call()中,它最終負責切換到用戶空間并讓用戶進程繼續執行下去。
綁定一個系統調用的最后步驟
當編寫完一個系統調用后,把它注冊成一個正式的系統調用是件瑣碎的工作:
- 首先,在系統調用表的最后加入一個表項。每種支持該系統調用的硬件體系都必須做這樣的工作。從0開始算起,系統調用在該表中的位置就是它的系統調用號。如第10個系統調用分配到的系統調用號為9。
- 對于所支持的各種體系結構,系統調用號都必須定義于include/asm/unistd.h中。
- 系統調用必須被編譯進內核映像(不能編譯成模塊)。這只要把它放進kernel/下的一個相關文件就行了。
我們通過一個虛構的系統調用foo()來仔細觀察這些步驟。首先,我們把sys_foo加入到系統調用表中去。對于大多數體系結構中,該表位于entry.s中,形式如下:
我們把新的系統調用加到這個表的末尾:
.long sys_foo
雖然沒有明確地指定編號,但我們加入這個系統調用被次序分配給了285這個系統調用號。對于每種需要支持的體系結構,我們都必須將自己的系統調用加入到其系統調用表中去。每種體系結構不需要對應相同的系統調用號。系統調用號是專屬體系結構ABI(應用程序二進制結構)的部分。
接下來,我們把系統調用號加入到include/asm/unistd.h中,它的格式如下
然后,我們在該列表中加入下面這行:
#define _NR_foo 285
同時#define NR_syscalls 285這個要改成#define NR_syscalls 286。因為系統調用處理程序system_call()函數通過將給定的系統調用號與NR_syscalls做比較來檢查其有效性。如果大于等于NR_syscalls,該函數返回ENOSYS。否則,就執行相應的系統調用。
最后,我們來實現foo()系統調用。無論何種配置,該系統調用都必須編譯到核心的內核映像中去,所以我們把它放在kernel/sys.c文件中。再次編譯內核就可以了。
從用戶空間訪問系統調用
Linux提供了一組宏,用于直接對系統調用進行訪問。它會設置好寄存器并調用陷入指令。這些宏是_syscalln(),其中n的范圍是0到6,代表需要傳遞給系統調用的參數個數。舉個例子,open系統調用的 定義為:
long open(const char *filename,int flags,int mode);
直接調用該系統調用的宏形式為:
_syscall3(long,open,const char *,filename,int flags,int mode)
這樣程序就可直接使用open。對于每個宏,都有2+2*n個參數,第一個參數是返回值類型。第二個參數是系統調用的名稱。
為什么不通過系統調用的方式實現
建立一個新的系統調用非常容易,但卻絕不倡導。
建立一個新的系統調用的好處:
- 系統調用創建容易且使用方便
- Linux系統調用的高性能顯而易見
問題是:
- 你需要一個系統調用號,而這需要在一個內核處于開發版本的時候由官方分配給你
- 系統調用被加入穩定內核后就被固化了
- 需要將系統調用分別注冊的每個需要的體系結構去
- 在腳本中不容易調用系統調用,也不能從文件系統直接訪問系統調用