1. 程式人生 > >分段分頁機制

分段分頁機制

意義:分頁機制是為了充分利用空間,將瑣碎的地址空間利用起來;

   分段機制是為了解決衝突問題,它是一種機制,這種機制使得很方便地管理記憶體;

 

1. 記憶體分段

 

 

1.1 為什麼分段?

 

 

在x86-16體系中,為了解決16位暫存器對20位地址線的定址問題,引入了分段式記憶體管理。而CPU則使用CS,DS,ES,SS等暫存器來儲存程式的段首地址。當CPU執行指令需要訪問記憶體時,只會送出段內的偏移地址,而通過指令的型別類確定訪問那一個段暫存器。具體可以參考:計算機原理學習(5)-- x86-16 CPU和記憶體管理

 

到了IA-32,Intel引入了保護模式,所以在IA-32中為了保持相容性,所以同樣支援記憶體分段管理。另外我們討論過了記憶體分頁,頁面中包含了程式的程式碼,資料等資訊,它們都有各自的地址。這些地址是在編譯的時候就確定的,因為每個程序都有獨立完整的記憶體空間,只需要把頁和物理頁對映就能執行,所以這個地址是可以在編譯時就決定的。在編譯時,編譯器會等程式進行語法詞法等分析,在編譯過程中會建立許多的表,來確定程式碼和變數的虛擬地址:

  • 被儲存起來供列印清單的源程式正文;
  • 符號表,包含變數的名字和屬性;
  • 包含所有用到的整形和浮點型資料的表;
  • 語法分析樹,包括程式語法分析的結果;
  • 編譯器內部過程呼叫的堆疊。

前面4張表會隨著編譯的進行不斷增大,而堆疊的資料也會變化,現在的問題就是,每一張表的大小都不確定,那麼如何指定每一張表在虛擬記憶體空間的地址呢?

如上圖,沒一張表都有自己的起始地址,但是當變數很多的時候,符號表需要的空間可能會超過程式正文的起始地址,這個時候就會把源程式的表的地址覆蓋掉。當然編譯器沒有這麼傻,它可以提示無法繼續編譯,當然這樣並不合適,另一個辦法就是拿出一部分沒有使用的空間給符號表。造成這個問題的原因就是分頁系統中的虛擬地址是一維的,所以在編譯過程中必須給變數,程式碼分配虛擬地址。這個有點類似沒有采用分頁之前,程序之間使用實體地址導致相互覆蓋的問題。

所以我們可以為不同的表分配自己的空間地址,也就是分段,這樣他們地址都是相對地址,全部編譯完成後確定了每張表的大小,就可以計算出實際的虛擬地址了。

 

 

 

1.2 分段的作用

 

 

分頁實際是一個純粹邏輯上的概念,因為實際的程式和記憶體並沒有被真正的分為了不同的頁面。而分段則不同,他是一個邏輯實體。一個段中可以是變數,原始碼或者堆疊。一般來說每個段中不會包含不同型別的內容。而分段主要有以下幾個作用:

  1. 解決編譯問題: 前面提到過在編譯時地址覆蓋的問題,可以通過分段來解決,從而簡化編譯程式。
  2. 重新編譯: 因為不同型別的資料在不同的段中,但其中一個段進行修改後,就不需要所有的段都重新進行編譯。
  3. 記憶體共享: 對記憶體分段,可以很容易把其中的程式碼段或資料段共享給其他程式,分頁中因為資料程式碼混合在一個頁面中,所以不便於共享。
  4. 安全性: 將記憶體分為不同的段之後,因為不同段的內容型別不同,所以他們能進行的操作也不同,比如程式碼段的內容被載入後就不應該允許寫的操作,因為這樣會改變程式的行為。而在分頁系統中,因為一個頁不是一個邏輯實體,程式碼和資料可能混合在一起,無法進行安全上的控制。
  5. 動態連結: 動態連結是指在作業執行之前,並不把幾個目標程式段連結起來。要執行時,先將主程式所對應的目標程式裝入記憶體並啟動執行,當執行過程中又需要呼叫某段時,才將該段(目標程式)調入記憶體並進行連結。可見,動態連結也要求以段作為管理的單位。
  6. 保持相容性

所以在現在的x86的體系結構中分段記憶體管理是必選的,而分頁管理則是可選的。

 

 

 

1.3 與x86-16分段管理的區別

 

 

Intel對分段記憶體管理邏輯地址的定義是【段號+段內地址】。在x86-16體系的分段管理中,CPU給出的記憶體地址是16位的段內偏移地址,段的基址從段暫存器中獲得,最後計算出24位的實體地址。 而在IA-32體系中引入了保護模式,每個程序有4G的獨立地址空間,CPU直接給出的是32位的段內偏移地址,段的基址從記憶體中獲得,最後計算出32為的實體地址。他們最大的不同就在於獲取基址的方式以及計算方法。

 

 

IA-32為了保持向前相容,保留了CS/DS/ES/SS這4個暫存器,但因為不在從段暫存器中獲得段價值,這4個段暫存器實際上已經失去了原本的作用(但不代表沒有使用)。IA-32在記憶體中使用一張段表來記錄各個段對映的實體記憶體地址(如下圖)。

在譯地的過程中,x86-16是通過16位的段基址和16位的段內偏移不是簡單的相加,而是通過 段值*0x10 + 偏移地址 對基址重定向的方式計算得到實體地址,而IA-32中則相對簡單,不需要對基址重定向,這一點和前面分頁記憶體管理是相似的。而CPU只需要為這個段表提供一個記錄其首地址的暫存器就可以了。 同樣也可以使用TLB來加速。

