Linux從頭學11:理解了這三個概念,才能徹底理解任務管理和任務切換

IOT物聯網小鎮發表於2021-09-09

作 者:道哥,10+年的嵌入式開發老兵。

公眾號:【IOT物聯網小鎮】,專注於:C/C++、Linux作業系統、應用程式設計、物聯網、微控制器和嵌入式開發等領域。 公眾號回覆【書籍】,獲取 Linux、嵌入式領域經典書籍。

轉 載:歡迎轉載文章,轉載需註明出處。

x86 系統中的保護模式,給系統的安全性提供了很大的保障,但是在我們之前的文章中,一直都淡化了特權級別這個概念。

例如:在保護模式下的段選擇器,我們一直都只把它看做一個段描述符的"索引號",用來在 GDT (全域性描述描述符表) 中查詢一個段描述符,例如:

圖中:程式碼段暫存器中的索引號是 4 GDT 中每一個表項佔用 4 個位元組,於是就在偏移量16 的位置,找到了程式碼段的描述符,進而從描述符中找到程式碼段的起始地址和長度界限

資料段、棧段的操作過程也是這樣的。

從現在開始,我們需要讓使用者程式擁有自己私有的描述符表 LDT(Local Descriptor Table),並且擁有自己的特權級別(總不能讓使用者程式與作業系統一樣,工作在非常高的 0 特權級別)。

因此,我們需要糾正之前的錯誤:段暫存器中,不僅僅有段的索引號,還有另外兩個屬性:TI 和 RPL,如下圖所示:

  1. TI 標誌位:表示到哪個表中(GDT or LDT)查詢描述符;

TI = 0: 到 GDT 中查詢描述符;
TI = 1: 到 LDT 中查詢描述符;

  1. RPL(Request Privilege Level) 標誌位:表示想給段暫存器賦值的請求者(也就是一段程式碼),它的特權級別;

此時,繼續把段暫存器中的內容稱作段索引符就不合適了,一般稱作:選擇子

LDT:區域性描述符表

在上一篇文章中,作業系統把應用程式從硬碟讀取到記憶體中之後,為應用程式建立了三個段描述符,這三個段描述符都放在了 GDT 表中,這是不合理的。

首先,在多工系統中,應用程式的數量是不確定的,應用程式也會執行結束。

如果把所有應用程式的段描述符都放在 GDT 中,對於作業系統來說,管理這個資料太複雜。

其次,當引入特權級別之後,如果應用程式的段描述符放在 GDT 中,那麼就意味著應用程式需要有許可權來訪問 GDT,而 x86 系統中只有一個 GDT(所以叫做 Global Description Table),只能被作業系統訪問

因此,作業系統需要為每一個應用程式,單獨申請一塊空間,用作這個程式自己的段描述附表,稱作:LDT(Local Description Table)。

例如:現在系統中有 2 個使用者程式: APP1 和 APP2,作業系統在載入每一個應用程式的時候,就會在應用程式自己的記憶體空間中,申請一塊,用作 LDT:

為什麼是 “應用程式自己的記憶體空間”?
因為每一個應用程式,都獨享 4G 大小的虛擬記憶體空間。

LDT 中,存放著當前應用程式自己的段描述符資訊,例如:程式碼段、資料段、棧段。

LDT 所佔用的空間也屬於記憶體的一部分,有起始地址和長度界限,因此也需要為它建立一個段描述符,這個描述符就放在 GDT 中。

在 Linux 應用層,我們會嚴格的區分程式、執行緒,但是在系統的底層,這樣的區分界限已經比較模糊了,用任務 task 來稱呼更通用些。

根據剛才的假設,現在系統中有 2 個使用者程式,那麼處理器怎麼知道:當前正在執行的是哪一個應用程式的 LDT 中的程式碼?

正如處理器中有一個暫存器 GDTR,儲存著 GDT 的開始地址和長度,處理器中還有一個暫存器 LDTR,儲存著當前正在執行的那個應用程式的 LDT 開始地址和長度

所有應用程式的虛擬記憶體的高階地址部分,對映的都是作業系統的記憶體空間,按照 Linux 中的做法,3G ~ 4G 空間被作業系統使用。

