Linux內存地址的分段、分頁機制(上)

在深入學習Linux內核源代碼之前,需要先對Linux運行的硬件基礎有個大概的認識,主要包括CPU中的寄存器和磁盤。
1.i386寄存器和系統指令

在Linux系統中使用的主要包括i386寄存器中的16位標誌寄存器,4個內存管理寄存器和4個控制寄存器及調試寄存器。
(1) 標誌寄存器
8086CPU中一種特殊的寄存器,用來存儲CPU的狀態或者指令執行後的結果,控制CPU的工作方式。
(2) 內存管理寄存器
處理器提供了4個內存管理寄存器(GDTR, IDTR, LDTR and TR),用於指定內存分段管理所用系統表的基地址,處理器爲致謝寄存器的加載和保護提供了特定指令。
(3) 控制寄存器 
控制寄存器(CR0~CR3)用於控制和確定處理器的操作模式以及黨慶執行任務的特性。
(4) 調試寄存器
Intel 80386以上的CPU提供了調試寄存器,用來調試軟件。386和486包括留個32位寄存器:Dr0~Dr3, Dr6, Dr7 .

2.總線 Bus

(1) 數據總線:是計算機中各個組成部件間進行數據傳輸時的公共通道;“內數據總線寬度”是指CPU芯片內部數據傳送的寬度;“外數據總線寬度”是指CPU與外部交換數據時的數據寬度。顯然,數據總線位數越多,數據交換的速度就越快。
(2) 地址總線:是載對存儲器或I/O端口進行訪問時,傳送由CPU提供的要訪問的存儲單元或I/O端口的地址信息的總線,其寬度決定了處理器能直接訪問的主存容量的大小。

3.三級存儲器組織結構

現在的微型計算機系統採用下圖的三級存儲器組織結構,即緩衝存儲器Cache、主存、和外存。高速緩衝存儲器Cache的使用,大大減少了CPU讀取指令和操作數所需的時間,使CPU的執行速度顯著提高。

在80X86CPU的發展過程中,存儲器的管理機制發生了較大的變化。8086/8088CPU對存儲器的管理採用分段的實方式;80286CPU除了可在實方式下工作,還可以在保護模式下工作;而80386CPU之後的處理器則具有三種工作方式:實方式、保護方式和虛擬8086方式。

三種工作方式的轉換如圖:
三級存儲器組織結構

三級存儲器結構系統

OK,上面簡單的介紹了Linux運行的硬件基礎,這些不是重點,重點是後面要說的分段、分頁機制。我也是邊學邊記,不足之處請不吝賜教~

1.問題來了,分段到時是怎麼回事?有何用?

