1. 程式人生 > >系統呼叫原理

系統呼叫原理

1什麼是系統呼叫

   系統呼叫,顧名思義,說的是作業系統提供給使用者程式呼叫的一組“特殊”介面。使用者程式可以通過這組“特殊”介面來獲得作業系統核心提供的服務,比如使用者可以通過檔案系統相關的呼叫請求系統開啟檔案、關閉檔案或讀寫檔案,可以通過時鐘相關的系統呼叫獲得系統時間或設定定時器等。

從邏輯上來說,系統呼叫可被看成是一個核心與使用者空間程式互動的介面——它好比一箇中間人,把使用者程序的請求傳達給核心,待核心把請求處理完畢後再將處理結果送回給使用者空間。

系統服務之所以需要通過系統呼叫來提供給使用者空間的根本原因是為了對系統進行“保護”,因為我們知道Linux的執行空間分為核心空間與使用者空間,它們各自執行在不同的級別中,邏輯上相互隔離。所以使用者程序在通常情況下不允許訪問核心資料,也無法使用核心函式,它們只能在使用者空間操作使用者資料,呼叫使用者空間函式。比如我們熟悉的“hello world”程式(執行時)就是標準的使用者空間程序,它使用的列印函式printf就屬於使用者空間函式,列印的字元“hello word”字串也屬於使用者空間資料。

但是很多情況下,使用者程序需要獲得系統服務(呼叫系統程式),這時就必須利用系統提供給使用者的“特殊介面”——系統呼叫了,它的特殊性主要在於規定了使用者程序進入核心的具體位置;換句話說,使用者訪問核心的路徑是事先規定好的,只能從規定位置進入核心,而不准許肆意跳入核心。有了這樣的陷入核心的統一訪問路徑限制才能保證核心安全無虞。我們可以形象地描述這種機制:作為一個遊客,你可以買票要求進入野生動物園,但你必須老老實實地坐在觀光車上,按照規定的路線觀光遊覽。當然,不準下車,因為那樣太危險,不是讓你丟掉小命,就是讓你嚇壞了野生動物。

2 Linux的系統呼叫 

     對於現代作業系統,系統呼叫是一種核心與使用者空間通訊的普遍手段,Linux系統也不例外。但是Linux系統的系統呼叫相比很多Unix和windows等系統具有一些獨特之處,無處不體現出Linux的設計精髓——簡潔和高效。

     Linux系統呼叫很多地方繼承了Unix的系統呼叫(但不是全部),但Linux相比傳統Unix的系統呼叫做了很多揚棄,它省去了許多Unix系統冗餘的系統呼叫,僅僅保留了最基本和最有用的系統呼叫,所以Linux全部系統呼叫只有250個左右(而有些作業系統系統呼叫多達1000個以上)。

        要彌補這個鴻溝,第一,你必須明白系統呼叫在核心裡的主要用途。雖然上面給出了數種分類,不過,總的概括來講,系統呼叫在系統中的主要用途無非以下幾類:

l控制硬體——系統呼叫往往作為硬體資源和使用者空間的抽象介面,比如讀寫檔案時用到的write/read呼叫。

l設定系統狀態或讀取核心資料——因為系統呼叫是使用者空間和核心的唯一通訊手段

[1][2],所以使用者設定系統狀態,比如開/關某項核心服務(設定某個核心變數),或讀取核心資料都必須通過系統呼叫。比如getpgid、getpriority、setpriority、sethostname

l程序管理——一系統呼叫介面是用來保證系統中程序能以多工在虛擬記憶體環境下得以執行。比如 fork、clone、execve、exit等

第二,什麼服務應該存在於核心;或者說什麼功能應該實現在核心而不是在使用者空間。這個問題並沒有明確的答案,有些服務你可以選擇在核心完成,也可以在使用者空間完成。選擇在核心完成通常基於以下考慮:

l服務必須獲得核心資料,比如一些服務必須獲得中斷或系統時間等核心資料。

l從安全形度考慮,在核心中提供的服務相比使用者空間提供的毫無疑問更安全,很難被非法訪問到。

l從效率考慮,在核心實現服務避免了和使用者空間來回傳遞資料以及保護現場等步驟,因此效率往往要比在使用者空間實現高許多。比如,httpd等服務。

l如果核心和使用者空間都需要使用該服務,那麼最好實現在核心空間,比如隨機數產生。

   理解上述道理對掌握系統呼叫的本質意義很大,希望網友們能從使用中多總結,多思考。

3系統呼叫、使用者程式設計介面(API)、系統命令和核心函式的關係 

系統呼叫並非直接和程式設計師或系統管理員打交道,它僅僅是一個通過軟中斷機制(我們後面講述)向核心提交請求,獲取核心服務的介面。而在實際使用中程式設計師呼叫的多是使用者程式設計介面——API,而管理員使用的則多是系統命令。

