Linux 2.6內核筆記【Process-2:切換】

在看Linux內核的時候發現,CPU自己認得(或者說is expecting)很多struct,很多時候內核要做的事情是在內存裏準備好這些struct裏CPU需要的數據,以供CPU完成相應的任務。比如尋址中的paging部分,內核只需要把page directory中的數據準備好,並把page directory的地址放入cr3,CPU自己就能根據page directory中的數據進行尋址。就像一種契約,CPU對struct的期望,正是內核所要做的事情,反過來說,內核要做的事情僅僅是滿足CPU的期望而已。

 

不知讀者是否與我有同感,但對於我而言,這使得寫操作系統突然變得遠遠不如想象中那麼困難了。因爲困難的地方在底層,在硬件。這正是學編程的世界, 沒學之前,你永遠覺得編程是不可能的事情——如果剛剛學會了C的語法,你會覺得,C裏頭把數據在內存裏移來移去, 加加減減,明明是隻能讓小孩子玩過家家的東西,怎麼就可以在屏幕上畫畫?讓機器做事?後來意識到了好多好多的庫,原來自己只需要調用API就好了,那 API的那一邊又是怎麼實現的呢?終於知道API裏面是怎麼實現的了,卻發現這些實現永遠也只是在調用另外一層API,只不過更爲底層的API。往地裏越鑽越深,穿越一層又一層的API,才發現最終不過是在爲硬件的期望準備內存中的數據。當然這樣的描述忽略了同時在底層我們也發出了彙編指令讓機器去做一些除了操作內存加加減減的事情,但硬件纔是生命自身,它的電路決定了它如何理會指令、中斷和各種事件,如何突然不執行我們(比如,當前用戶進程)給它的下一 個指令,突然知道利用內存中的數據去進行上下文轉換,如此等等。

 

其實上面這番話也可以反過來說。每當我們的知識前進一步,學的更深了,回頭望去,我們承學的東西,不過是一層API,一層界面罷了。

 

一點感想,下面進入正題,這次的筆記是講述Process的切換:

 

TSS

先介紹一下對80x86的hardware context switch很重要的TSS結構。

 

Task State Segment

 

A task gate descriptor provides an indirect, protected reference to a Task State Segment.

The Task State Segment is a special x86 structure which holds information about a task. It is used by the operating system kernel for task management. Specifically, the following information is stored in the TSS:

    * Processor register state
    * I/O Port permissions
    * Inner level stack pointers
    * Previous TSS link
All this information should be stored at specific locations within the TSS as specified in the IA-32 manuals.

 

在Linux低版本中,進程切換僅僅需要far jmp到要切換的進程的TSS的selector所在就可以了。(far jmp除了修改eip還修改cs)。

 

在Linux 2.6當中,TSS保存在每CPU一個的GDT(其地址存在gdtr中)中,雖然每個process的TSS不同,但Linux 2.6卻不利用其中的 hardware context switch以一個far jmp來實現任務轉換,而用一系列的mov指令來實現。這樣做的原因是:

1、可以檢驗ds和es的值,以防惡意的forge。
2、硬轉換和軟轉換所用時間相近,而且硬轉換是無法再優化的,軟轉換則可以。

 

Linux 2.6對TSS的使用僅限於:

 

1、User Mode向Kernel Mode切換的時候,從TSS中獲取Kernel Stack。

2、User Mode使用in或者out指令的時候,用TSS中的 I/O port permission bitmap驗證權限.

 

有一點要注意,process switching是發生在Kernel Mode,在轉爲Kernel Mode的時候,用戶進程使用的通用register已經保存在Kernel Stack上了。然而非通用的register,如esp,由於不能放在TSS中,所以是放在task_t中的一個類型爲thread_struct的 thread字段中。

 

process切換兩部分:切換paging這裏不講,切換kernel stack、hardware context是由switch_to宏完成的。

 

switch_to宏中的last

switch_to宏的任務就是讓一個process停下來,然後讓另外一個process運行起來。

 

switch_to(prev, next, last)。prev、next分別是切換前後的process的process descriptor(task_t)的地址。last的存在要解釋一下:

 

由於switch_to中造成了進程的切換,所以其中前半部分指令在prev的語境(context、Kernel Stack)中執行,後半部分卻在next的語境中執行。

 

假設B曾切換爲O,那麼由於一切換,B就停下來了,所以在B的感覺保持是next爲O,prev爲B。當我們要從A切換到B的時候,一切換B就醒了,但它卻仍然以爲next是O,prev是B,就不認識A了。然而A switch_to B中的後半部分卻需要B知道A。

 

因此這個宏通常都是這麼用的:switch_to(X, Y, X)。

 

switch_to詳解

 