實模式的誕生(16位處理器及尋址

在8086處理器誕生之前,內存尋址方式就是直接訪問物理地址。8086處理器爲了尋址1M的內存空間,把地址總線擴展到了20位。但是,一個尷尬的問題出現了,ALU(算術邏輯單元)的寬度只有16位,也就是說,ALU不能計算20位的地址。爲了解決這個問題,就引入了分段機制。

爲了支持分段,8086處理器設置了四個段寄存器:CS, DS, SS, ES. 每個段寄存器都是16位的,同時訪問內存的指令中的地址也是16位的。但是,在送入地址總線之前,CPU先把它與某個段寄存器內的值相加。這裏要注意:段寄存器的值對應於20位地址總線的中的高16位,所以相加時實際上是內存總線中的高12位與段寄存器中的16位相加,而低4位保留不變,這樣就形成一個20位的實際地址,也就實現了從16位內存地址到20位實際地址的轉換,或者叫“映射”。

保護模式的誕生(32位處理器及尋址)

* 80286處理器的地址總線爲24位,尋址空間達16M,同時引入了保護模式(內存段的訪問受到限制)
* 80386處理器是一個32位處理器,ALU和地址總線都是32位的,尋址空間達 4G。也就是說它可以不通過分段機制,直接訪問4G的內存空間。雖然它是新時代的小王子,超越它的無數前輩,然而,它需要揹負家族的使命–兼容前代的處理器。也就是說,它必須支持實模式和保護模式。所以,80386在段寄存器的基礎上構築保護模式,並且保留16位的段寄存器。
* 從80386之後的處理器,架構基本相似,統稱爲IA32(32 Bit Intel Architecture)。

                                    從80386之後的處理器統稱爲IA32

2.IA32的內存尋址機制

尋址硬件

在 8086 的實模式下,把某一段寄存器左移4位,然後與地址ADDR相加後被直接送到內存總線上,這個相加後的地址就是內存單元的物理地址,而程序中的這個地址就叫邏輯地址(或叫虛地址)。在IA32的保護模式下,這個邏輯地址不是被直接送到內存總線而是被送到內存管理單元(MMU)。MMU由一個或一組芯片組成,其功能是把邏輯地址映射爲物理地址,即進行地址轉換,如圖所示

MMU

IA32的三種地址

* 邏輯地址:
機器語言指令仍用這種地址指定一個操作數的地址或一條指令的地址。 這種尋址方式在Intel的分段結構中表現得尤爲具體,它使得MS-DOS或Windows程序員把程序分爲若干段。每個邏輯地址都由一個段和偏移量組成。
* 線性地址:
線性地址是一個32位的無符號整數,可以表達高達232(4GB)的地址。通常用16進製表示線性地址,其取值範圍爲0x00000000~0xFFFFFFFF.
* 物理地址:
也就是內存單元的實際地址,用於芯片級內存單元尋址。 物理地址也由32位無符號整數表示。

MMU地址轉化過程

MMU是一種硬件電路,它包含兩個部件,一個是分段部件,一個是分頁部件,在此,我們把它們分別叫做分段機制和分頁機制,以利於從邏輯的角度來理解硬件的實現機制。分段機制把一個邏輯地址轉換爲線性地址;接着,分頁機制把一個線性地址轉換爲物理地址。

MMU_translater

IA32的段寄存器

IA32中有六個16位段寄存器:CS, DS, SS, ES,FS, GS.跟8086的段寄存器不同的是,這些寄存器存放的不再是某個段的基地址,而是某個段的選擇符(Selector)

分段機制的實現

段是虛擬地址空間的基本單位,分段機制必須把虛擬地址空間的一個地址轉換爲線性地址空間的一個線性地址。

爲了實現這種映射,僅僅用段寄存器來確定一個基地址是不夠的,還得描述段的長度,段的屬性等。這就是段描述符:

段的基地址(Base Address):在線性地址空間中段的起始地址。
段的界限(Limit):在虛擬地址空間中,段內可以使用的最大偏移量。
段的保護屬性(Attribute):表示段的特性。例如,該段是否可被讀出或寫入,或者該段是否作爲一個程序來執行,以及段的特權級等等。

多個段描述符組成的表稱爲段描述符表。

段描述符表

各種各樣的用戶描述符和系統描述符,都放在對應的全局描述符表、局部描述符表和中斷描述符表中。描述符表(即段表)定義了IA32系統的所有段的情況。所有的描述符表本身都佔據一個字節爲8的倍數的存儲器空間,空間大小在8個字節(至少含一個描述符)到64K字節(至多含8K)個描述符之間。

1.全局描述符表(GDT)
全局描述符表GDT(Global Descriptor Table),除了任務門,中斷門和陷阱門描述符外,包含着系統中所有任務都共用的那些段的描述符。 它的第一個8字節位置沒有使用。

2.中斷描述符表IDT(Interrupt Descriptor Table)
中斷描述符表IDT(Interrupt Descriptor Table),包含256個門描述符。IDT中只能包含任務門、中斷門和陷阱門描述符,雖然IDT表最長也可以爲64K字節,但只能存取2K字節以內的描述符,即256個描述符,這個數字是爲了和8086保持兼容。

3.局部描述符表(LDT)
局部描述符表LDT(local Descriptor Table),包含了與一個給定任務有關的描述符,每個任務各自有一個的LDT。 有了LDT,就可以使給定任務的代碼、 數據與別的任務相隔離。每一個任務的局部描述符表LDT本身也用一個描述符來表示,稱爲LDT描述符,它包含了有關局部描述符表的信息,被放在全局描述符表GDT中。

總結

IA32的內存尋址機制完成從邏輯地址–線性地址–物理地址的轉換。其中,邏輯地址的段寄存器中的值提供段描述符,然後從段描述符中得到段基址和段界限,然後加上邏輯地址的偏移量,就得到了線性地址,線性地址通過分頁機制得到物理地址。
首先,我們要明確,分段機制是IA32提供的尋址方式,這是硬件層面的。就是說,不管你是windows還是linux,只要使用IA32的CPU訪問內存,都要經過MMU的轉換流程才能得到物理地址,也就是說必須經過邏輯地址–線性地址–物理地址的轉換。

Linux中分段的實現

前面說了那麼多關於分段機制的實現,其實,對於Linux來說並沒有什麼卵用。因爲Linux基本不使用分段機制,或者說,Linux中的分段機制只是爲了兼容IA32的硬件而設計的。

Intel微處理器的段機制是從8086開始提出的, 那時引入的段機制解決了從CPU內部16位地址到20位實地址的轉換。爲了保持這種兼容性,386仍然使用段機制,但比以前複雜得多。因此,Linux內核的設計並沒有全部採用Intel所提供的段方案,僅僅有限度地使用了一下分段機制。這不僅簡化了Linux內核的設計,而且爲把Linux移植到其他平臺創造了條件,因爲很多RISC處理器並不支持段機制。但是,對段機制相關知識的瞭解是進入Linux內核的必經之路。

從Linux2.2開始,Linux讓所有的進程(或叫任務)都使用相同的邏輯地址空間,因此就沒有必要使用局部描述符表LDT。但內核中也用到LDT,那只是在VM86模式中運行Wine,因爲就是說在Linux上模擬運行Winodws軟件或DOS軟件的程序時才使用。

在 IA32 上任意給出的地址都是一個虛擬地址,即任意一個地址都是通過“選擇符:偏移量”的方式給出的,這是段機制存訪問模式的基本特點。所以在IA32上設計操作系統時無法迴避使用段機制。一個虛擬地址最終會通過“段基地址+偏移量”的方式轉化爲一個線性地址。 但是,由於絕大多數硬件平臺都不支持段機制,只支持分頁機制,所以爲了讓 Linux 具有更好的可移性,我們需要去掉段機制而只使用分頁機制。但不幸的是,IA32規定段機制是不可禁止的,因此不可能繞過它直接給出線性地址空間的地址。

怎麼辦呢?Linux的設計人員乾脆讓段的基地址爲0,而段的界限爲4GB,這時任意給出一個偏移量,則等式爲“0+偏移量=線性地址”,也就是說“偏移量=線性地址”。另外由於段機制規定“偏移量<4GB”,所以偏移量的範圍爲0H~FFFFFFFFH,這恰好是線性地址空間範圍,也就是說虛擬地址直接映射到了線性地址,我們以後所提到的虛擬地址和線性地址指的也就是同一地址。看來,Linux在沒有迴避段機制的情況下巧妙地把段機制給繞過去了。

另外,由於IA32段機制還規定,必須爲代碼段和數據段創建不同的段,所以Linux必須爲代碼段和數據段分別創建一個基地址爲0,段界限爲4GB的段描述符。不僅如此,由於Linux內核運行在特權級0,而用戶程序運行在特權級別3,根據IA32段保護機制規定,特權級3的程序是無法訪問特權級爲0的段的,所以Linux必須爲內核用戶程序分別創建其代碼段和數據段。這就意味着Linux必須創建4個段描述符——特權級0的代碼段和數據段,特權級3的代碼段和數據段。

總結

分段機制是IA32架構CPU的特色,並不是操作系統尋址方式的必然選擇。Linux爲了跨平臺,巧妙的繞開段機制,主要使用分頁機制來尋址。
參考資料

<<Linux內核源碼註釋>>
<<深入分析Linux內核源碼>>
以及"benpaobagzb"的博客
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章