天天看點

linux核心棧與使用者棧【轉】

最近linux核心的中斷部分,總是被書裡的棧弄暈,一會兒核心棧,一會兒使用者棧的……很是崩潰,在網上google了一下 找了一篇不錯的文章拿來分享。

本節内容概要描述了Linux核心從開機引導到系統正常運作過程中對堆棧的使用方式。這部分内容的說明與核心代碼關系比較密切,可以先跳過。在開始閱讀相應代碼時再回來仔細研究。

Linux 0.12系統中共使用了4種堆棧。第1種是系統引導初始化時臨時使用的堆棧;第2種是進入保護模式之後提供核心程式初始化使用的堆棧,位于核心代碼位址空間固定位置處。該堆棧也是後來任務0使用的使用者态堆棧;第3種是每個任務通過系統調用,執行核心程式時使用的堆棧,我們稱之為任務的核心态堆棧。每個任務都有自己獨立的核心态堆棧;第4種是任務在使用者态執行的堆棧,位于任務(程序)邏輯位址空間近末端處。

使用多個棧或在不同情況下使用不同棧的主要原因有兩個。首先是由于從實模式進入保護模式,使得CPU對記憶體尋址通路方式發生了變化,是以需要重新調整設定棧區域。另外,為了解決不同CPU特權級共享使用堆棧帶來的保護問題,執行0級的核心代碼和執行3級的使用者代碼需要使用不同的棧。當一個任務進入核心态運作時,就會使用其TSS段中給出的特權級0的堆棧指針tss.ss0、tss.esp0,即核心棧。原使用者棧指針會被儲存在核心棧中。而當從核心态傳回使用者态時,就會恢複使用使用者态的堆棧。下面分别對它們進行說明。

(1)開機初始化時(bootsect.S,setup.s)

當bootsect代碼被ROM BIOS引導加載到實體記憶體0x7c00處時,并沒有設定堆棧段,當然程式也沒有使用堆棧。直到bootsect被移動到0x9000:0處時,才把堆棧段寄存器SS設定為0x9000,堆棧指針esp寄存器設定為0xff00,即堆棧頂端在0x9000:0xff00處,參見boot/bootsect.s第61、62行。setup.s程式中也沿用了bootsect中設定的堆棧段。這就是系統初始化時臨時使用的堆棧。

(2)進入保護模式時(head.s)

從head.s程式起,系統開始正式在保護模式下運作。此時堆棧段被設定為核心資料段(0x10),堆棧指針esp設定成指向user_stack數組的頂端(參見head.s,第31行),保留了1頁記憶體(4KB)作為堆棧使用。user_stack數組定義在sched.c的67~72行,共含有1024個長字。它在實體記憶體中的位置示意圖可參見圖5-23。此時該堆棧是核心程式自己使用的堆棧。其中給出的位址是大約值,它們與編譯時的實際設定參數有關。這些位址位置是從編譯核心時生成的system.map檔案中查到的。

圖5-23  剛進入保護模式時核心使用的堆棧示意圖

(3)初始化時(main.c)

在init/main.c程式中,在執行move_to_user_mode()代碼把控制權移交給任務0之前,系統一直使用上述堆棧。而在執行過move_to_user_mode()之後,main.c的代碼被“切換”成任務0中執行。通過執行fork()系統調用,main.c中的init()将在任務1中執行,并使用任務1的堆棧。而main()本身則在被“切換”成為任務0後,仍然繼續使用上述核心程式自己的堆棧作為任務0的使用者态堆棧。關于任務0所使用堆棧的較長的描述見後面說明。

每個任務都有兩個堆棧,分别用于使用者态和核心态程式的執行,并且分别稱為使用者态堆棧和核心态堆棧。除了處于不同CPU特權級中,這兩個堆棧之間的主要差別在于任務的核心态堆棧很小,所儲存的資料量最多不能超過4096 – 任務資料結構塊個位元組,大約為3KB。而任務的使用者态堆棧卻可以在使用者的64MB空間内延伸。