使用者程式設計介面其實是一個函式定義,說明了如何獲得一個給定的服務,比如read()、malloc()、free()、abs()等。它有可能和系統呼叫形式上一致,比如read()介面就和read系統呼叫對應,但這種對應並非一一對應,往往會出現幾種不同的API內部用到同一個系統呼叫,比如malloc()、free()內部利用brk( )系統呼叫來擴大或縮小程序的堆;或一個API利用了好幾個系統呼叫組合完成服務。更有些API甚至不需要任何系統呼叫——因為它並不是必需要使用核心服務,如計算整數絕對值的abs()介面。

另外要補充的是Linux的使用者程式設計介面遵循了在Unix世界中最流行的應用程式設計介面標準——POSIX標準,這套標準定義了一系列API。在Linux中(Unix也如此),這些API主要是通過C庫(libc)實現的,它除了定義的一些標準的C函式外,一個很重要的任務就是提供了一套封裝例程(wrapper routine)將系統呼叫在使用者空間包裝後供使用者程式設計使用。

下一個需要解釋一下的問題是核心函式和系統呼叫的關係。大家不要把核心函式想像的過於複雜,其實它們和普通函式很像,只不過在核心實現,因此要滿足一些核心程式設計的要求[3]。系統呼叫是一層使用者進入核心的介面,它本身並非核心函式,進入核心後,不同的系統呼叫會找到對應到各自的核心函式——換個專業說法就叫:系統呼叫服務例程。實際上針對請求提供服務的是核心函式而非呼叫介面。

    比如系統呼叫 getpid實際上就是呼叫核心函式sys_getpid。

asmlinkage long sys_getpid(void)

{

       return current->tpid;

}

Linux系統中存在許多核心函式,有些是核心檔案中自己使用的,有些則是可以export出來供核心其他部分共同使用的,具體情況自己決定。

核心公開的核心函式——export出來的——可以使用命令ksyms 或 cat /proc/ksyms來檢視。另外,網上還有一本歸納分類核心函式的書叫作《The Linux Kernel API Book》,有興趣的讀者可以去看看。

    總而言之,從使用者角度向核心看,依次是系統命令、程式設計介面、系統呼叫和核心函式。在講述了系統呼叫實現後,我們會回過頭來看看整個執行路徑。

4系統呼叫的實現
Linux中實現系統呼叫利用了0x86體系結構中的軟體中斷[4]。軟體中斷和我們常說的中斷(硬體中斷)不同之處在於——它是通過軟體指令觸發而並非外設引發的中斷,也就是說,又是程式設計人員開發出的一種異常,具體的講就是呼叫int $0x80彙編指令,這條彙編指令將產生向量為128的程式設計異常。

之所以系統呼叫需要藉助異常來實現,是因為當用戶態的程序呼叫一個系統呼叫時,CPU便被切換到核心態執行核心函式[5],而我們在i386體系結構部分已經講述過了進入核心——進入高特權級別——必須經過系統的門機制,這裡的異常實際上就是通過系統門陷入核心(除了int 0x80外使用者空間還可以通過int3——向量3、into——向量4 、bound——向量5等異常指令進入核心,而其他異常無法被使用者空間程式利用,都是由系統使用的)。

我們更詳細地解釋一下這個過程。int $0x80指令的目的是產生一個編號為128的程式設計異常,這個程式設計異常對應的是中斷描述符表IDT中的第128項——也就是對應的系統門描述符。門描述符中含有一個預設的核心空間地址,它指向了系統呼叫處理程式:system_call()(別和系統呼叫服務程式混淆,這個程式在entry.S檔案中用匯編語言編寫)。

很顯然,所有的系統呼叫都會統一地轉到這個地址,但Linux一共有2、3百個系統呼叫都從這裡進入核心後又該如何派發到它們到各自的服務程式去呢?別發昏,解決這個問題的方法非常簡單:首先Linux為每個系統呼叫都進行了編號(0—NR_syscall),同時在核心中儲存了一張系統呼叫表,該表中儲存了系統呼叫編號和其對應的服務例程,因此在系統調入通過系統門陷入核心前,需要把系統呼叫號一併傳入核心,在x86上,這個傳遞動作是通過在執行int0x80前把呼叫號裝入eax暫存器實現的。這樣系統呼叫處理程式一旦執行,就可以從eax中得到資料,然後再去系統呼叫表中尋找相應服務例程了。

除了需要傳遞系統呼叫號以外,許多系統呼叫還需要傳遞一些引數到核心,比如sys_write(unsigned int fd, const char * buf, size_t count)呼叫就需要傳遞檔案描述符fd、要寫入的內容buf、以及寫入位元組數count等幾個內容到核心。碰到這種情況,Linux會有6個暫存器可被用來傳遞這些引數:eax (存放系統呼叫號)、 ebx、ecx、edx、esi及edi來存放這些額外的引數(以字母遞增的順序)。具體做法是在system_call( )中使用SAVE_ALL巨集把這些暫存器的值儲存在核心態堆疊中.