天天看點

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

每天十五分鐘,熟讀一個技術點,水滴石穿,一切隻為渴望更優秀的你!

————零聲學院

記憶體位址

在任何一台計算機上,都存在一個程式能産生的記憶體位址的集合。當程式執行這樣一條

指令時:

MOVE REG,ADDR

它把位址為 ADDR(假設為 10000)的記憶體單元的内容複制到 REG 中,位址 ADDR 可以通

過索引、基址寄存器、段寄存器和其他方式産生。

在 8086 的實模式下,把某一段寄存器左移 4 位,然後與位址 ADDR 相加後被直接送到内

存總線上,這個相加後的位址就是記憶體單元的實體位址,而程式中的這個位址就叫邏輯位址

(或叫虛位址)。在 80386 的保護模式下,這個邏輯位址不是被直接送到記憶體總線,而是被送

到記憶體管理單元(MMU)。MMU 由一個或一組晶片組成,其功能是把邏輯位址映射為實體位址,

即進行位址轉換,如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

當使用 80386 時,我們必須區分以下 3 種不同的位址。

1.邏輯位址

機器語言指令仍用這種位址指定一個操作數的位址或一條指令的位址。這種尋址方式在

Intel 的分段結構中表現得尤為具體,它使得 MS-DOS 或 Windows 程式員把程式分為若幹段。

每個邏輯位址都由一個段和偏移量組成。

2.線性位址

線性位址是一個 32 位的無符号整數,可以表達高達 232(4GB)的位址。通常用 16 進制

表示線性位址,其取值範圍為 0x00000000~0xffffffff。

3.實體位址

實體位址是記憶體單元的實際位址,用于晶片級記憶體單元尋址。實體位址也由 32 位無符

号整數表示。

從圖可以看出,MMU 是一種硬體電路,它包含兩個部件,一個是分段部件,一個是

分頁部件,在本書中,我們把它們分别叫做分段機制和分頁機制,以利于從邏輯的角度來理

解硬體的實作機制。分段機制把一個邏輯位址轉換為線性位址;接着,分頁機制把一個線性

位址轉換為實體位址,如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

段機制和描述符

1 段機制

在 80386 的段機制中,邏輯位址由兩部分組成,即段部分(選擇符)及偏移部分。

段是形成邏輯位址到線性位址轉換的基礎。如果我們把段看成一個對象的話,那麼對它

的描述如下。

(1)段的基位址(Base Address):線上性位址空間中段的起始位址。

(2)段的界限(Limit):表示在邏輯位址中,段内可以使用的最大偏移量。

(3)段的屬性(Attribute): 表示段的特性。例如,該段是否可被讀出或寫入,或者

該段是否作為一個程式來執行,以及段的特權級等。

段的界限定義邏輯位址空間中段的大小。段内在偏移量從 0 到 limit 範圍内的邏輯位址,

對應于從 Base 到 Base+Limit 範圍内的線性位址。在一個段内,偏移量大于段界限的邏輯地

址将沒有意義,使用這樣的邏輯位址,系統将産生異常。另外,如果要對一個段進行通路,

系統會根據段的屬性檢查通路者是否具有通路權限,如果沒有,則産生異常。例如,在 80386

中,如果要在隻讀段中進行寫入,80386 将根據該段的屬性檢測到這是一種違規操作,則産

生異常。

圖表示一個段如何從邏輯位址空間,重新定位到線性位址空間。圖的左側表示邏輯

位址空間,定義了 A、B 及 C 三個段,段容量分别為 LimitA、LimitB及 LimitC。圖中虛線把邏

輯位址空間中的段 A、B 及 C 與線性位址空間區域連接配接起來表示了這種轉換。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

段的基位址、界限及保護屬性,存儲在段的描述符表中,在邏輯—線性位址轉換過程中

要對描述符進行通路。段描述符又存儲在存儲器的段描述符表中,該描述符表是段描述符的

一個數組,關于這些内容,我們将在後面詳細介紹。

2 描述符的概念

所謂描述符(Descriptor),就是描述段的屬性的一個 8 位元組存儲單元。在實模式下,

段的屬性不外乎是代碼段、堆棧段、資料段、段的起始位址、段的長度等,而在保護模式下

則複雜一些。80386 将它們結合在一起用一個 8 位元組的數表示,稱為描述符。80386 的一個通

用的段描述符的結構如圖 2.10 所示。

從圖可以看出,一個段描述符指出了段的 32 位基位址和 20 位段界限(即段長)。

第 6 個位元組的 G 位是粒度位,當 G=0 時,段長表示段格式的位元組長度,即一個段最長可

