天天看點

邏輯位址、線性位址、實體位址和虛拟位址了解

邏輯位址(Logical Address) 是指由程式産生的和段相關的偏移位址部分。例如,你在進行C語言指針程式設計中,能讀取指針變量本身值(&操作),實際上這個值就是邏輯位址,他是相對于你目前程序資料段的位址,不和絕對實體位址相幹。隻有在Intel實模式下,邏輯位址才和實體位址相等(因為實模式沒有分段或分頁機制,Cpu不進行自動位址轉換);邏輯也就是在Intel保護模式下程式執行代碼段限長内的偏移位址(假定代碼段、資料段如果完全相同)。應用程式員僅需和邏輯位址打交道,而分段和分頁機制對你來說是完全透明的,僅由系統程式設計人員涉及。應用程式員雖然自己能直接操作記憶體,那也隻能在作業系統給你配置設定的記憶體段操作。

線性位址(Linear Address) 是邏輯位址到實體位址變換之間的中間層。程式代碼會産生邏輯位址,或說是段中的偏移位址,加上相應段的基位址就生成了一個線性位址。如果啟用了分頁機制,那麼線性位址能再經變換以産生一個實體位址。若沒有啟用分頁機制,那麼線性位址直接就是實體位址。Intel 80386的線性位址空間容量為4G(2的32次方即32根位址總線尋址)。

實體位址(Physical Address) 是指出目前CPU外部位址總線上的尋址實體記憶體的位址信号,是位址變換的最終結果位址。如果啟用了分頁機制,那麼線性位址會使用頁目錄和頁表中的項變換成實體位址。如果沒有啟用分頁機制,那麼線性位址就直接成為實體位址了。

虛拟記憶體(Virtual Memory)是指計算機呈現出要比實際擁有的記憶體大得多的記憶體量。是以他允許程式員編制并運作比實際系統擁有的記憶體大得多的程式。這使得許多大型項目也能夠在具有有限記憶體資源的系統上實作。一個非常恰當的比喻是:你不必非常長的軌道就能讓一列火車從上海開到北京。你隻需要足夠長的鐵軌(比如說3公裡)就能完成這個任務。采取的方法是把後面的鐵軌即時鋪到火車的前面,隻要你的操作足夠快并能滿足需求,列車就能象在一條完整的軌道上運作。這也就是虛拟記憶體管理需要完成的任務。在Linux0.11核心中,給每個程式(程序)都劃分了總容量為64MB的虛拟記憶體空間。是以程式的邏輯位址範圍是0x0000000到0x4000000。有時我們也把邏輯位址稱為 虛拟位址。因為和虛拟記憶體空間的概念類似,邏輯位址也是和實際實體記憶體容量無關的。邏輯位址和實體位址的“差距”是0xC0000000,是由于虛拟位址->線性位址->實體位址映射正好差這個值。這個值是由作業系統指定的。機理 邏輯位址(或稱為虛拟位址)到線性位址是由CPU的段機制自動轉換的。如果沒有開啟分頁管理,則線性位址就是實體位址。如果開啟了分頁管理,那麼系統程式需要參和線性位址到實體位址的轉換過程。具體是通過設定頁目錄表和頁表項進行的。

一、概念 實體位址(physical address)

