Linux系統調用的運行過程

  在Linux中,系統調用是用戶空間訪問內核的惟一手段,它們是內核惟一的合法入口。linux

       通常狀況下,應用程序經過應用編程接口(API)而不是直接經過系統調用來編程,並且這種編程接口實際上並不須要和內核提供的系統調用對應。一個API定義了一組應用程序使用的編程接口。它們能夠實現成一個系統調用,也能夠經過調用多個系統調用來實現,即便不使用任何系統調用也不存在問題。實際上,API能夠在各類不一樣的操做系統上實現,給應用程序提供徹底相同的接口,而它們自己在這些系統上的實現卻可能迥異。程序員

       在Unix世界中,最流行的應用編程接口是基於POSIX標準的,Linux是與POSIX兼容的。編程

       從程序員的角度看,他們只須要給API打交道就能夠了,而內核只跟系統調用打交道;庫函數及應用程序是怎麼使用系統調用不是內核關心的。安全

       系統調用(在linux中常稱做syscalls)一般經過函數進行調用。它們一般都須要定義一個或幾個參數(輸入)並且可能產生一些反作用。這些反作用經過一個long類型的返回值來表示成功(0值)或者錯誤(負值)。在系統調用出現錯誤的時候會把錯誤碼寫入errno全局變量。經過調用perror()函數,能夠把該變量翻譯成用戶能夠理解的錯誤字符串。函數

       系統調用的實現有兩個特別之處:spa

       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個參數。每一個參數的意義簡單明瞭,這裏就不詳細說明了。

相關文章
相關標籤/搜索