達 1M 位元組。當 G=1 時,段長表示段的以 4K 位元組為一頁的頁的數目,即一個段最長可達

1M×4K=4G 位元組。D 位表示預設操作數的大小,如果 D=0,操作數為 16 位,如果 D=1,操作數

為 32 位。第 6 個位元組的其餘兩位為 0,這是為了與将來的處理器相容而必須設定為 0 的位。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

第 5 個位元組是存取權位元組,它的一般格式如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

第 7 位 P 位(Present) 是存在位,表示段描述符描述的這個段是否在記憶體中,如果在

記憶體中。P=1;如果不在記憶體中,P=0。

DPL(Descriptor Privilege Level),就是描述符特權級,它占兩位,其值為 0~3,

用來确定這個段的特權級即保護等級。

S 位(System)表示這個段是系統段還是使用者段。如果 S=0,則為系統段,如果 S=1,則

為使用者程式的代碼段、資料段或堆棧段。系統段與使用者段有很大的不同,後面會具體介紹。

類型占 3 位,第 3 位為 E 位,表示段是否可執行。當 E=0 時,為資料段描述符,這時的

第 2 位 ED 表示擴充方向。當 ED=0 時,為向位址增大的方向擴充,這時存取資料段中的資料

的偏移量必須小于或等于段界限,當 ED=1 時,表示向位址減少的方向擴充,這時偏移量必須

大于界限。當表示資料段時,第 1 位(W)是可寫位,當 W=0 時,資料段不能寫,W=1 時,數

據段可寫入。在 80386 中,堆棧段也被看成資料段,因為它本質上就是特殊的資料段。當描

述堆棧段時,ED=0,W=1,即堆棧段朝位址增大的方向擴充。

也就是說,當段為資料段時,存取權位元組的格式如圖所示。

當段為代碼段時,第 3 位 E=1,這時第 2 位為一緻位(C)。當 C=1 時,如果目前特權級

低于描述符特權級,并且目前特權級保持不變,那麼代碼段隻能執行。所謂目前特權級

(Current Privilege Level),就是目前正在執行的任務的特權級。第 1 位為可讀位 R,當

R=0 時,代碼段不能讀,當 R=1 時可讀。也就是說,當段為代碼段時,存取權位元組的格式如

圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)
深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

存取權位元組的第 0 位 A 位是通路位,用于請求分段不分頁的系統中,每當該段被通路時,

将 A 置 1。對于分頁系統,則 A 被忽略未用。

3 系統段描述符

以上介紹了使用者段描述符。系統段描述符的一般格式如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

可以看出,系統段描述符的第 5 個位元組的第 4 位為 0,說明它是系統段描述符,類型占

4 位,沒有 A 位。第 6 個位元組的第 6 位為 0,說明系統段的長度是位元組粒度,是以,一個系統

段的最大長度為 1M 位元組。

系統段的類型為 16 種,如圖 2.15 所示。

在這 16 種類型中,保留類型和有關 286 的類型不予考慮。

門也是一種描述符,有調用門、任務門、中斷門和陷阱門 4 種門描述符。有關門描述符

的内容将在第四章中進行具體讨論。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

4 描述符表

各種各樣的使用者描述符和系統描述符,都放在對應的全局描述符表、局部描述符表和中

斷描述符表中。

描述符表(即段表)定義了 386 系統的所有段的情況。所有的描述符表本身都占據一個

位元組為 8 的倍數的存儲器空間,空間大小在 8 個位元組(至少含一個描述符)到 64K 位元組(至

多含 8K)個描述符之間。

1.全局描述符表(GDT)

全局描述符表 GDT(Global Descriptor Table),除了任務門,中斷門和陷阱門描述符

外,包含着系統中所有任務都共用的那些段的描述符。它的第一個 8 位元組位置沒有使用。

2.中斷描述符表(IDT)

中斷描述符表 IDT(Interrupt Descriptor Table),包含 256 個門描述符。IDT 中隻

能包含任務門、中斷門和陷阱門描述符,雖然 IDT 表最長也可以為 64K 位元組,但隻能存取 2K

位元組以内的描述符,即 256 個描述符,這個數字是為了和 8086 保持相容。

3.局部描述符表(LDT)

局部描述符表 LDT(Local Descriptor Table),包含了與一個給定任務有關的描述符,

每個任務各自有一個的 LDT。有了 LDT,就可以使給定任務的代碼、資料與别的任務相隔離。

每一個任務的局部描述符表 LDT 本身也用一個描述符來表示,稱為 LDT 描述符,它包含

了有關局部描述符表的資訊,被放在全局描述符表 GDT 中。

5 選擇符與描述符表寄存器