書上認爲直接看pseudo的彙編代碼比較好,我卻覺得直接看Linux源代碼中的inline彙編代碼更爲自在(爲了閱讀方便和語法高亮有效,卻掉了原代碼中宏定義的換行,想查看原來的代碼,請訪問http://lxr.linux.no/linux+v2.6.11/include/asm-i386/system.h#L15 ):

 

#define switch_to(prev,next,last)
do {                               
      unsigned long esi,edi;                                         
      asm volatile("pushfl\n\t"                                      
                   "pushl %%ebp\n\t"                                 
                   "movl %%esp,%0\n\t"        /* save ESP */         
                   "movl %5,%%esp\n\t"        /* restore ESP */      
                   "movl $1f,%1\n\t"          /* save EIP */         
                   "pushl %6\n\t"             /* restore EIP */      
                   "jmp __switch_to\n"                               
                   "1:\t"                                            
                   "popl %%ebp\n\t"                                  
                   "popfl"                                           
                   :"=m" (prev->thread.esp),"=m" (prev->thread.eip), 
                    "=a" (last),"=S" (esi),"=D" (edi)                
                   :"m" (next->thread.esp),"m" (next->thread.eip),   
                    "2" (prev), "d" (next));                         
} while (0)
 

 

  簡單解說一下這裏用到的gcc的inline彙編語法。首先看上去像是彙編代碼的自然就是彙編代碼了,每個指令寫到一對""中(這是換行接着寫同一個 string的好辦法)還要加\n\t實在是比較麻煩但還算清晰可讀。如果熟悉AT&T的彙編語法,讀起來不是難事。

 

第一個冒號後面有很多類似於"=m" (prev->thread.esp)的東東以逗號相隔,這些是這段彙編所輸出的操作數,=表達了這個意思。其中m代表內存中的變量,a代 表%eax,S代表%esi,D代表%edi。但"=m" (prev->thread.esp)和"=a"(last)是完全不同的輸出方向,前者在movl %%esp,%0一句中(%0代表了prev->thread.esp)把%esp的內容輸出給了prev->thread.esp,後者則 獨立成句,直接在整段彙編的最後自動將last的值寫到%eax,完成了last的使命。

 

第二個冒號後面的則是輸入給這段彙編的操作數。其中d代表%edx。2代表了prev的值將與%2(也就是"=a"(last))共用一個寄存器。

 

這些操作數在彙編中以%n(n是數字)的形式引用,輸出和輸入站在一個隊裏報數:輸出的第一個是%0,順次遞增,到了"m" (next->thread.esp)就排到了%5,依此類推。

 

本來還應該有一個冒號,用來告訴編譯器會被破壞的寄存器(因爲笨笨的C編譯器認爲只有他自己在改寄存器,常常自作主張作出假設進行優化)。這裏中途 在jmp __switch_to我們的確破壞過%eax,但我們巧妙地改回來了(看下面),我們也破壞了%ebp和eflags,但我們通過一對push和pop 卻也恢復了它們。因此我們不需要告訴編譯器我們改過,因爲我們改回來了。

 

asm後面的volatile是告訴C編譯器不要隨便以優化爲理由改變其中代碼的執行順序。

 

還有一個地方需要解釋,那就是$1f,這個指的是標號爲1的代碼的起始地址。在"1:\t"這一行我們定義了這個標號。

如果對gcc的inline彙編產生了興趣,參見:http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html#s5

 

下面開始詳細分析:

 

/* 首先,我們在prev的語境中執行 */


/* 保存ebp和eflags於prev的Kernel Stack上 */

pushfl
pushl %ebp

%esp => prev->thread.esp   /*保存了prev的esp */
next->thread.esp => %esp   /*讀出next之前保存的esp。這個時候,由於esp被改成了next的Kernel Stack,而標示process的thread_info挨着esp(參見筆記process-1中的對current的解釋),我們現在實際變成了是在進程next的語境中執行了。不過我們還沒有真正開始執行next自己的代碼,且看下面 */

1 => prev->thread.eip /*把標號爲1的代碼的地址存入prev->thread.eip中,以備將來恢復。如果有人不知道,說明一下:CPU的eip寄存器中放的是CPU要執行的下一行代碼的地址 */

/* 正是下面這兩句的巧妙配合使得這兩句執行完後,CPU完完全全跑去執行next代碼,不再執行後面的代碼。這也正是原書沒有講清楚的(過於分散了),各位讀者注意咯!*/

pushl next->thread.eip /* 把原先保存下來的next的下一條指令地址,push到next的Kernel stack頂部。這個next->thread.eip通常儲存的是next被切換之前push進stack的那個標號爲1的代碼地址(簡稱:next的1),但如果next從未被切換過,即是一個剛被fork了、新開始執行的進程,那麼存在next->thread.eip中的就是 ret_from_fork()函數的起始地址。 */

jmp __switch_to /* __switch_to是一個用寄存器來傳達參數的函數,裏面執行了檢查、保存FPU、保存debug寄存器等瑣事。重點是:__switch_to是一個函數!這裏居然用的是jmp而不是call!這正是巧妙之處。__switch_to()作爲一個函數執行完了之後會返回(ret),但由於我們不是call它的(call 會自動把下一條指令的地址push入stack頂部,相應地返回的時候ret會從stack的頂部獲取返回地址——下一條指令的地址,這是一種完美的配合),ret就把上一句push入stack頂部的next->thread.eip當作下一條指令了,於是我們就自然而然地順着next之前執行的地址執行下去了,直到下一次process切換回來。 */

/* .......下面的代碼不會繼續執行......直到進程切換回來然後跳到prev的1 */

1:

popl %ebp
popfl

/* 到這裏這個宏就結束了,所以就會順着執行prev的接下來的代碼。這也正是爲什麼我們之前把prev的1的地址push進stack就可以達到回到prev自己的代碼的原因。 */
 

這篇筆記不會解釋__switch內部瑣屑的細節了,因爲最神奇的事情不是發生在裏面,人生苦短,不用去琢磨過於瑣屑的事情。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章