1. 程式人生 > >Linux系統呼叫詳解(如何從使用者空間進入核心空間)

Linux系統呼叫詳解(如何從使用者空間進入核心空間)

系統呼叫概述

        計算機系統的各種硬體資源是有限的,在現代多工作業系統上同時執行的多個程序都需要訪問這些資源,為了更好的管理這些資源程序是不允許直接操作的,所有對這些資源的訪問都必須有作業系統控制。也就是說作業系統是使用這些資源的唯一入口,而這個入口就是作業系統提供的系統呼叫(System Call)。在linux中系統呼叫是使用者空間訪問核心的唯一手段,除異常和陷入外,他們是核心唯一的合法入口。

       一般情況下應用程式通過應用程式設計介面API,而不是直接通過系統呼叫來程式設計。在Unix世界,最流行的API是基於POSIX標準的。

       作業系統一般是通過中斷從使用者態切換到核心態。中斷就是一個硬體或軟體請求,要求CPU暫停當前的工作,去處理更重要的事情。比如,在x86機器上可以通過int指令進行軟體中斷,而在磁碟完成讀寫操作後會向CPU發起硬體中斷。

        中斷有兩個重要的屬性,中斷號和中斷處理程式。中斷號用來標識不同的中斷,不同的中斷具有不同的中斷處理程式。在作業系統核心中維護著一箇中斷向量表(Interrupt Vector Table),這個陣列儲存了所有中斷處理程式的地址,而中斷號就是相應中斷在中斷向量表中的偏移量。

        一般地,系統呼叫都是通過軟體中斷實現的,x86系統上的軟體中斷由int $0x80指令產生,而128號異常處理程式就是系統呼叫處理程式system_call(),它與硬體體系有關,在entry.S中用匯編寫。接下來就來看一下Linux下系統呼叫具體的實現過程。

Linux下系統呼叫的實現

        前文已經提到了Linux下的系統呼叫是通過0x80實現的,但是我們知道作業系統會有多個系統呼叫(Linux下有319個系統呼叫),而對於同一個中斷號是如何處理多個不同的系統呼叫的?最簡單的方式是對於不同的系統呼叫採用不同的中斷號,但是中斷號明顯是一種稀缺資源,Linux顯然不會這麼做;還有一個問題就是系統呼叫是需要提供引數,並且具有返回值的,這些引數又是怎麼傳遞的?也就是說,對於系統呼叫我們要搞清楚兩點:

        1. 系統呼叫的函式名稱轉換。

        2. 系統呼叫的引數傳遞。

        首先看第一個問題。實際上,Linux中每個系統呼叫都有相應的系統呼叫號作為唯一的標識,核心維護一張系統呼叫表,sys_call_table,表中的元素是系統呼叫函式的起始地址,而系統呼叫號就是系統呼叫在呼叫表的偏移量。在x86上,系統呼叫號是通過eax暫存器傳遞給核心的。比如fork()的實現:

在/usr/include/asm/unistd_32.h,可以通過find / -name unistd_32.h -print查詢)

[cpp] view plaincopyprint
?
  1. #ifndef _ASM_X86_UNISTD_32_H
  2. #define _ASM_X86_UNISTD_32_H
  3. /* 
  4.  * This file contains the system call numbers. 
  5.  */
  6. #define __NR_restart_syscall      0
  7. #define __NR_exit                 1
  8. #define __NR_fork                 2
  9. #define __NR_read                 3
  10. #define __NR_write                4
  11. #define __NR_open                 5
      所以具體呼叫fork的過程是:將2存入%eax中,然後進行系統呼叫,虛擬碼: [plain] view plaincopyprint?
  1. mov     eax, 2  
  2. int     0x80  
        對於引數傳遞,Linux是通過暫存器完成的。Linux最多允許向系統呼叫傳遞6個引數,分別依次由%ebx,%ecx,%edx,%esi,%edi這5個暫存器完成,需要6個及以上引數情況不多見,另外應該有一個單獨的暫存器存放指向所有這些引數在使用者空間的地址的指標,給使用者空間的返回值通過eax暫存器傳遞。比如,呼叫exit(1),虛擬碼是: [plain] view plaincopyprint?
  1. mov    eax, 2  
  2. mov    ebx, 1  
  3. int    0x80  
        因為exit需要一個引數1,所以這裡只需要使用ebx。這6個暫存器可能已經被使用,所以在傳參前必須把當前暫存器的狀態儲存下來,待系統呼叫返回後再恢復,這個在後面棧切換再具體講。
        Linux中,在使用者態和核心態執行的程序使用的棧是不同的,分別叫做使用者棧和核心棧,兩者各自負責相應特權級別狀態下的函式呼叫。當進行系統呼叫時,程序不僅要從使用者態切換到核心態,同時也要完成棧切換,這樣處於核心態的系統呼叫才能在核心棧上完成呼叫。系統呼叫返回時,還要切換回使用者棧,繼續完成使用者態下的函式呼叫。

        暫存器%esp(棧指標,指向棧頂)所在的記憶體空間叫做當前棧,比如%esp在使用者空間則當前棧就是使用者棧,否則是核心棧。棧切換主要就是%esp在使用者空間和核心空間間的來回賦值。在Linux中,每個程序都有一個私有的核心棧,當從使用者棧切換到核心棧時,需完成儲存%esp以及相關暫存器的值(%ebx,%ecx...)並將%esp設定成核心棧的相應值。而從核心棧切換會使用者棧時,需要恢復使用者棧的%esp及相關暫存器的值以及儲存核心棧的資訊。一個問題就是使用者棧的%esp和暫存器的值儲存到什麼地方,以便於恢復呢?答案就是核心棧,在呼叫int指令機型系統呼叫後會把使用者棧的%esp的值及相關暫存器壓入核心棧中,系統呼叫通過iret指令返回,在返回之前會從核心棧彈出使用者棧的%esp和暫存器的狀態,然後進行恢復。

        相信大家一定聽過說,系統呼叫很耗時,要儘量少用。通過上面描述系統呼叫的實現原理,大家也應該知道這其中的原因了。第一,系統呼叫通過中斷實現,需要完成棧切換。第二,使用暫存器傳參,這需要額外的儲存和恢復的過程。

        上面關於系統呼叫的闡述,如有錯誤歡迎指正。。