因為操作系統的很多操作會消耗系統的物理資源,例如創建一個新進程時,要做很多底層的細致工作,如分配物理內存,從父進程拷貝相關信息,拷貝設置頁目錄、頁表等,這些操作顯然不能隨便讓任何程序都可以做,於是就產生了特權級別的概念,與系統相關的一些特別關鍵性的操作必須由高級別的程序來完成,這樣可以做到集中管理,減少有限資源的訪問和使用沖突。Intel的X86架構的CPU提供了0到3四個特權級,而在我們Linux操作系統中則主要采用了0和3兩個特權級,也就是我們通常所說的內核態和用戶態。
運行於用戶態的進程可以執行的操作和訪問的資源都受到極大的限制,而運行於內核態的進程則可以執行任何操作並且在資源的使用上也沒有限制。很多程序開始時運行於用戶態,但在執行的過程中,一些操作需要在內核權限下才能執行,這就涉及到一個從用戶態切換到內核態的過程。本文主要要介紹的就是這個過程。
這里再明確一個概念,每個進程都有一個4G大小的虛擬地址空間,在這個4G大小的虛擬地址空間中,前0~3G為用戶空間,每個進程的用戶空間之間是相互獨立的,互不相干。而3G~4G為內核空間,因為每個進程都可以從用戶態切換到內核態,因此,內核空間對於所有進程來說,可以說是共享的,不過這么說有些不太嚴謹,應該說內核空間中大部分區域對於所有的進程來說都是共享的,這不共享的小部分區域是存儲所有進程內核棧的區域,為什么這么說,因為每個進程都存在一個內核棧,而各個進程的內核棧之間一定是不共享的。關於內核空間的詳細描述,參見
了解了上面所說的這些之后,相信對於內核態和用戶態的概念已經有了一定的了解,下面正式開始進入由用戶態向內核態切換的過程。
首先需要了解,什么情況下會發生從用戶態向內核態切換。這里細分為3種情況。
1、發生系統調用時
這是處於用戶態的進程主動請求切換到內核態的一種方式。用戶態的進程通過系統調用申請使用操作系統提供的系統調用服務例程來處理任務。而系統調用的機制,其核心仍是使用了操作系統為用戶特別開發的一個中斷機制來實現的,即軟中斷。
2、產生異常時
當CPU執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行的進程切換到處理此異常的內核相關的程序中,也就是轉到了內核態,如缺頁異常。
3、外設產生中斷時
當外圍設備完成用戶請求的操作后,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那么這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作的完成,系統會切換到硬盤讀寫的中斷處理程序中執行后續操作等。
可以看到上述三種由用戶態切換到內核態的情況中,只有系統調用是進程主動請求發生切換的,中斷和異常都是被動的。
由於系統調用、中斷和異常由用戶態切換到內核態的機制大同小異,所以這里僅就系統調用的切換過程進行具體說明。
如果一個用戶程序需要調用底層的系統接口,如fork等諸如libc里面的系統調用函數,就牽涉到用戶態與內核態的切換問題,因為系統調用處理程序都是運行在內核態下。
在系統調用時由於用戶態和內核態是運行於兩個獨立的棧上面,即分別為內核棧和用戶棧,因此,不能僅簡單的傳遞函數指針,因為對於內核態堆棧在用戶態下是不可見的,所以對於系統調用函數的處理程序對於用戶態是不可見的;同時,因為內核棧和用戶棧是相互獨立的,所以在參數傳遞的過程中不能使用普通的壓棧出棧的方式來進行參數傳遞。
每一個系統調用函數在內核當中都存在對應的句柄處理函數,一般以sys_開頭,這些句柄處理函數作為一個系統調用表形式存在:linux-3.9.4/arch/x86/syscalls/syscall_32.tbl

PS:在3.9.4內核中系統調用初始為350個,系統調用的最大個數是動態變化的,即不用如2.6內核中,在添加系統調用時需先查看MAX是否滿足,若不滿足則需要進行修改。在3.9.4內核中則不需要這個過程,現在編譯出的內核其syscall_MAX為351,若添加一個系統調用,則編譯出內核之后,該值為352。
每一個系統調用的函數對應着內核里的一個具體實現,每一個系統函數都有一個相應的數字對應,即系統調用號,這個數字事實上是系統調用函數指針的偏移。
當我們運行一個系統調用時,運行時庫通過查找這個表來決定對應的函數代碼,即系統調用號,然后存入到寄存器中,通常為eax寄存器,然后當切換到到內核態后,內核根據系統調用號來查找到對應的系統調用處理例程的函數名,從而找到對應的代碼入口地址。系統調用切換過程如圖所示:

因為在前面已經說過,內核棧和用戶棧分別處於內核空間和用戶空間兩個不同的空間中,因此,這兩個棧是相互獨立的,所以參數傳遞不能只是簡單的壓棧出棧,因此,Linux內核中主要是才用寄存器的方式來完成這個任務。

可以看到,在發生系統調用時,先是RING0_INT_FRAME,

可以看到這個過程是對esp和eip進行處理,使其指向內核棧。然后把寄存器eax中的系統調用號入棧,然后SAVE_ALL,

而SAVE_ALL中首先是各個寄存器的入棧操作,即將傳遞的參數壓到內核棧中。到此,完成了由用戶態向內核態的切換過程。由於這次時間有限,沒有細致的由源碼角度去研究這個具體的過程,過段時間有空了,好好研究一下,再進行修改補充。
歡迎大家提出問題,進行交流指導~
