1. 程式人生 > >使用者態到核心態切換分析

使用者態到核心態切換分析

本文將主要研究在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中的值開始執行核心入口點的第一條指令。