在Linux中,系統調用是用戶(hù)空間訪(fǎng)問(wèn)內核的唯一手段,它們是內核唯一的合法入口。實(shí)際上,其他的像設備文件和/proc之類(lèi)的方式,最終也還是要通過(guò)系統調用進(jìn)行的。
一般情況下,應用程序通過(guò)應用編程接口(API)而不是直接通過(guò)系統調用來(lái)編程,而且這種編程接口實(shí)際上并不需要和內核提供的系統調用對應。一個(gè)API定義了一組應用程序使用的編程接口。它們可以實(shí)現成一個(gè)系統調用,也可以通過(guò)調用多個(gè)系統調用來(lái)實(shí)現,即使不使用任何系統調用也不存在問(wèn)題。實(shí)際上,API可以在各種不同的操作系統上實(shí)現,給應用程序提供完全相同的接口,而它們本身在這些系統上的實(shí)現卻可能迥異。
在Unix世界中,最流行的應用編程接口是基于POSIX標準的,Linux是與POSIX兼容的。
從程序員的角度看,他們只需要給API打交道就可以了,而內核只跟系統調用打交道;庫函數及應用程序是怎么使用系統調用不是內核關(guān)心的。
系統調用(在linux中常稱(chēng)作syscalls)通常通過(guò)函數進(jìn)行調用。它們通常都需要定義一個(gè)或幾個(gè)參數(輸入)而且可能產(chǎn)生一些副作用。這些副作用通過(guò)一個(gè)long類(lèi)型的返回值來(lái)表示成功(0值)或者錯誤(負值)。在系統調用出現錯誤的時(shí)候會(huì )把錯誤碼寫(xiě)入errno全局變量。通過(guò)調用perror()函數,可以把該變量翻譯成用戶(hù)可以理解的錯誤字符串。
系統調用的實(shí)現有兩個(gè)特別之處:1)函數聲明中都有asmlinkage限定詞,用于通知編譯器僅從棧中提取該函數的參數。2)系統調用getXXX()在內核中被定義為sys_getXXX()。這是Linux中所有系統調用都應該遵守的命名規則。
系統調用號:在linux中,每個(gè)系統調用都賦予一個(gè)系統調用號,通過(guò)這個(gè)獨一無(wú)二的號就可以關(guān)聯(lián)系統調用。當用戶(hù)空間的進(jìn)程執行一個(gè)系統調用的時(shí)候,這個(gè)系統調用號就被用來(lái)指明到底要執行哪個(gè)系統調用;進(jìn)程不會(huì )提及系統調用的名稱(chēng)。系統調用號一旦分配就不能再有任何變更(否則編譯好的應用程序就會(huì )崩潰),如果一個(gè)系統調用被刪除,它所占用的系統調用號也不允許被回收利用。Linux有一個(gè)"未使用"系統調用sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,這個(gè)錯誤號就是專(zhuān)門(mén)針對無(wú)效的系統調用而設的。雖然很罕見(jiàn),但如果有一個(gè)系統調用被刪除,這個(gè)函數就要負責“填補空位”。
內核記錄了系統調用表中所有已注冊過(guò)的系統調用的列表,存儲在sys_call_table中。它與體系結構有關(guān),一般在entry.s中定義。這個(gè)表中為每一個(gè)有效的系統調用指定了唯一的系統調用號。
用戶(hù)空間的程序無(wú)法直接執行內核代碼。它們不能直接調用內核空間的函數,因為內核駐留在受保護的地址空間上,應用程序應該以某種方式通知系統,告訴內核自己需要執行一個(gè)系統調用,系統系統切換到內核態(tài),這樣內核就可以代表應用程序來(lái)執行該系統調用了。這種通知內核的機制是通過(guò)軟中斷實(shí)現的。x86系統上的軟中斷由int$0x80指令產(chǎn)生。這條指令會(huì )觸發(fā)一個(gè)異常導致系統切換到內核態(tài)并執行第128號異常處理程序,而該程序正是系統調用處理程序,名字叫system_call().它與硬件體系結構緊密相關(guān),通常在entry.s文件中通過(guò)匯編語(yǔ)言編寫(xiě)。
所有的系統調用陷入內核的方式都是一樣的,所以?xún)H僅是陷入內核空間是不夠的。因此必須把系統調用號一并傳給內核。在x86上,這個(gè)傳遞動(dòng)作是通過(guò)在觸發(fā)軟中斷前把調用號裝入eax寄存器實(shí)現的。這樣系統調用處理程序一旦運行,就可以從eax中得到數據。上述所說(shuō)的system_call()通過(guò)將給定的系統調用號與NR_syscalls做比較來(lái)檢查其有效性。如果它大于或者等于NR_syscalls,該函數就返回-ENOSYS.否則,就執行相應的系統調用:call *sys_call_table(, %eax, 4);
由于系統調用表中的表項是以32位(4字節)類(lèi)型存放的,所以?xún)群诵枰獙⒔o定的系統調用號乘以4,然后用所得到的結果在該表中查詢(xún)器位置。如圖圖一所示:
上面已經(jīng)提到,除了系統調用號以外,還需要一些外部的參數輸入。最簡(jiǎn)單的辦法就是像傳遞系統調用號一樣把這些參數也存放在寄存器里。在x86系統上ebx,ecx,edx,esi和edi按照順序存放前5個(gè)參數。需要六個(gè)或六個(gè)以上參數的情況不多見(jiàn),此時(shí),應該用一個(gè)單獨的寄存器存放指向所有這些參數在用戶(hù)空間地址的指針。給用戶(hù)空間的返回值也通過(guò)寄存器傳遞。在x86系統上,它存放在eax寄存器中。
系統調用必須仔細檢查它們所有的參數是否合法有效。系統調用在內核空間執行。如果任由用戶(hù)將不合法的輸入傳遞給內核,那么系統的安全和穩定將面臨極大的考驗。最重要的一種檢查就是檢查用戶(hù)提供的指針是否有效,內核在接收一個(gè)用戶(hù)空間的指針之前,內核必須要保證:
1)指針指向的內存區域屬于用戶(hù)空間
2)指針指向的內存區域在進(jìn)程的地址空間里
3)如果是讀,讀內存應該標記為可讀。如果是寫(xiě),該內存應該標記為可寫(xiě)。
內核提供了兩種方法來(lái)完成必須的檢查和內核空間與用戶(hù)空間之間數據的來(lái)回拷貝。這兩個(gè)方法必須有一個(gè)被調用。
copy_to_user():向用戶(hù)空間寫(xiě)入數據,需要3個(gè)參數。第一個(gè)參數是進(jìn)程空間中的目的內存地址。第二個(gè)是內核空間內的源地址
.第三個(gè)是需要拷貝的數據長(cháng)度(字節數)。
copy_from_user():向用戶(hù)空間讀取數據,需要3個(gè)參數。第一個(gè)參數是進(jìn)程空間中的目的內存地址。第二個(gè)是內核空間內的源地
址.第三個(gè)是需要拷貝的數據長(cháng)度(字節數)。
注意:這兩個(gè)都有可能引起阻塞。當包含用戶(hù)數據的頁(yè)被換出到硬盤(pán)上而不是在物理內存上的時(shí)候,這種情況就會(huì )發(fā)生。此時(shí),進(jìn)程就會(huì )休眠,直到缺頁(yè)處理程序將該頁(yè)從硬盤(pán)重新?lián)Q回到物理內存。
內核在執行系統調用的時(shí)候處于進(jìn)程上下文,current指針指向當前任務(wù),即引發(fā)系統調用的那個(gè)進(jìn)程。在進(jìn)程上下文中,內核可以休眠(比如在系統調用阻塞或顯式調用schedule()的時(shí)候)并且可以被搶占。當系統調用返回的時(shí)候,控制權仍然在system_call()中,它最終會(huì )負責切換到用戶(hù)空間并讓用戶(hù)進(jìn)程繼續執行下去。
給linux添加一個(gè)系統調用時(shí)間很簡(jiǎn)單的事情,怎么設計和實(shí)現一個(gè)系統調用是難題所在。實(shí)現系統調用的第一步是決定它的用途,這個(gè)用途是明確且唯一的,不要嘗試編寫(xiě)多用途的系統調用。ioctl則是一個(gè)反面教材。新系統調用的參數,返回值和錯誤碼該是什么,這些都很關(guān)鍵。一旦一個(gè)系統調用編寫(xiě)完成后,把它注冊成為一個(gè)正式的系統調用是件瑣碎的工作,一般下面幾步:
1)在系統調用表(一般位于entry.s)的最后加入一個(gè)表項。從0開(kāi)始算起,系統表項在該表中的位置就是它的系統調用號。如第
10個(gè)系統調用分配到系統調用號為9
2)任何體系結構,系統調用號都必須定義于include/asm/unistd.h中
3)系統調用必須被編譯進(jìn)內核映像(不能編譯成模塊)。這只要把它放進(jìn)kernel/下的一個(gè)相關(guān)文件就可以。
通常,系統調用靠C庫支持,用戶(hù)程序通過(guò)包含標準頭文件并和C庫鏈接,就可以使用系統調用(或者使用庫函數,再由庫函數實(shí)際調用)。慶幸的是linux本身提供了一組宏用于直接對系統調用進(jìn)行訪(fǎng)問(wèn)。它會(huì )設置好寄存器并調用int $0x80指令。這些宏是_syscalln(),其中n的范圍是從0到6.代表需要傳遞給系統調用的參數個(gè)數。這是由于該宏必須了解到底有多少參數按照什么次序壓入寄存器。以open系統調用為例:
open()系統調用定義如下是:
long open(const char *filename, int flags, int mode)
直接調用此系統調用的宏的形式為:
#define NR_open 5
_syscall3(long, open, const char *, filename, int , flags, int, mode)
這樣,應用程序就可以直接使用open().調用open()系統調用直接把上面的宏放置在應用程序中就可以了。對于每個(gè)宏來(lái)說(shuō),都有2+2*n個(gè)參數。每個(gè)參數的意義簡(jiǎn)單明了,這里就不詳細說(shuō)明了。