在實模式下,段寄存器存儲的是真實的段位址,在保護模式下,16 位的段寄存器無法放

下 32 位的段位址,是以,它們被稱為選擇符,即段寄存器的作用是用來選擇描述符。選擇符

的結構如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

可以看出,選擇符有 3 個域:第 15~3 位這 13 位是索引域,表示的資料為 0~8129,用于

指向全局描述符表中相應的描述符。第 2 位為選擇域,如果 TI=1,就從局部描述符表中選擇

相應的描述符,如果 TI=0,就從全局描述符表中選擇描述符。第 1、0 位是特權級,表示選

擇符的特權級,被稱為請求者特權級 RPL(Requestor Privilege Level)。隻有請求者特權

級 RPL 高于(數字低于)或等于相應的描述符特權級 DPL,描述符才能被存取,這就可以實

現一定程度的保護。

我們知道,實模式下是直接在段寄存器中放置段基位址,現在則是通過它來存取相應的

描述符來獲得段基位址和其他資訊,這樣以來,存取速度會不會變慢呢?為了解決這個問題,

386 的每一個段選擇符都有一個程式員不可見(也就是說程式員不能直接操縱)的 88 位寬的

段描述符高速緩沖寄存器與之對應。無論什麼時候改變了段寄存器的内容,隻要特權級合理,

描述符表中的相應的 8 位元組描述符就會自動從描述符表中取出來,裝入高速緩沖寄存器中(還

有 24 位其他内容)。一旦裝入,以後對那個段的通路就都使用高速緩沖寄存器的描述符資訊,

而不會再重新從表中去取,這就大大加快了執行的時間,如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

由于段描述符高速緩沖寄存器的内容隻有在重新設定選擇符時才被重新裝入,是以,當

你修改了選擇符所選擇的描述符後,必須對相應的選擇符重新裝入,這樣,88 位描述符高速

緩沖寄存器的内容才會發生變化。無論如何,當選擇符的值改變時,處理器自動裝載不可見

部分。

下面講一下在沒有分頁操作時,尋址一個存儲器操作數的步驟。

(1)在段選擇符中裝入 16 位數,同時給出 32 位位址偏移量(比如在 ESI、EDI 中等)。

(2)根據段選擇符中的索引值、TI 及 RPL 值,再根據相應描述符表寄存器中的段位址和

段界限,進行一系列合法性檢查(如特權級檢查、界限檢查),該段無問題,就取出相應的

描述符放入段描述符高速緩沖寄存器中。

(4)将描述符中的 32 位段基位址和放在 ESI、EDI 等中的 32 位有效位址相加,就形成

了 32 位實體位址。

注意:在保護模式下,32 位段基位址不必向左移 4 位,而是直接和偏移量相加形成 32

位實體位址(隻要不溢出)。這樣做的好處是:段不必再定位在被 16 整除的位址上,也不必

左移 4 位再相加。

尋址過程如圖所示。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

6 描述符投影寄存器

為了避免在每次存儲器通路時,都要通路描述符表,讀出描述符并對段進行譯碼以得到

描述符本身的各種資訊,每個段寄存器都有與之相聯系的描述符投影寄存器。在這些寄存器

中,容納有由段寄存器中的選擇符确定的段的描述符資訊。段寄存器對程式設計人員是可見的,

而與之相聯系的容納描述符的寄存器,則對程式設計人員是不可見的,故稱之為投影寄存器。圖

中所示的是 6 個寄存器及其投影寄存器。用實線畫出的寄存器是段寄存器,用以表示這

些寄存器對程式設計人員可見;用虛線畫出的寄存器是投影寄存器,表示對程式設計人員不可見。

投影寄存器容納有相應段寄存器尋址的段的基位址、界限及屬性。每當用選擇符裝入段

寄存器時,CPU 硬體便自動地把描述符的全部内容裝入對應的投影寄存器。是以,在多次訪

問同一段時,就可以用投影寄存器中的基位址來通路存儲器。投影寄存器存儲在 80386 的芯

片上,因而可以由段基址硬體進行快速通路。因為多數指令通路的資料是在其選擇符已經裝

入到段寄存器之後進行的,是以使用投影寄存器可以得到很好的執行性能。

7 Linux 中的段

Intel 微處理器的段機制是從 8086 開始提出的, 那時引入的段機制解決了從 CPU 内部

16 位位址到 20 位實位址的轉換。為了保持這種相容性,386 仍然使用段機制,但比以前複雜

得多。是以,Linux 核心的設計并沒有全部采用 Intel 所提供的段方案,僅僅有限度地使用

了一下分段機制。這不僅簡化了 Linux 核心的設計,而且為把 Linux 移植到其他平台創造了