圖中的綠色部分,表示作業系統空間(1G),在分頁機制下,它們都對映到相同的實體記憶體頁上(藍色虛線箭頭)。

當作業系統切換到應用程式2時,處理器中的 LDTR 就會被賦值為應用程式2LDT 的線性地址和長度資訊。

GDTR 中的內容不變,因為每個應用程式中的 GDT 都是從作業系統“繼承”而來的,開始地址和長度都是一樣的。

TSS: 任務狀態段

顧名思義,任務狀態段就是用來儲存和恢復任務的狀態資訊

經常聽到一個術語:任務上下文

所謂的上下文,就是體現一個任務正在被執行時的環境資訊,主要就是處理器中的各種暫存器內容,也就是下面這張圖中的暫存器們:

這張圖反映了一個任務上下文的所有暫存器資訊。

當任務被排程器中止執行之前,需要把這些暫存器中的值都儲存下來,相當於做一個快照

當這個任務以後又被恢復執行時,再把這個快照中儲存的資訊,原樣的賦值給圖中的所有暫存器,這樣就稱作恢復任務上下文,這個任務就從上次被中止的地方繼續執行(因為指令指標暫存器 EIP 被恢復了)。

就如同 LDT 一樣,TSS 也是作業系統為應用程式分配的一塊記憶體空間,只不過這塊空間是位於作業系統的勢力範圍內,只能由作業系統來操作。

TSS 也有起始地址和長度界限,也需要為它在 GDT 中建立一個段描述符。

LDT 類似,在處理器中也有一個暫存器 TR,用來指向當前正在執行的那個任務的 TSS

當進行任務切換的時候:

  1. 首先,把處理器中的暫存器內容,儲存到 TR 暫存器指向的 TSS 段中(即將被停止的任務);

  2. 然後,把新的任務的 TSS 段中的內容,複製到處理器的各暫存器中,並且把 TSS 地址賦值給 TR 暫存器;

TCB: 任務控制塊

任務控制塊,可以說是系統中用來管理任務的最重要的資料結構了,作業系統用來管理任務的所有資訊都可以放在這裡。

看一下 Linux 2.6 核心程式碼中的結構體:struct task_struct{ ... },就知道 TCB 有多複雜了,有些書籍上也稱之為 PCB(Process Control Block,程式控制塊)

在這個結構中,一些常用的資訊包括:

  1. 程式的載入地址;

  2. 任務的優先順序;

  3. 任務的當前狀態;

  4. 任務開啟的一些資源:網路、檔案裝置等待;

。。。

需要注意的是:上面的 LDT、TSS,是 x86 處理器中設計的執行機制,是處理器要求這樣的

TCB 不是處理器要求的,它是作業系統的實現者自己來構建的,因此可以根據自己的需要來進行設計。

每一個應用程式需要一個 TCP 結構,所有的 TCB 結構就可以構成一個連結串列,便於作業系統來管理。

比如:在發生任務切換的時候,就可以順著連結串列頭,一次掃描連結串列上的每一個 TCB 節點。

如果找到了當前正在被執行(即將被中止)的任務,就把這個任務的狀態標記為暫停,並移動到連結串列的末尾,然後把連結串列頭部的第一個處於 ready 狀態的任務,載入到處理器中去執行。

當然,Linux 系統中的處理過程更為複雜,它把每一個任務按照優先順序放在不同的等待佇列中,然後利用哈系桶演算法來查詢任務。


------ End ------

x86 處理器中的這三個概念,對於理解任務切換非常重要。

寫到這裡,我總是覺得以上的文字描述還是有點朦朦朧朧,也許是自己還需要進一步的理解其中的脈絡。

就先這樣吧,以後想到更好的描述方式了再與大家分享,謝謝!

推薦閱讀

【1】C語言指標-從底層原理到花式技巧,用圖文和程式碼幫你講解透徹
【2】一步步分析-如何用C實現物件導向程式設計
【3】原來gdb的底層除錯原理這麼簡單
【4】內聯彙編很可怕嗎?看完這篇文章,終結它!

其他系列專輯精選文章C語言Linux作業系統應用程式設計物聯網

星標公眾號,能更快找到我!

相關文章