用于記憶體晶片級的單元尋址,與處理器和CPU連接配接的位址總線相對應。 ——這個概念應該是這幾個概念中最好了解的一個,但是值得一提的是,雖然可以直接把實體位址了解成插在機器上那根記憶體本身,把記憶體看成一個從0位元組一直到最大空量逐位元組的編号的大數組,然後把這個數組叫做實體位址,但是事實上,這隻是一個硬體提供給軟體的抽像,記憶體的尋址方式并不是這樣。是以,說它是“與位址總線相對應”,是更貼切一些,不過抛開對實體記憶體尋址方式的考慮,直接把實體位址與實體的記憶體一一對應,也是可以接受的。也許錯誤的了解更利于形而上的抽像。 虛拟記憶體(virtual memory) 這是對整個記憶體(不要與機器上插那條對上号)的抽像描述。它是相對于實體記憶體來講的,可以直接了解成“不直實的”,“假的”記憶體,例如,一個0x08000000記憶體位址,它并不對就實體位址上那個大數組中0x08000000 - 1那個位址元素;之是以是這樣,是因為現代作業系統都提供了一種記憶體管理的抽像,即虛拟記憶體(virtual memory)。程序使用虛拟記憶體中的位址,由作業系統協助相關硬體,把它“轉換”成真正的實體位址。這個“轉換”,是所有問題讨論的關鍵。有了這樣的抽像,一個程式,就可以使用比真實實體位址大得多的位址空間。(拆東牆,補西牆,銀行也是這樣子做的),甚至多個程序可以使用相同的位址。不奇怪,因為轉換後的實體位址并非相同的。 ——可以把連接配接後的程式反編譯看一下,發現連接配接器已經為程式配置設定了一個位址,例如,要調用某個函數A,代碼不是call A,而是call 0x0811111111 ,也就是說,函數A的位址已經被定下來了。沒有這樣的“轉換”,沒有虛拟位址的概念,這樣做是根本行不通的。打住了,這個問題再說下去,就收不住了。 邏輯位址(logical address) Intel為了相容,将遠古時代的段式記憶體管理方式保留了下來。邏輯位址指的是機器語言指令中,用來指定一個操作數或者是一條指令的位址。以上例,我們說的連接配接器為A配置設定的0x08111111這個位址就是邏輯位址。 ——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對邏輯位址要求,“一個邏輯位址,是由一個段辨別符加上一個指定段内相對位址的偏移量,表示為 [段辨別符:段内偏移量],也就是說,上例中那個0x08111111,應該表示為[A的代碼段辨別符: 0x08111111],這樣,才完整一些” 線性位址(linear address)或也叫虛拟位址(virtual address) 跟邏輯位址類似,它也是一個不真實的位址,如果邏輯位址是對應的硬體平台段式管理轉換前位址的話,那麼線性位址則對應了硬體頁式記憶體的轉換前位址。

CPU将一個虛拟記憶體空間中的位址(邏輯位址)轉換為實體位址,需要進行兩步:首先将給定一個邏輯位址,CPU要利用其段式記憶體管理單元,先将為個邏輯位址轉換成一個線程位址,再利用其頁式記憶體管理單元,轉換為最終實體位址。 這樣做兩次轉換,的确是非常麻煩而且沒有必要的,因為直接可以把線性位址抽像給程序。之是以這樣備援,Intel完全是為了相容而已。

二、CPU段式記憶體管理

邏輯位址如何轉換為線性位址 一個邏輯位址由兩部份組成,段辨別符: 段内偏移量。段辨別符是由一個16位長的字段組成,稱為段選擇符。其中前13位是一個索引号。後面3位包含一些硬體細節,如圖:

邏輯位址、線性位址、實體位址和虛拟位址了解

最後兩位涉及權限檢查,本貼中不包含。 索引号,或者直接了解成數組下标——那它總要對應一個數組吧,它又是什麼索引呢?這是“段描述符(segment descriptor)”,段描述符具體位址描述了一個段。這樣,很多個段描述符,就組了一個數組,叫“段描述符表”,這樣,可以通過段辨別符的前13位,直接在段描述符表中找到一個具體的段描述符,這個描述符就描述了一個段,由8個位元組組成,如下圖:

邏輯位址、線性位址、實體位址和虛拟位址了解

圖示比較複雜,可以利用一個資料結構來定義它,不過,在此隻關心一樣,就是Base字段,它描述了一個段的開始位置的線性位址。 Intel設計的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每個程序自己的,就放在所謂的“局部段描述符表(LDT)”中。那究竟什麼時候該用GDT,什麼時候該用LDT呢?這是由段選擇符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。 GDT在記憶體中的位址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。 再看這張圖比起來要直覺些:

邏輯位址、線性位址、實體位址和虛拟位址了解

首先,給定一個完整的邏輯位址[段選擇符:段内偏移位址],

1、看段選擇符的T1=0還是1,知道目前要轉換是GDT中的段,還是LDT中的段,再根據相應寄存器,得到其位址和大小。我們就有了一個數組了。

2、拿出段選擇符中前13位,可以在這個數組中,查找到對應的段描述符,這樣,它了Base,即基位址就知道了。

3、把Base + offset,就是要轉換的線性位址了。 還是挺簡單的,對于軟體來講,原則上就需要把硬體轉換所需的資訊準備好,就可以讓硬體來完成這個轉換了。

三、Linux的段式管理

