因為操作系統的很多操作會消耗系統的物理資源,例如創建一個新進程時,要做很多底層的細致工作,如分配物理內存,從父進程拷貝相關信息,拷貝設置頁目錄、頁表等,這些操作顯然不能隨便讓任何程序都可以做,於是就產生了特權級別的概念,與系統相關的一些特別關鍵性的操作必須由高級別的程序來完成,這樣可以做到集中管理,減少有限資源的訪問和使用沖突。Intel的X86架構的CPU提供了0到3四個特權級,而在我們Linux操作系統中則主要采用了0和3兩個特權級,也就是我們通常所說的內核態和用戶態。
運行於用戶態的進程可以執行的操作和訪問的資源都受到極大的限制,而運行於內核態的進程則可以執行任何操作並且在資源的使用上也沒有限制。很多程序開始時運行於用戶態,但在執行的過程中,一些操作需要在內核權限下才能執行,這就涉及到一個從用戶態切換到內核態的過程。本文主要要介紹的就是這個過程。
這裡再明確一個概念,每個進程都有一個4G大小的虛擬地址空間,在這個4G大小的虛擬地址空間中,前0~3G為用戶空間,每個進程的用戶空間之間是相互獨立的,互不相干。而3G~4G為內核空間,因為每個進程都可以從用戶態切換到內核態,因此,內核空間對於所有進程來說,可以說是共享的,不過這麼說有些不太嚴謹,應該說內核空間中大部分區域對於所有的進程來說都是共享的,這不共享的小部分區域是存儲所有進程內核棧的區域,為什麼這麼說,因為每個進程都存在一個內核棧,而各個進程的內核棧之間一定是不共享的。關於內核空間的詳細描述,參見
1、Linux內核--內核地址空間分布和進程地址空間
2、Linux用戶空間與內核空間
下面正式開始進入由用戶態向內核態切換的過程。
首先需要了解,什麼情況下會發生從用戶態向內核態切換。這裡細分為3種情況。
1、發生系統調用時
這是處於用戶態的進程主動請求切換到內核態的一種方式。用戶態的進程通過系統調用申請使用操作系統提供的系統調用服務例程來處理任務。而系統調用的機制,其核心仍是使用了操作系統為用戶特別開發的一個中斷機制來實現的,即軟中斷。
2、產生異常時
當CPU執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行的進程切換到處理此異常的內核相關的程序中,也就是轉到了內核態,如缺頁異常。
3、外設產生中斷時
當外圍設備完成用戶請求的操作後,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那麼這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作的完成,系統會切換到硬盤讀寫的中斷處理程序中執行後續操作等。
可以看到上述三種由用戶態切換到內核態的情況中,只有系統調用是進程主動請求發生切換的,中斷和異常都是被動的。
由於系統調用、中斷和異常由用戶態切換到內核態的機制大同小異,所以這裡僅就系統調用的切換過程進行具體說明。
如果一個用戶程序需要調用底層的系統接口,如fork等諸如libc裡面的系統調用函數,就牽涉到用戶態與內核態的切換問題,因為系統調用處理程序都是運行在內核態下。
在系統調用時由於用戶態和內核態是運行於兩個獨立的棧上面,即分別為內核棧和用戶棧,因此,不能僅簡單的傳遞函數指針,因為對於內核態堆棧在用戶態下是不可見的,所以對於系統調用函數的處理程序對於用戶態是不可見的;同時,因為內核棧和用戶棧是相互獨立的,所以在參數傳遞的過程中不能使用普通的壓棧出棧的方式來進行參數傳遞。
二:內核棧內核棧:Linux中每個進程有兩個棧,分別用於用戶態和內核態的進程執行,其中的內核棧就是用於內核態的堆棧,它和進程的task_struct結構,更具體的是thread_info結構一起放在兩個連續的頁框大小的空間內。
在內核源代碼中使用C語言定義了一個聯合結構方便地表示一個進程的thread_info和內核棧:
此結構在3.3內核版本中的定義在include/linux/sched.h文件的第2106行:
[code]2016 union thread_union {
2017 struct thread_info thread_info;
2018 unsigned long stack[THREAD_SIZE/sizeof(long)];
2019 }; <span ></span>
其中thread_info結構定義如下
3.3內核 /arch/x86/include/asm/thread_info.h文件第26行:
26 struct thread_info {
27 struct task_struct *task; /* main task structure */
28 struct exec_domain *exec_domain; /* execution domain */
29 __u32 flags; /* low level flags */
30 __u32 status; /* thread synchronous flags */
31 __u32 cpu; /* current CPU */
32 int preempt_count; /* 0 => preemptable,
33 <0 => BUG */
34 mm_segment_t addr_limit;
35 struct restart_block restart_block;
36 void __user *sysenter_return;
37 #ifdef CONFIG_X86_32
38 unsigned long previous_esp; /* ESP of the previous stack in
39 case of nested (IRQ) stacks
40 */
41 __u8 supervisor_stack[0];
42 #endif
43 unsigned int sig_on_uaccess_error:1;
44 unsigned int uaccess_err:1; /* uaccess failed */
45 };
它們的結構圖大致如下:
esp寄存器是CPU棧指針,存放內核棧棧頂地址。在X86體系中,棧開始於末端,並朝內存區開始的方向增長。從用戶態剛切換到內核態時,進程的內核棧總是空的,此時esp指向這個棧的頂端。
在X86中調用int指令型系統調用後會把用戶棧的%esp的值及相關寄存器壓入內核棧中,系統調用通過iret指令返回,在返回之前會從內核棧彈出用戶棧的%esp和寄存器的狀態,然後進行恢復。所以在進入內核態之前要保存進程的上下文,中斷結束後恢復進程上下文,那靠的就是
內核棧。
這裡有個
細節問題,就是要想在內核棧保存用戶態的esp,eip等寄存器的值,首先得知道內核棧的棧指針,那在進入內核態之前,通過什麼才能獲得內核棧的棧指針呢?答案是:
TSS三:TSSX86體系結構中包括了一個特殊的段類型:任務狀態段(TSS),用它來存放硬件上下文。TSS反映了CPU上的當前進程的特權級。
linux為每一個cpu提供一個tss段,並且在tr寄存器中保存該段。
在從用戶態切換到內核態時,可以通過獲取TSS段中的esp0來獲取當前進程的內核棧 棧頂指針,從而可以保存用戶態的cs,esp,eip等上下文。
注:linux中之所以為每一個cpu提供一個tss段,而不是為每個進程提供一個tss段,主要原因是tr寄存器永遠指向它,在任務切換的適合不必切換tr寄存器,從而減小開銷。
下面我們看下在X86體系中Linux內核對TSS的具體實現:內核代碼中TSS結構的定義:
3.3內核中:/arch/x86/include/asm/processor.h文件的第248行處:
248 struct tss_struct {
249 /*
250 * The hardware state:
251 */
252 struct x86_hw_tss x86_tss;
253
254 /*
255 * The extra 1 is there because the CPU will access an
256 * additional byte beyond the end of the IO permission
257 * bitmap. The extra byte must be all 1 bits, and must
258 * be within the limit.
259 */
260 unsigned long io_bitmap[IO_BITMAP_LONGS + 1];
261
262 /*
263 * .. and then another 0x100 bytes for the emergency kernel stack:
264 */
265 unsigned long stack[64];
266
267 } ____cacheline_aligned;
其中主要的內容是:
硬件狀態結構 : x86_hw_tss
IO權位圖 : io_bitmap
備用內核棧: stack
其中硬件狀態結構:其中在32位X86系統中x86_hw_tss的具體定義如下:
/arch/x86/include/asm/processor.h文件中第190行處:
190#ifdef CONFIG_X86_32
191 /* This is the TSS defined by the hardware. */
192 struct x86_hw_tss {
193 unsigned short back_link, __blh;
194 unsigned long sp0; //當前進程的內核棧頂指針
195 unsigned short ss0, __ss0h; //當前進程的內核棧段描述符
196 unsigned long sp1;
197 /* ss1 caches MSR_IA32_SYSENTER_CS: */
198 unsigned short ss1, __ss1h;
199 unsigned long sp2;
200 unsigned short ss2, __ss2h;
201 unsigned long __cr3;
202 unsigned long ip;
203 unsigned long flags;
204 unsigned long ax;
205 unsigned long cx;
206 unsigned long dx;
207 unsigned long bx;
208 unsigned long sp; //當前進程用戶態棧頂指針
209 unsigned long bp;
210 unsigned long si;
211 unsigned long di;
212 unsigned short es, __esh;
213 unsigned short cs, __csh;
214 unsigned short ss, __ssh;
215 unsigned short ds, __dsh;
216 unsigned short fs, __fsh;
217 unsigned short gs, __gsh;
218 unsigned short ldt, __ldth;
219 unsigned short trace;
220 unsigned short io_bitmap_base;
221
222 } __attribute__((packed));
linux的tss段中只使用esp0和iomap等字段,並且不用它的其他字段來保存寄存器,在一個用戶進程被中斷進入內核態的時候,從tss中的硬件狀態結構中取出
esp0(即內核棧棧頂指針),然後切到esp0,其它的寄存器則保存在esp0指的內核棧上而不保存在tss中。
每個CPU定義一個TSS段的具體實現代碼:
3.3內核中/arch/x86/kernel/init_task.c第35行:
35 * per-CPU TSS segments. Threads are completely 'soft' on Linux,
36 * no more per-task TSS's. The TSS size is kept cacheline-aligned
37 * so they are allowed to end up in the .data..cacheline_aligned
38 * section. Since TSS's are completely CPU-local, we want them
39 * on exact cacheline boundaries, to eliminate cacheline ping-pong.
40 */
41 DEFINE_PER_CPU_SHARED_ALIGNED(struct tss_struct, init_tss) = INIT_TSS;
INIT_TSS的定義如下:
3.3內核中 /arch/x86/include/asm/processor.h文件的第879行:
879 #define INIT_TSS { \
880 .x86_tss = { \
881 .sp0 = sizeof(init_stack) + (long)&init_stack, \
882 .ss0 = __KERNEL_DS, \
883 .ss1 = __KERNEL_CS, \
884 .io_bitmap_base = INVALID_IO_BITMAP_OFFSET, \
885 }, \
886 .io_bitmap = { [0 ... IO_BITMAP_LONGS] = ~0 }, \
887 }
其中init_stack是宏定義,指向內核棧:
61 #define init_stack (init_thread_union.stack)
這裡可以看到分別把內核棧棧頂指針、內核代碼段、內核數據段賦值給TSS中的相應項。從而進程從用戶態切換到內核態時,可以從TSS段中獲取內核棧棧頂指針,進而保存進程上下文到內核棧中。
總結:有了上面的一些准備,現總結在進程從用戶態到內核態切換過程中,Linux主要做的事:
1:讀取tr寄存器,訪問TSS段
2:從TSS段中的sp0獲取進程內核棧的棧頂指針
3: 由控制單元在內核棧中保存當前eflags,cs,ss,eip,esp寄存器的值。
4:由SAVE_ALL保存其寄存器的值到內核棧
5:把內核代碼選擇符寫入CS寄存器,內核棧指針寫入ESP寄存器,把內核入口點的線性地址寫入EIP寄存器
此時,CPU已經切換到內核態,根據EIP中的值開始執行內核入口點的第一條指令。