用戶態與內核態的切換


內核態與用戶態的理解:

 

 

2)特權級

熟悉Unix/Linux系統的人都知道,fork的工作實際上是以系統調用的方式完成相應功能的,具體的工作是由sys_fork負責實施。其實無論是不是Unix或者Linux,對於任何操作系統來說,創建一個新的進程都是屬於核心功能,因為它要做很多底層細致地工作,消耗系統的物理資源,比如分配物理內存,從父進程拷貝相關信息,拷貝設置頁目錄頁表等等,這些顯然不能隨便讓哪個程序就能去做,於是就自然引出特權級別的概念,顯然,最關鍵性的權力必須由高特權級的程序來執行,這樣才可以做到集中管理,減少有限資源的訪問和使用沖突。

特權級顯然是非常有效的管理和控制程序執行的手段,因此在硬件上對特權級做了很多支持,就Intel x86架構的CPU來說一共有0~3四個特權級,0級最高,3級最低,硬件上在執行每條指令時都會對指令所具有的特權級做相應的檢查,相關的概念有CPL、DPL和RPL,這里不再過多闡述。硬件已經提供了一套特權級使用的相關機制,軟件自然就是好好利用的問題,這屬於操作系統要做的事情,對於Unix/Linux來說,只使用了0級特權級和3級特權級。也就是說在Unix/Linux系統中,一條工作在0級特權級的指令具有了CPU能提供的最高權力,而一條工作在3級特權級的指令具有CPU提供的最低或者說最基本權力。

 

3)用戶態和內核態

現在我們從特權級的調度來理解用戶態和內核態就比較好理解了,當程序運行在3級特權級上時,就可以稱之為運行在用戶態,因為這是最低特權級,是普通的用戶進程運行的特權級,大部分用戶直接面對的程序都是運行在用戶態;反之,當程序運行在0級特權級上時,就可以稱之為運行在內核態。

雖然用戶態下和內核態下工作的程序有很多差別,但最重要的差別就在於特權級的不同,即權力的不同。運行在用戶態下的程序不能直接訪問操作系統內核數據結構和程序,比如上面例子中的testfork()就不能直接調用sys_fork(),因為前者是工作在用戶態,屬於用戶態程序,而sys_fork()是工作在內核態,屬於內核態程序。

當我們在系統中執行一個程序時,大部分時間是運行在用戶態下的,在其需要操作系統幫助完成某些它沒有權力和能力完成的工作時就會切換到內核態,比如testfork()最初運行在用戶態進程下,當它調用fork()最終觸發sys_fork()的執行時,就切換到了內核態。

 

2. 用戶態和內核態的轉換

1)用戶態切換到內核態的3種方式

a. 系統調用

這是用戶態進程主動要求切換到內核態的一種方式,用戶態進程通過系統調用申請使用操作系統提供的服務程序完成工作,比如前例中fork()實際上就是執行了一個創建新進程的系統調用。而系統調用的機制其核心還是使用了操作系統為用戶特別開放的一個中斷來實現,例如Linux的int 80h中斷。

b. 異常

當CPU在執行運行在用戶態下的程序時,發生了某些事先不可知的異常,這時會觸發由當前運行進程切換到處理此異常的內核相關程序中,也就轉到了內核態,比如缺頁異常。

c. 外圍設備的中斷

當外圍設備完成用戶請求的操作后,會向CPU發出相應的中斷信號,這時CPU會暫停執行下一條即將要執行的指令轉而去執行與中斷信號對應的處理程序,如果先前執行的指令是用戶態下的程序,那么這個轉換的過程自然也就發生了由用戶態到內核態的切換。比如硬盤讀寫操作完成,系統會切換到硬盤讀寫的中斷處理程序中執行后續操作等。

 

這3種方式是系統在運行時由用戶態轉到內核態的最主要方式,其中系統調用可以認為是用戶進程主動發起的,異常和外圍設備中斷則是被動的。

 

2)具體的切換操作

從觸發方式上看,可以認為存在前述3種不同的類型,但是從最終實際完成由用戶態到內核態的切換操作上來說,涉及的關鍵步驟是完全一致的,沒有任何區別,都相當於執行了一個中斷響應的過程,因為系統調用實際上最終是中斷機制實現的,而異常和中斷的處理機制基本上也是一致的,關於它們的具體區別這里不再贅述。關於中斷處理機制的細節和步驟這里也不做過多分析,涉及到由用戶態切換到內核態的步驟主要包括:

[1] 從當前進程的描述符中提取其內核棧的ss0及esp0信息。

[2] 使用ss0和esp0指向的內核棧將當前進程的cs,eip,eflags,ss,esp信息保存起來,這個

過程也完成了由用戶棧到內核棧的切換過程,同時保存了被暫停執行的程序的下一

條指令。

[3] 將先前由中斷向量檢索得到的中斷處理程序的cs,eip信息裝入相應的寄存器,開始

執行中斷處理程序,這時就轉到了內核態的程序執行了。

 

 

 

 

 

 

本文將主要研究在X86體系下Linux系統中用戶態到內核態切換條件,及切換過程中內核棧和任務狀態段TSS在中斷機制/任務切換中的作用及相關寄存器的變化。

 

一:用戶態到內核態切換途徑:

        1:系統調用        2:中斷   3:異常

對應代碼,在3.3內核中,可以在/arch/x86/kernel/entry_32.S文件中查看。

二:內核棧

內核棧:Linux中每個進程有兩個棧,分別用於用戶態和內核態的進程執行,其中的內核棧就是用於內核態的堆棧,它和進程的task_struct結構,更具體的是thread_info結構一起放在兩個連續的頁框大小的空間內。

在內核源代碼中使用C語言定義了一個聯合結構方便地表示一個進程的thread_info和內核棧:

此結構在3.3內核版本中的定義在include/linux/sched.h文件的第2106行:

2016  union thread_union {
2017          struct thread_info thread_info;
2018          unsigned long stack[THREAD_SIZE/sizeof(long)];
2019     };        

 

其中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

三:TSS

X86體系結構中包括了一個特殊的段類型:任務狀態段(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中的值開始執行內核入口點的第一條指令


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM