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