Intel要求兩次轉換,這樣雖說是相容了,但是卻是很備援,硬體要求這樣做了,軟體就隻能照辦,形式主義。另一方面,其它某些硬體平台,沒有二次轉換的概念,Linux也需要提供一個高層抽像,來提供一個統一的界面。按照Intel的本意,全局的用GDT,每個程序自己的用LDT——不過Linux則對所有的程序都使用了相同的段來對指令和資料尋址。即使用者資料段,使用者代碼段,對應的,核心中的是核心資料段和核心代碼段。include/asm-i386/segment.h

CODE:

#define          GDT_ENTRY_DEFAULT_USER_CS       

#define          __USER_CS       (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)

#define          GDT_ENTRY_DEFAULT_USER_DS       

#define          __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)

#define          GDT_ENTRY_KERNEL_BASE       

#define          GDT_ENTRY_KERNEL_CS   (GDT_ENTRY_KERNEL_BASE+ 0)

#define          __KERNEL_CS  (GDT_ENTRY_KERNEL_CS * 8)

#define          GDT_ENTRY_KERNEL_DS   (GDT_ENTRY_KERNEL_BASE+ 1)

#define          __KERNEL_DS  (GDT_ENTRY_KERNEL_DS * 8)

把其中的宏替換成數值,則為:

CODE:

#define __USER_CS                115     [00000000 1110  0  11]

#define __USER_DS                123     [00000000 1111  0  11]

#define __KERNEL_CS           96      [00000000 1100  0  00]

#define __KERNEL_DS           104    [00000000 1101  0  00]

方括号後是這四個段選擇符的16位二制表示,它們的索引号和T1字段值也可以算出來了

CODE:

__USER_CS              index= 14             T1=0

__USER_DS              index= 15             T1=0

__KERNEL_C           index= 12             T1=0

__KERNEL_DS         index= 13             T1=0

T1均為0,則表示都使用了GDT,再來看初始化GDT的内容中相應的12-15項(arch/i386/head.S):

CODE:

.quad 0x00cf9a000000ffff        

.quad 0x00cf92000000ffff        

.quad 0x00cffa000000ffff         

.quad 0x00cff2000000ffff         

按照前面段描述符表中的描述,可以把它們展開,發現其16-31位全為0,即四個段的基位址全為0。 這樣,給定一個段内偏移位址,按照前面轉換公式,0 + 段内偏移,轉換為線性位址,可以得出重要的結論,“在Linux下,邏輯位址與線性位址總是一緻的,即邏輯位址的偏移量字段的值與線性位址的值總是相同的。” Linux中,絕大部份程序并不例用LDT,除非使用Wine ,仿真Windows程式的時候。

四、CPU的頁式記憶體管理

CPU的頁式記憶體管理單元,負責把一個線性位址,轉換為實體位址。從管理和效率的角度出發,線性位址被分為以固定長度為機關的組,稱為頁(page),例如一個32位的機器,線性位址最大可為4G,可以用4KB為一個頁來劃分,這頁,整個線性位址就被劃分為一個tatol_page[2^20]的大數組,共有2的20個次方個頁。這個大數組我們稱之為頁目錄。目錄中的每一個目錄項,就是一個位址——對應的頁的位址。 另一類“頁”,我們稱之為實體頁,或者是頁框(frame)、頁桢的。是分頁單元把所有的實體記憶體也劃分為固定長度的管理機關,它的長度一般與記憶體頁是一一對應的。 這裡注意到,這個total_page數組有2^20個成員,每個成員是一個位址(32位機,一個位址也就是4位元組),那麼要單單要表示這麼一個數組,就要占去4MB的記憶體空間。為了節省空間,引入了一個二級管理模式的機器來組織分頁單元。文字描述太累,看圖直覺一些:

邏輯位址、線性位址、實體位址和虛拟位址了解

如上圖,

      分頁單元中,頁目錄是唯一的,它的位址放在CPU的cr3寄存器中,是進行位址轉換的開始點。

      每一個活動的程序,因為都有其獨立的對應的虛似記憶體(頁目錄也是唯一的),那麼它也對應了一個獨立的頁目錄位址。——運作一個程序,需要将它的頁目錄位址放到cr3寄存器中,将别個的儲存下來。

      每一個32位的線性位址被劃分為三部份,面目錄索引(10位):頁表索引(10位):偏移(12位) 依據以下步驟進行轉換:

(1)     從cr3中取出程序的頁目錄位址(作業系統負責在排程程序的時候,把這個位址裝入對應寄存器);

(2)     根據線性位址前十位,在數組中,找到對應的索引項,因為引入了二級管理模式,頁目錄中的項,不再是頁的位址,而是一個頁表的位址。(又引入了一個數組),頁的位址被放到頁表中去了。