條件,因為很多 RISC 處理器并不支援段機制。但是,對段機制相關知識的了解是進入 Linux

核心的必經之路。

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

從 2.2 版開始,Linux 讓所有的程序(或叫任務)都使用相同的邏輯位址空間,是以就

沒有必要使用局部描述符表 LDT。但核心中也用到 LDT,那隻是在 VM86 模式中運作 Wine 時,

即在 Linux 上模拟運作 Windows 軟體或 DOS 軟體的程式時才使用。

Linux 在啟動的過程中設定了段寄存器的值和全局描述符表 GDT 的内容,段的定義在

include/asm-i386/segment.h 中:

#define __KERNEL_CS0x10 /*核心代碼段,index=2,TI=0,RPL=0*/

#define __KERNEL_DS0x18 /*核心資料段, index=3,TI=0,RPL=0*/

#define __USER_CS 0x23 /*使用者代碼段, index=4,TI=0,RPL=3*/

#define __USER_DS 0x2B /*使用者資料段, index=5,TI=0,RPL=3*/

從定義看出,沒有定義堆棧段,實際上,Linux 核心不區分資料段和堆棧段,這也展現

了 Linux 核心盡量減少段的使用。因為沒有使用 LDT,是以,TI=0,并把這 4 個段都放在 GDT

中, index 就是某個段在 GDT 表中的下标。核心代碼段和資料段具有最高特權,是以其 RPL

為 0,而使用者代碼段和資料段具有最低特權,是以其 RPL 為 3。可以看出,Linux 核心再次簡

化了特權級的使用,使用了兩個特權級而不是 4 個。

全局描述符表的定義在 arch/i386/kernel/head.S 中:

ENTRY(gdt_table)

.quad 0x0000000000000000

從代碼可以看出,GDT 放在數組變量 gdt_table 中。按 Intel 規定,GDT 中的第一項為

空,這是為了防止加電後段寄存器未經初始化就進入保護模式而使用 GDT 的。第二項也沒用。

從下标 2~5 共 4 項對應于前面的 4 種段描述符值。對照圖 2.10,從描述符的數值可以得出:

• 段的基位址全部為 0x00000000;

• 段的上限全部為 0xffff;

• 段的粒度 G 為 1,即段長機關為 4KB;

• 段的 D 位為 1,即對這 4 個段的通路都為 32 位指令;

• 段的 P 位為 1,即 4 個段都在記憶體。

由此可以得出,每個段的邏輯位址空間範圍為 0~4GB。讀者可能對此不太了解,但隻要

對照圖 2.9 就可以發現,這種設定既簡單又巧妙。因為每個段的基位址為 0,是以,邏輯地

址到線性位址映射保持不變,也就是說,偏移量就是線性位址,我們以後所提到的邏輯位址

(或虛拟位址)和線性位址指的也就是同一位址。看來,Linux 巧妙地把段機制給繞過去了,

而完全利用了分頁機制。

從邏輯上說,Linux 巧妙地繞過了邏輯位址到線性位址的映射,但實質上還得應付 Intel

所提供的段機制。隻不過,Linux 把段機制變得相當簡單,它隻把段分為兩種:使用者态(RPL

=3)的段和核心态(RPL=0)的段,是以,描述符投影寄存器的内容很少發生變化,隻在進

程從使用者态切換到核心态或者反之時才發生變化。另外,使用者段和核心段的差別也僅僅在其

RPL 不同,是以核心根本無需通路描述符投影寄存器,當然也無需通路 GDT,而僅從段寄存器

的最低兩位就可以擷取 RPL 的資訊。Linux 這樣設計所帶來的好處是顯而易見的,Intel 的分

段部件對 Linux 性能造成的影響可以忽略不計。

在上面描述的 GDT 表中,緊接着那 4 個段描述的兩個描述符被保留,然後是 4 個進階電

源管理(APM)特征描述符,對此不進行詳細讨論。

按 Intel 的規定,每個程序有一個任務狀态段(TSS)和局部描述符表 LDT,但 Linux 也

沒有完全遵循 Intel 的設計思路。如前所述,Linux 的程序沒有使用 LDT,而對 TSS 的使用也

非常有限,每個 CPU 僅使用一個 TSS。

通過上面的介紹可以看出,Intel 的設計可謂周全細緻,但 Linux 的設計者并沒有完全

陷入這種沼澤,而是選擇了簡潔而有效的途徑,以完成所需功能并達到較好的性能為目标。

每日分享15分鐘技術摘要選讀,關注一波,一起保持學習動力!

深入分析Linux核心源代碼-Linux 運作的硬體基礎(下)

繼續閱讀