1. 程式人生 > >LINUX 邏輯地址、線性地址、虛擬地址和實體地址

LINUX 邏輯地址、線性地址、虛擬地址和實體地址

1、概念解釋

實體地址:
  用於記憶體晶片級的單元定址,與地址匯流排相對應。這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把實體地址理解成插在機器上那根記憶體本身,把記憶體看成一個從0位元組一直到最大空量逐位元組的編號的大陣列,然後把這個陣列叫做實體地址,但是事實上,這只是一個硬體提供給軟體的抽像,記憶體的定址方式並不是這樣。所以,說它是“與地址匯流排相對應”,是更貼切一些,不過拋開對實體記憶體定址方式的考慮,直接把實體地址與物理的記憶體一一對應,也是可以接受的。也許錯誤的理解更利於形而上的抽像。

虛擬記憶體:
  這是對整個記憶體(不要與機器上插那條對上號)的抽像描述。它是相對於實體記憶體來講的,可以直接理解成“不直實的”,“假的”記憶體,例如,一個0x08000000記憶體地址,它並不對就實體地址上那個大陣列中0x08000000 - 1那個地址元素;有了這樣的抽像,一個程式,就可以使用比真實實體地址大得多的地址空間。(拆東牆,補西牆,銀行也是這樣子做的),甚至多個程序可以使用相同的地址。不奇怪,因為轉換後的實體地址並非相同的。
  ——可以把連線後的程式反編譯看一下,發現聯結器已經為程式分配了一個地址,例如,要呼叫某個函式A,程式碼不是call A,而是call 0x0811111111 ,也就是說,函式A的地址已經被定下來了。沒有這樣的“轉換”,沒有虛擬地址的概念,這樣做是根本行不通的。
打住了,這個問題再說下去,就收不住了。

邏輯地址:
  可以認為是cpu執行程式過程中的一種中間地址。Intel為了相容,將遠古時代的段式記憶體的管理方式保留了下來,至於為什麼會產生段式記憶體的管理方式,參見[2]。一個邏輯地址,是由一個段識別符號加上一個指定段內的相對地址的偏移量,表示為[段識別符號:段內偏移量],也就是說上面例子中的那個0x08111111應該表示為 [A的程式碼的段識別符號:0x08111111] 這樣才完整一些。

線性地址:
  線性地址,也即虛擬地址,如果邏輯地址對應的是硬體平臺段式管理轉換前的地址的話,那麼線性地址則對應了硬體頁式記憶體的轉換前的地址。

2、初步理解

對上面的各種地址的階段性總結如下:
  CPU將一個虛擬地址空間的地址轉換為實體地址,需要進行兩步:首先將給定的邏輯地址,即[段識別符號:段內偏移量]這樣的形式,利用段式管理單元,轉化為線性地址,然後利用頁式記憶體管理單元,轉化為最終的實體地址。圖形表示如下(下圖中的左半部分):
這裡寫圖片描述

3、深層次的理解

更進一步的對於段式記憶體管理和頁式記憶體管理的解釋如下:
 - 段識別符號也即段選擇符,它用來從段描述符表中選擇一個具體的段,某個段描述符表項的base欄位描述了一個段的開始位置的線性地址。
 - gdt是全域性段描述符表,
 - ldt是區域性段描述符表,
 - 段選擇符中的TI = 0表示用GDT,TI=1表示用LDT
 - gdt在記憶體中的地址和大小存放在CPU的gdtr控制暫存器中,而ldt則在ldtr暫存器中。

3.1、段式管理單元

有了上面的這些概念,對於上圖的右半部分的段式管理單元就好理解了:
  程式過來一個邏輯地址,使用其段識別符號(也即段選擇符)的Index欄位去索引段描述符表,若TI=0,索引全域性段描述符表,TI=1,索引區域性段描述符表,表的地址在相應的暫存器中。通過Index欄位和段描述符表的位置能找到某項具體的段描述符。將段描述符中的base欄位和邏輯地址中的offset欄位合併即得到了線性地址。
  按照Intel的本意,全域性的用GDT,每個程序自己的用LDT——不過Linux則對所有的程序都使用了相同的段來對指令和資料定址。即使用者資料段,使用者程式碼段,對應的,核心中的是核心資料段和核心程式碼段。

[1]中有介紹,四個段的基地址全為0。這樣,給定一個段內偏移地址,按照前面轉換公式,0 + 段內偏移,轉換為線性地址,可以得出重要的結論,“在Linux下,邏輯地址與線性地址總是一致(是一致,不是有些人說的相同)的,即邏輯地址的偏移量欄位的值與線性地址的值總是相同的。!!!”所以如果做linux下核心開發,對於上述的x86的段式管理可以完全不用理會,我們可以認為linux根本沒有用intel弄出來的這個段式管理,而是以頁式管理完成了所有的記憶體管理工作。