與x86-16中分段管理另一個不同是,在IA-32中,因為有了獨立的地址空間,對多程式也支援的非常好。而分段可以很好的支援程序間資料的共享。

 

 

 

 

2. 分段記憶體管理

 

 

2.1 段選擇器

 

在IA-32中保留的CS/DS/ES/SS這4個16位段暫存器不再被解釋為段的基地址,Intel為了保持相容性將這些暫存器的16個位分成3個用於不同功能的域,稱為段選擇器

其中3-15是選擇子,存放的是段描述符的索引(可以理解為段號),該描述符為64bit用於描述儲存器段的位置、長度和訪問許可權。而段描述符可以分為兩種,全域性描述符(GDT)區域性描述符(LDT),對應著第2位,而0,1兩位是表示CPU的許可權級別(0-4級)。在IA-32中一共有6個段選擇器

 

  • CS儲存了程式碼段描述符的索引;
  • DS儲存了資料段描述符的索引;
  • SS儲存堆疊段描述符索引;
  • ES、FS、GS則作為一般用途,可以指向任意的資料段,實現自定義定址。

 

 

2.2 段描述符

 

段描述符就是前面說到的段表中的每一個專案的,一個段描述符由8個位元組組成。它描述了段的特徵。前面提到段描述符可以分為GDT和LDT兩類。通常來說系統只定義一個GDT,而每個程序如果需要放置一些自定義的段,就可以放在自己的LDT中。IA-32中引入了GDTR和LDTR兩個暫存器,就是用來存放當前正在使用的GDT和LDT的首地址。

 

上面的圖是Linux中不同段的描述符,結構基本是一致的,只有少數字段有差別。其中最重要的就是BASE欄位,一共32位,儲存的是當前段的首地址。 在Linux系統中,每個CPU對應一個GDT。一個GDT中有18個段描述符和14個未使用或保留項。其中使用者和核心各有一個程式碼段和資料段,然後還包含一個TSS任務段來儲存暫存器的狀態。其他的段則包括區域性執行緒儲存,電源管理,即插即用等多個段。而Linux系統中,大多數使用者態的程式都不使用LDT。

 

 

 

2.3 段地址轉換

 

 

在IA-32中,邏輯地址是16位的段選擇符+32位偏移地址,段暫存器不在儲存段基址,而是儲存段描述符的索引。

 

 

  1. IA-32首選確定要訪問的段(方式x86-16相同),然後決定使用的段暫存器。
  2. 根據段選擇符號的TI欄位決定是訪問GDT還是LDT,他們的首地址則通過GTDR和LDTR來獲得。
  3. 將段選擇符的Index欄位的值*8,然後加上GDT或LDT的首地址,就能得到當前段描述符的地址。(乘以8是因為段描述符為8位元組)
  4. 得到段描述符的地址後,可以通過段描述符中BASE獲得段的首地址。
  5. 將邏輯地址中32位的偏移地址和段首地址相加就可以得到實際要訪問的實體地址。

 

 

2.4 快取段描述符

 

為了加速地址轉換的過程,根據程式的區域性性原理,我們可以講當前段暫存器指向的段描述符快取在特定的暫存器中。這裡為6個段暫存器準備了6個用來快取段描述符的非程式設計暫存器。這樣就能加快地址轉換的過程。而僅當段暫存器內容變化時,才有必要去訪問記憶體中的GDT或LDT。

 

 

 

3. 段頁式記憶體管理

 

 

分段記憶體管理的優勢在於記憶體共享和安全控制,而分頁記憶體管理的優勢在於提高內利用率。他們之間並不是相互對立的競爭關係,而是可以相互補充的。也就是可以把2種方式結合起來,也就是目前計算機中最普遍採用的段頁式記憶體管理。段頁式管理的核心就是對記憶體進行分段,對每個段進行分頁。這樣在擁有了分段的優勢的同時,可以更加合理的使用記憶體的物理頁。

 

 

2.1  段頁式記憶體管理結構

 

對於段頁式管理來說,我們需要通過段表來儲存每一個段的資訊,通過頁表儲存每個段中虛擬頁的資訊。在段頁管理的系統中,CPU給出的不再是分頁系統中的虛擬地址,而是給出的邏輯地址。(前面2篇有介紹邏輯地址和虛擬地址,簡單的說邏輯地址是二維的,而虛擬地址是一維的,平坦的)。

 

 

 

2.2 段頁式地址轉換

 

 

 

上面的圖簡單的描述了在段頁式記憶體管理的系統中,地址轉換的過程。實際上就是我們前面介紹的分段和分頁地址轉換的結合。

  1. CPU給出要訪問的邏輯地址;
  2. 通過分段記憶體管理的地址轉換機制,將邏輯地址轉換為線性地址,也就是分頁系統中的虛擬地址;
  3. 通過分頁記憶體管理的地址轉換機制,將虛擬地址轉換為實體地址;

 

 

 

 4. 總結

 

 

這一篇文章主要介紹了IA-32系統中分段式記憶體管理是如何工作的。而目前主流的系統中都採用了段頁相結合的記憶體管理方式,當然不同的系統具體實現起來是不同的。比如在Linux中,所有段首地址都是從0x0000000開始,所以邏輯地址和轉換得到的線性地址完全是一樣的。到這一篇,有關x86的記憶體管理方式以及介紹晚了。記憶體的分頁和分段也決定了程式的編譯,可執行檔案的結構,程式的記憶體佈局等等。這個將在後面介紹到。