CPU通過任務管理的方式提供了基于硬體的多任務,為了實作多任務CPU提供了一下額外的資料結構和寄存器。這篇文章并不打算詳細的介紹任務管理的結構和原理,因為Intel官方文檔關于任務管理的章節描寫的非常清楚,我隻是想通過一個例子來了解整個任務管理的機制。
任務管理結構
多任務就是要實作多個任務之間的并行執行,就會涉及到任務之間的切換,既然要切換就要保證任務在切換回來之後還能夠順利的繼續執行下去,那麼在切換之前就要儲存任務能夠繼續執行下去所必須的堆棧,寄存器等等上下文環境,這個上下文環境叫做任務狀态。Intel官方手冊中已經明确的描述了任務狀态包含那些内容,這裡就不贅述了。為了儲存任務狀态CPU提供了一種叫做TSS(任務狀态段)的資料結構。TSS是與代碼段,資料段等一樣的一塊記憶體區域,為了找到它還要在GDT中儲存一個TSS的描述符,但是與其他的段描述符不同的是TSS描述符隻能在GDT中,而不能夠出現在LDT或者IDT中。與代碼段或者資料段一樣也需要将TSS的段選擇子加載到一個段寄存器中來獲得TSS,這個加載TSS段選擇子的寄存器就叫做TR(任務寄存器),通過ltr指令來加載TSS段選擇子,不過TR寄存器都是在任務切換的過程中由CPU自動的加載的,隻有在初始化代碼的時候需要程式員顯式的調用ltr加載。
多任務環境中各個任務的任務狀态是相對獨立的,但是每個任務的狀态都包很多段,如果把這些段描述符都放在GDT中,GDT會變得很長,很亂。CPU提供了LDT來儲存一個任務自身相關的段,LDT是與GDT相似的結構,就是描述符的表,但是與GDT不同的是,LDT可能有很多,對于目前的任務,并不是所有的LDT都是可見的,隻有LDTR寄存器中儲存的選擇子標明的LDT是可見的。此外LDT沒有NULL descriptor,并且每一個LDT必須都在GDT中有相應的LDT描述符。
CPU還提供了一種任務門描述符的結構,它與TSS描述符不同,任務門描述符儲存的是TSS描述符在GDT中的選擇子,并且任務門描述符可以儲存在GDT,LDT和IDT中。
任務切換
任務切換是一個很複雜的過程,因為切換過程中每一個步驟都要進行相應的檢查,如果違規就會觸發相應的異常。但是切換的流程是挺清晰的:
- 通過指令(ljmp/lcall/iret)獲得新任務的選擇子。
- 将目前的任務狀态儲存到目前的TSS中。
- 讀取新任務的TSS,并從TSS中加載新任務的任務狀态。
- 跳轉到新任務開始之行。
其他步驟都是很簡單的,第三步其實是挺複雜的過程,這裡涉及到了多個資料結構互動,用圖形表示一下最複雜的情況:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0NXYFhGd192UvwVe0lmdhJ3ZvwFM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2LcpHbzMGbahVYw40VZZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39jNwQDO0kTNxIDNygDMzEDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
這裡是最複雜的情況,一個任務涉及到的段都儲存在自己的LDT中:
- 通過任務切換指令獲得TSS selector,利用這個TSS selector擷取GDT中的kernel TSS descriptor。
- 通過kernel TSS descriptor獲得kernel TSS。
- kernel TSS中的LDT selector有效,通過LDT selector獲得GDT中的kernel LDT descriptor。
- 通過kernel LDT descriptor獲得kernel LDT。
- 通過kernel TSS中的CS selector獲得kernel LDT中的CS descriptor。
- 通過CS descriptor獲得kernel code segment。
這裡以kernel任務的碼段為例,對于資料段或堆棧段也是類似的。
示例
關于任務管理的例子代碼比較多,托管在https://github.com/activesys/learning_cpu/tree/master/x86/task這裡僅僅是對于各個檔案的簡單介紹:
- boot.s是啟動扇區代碼,它還負責将setup,中斷處理代碼和資料,kernel代碼和資料以及user代碼和資料從相應的扇區中讀取出來放到指定的記憶體位置。
- common.inc定義了一下常量和宏,主要是對于整個代碼的記憶體布局,以及GDT,LDT,IDT中的各個項的常量的定義。
- setup.s負責安裝GDT,LDT,IDT以及TSS,還負責切換到保護模式并且初始化保護模式後的任務環境。
- kernel.s是ring0級别的代碼和資料。
- user.s是ring3級别的代碼和資料。
- build.sh是編譯腳本,并且生成最終的鏡像檔案。
整個例子代碼設定了三個任務,setup任務,kernel任務和user任務。setup任務是從實模式切換到保護模式後進入的任務并且中斷和異常處理都是屬于這個任務的。kernel任務單純的模仿ring0級别的任務,user任務單純的模仿ring3級别的任務。kernel和user任務都擁有自己的LDT,代碼段和資料段等都放在LDT中,切換到這兩個任務的流程就和上一節介紹的最複雜的流程一緻。 這個例子中采用了動态設定GDT,IDT,LDT以及TSS的方式,通過調用_setup_gdt,_setup_idt,_setup_ldt以及_setup_tss,_setup_tss_segment來動态的更改GDT,IDT,LDT以及TSS中的内容。這些全局結構設定完成之後代碼開啟CR0.PE,然後跳轉到保護模式代碼_setup:
# switch to protected-mode
movl %cr0, %eax
orl $1, %eax
movl %eax, %cr0
# far jmp
ljmp $SETUP_SELECTOR, $_setup
保護模式代碼初始化各個段,然後調用ltr來加載TR寄存器:
###############################################################
# Protected-mode code for setup
.code32
.type _setup, @function
_setup:
xorl %eax, %eax
movw $INT_DATA_SELECTOR, %ax
movw %ax, %ds
movw $INT_STACK_SELECTOR, %ax
movw %ax, %ss
movl $INT_STACK_INIT_ESP, %esp
movw $INT_VIDEO_SELECTOR, %ax
movw %ax, %es
movw $NULL_SELECTOR, %ax
movw %ax, %fs
movw %ax, %gs
movl $SETUP_TSS_SELECTOR, %eax
ltr %ax
ljmp $KERNEL_TSS_SELECTOR, $0x00
最後使用ljmp跳轉到kernel任務,進行任務切換。ljmp後面跟着的選擇是不再是段選擇子而是tss的選擇子。kernel任務的代碼很簡單隻是向螢幕中列印了一些資訊,然後使用lcall切換到了user任務,等user任務切換後來之後再次列印了一些資訊并且進入無限循環:
###############################################################
# code for kernel
.code32
.globl _start
_start:
movl $KERNEL_STACK_INIT_ESP, %esp
movl $KERNEL_MSG_OFFSET, %edi
movl $KERNEL_MSG_LENGTH, %ecx
movl $KERNEL_FIRST_VIDEO_OFFSET, %edx
call _kernel_echo
# jmp to user code.
lcall $USER_TSS_SELECTOR, $0x00
movl $KERNEL_MSG2_OFFSET, %edi
movl $KERNEL_MSG2_LENGTH, %ecx
movl $KERNEL_SECOND_VIDEO_OFFSET, %edx
call _kernel_echo
jmp .
user任務的代碼更是簡單,隻是向螢幕上列印了一些消息,展示了一下我們位于user任務代碼段,然後使用iret指令切換回kernel任務:
###############################################################
# code for user
.code32
.globl _start
_start:
movl $USER_STACK_INIT_ESP, %esp
movl $USER_MSG_OFFSET, %edi
movl $USER_MSG_LENGTH, %ecx
movl $USER_FIRST_VIDEO_OFFSET, %edx
call _user_echo
iret
最終的運作結果:
從螢幕中輸出的消息可以看出,代碼首先進入了ring0級别的kernel任務,然後切換到了ring3級别的user任務,然後又切換回ring0級别的kernel任務。
參考
《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide》 《自己動手寫作業系統》 《x86/x64體系探索與程式設計》