3.2、頁式管理單元:

  CPU的頁式記憶體管理單元,負責把一個線性地址,最終翻譯為一個實體地址。從管理和效率的角度出發,線性地址被分為以固定長度為單位的組,稱為頁(page),例如一個32位的機器,線性地址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性地址就被劃分為一個tatol_page[2^20]的大陣列,共有2的20個次方個頁。這個大陣列我們稱之為頁目錄。目錄中的每一個目錄項,就是一個地址——對應的頁的地址。
  另一類“頁”,我們稱之為物理頁,或者是頁框、頁楨的。是分頁單元把所有的實體記憶體也劃分為固定長度的管理單位,它的長度一般與記憶體頁是一一對應的。
  這裡注意到,這個total_page陣列有2^20個成員,每個成員是一個地址(32位機,一個地址也就是4位元組),那麼要單單要表示這麼一個數組,就要佔去4MB的記憶體空間。為了節省空間,引入了一個二級管理模式的機器來組織分頁單元。如上圖中的頁式管理單元部分,我們單獨拿出來看:
這裡寫圖片描述
1、分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3暫存器中,是進行地址轉換的開始點。萬里長征就從此長始了。
2、每一個活動的程序,因為都有其獨立的對應的虛似記憶體(頁目錄也是唯一的),那麼它也對應了一個獨立的頁目錄地址。——執行一個程序,需要將它的頁目錄地址放到cr3暫存器中,將別個的儲存下來。
3、每一個32位的線性地址被劃分為三部份,面目錄索引(10位):頁表索引(10位):偏移(12位)
4、依據以下步驟進行轉換:
 ① 從cr3中取出程序的頁目錄地址(作業系統負責在排程程序的時候,把這個地址裝入對應暫存器);
 ② 根據線性地址前十位,在陣列中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的地址,而是一個頁表的地址。(又引入了一個數組),頁的地址被放到頁表中去了。
 ③ 根據線性地址的中間十位,在頁表(也是陣列)中找到頁的起始地址;
 ④ 將頁的起始地址與線性地址中最後12位相加,得到最終我們想要的葫蘆;

疑問解答:
1、我理解的所謂的記憶體對映,不過是將虛擬地址空間 和 可執行檔案建立對映關係,這種對映關係的建立,可能是做一張表,來記錄虛擬地址空間中的地址和可執行檔案在磁碟上的地址的對應關係。這樣當程式執行第一條指令時,會發生缺頁中斷,根據之前建立的對映關係從磁碟中拿到需要的東西搬移到實體記憶體中,並且將實體記憶體的地址和此時的第一條指令的虛擬地址這一對對映關係寫到頁表中。
  該異常是虛擬記憶體機制賴以存在的基本保證——它會告訴核心去真正為程序分配物理頁,並建立對應的頁表,這之後虛擬地址才實實在在地對映到了系統的實體記憶體上。——《Linux記憶體管理(最透徹的一篇)
2、程式的記憶體映像從低地址到高地址依次是:
· txt段
· data段: 已初始化的全域性變數和已初始化的static變數;
· bss段: 未初始化的全域性變數和未初始化的static變數以及初始化為零的全域性變數和靜態變數(參考這裡這裡)(未初始化的全域性變數和static變數,系統自動賦值為零。這個段在編譯成 .exe可執行檔案時,只是標記了一下這個段的大小,並沒有實際的分配全為零的頁框。
  例如:一個程式的txt段的大小是8kB,初始化資料段的大小是8kB,未初始化的資料段(BSS)的大小是4kB,那麼可執行檔案的大小是 16kB(程式碼+初始化的資料)加上一個很短的頭部來告訴系統在初始化的資料後再另外分配4KB,同時在程式啟動之後把他們初始化零為0。這個技巧巧妙的避免了在可執行檔案中中儲存4kB的0.
  更進一步,為了避免分配一個全是0的物理頁框,在初始化的時候,linux就分配了一個靜態零頁面,即一個全為零的防寫頁面。當載入程式的時候,未初始化的資料區域被設定為指向該零頁面。當一個程序真正要寫這個區域的時候,寫時複製機制就開始起作用,一個實際的頁框就被分配給該程序。——《現代作業系統P428》
· 堆: 通常情況下堆也是請求二進位制零的頁面** ,往上生長——《深入理解計算機系統P585, P587》
· 棧: 通常情況下往下生長
· 共享庫的記憶體對映區域:在使用者堆和棧之間存在一個共享庫的記憶體對映區域,比如標準C庫的libc.so,這些物件都是動態連結到這個程式的,然後再對映到使用者虛擬地址空間中的共享區域。

  不管怎麼樣,在程序切換的時候,要將程序的 頁表基地址 裝入到 頁表基址暫存器中,這個頁表基址暫存器在X86架構中是cr3暫存器,在arm架構中是TTB暫存器,詳細見:ARM協處理器CP15
  並且,為了在切換程序時,不完全的重新整理TLB,還需要將程序id記錄到TLB中,只有這樣才能知道TLB中當前的表項是否是是當前程序的(因為此時TLB的定址是使用的虛擬定址,兩個不同的程序可能使用同一個虛擬地址來定址),在X86架構中實現這種功能的一個結構叫做PCID(程序上下文識別符號),在ARM結構中叫做ASID,詳見:什麼是TLB和PCID?為什麼要有PCID?為什麼Linux現在才開始使用它?swapper程序修改頁表項 vs kvm中guest頁表的防寫