(1)在使用者态運作時

每個任務(除了任務0和任務1)有自己的64MB位址空間。當一個任務(程序)剛被建立時,它的使用者态堆棧指針被設定在其位址空間的靠近末端(64MB頂端)部分。實際上末端部分還要包括執行程式的參數和環境變量,然後才是使用者堆棧空間,如圖5-24所示。應用程式在使用者态下運作時就一直使用這個堆棧。堆棧實際使用的實體記憶體則由CPU分頁機制确定。由于Linux實作了寫時複制功能(Copy on Write),是以在程序被建立後,若該程序及其父程序都沒有使用堆棧,則兩者共享同一堆棧對應的實體記憶體頁面。隻有當其中一個程序執行堆棧寫操作(如push操作)時核心記憶體管理程式才會為寫操作程序配置設定新的記憶體頁面。而程序0和程序1的使用者堆棧比較特殊,見後面說明。

圖5-24  邏輯空間中的使用者态堆棧

(2)在核心态運作時

每個任務都有自己的核心态堆棧,用于任務在核心代碼中執行期間。其所線上性位址中的位置由該任務TSS段中ss0和esp0兩個字段指定。ss0是任務核心态堆棧的段選擇符,esp0是堆棧棧底指針。是以每當任務從使用者代碼轉移進入核心代碼中執行時,任務的核心态棧總是空的。任務核心态堆棧被設定在位于其任務資料結構所在頁面的末端,即與任務的任務資料結構(task_struct)放在同一頁面内。這是在建立新任務時,fork()程式在任務tss段的核心級堆棧字段(tss.esp0和tss.ss0)中設定的,參見kernel/fork.c,92行:

    p->tss.esp0 = PAGE_SIZE + (long)p;

    p->tss.ss0 = 0x10;

其中,p是新任務的任務資料結構指針,tss是任務狀态段結構。核心為新任務申請記憶體用作儲存其task_struct結構資料,而tss結構(段)是task_struct中的一個字段。該任務的核心堆棧段值tss.ss0也被設定成為0x10(即核心資料段選擇符),而tss.esp0則指向儲存task_struct結構頁面的末端。如圖5-25所示。實際上tss.esp0被設定成指向該頁面(外)上一位元組處(圖中堆棧底處)。這是因為Intel CPU執行堆棧操作時是先遞減堆棧指針esp值,然後在esp指針處儲存入棧内容。

圖5-25  程序的核心态堆棧示意圖

為什麼從主記憶體區申請得來的用于儲存任務資料結構的一頁記憶體也能被設定成核心資料段中的資料呢,即tss.ss0為什麼能被設定成0x10呢?這是因為使用者核心态棧仍然屬于核心資料空間。我們可以從核心代碼段的長度範圍來說明。在head.s程式的末端,分别設定了核心代碼段和資料段的描述符,段長度都被設定成了16MB。這個長度值是Linux 0.12核心所能支援的最大實體記憶體長度(參見head.s,110行開始的注釋)。是以,核心代碼可以尋址到整個實體記憶體範圍中的任何位置,當然也包括主記憶體區。每當任務執行核心程式而需要使用其核心棧時,CPU就會利用TSS結構把它的核心态堆棧設定成由tss.ss0和tss.esp0這兩個值構成。在任務切換時,老任務的核心棧指針esp0不會被儲存。對CPU來講,這兩個值是隻讀的。是以每當一個任務進入核心态執行時,其核心态堆棧總是空的。

(3)任務0和任務1的堆棧