(3)     根據線性位址的中間十位,在頁表(也是數組)中找到頁的起始位址;

(4)     将頁的起始位址與線性位址中最後12位相加,得到最終我們想要的葫蘆;這個轉換過程,應該說還是非常簡單地。

全部由硬體完成,雖然多了一道手續,但是節約了大量的記憶體,還是值得的。那麼再簡單地驗證一下:

1、這樣的二級模式是否仍能夠表示4G的位址;頁目錄共有:2^10項,也就是說有這麼多個頁表每個目表對應了:2^10頁;每個頁中可尋址:2^12個位元組。還是2^32 = 4GB

2、這樣的二級模式是否真的節約了空間;也就是算一下頁目錄項和頁表項共占空間 (2^10 * 4 + 2 ^10 *4) = 8KB。值得一提的是,雖然頁目錄和頁表中的項,都是4個位元組,32位,但是它們都隻用高20位,低12位屏蔽為0,因為這樣,它剛好和一個頁面大小對應起來,大家都成整數增加。計算起來就友善多了。但是,為什麼同時也要把頁目錄低12位屏蔽掉呢?因為按同樣的道理,隻要屏蔽其低10位就可以了,不過我想,因為12>10,這樣,可以讓頁目錄和頁表使用相同的資料結構,友善。

五、Linux的頁式記憶體管理

原理上來講,Linux隻需要為每個程序配置設定好所需資料結構,放到記憶體中,然後在排程程序的時候,切換寄存器cr3,剩下的就交給硬體來完成了(事實上要複雜得多,在此隻分析最基本的流程)。前面說了i386的二級頁管理架構,不過有些CPU,還有三級,甚至四級架構,Linux為了在更高層次提供抽像,為每個CPU提供統一的界面。提供了一個四層頁管理架構,來相容這些二級、三級、四級管理架構的CPU。這四級分别為: 頁全局目錄PGD、頁上級目錄PUD、頁中間目錄PMD、頁表PT。 整個轉換依據硬體轉換原理,隻是多了二次數組的索引罷了,如下圖:

邏輯位址、線性位址、實體位址和虛拟位址了解

那麼,對于使用二級管理架構32位的硬體,四級轉換怎麼能夠協調地工作呢?嗯,來看這種情況下,怎麼來劃分線性位址吧!從硬體的角度,32位位址被分成了三部份;從軟體的角度,由于多引入了兩部份,也就是說,共有五部份。——要讓二層架構的硬體認識五部份也很容易,在位址劃分的時候,将頁上級目錄和頁中間目錄的長度設定為0就可以了。這樣,作業系統見到的是五部份,硬體還是按它死闆的三部份劃分,也就共建了和諧計算機系統。 這樣,雖說是多此一舉,但是考慮到64位位址,使用四層轉換架構的CPU,此時不再把中間兩個設為0了,這樣,軟體與硬體再次共建了和諧計算機系統——抽像,強大呀! 例如,一個邏輯位址已經被轉換成了線性位址,0x08147258,換成二制進是: 0000100000 0101000111 001001011000 核心對這個位址進行劃分 PGD = 0000100000 PUD = 0 PMD = 0 PT = 0101000111 offset = 001001011000

現在來了解Linux高招,因為硬體根本看不到所謂PUD,PMD,是以,本質上要求PGD索引,直接就對應了PT的位址。而不是再到PUD和PMD中去查數組(雖然它們兩個線上性位址中,長度為0,2^0 =1,也就是說,它們都是有一個數組元素的數組),那麼,核心如何合理安排位址呢?從軟體的角度上來講,因為它的項隻有一個,32位,剛好可以存放與PGD中長度一樣的位址指針。那麼所謂先到PUD,到到PMD中做映射轉換,就變成了保持原值不變,一一轉手就可以了。這樣,就實作了“邏輯上指向一個PUD,再指向一個PDM,但在實體上是直接指向相應的PT的這個抽像,因為硬體根本不知道有PUD、PMD這個東西”。 然後交給硬體,硬體對這個位址進行劃分,看到的是:頁目錄 = 0000100000 PT = 0101000111 offset = 001001011000 嗯,先根據0000100000(32),在頁目錄數組中索引,找到其元素中的位址,取其高20位,找到頁表的位址,頁表的位址是由核心動态配置設定的,接着,再加一個offset,就是最終的實體位址了。

繼續閱讀