1. 程式人生 > >6.Linux核心設計與實現 P57---系統呼叫(轉)

6.Linux核心設計與實現 P57---系統呼叫(轉)

在Linux中,系統呼叫是使用者空間訪問核心的唯一手段,它們是核心唯一的合法入口。實際上,其他的像裝置檔案和/proc之類的方式,最終也還是要通過系統呼叫進行的。

       一般情況下,應用程式通過應用程式設計介面(API)而不是直接通過系統呼叫來程式設計,而且這種程式設計介面實際上並不需要和核心提供的系統呼叫對應。一個API定義了一組應用程式使用的程式設計介面。它們可以實現成一個系統呼叫,也可以通過呼叫多個系統呼叫來實現,即使不使用任何系統呼叫也不存在問題。實際上,API可以在各種不同的作業系統上實現,給應用程式提供完全相同的介面,而它們本身在這些系統上的實現卻可能迥異。

       在Unix世界中,最流行的應用程式設計介面是基於POSIX標準的,Linux是與POSIX相容的。

       從程式設計師的角度看,他們只需要給API打交道就可以了,而核心只跟系統呼叫打交道;庫函式及應用程式是怎麼使用系統呼叫不是核心關心的。

       系統呼叫(在linux中常稱作syscalls)通常通過函式進行呼叫。它們通常都需要定義一個或幾個引數(輸入)而且可能產生一些副作用。這些副作用通過一個long型別的返回值來表示成功(0值)或者錯誤(負值)。在系統調用出現錯誤的時候會把錯誤碼寫入errno全域性變數。通過呼叫perror()函式,可以把該變數翻譯成使用者可以理解的錯誤字符串。

       系統呼叫的實現有兩個特別之處:1)函式宣告中都有asmlinkage限定詞,用於通知編譯器僅從棧中提取該函式的引數。2)系統呼叫getXXX()在核心中被定義為sys

_getXXX()。這是Linux中所有系統呼叫都應該遵守的命名規則。

       系統呼叫號:在linux中,每個系統呼叫都賦予一個系統呼叫號,通過這個獨一無二的號就可以關聯絡統呼叫。當用戶空間的程序執行一個系統呼叫的時候,這個系統呼叫號就被用來指明到底要執行哪個系統呼叫;程序不會提及系統呼叫的名稱。系統呼叫號一旦分配就不能再有任何變更(否則編譯好的應用程式就會崩潰),如果一個系統呼叫被刪除,它所佔用的系統呼叫號也不允許被回收利用。Linux有一個"未使用"系統呼叫sys_ni_syscall(),它除了返回-ENOSYS外不做任何其他工作,這個錯誤號就是專門針對無效的系統呼叫而設的。雖然很罕見,但如果有一個系統呼叫被刪除,這個函式就要負責“填補空位”。

       核心記錄了系統呼叫表中所有已註冊過的系統呼叫的列表,儲存在sys_call_table中。它與體系結構有關,一般在entry.s中定義。這個表中為每一個有效的系統呼叫指定了唯一的系統呼叫號。

       使用者空間的程式無法直接執行核心程式碼。它們不能直接呼叫核心空間的函式,因為核心駐留在受保護的地址空間上,應用程式應該以某種方式通知系統,告訴核心自己需要執行一個系統呼叫,系統系統切換到核心態,這樣核心就可以代表應用程式來執行該系統呼叫了。這種通知核心的機制是通過軟中斷實現的。x86系統上的軟中斷由int$0x80指令產生。這條指令會觸發一個異常導致系統切換到核心態並執行第128號異常處理程式,而該程式正是系統呼叫處理程式,名字叫system_call().它與硬體體系結構緊密相關,通常在entry.s檔案中通過組合語言編寫。

       所有的系統呼叫陷入核心的方式都是一樣的,所以僅僅是陷入核心空間是不夠的。因此必須把系統呼叫號一併傳給核心。在x86上,這個傳遞動作是通過在觸發軟中斷前把呼叫號裝入eax暫存器實現的。這樣系統呼叫處理程式一旦執行,就可以從eax中得到資料。上述所說的system_call()通過將給定的系統呼叫號與NR_syscalls做比較來檢查其有效性。如果它大於或者等於NR_syscalls,該函式就返回-ENOSYS.否則,就執行相應的系統呼叫:call *sys_call_table(, %eax, 4);

       由於系統呼叫表中的表項是以32位(4位元組)型別存放的,所以核心需要將給定的系統呼叫號乘以4,然後用所得到的結果在該表中查詢器位置。如圖圖一所示:

                                      結構 

     上面已經提到,除了系統呼叫號以外,還需要一些外部的引數輸入。最簡單的辦法就是像傳遞系統呼叫號一樣把這些引數也存放在暫存器裡。在x86系統上ebx,ecx,edx,esi和edi按照順序存放前5個引數。需要六個或六個以上引數的情況不多見,此時,應該用一個單獨的暫存器存放指向所有這些引數在使用者空間地址的指標。給使用者空間的返回值也通過暫存器傳遞。在x86系統上,它存放在eax暫存器中。

       系統呼叫必須仔細檢查它們所有的引數是否合法有效。系統呼叫在核心空間執行。如果任由使用者將不合法的輸入傳遞給核心,那麼系統的安全和穩定將面臨極大的考驗。最重要的一種檢查就是檢查使用者提供的指標是否有效,核心在接收一個使用者空間的指標之前,核心必須要保證:

1)指標指向的記憶體區域屬於使用者空間
2)指標指向的記憶體區域在程序的地址空間裡
3)如果是讀,讀記憶體應該標記為可讀。如果是寫,該記憶體應該標記為可寫。

       核心提供了兩種方法來完成必須的檢查和核心空間與使用者空間之間資料的來回拷貝。這兩個方法必須有一個被呼叫。

copy_to_user():向用戶空間寫入資料,需要3個引數。第一個引數是程序空間中的目的記憶體地址。第二個是核心空間內的源地址
                     .第三個是需要拷貝的資料長度(位元組數)。
copy_from_user():向用戶空間讀取資料,需要3個引數。第一個引數是程序空間中的目的記憶體地址。第二個是核心空間內的源地
                     址.第三個是需要拷貝的資料長度(位元組數)。
注意:這兩個都有可能引起阻塞。當包含使用者資料的頁被換出到硬碟上而不是在實體記憶體上的時候,這種情況就會發生。此時,程序就會休眠,直到缺頁處理程式將該頁從硬碟重新換回到實體記憶體。

       核心在執行系統呼叫的時候處於程序上下文,current指標指向當前任務,即引發系統呼叫的那個程序。在程序上下文中,核心可以休眠(比如在系統呼叫阻塞或顯式呼叫schedule()的時候)並且可以被搶佔。當系統呼叫返回的時候,控制權仍然在system_call()中,它最終會負責切換到使用者空間並讓使用者程序繼續執行下去。

       給linux新增一個系統呼叫時間很簡單的事情,怎麼設計和實現一個系統呼叫是難題所在。實現系統呼叫的第一步是決定它的用途,這個用途是明確且唯一的,不要嘗試編寫多用途的系統呼叫。ioctl則是一個反面教材。新系統呼叫的引數,返回值和錯誤碼該是什麼,這些都很關鍵。一旦一個系統呼叫編寫完成後,把它註冊成為一個正式的系統呼叫是件瑣碎的工作,一般下面幾步:

1)在系統呼叫表(一般位於entry.s)的最後加入一個表項。從0開始算起,系統表項在該表中的位置就是它的系統呼叫號。如第
   10個系統呼叫分配到系統呼叫號為9
2)任何體系結構,系統呼叫號都必須定義於include/asm/unistd.h中
3)系統呼叫必須被編譯進核心映像(不能編譯成模組)。這隻要把它放進kernel/下的一個相關檔案就可以。

       通常,系統呼叫靠C庫支援,使用者程式通過包含標準標頭檔案並和C庫連結,就可以使用系統呼叫(或者使用庫函式,再由庫函式實際呼叫)。慶幸的是linux本身提供了一組巨集用於直接對系統呼叫進行訪問。它會設定好暫存器並呼叫int $0x80指令。這些巨集是_syscalln(),其中n的範圍是從0到6.代表需要傳遞給系統呼叫的引數個數。這是由於該巨集必須瞭解到底有多少引數按照什麼次序壓入暫存器。以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()系統呼叫直接把上面的巨集放置在應用程式中就可以了。對於每個巨集來說,都有2+2*n個引數。每個引數的意義簡單明瞭,這裡就不詳細說明了。