任務0(空閑程序idle)和任務1(初始化程序init)的堆棧比較特殊,需要特别予以說明。任務0和任務1的代碼段和資料段相同,限長也都是640KB,但它們被映射到不同的線性位址範圍中。任務0的段基位址從線性位址0開始,而任務1的段基位址從64MB開始。但是它們全都映射到實體位址0~640KB範圍中。這個位址範圍也就是核心代碼和基本資料所存放的地方。在執行了move_to_user_mode()之後,任務0和任務1的核心态堆棧分别位于各自任務資料結構所在頁面的末端,而任務0的使用者态堆棧就是前面進入保護模式後所使用的堆棧,即sched.c的user_stack[]數組的位置。由于任務1在建立時複制了任務0的使用者堆棧,是以剛開始時任務0和任務1共享使用同一個使用者堆棧空間。但是當任務1開始運作時,由于任務1映射到user_stack[]處的頁表項被設定成隻讀,使得任務1在執行堆棧操作時将會引起寫頁面異常,進而核心會使用寫時複制機制(關于寫時複制技術的說明請參見第13章)為任務1另行配置設定主記憶體區頁面作為堆棧空間使用。隻有到此時,任務1才開始使用自己獨立的使用者堆棧記憶體頁面。是以任務0的堆棧需要在任務1實際開始使用之前保持“幹淨”,即任務0此時不能使用堆棧,以確定複制的堆棧頁面中不含有任務0的資料。

任務0的核心态堆棧是在其人工設定的初始化任務資料結構中指定的,而它的使用者态堆棧是在執行move_to_user_mode()時,在模拟iret傳回之前的堆棧中設定的,參見圖5-22所示。我們知道,當進行特權級會發生變化的控制權轉移時,目的代碼會使用新特權級的堆棧,而原特權級代碼堆棧指針将保留在新堆棧中。是以這裡先把任務0使用者堆棧指針壓入目前處于特權級0的堆棧中,同時把代碼指針也壓入堆棧,然後執行IRET指令即可實作把控制權從特權級0的代碼轉移到特權級3的任務0代碼中。在這個人工設定内容的堆棧中,原esp值被設定成仍然是user_stack中原來的位置值,而原ss段選擇符被設定成0x17,即設定成使用者态局部表LDT中的資料段選擇符。然後把任務0代碼段選擇符0x0f壓入堆棧作為棧中原CS段的選擇符,把下一條指令的指針作為原EIP壓入堆棧。這樣,通過執行IRET指令即可“傳回”到任務0的代碼中繼續執行了。

在Linux 0.12系統中,所有中斷服務程式都屬于核心代碼。如果一個中斷産生時任務正在使用者代碼中執行,那麼該中斷就會引起CPU特權級從3級到0級的變化,此時CPU就會進行使用者态堆棧到核心态堆棧的切換操作。CPU會從目前任務的任務狀态段TSS中取得新堆棧的段選擇符和偏移值。因為中斷服務程式在核心中,屬于0級特權級代碼,是以48位的核心态堆棧指針會從TSS的ss0和esp0字段中獲得。在定位了新堆棧(核心态堆棧)之後,CPU就會首先把原使用者态堆棧指針ss和esp壓入核心态堆棧,随後把标志寄存器eflags的内容和傳回位置cs、eip壓入核心态堆棧。

核心的系統調用是一個軟體中斷,是以任務調用系統調用時就會進入核心并執行核心中的中斷服務代碼。此時核心代碼就會使用該任務的核心态堆棧進行操作。同樣,當進入核心程式時,由于特權級别發生了改變(從使用者态轉到核心态),使用者态堆棧的堆棧段和堆棧指針以及eflags會被儲存在任務的核心态堆棧中。而在執行iret退出核心程式傳回到使用者程式時,将恢複使用者态的堆棧和eflags。這個過程如圖5-26所示。

圖5-26  核心态和使用者态堆棧的切換

如果一個任務正在核心态中運作,那麼若CPU響應中斷就不再需要進行堆棧切換操作,因為此時該任務運作的核心代碼已經在使用核心态堆棧,并且不涉及優先級别的變化,是以CPU僅把eflags和中斷傳回指針cs、eip壓入目前核心态堆棧,然後執行中斷服務過程。

【新浪微網誌】 張昺華--sky

【twitter】 @sky2030_

【facebook】 張昺華 zhangbinghua

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.

繼續閱讀