涉及的硬件平臺(tái)是X86,如果是其它平臺(tái),嘻嘻,不保證能一一對(duì)號(hào)入座,但是舉一反三,我想是完全可行的。
一、概念 物理地址(physical address) 用于內(nèi)存芯片級(jí)的單元尋址,與處理器和CPU連接的地址總線相對(duì)應(yīng)。 ——這個(gè)概念應(yīng)該是這幾個(gè)概念中最好理解的一個(gè),但是值得一提的是,雖然可以直接把物理地址理解成插在機(jī)器上那根內(nèi)存本身,把內(nèi)存看成一個(gè)從0字節(jié)一直到最大空量逐字節(jié)的編號(hào)的大數(shù)組,然后把這個(gè)數(shù)組叫做物理地址,但是事實(shí)上,這只是一個(gè)硬件提供給軟件的抽像,內(nèi)存的尋址方式并不是這樣。所以,說它是“與地址總線相對(duì)應(yīng)”,是更貼切一些,不過拋開對(duì)物理內(nèi)存尋址方式的考慮,直接把物理地址與物理的內(nèi)存一一對(duì)應(yīng),也是可以接受的。也許錯(cuò)誤的理解更利于形而上的抽像。 虛擬內(nèi)存(virtual memory) 這是對(duì)整個(gè)內(nèi)存(不要與機(jī)器上插那條對(duì)上號(hào))的抽像描述。它是相對(duì)于物理內(nèi)存來講的,可以直接理解成“不直實(shí)的”,“假的”內(nèi)存,例如,一個(gè)0x08000000內(nèi)存地址,它并不對(duì)就物理地址上那個(gè)大數(shù)組中0x08000000 - 1那個(gè)地址元素;之所以是這樣,是因?yàn)楝F(xiàn)代操作系統(tǒng)都提供了一種內(nèi)存管理的抽像,即虛擬內(nèi)存(virtual memory)。進(jìn)程使用虛擬內(nèi)存中的地址,由操作系統(tǒng)協(xié)助相關(guān)硬件,把它“轉(zhuǎn)換”成真正的物理地址。這個(gè)“轉(zhuǎn)換”,是所有問題討論的關(guān)鍵。有了這樣的抽像,一個(gè)程序,就可以使用比真實(shí)物理地址大得多的地址空間。(拆東墻,補(bǔ)西墻,銀行也是這樣子做的),甚至多個(gè)進(jìn)程可以使用相同的地址。不奇怪,因?yàn)檗D(zhuǎn)換后的物理地址并非相同的。 ——可以把連接后的程序反編譯看一下,發(fā)現(xiàn)連接器已經(jīng)為程序分配了一個(gè)地址,例如,要調(diào)用某個(gè)函數(shù)A,代碼不是call A,而是call 0x0811111111 ,也就是說,函數(shù)A的地址已經(jīng)被定下來了。沒有這樣的“轉(zhuǎn)換”,沒有虛擬地址的概念,這樣做是根本行不通的。打住了,這個(gè)問題再說下去,就收不住了。
邏輯地址(logical address) Intel為了兼容,將遠(yuǎn)古時(shí)代的段式內(nèi)存管理方式保留了下來。邏輯地址指的是機(jī)器語言指令中,用來指定一個(gè)操作數(shù)或者是一條指令的地址。以上例,我們說的連接器為A分配的0x08111111這個(gè)地址就是邏輯地址。 ——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對(duì)邏輯地址要求,“一個(gè)邏輯地址,是由一個(gè)段標(biāo)識(shí)符加上一個(gè)指定段內(nèi)相對(duì)地址的偏移量,表示為 [段標(biāo)識(shí)符:段內(nèi)偏移量],也就是說,上例中那個(gè)0x08111111,應(yīng)該表示為[A的代碼段標(biāo)識(shí)符: 0x08111111],這樣,才完整一些”
線性地址(linear address)或也叫虛擬地址(virtual address) 跟邏輯地址類似,它也是一個(gè)不真實(shí)的地址,如果邏輯地址是對(duì)應(yīng)的硬件平臺(tái)段式管理轉(zhuǎn)換前地址的話,那么線性地址則對(duì)應(yīng)了硬件頁式內(nèi)存的轉(zhuǎn)換前地址。 ------------------------------------------------------------- CPU將一個(gè)虛擬內(nèi)存空間中的地址轉(zhuǎn)換為物理地址,需要進(jìn)行兩步:首先將給定一個(gè)邏輯地址(其實(shí)是段內(nèi)偏移量,這個(gè)一定要理解?。。。?,CPU要利用其段式內(nèi)存管理單元,先將為個(gè)邏輯地址轉(zhuǎn)換成一個(gè)線程地址,再利用其頁式內(nèi)存管理單元,轉(zhuǎn)換為最終物理地址。 這樣做兩次轉(zhuǎn)換,的確是非常麻煩而且沒有必要的,因?yàn)橹苯涌梢园丫€性地址抽像給進(jìn)程。之所以這樣冗余,Intel完全是為了兼容而已。
2、CPU段式內(nèi)存管理,邏輯地址如何轉(zhuǎn)換為線性地址 一個(gè)邏輯地址由兩部份組成,段標(biāo)識(shí)符: 段內(nèi)偏移量。段標(biāo)識(shí)符是由一個(gè)16位長的字段組成,稱為段選擇符。其中前13位是一個(gè)索引號(hào)。后面3位包含一些硬件細(xì)節(jié),如圖:
最后兩位涉及權(quán)限檢查,本貼中不包含。 索引號(hào),或者直接理解成數(shù)組下標(biāo)——那它總要對(duì)應(yīng)一個(gè)數(shù)組吧,它又是什么東東的索引呢?這個(gè)東東就是“段描述符(segment descriptor)”,呵呵,段描述符具體地址描述了一個(gè)段(對(duì)于“段”這個(gè)字眼的理解,我是把它想像成,拿了一把刀,把虛擬內(nèi)存,砍成若干的截——段)。這樣,很多個(gè)段描述符,就組了一個(gè)數(shù)組,叫“段描述符表”,這樣,可以通過段標(biāo)識(shí)符的前13位,直接在段描述符表中找到一個(gè)具體的段描述符,這個(gè)描述符就描述了一個(gè)段,我剛才對(duì)段的抽像不太準(zhǔn)確,因?yàn)榭纯疵枋龇锩婢烤褂惺裁礀|東——也就是它究竟是如何描述的,就理解段究竟有什么東東了,每一個(gè)段描述符由8個(gè)字節(jié)組成,如下圖:
這些東東很復(fù)雜,雖然可以利用一個(gè)數(shù)據(jù)結(jié)構(gòu)來定義它,不過,我這里只關(guān)心一樣,就是Base字段,它描述了一個(gè)段的開始位置的線性地址。 Intel設(shè)計(jì)的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每個(gè)進(jìn)程自己的,就放在所謂的“局部段描述符表(LDT)”中。那究竟什么時(shí)候該用GDT,什么時(shí)候該用LDT呢?這是由段選擇符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。 GDT在內(nèi)存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT則在ldtr寄存器中。 好多概念,像繞口令一樣。這張圖看起來要直觀些:
首先,給定一個(gè)完整的邏輯地址[段選擇符:段內(nèi)偏移地址], 1、看段選擇符的T1=0還是1,知道當(dāng)前要轉(zhuǎn)換是GDT中的段,還是LDT中的段,再根據(jù)相應(yīng)寄存器,得到其地址和大小。我們就有了一個(gè)數(shù)組了。 2、拿出段選擇符中前13位,可以在這個(gè)數(shù)組中,查找到對(duì)應(yīng)的段描述符,這樣,它了Base,即基地址就知道了。 3、把Base + offset,就是要轉(zhuǎn)換的線性地址了。 還是挺簡(jiǎn)單的,對(duì)于軟件來講,原則上就需要把硬件轉(zhuǎn)換所需的信息準(zhǔn)備好,就可以讓硬件來完成這個(gè)轉(zhuǎn)換了。OK,來看看
Linux怎么做的。
3、Linux的段式管理 Intel要求兩次轉(zhuǎn)換,這樣雖說是兼容了,但是卻是很冗余,呵呵,沒辦法,硬件要求這樣做了,軟件就只能照辦,怎么著也得形式主義一樣。另一方面,其它某些硬件平臺(tái),沒有二次轉(zhuǎn)換的概念,Linux也需要提供一個(gè)高層抽像,來提供一個(gè)統(tǒng)一的界面。所以,Linux的段式管理,事實(shí)上只是“哄騙”了一下硬件而已。 按照Intel的本意,全局的用GDT,每個(gè)進(jìn)程自己的用LDT——不過Linux則對(duì)所有的進(jìn)程都使用了相同的段來對(duì)指令和數(shù)據(jù)尋址。即用戶數(shù)據(jù)段,用戶代碼段,對(duì)應(yīng)的,內(nèi)核中的是內(nèi)核數(shù)據(jù)段和內(nèi)核代碼段。這樣做沒有什么奇怪的,本來就是走形式嘛,像我們寫年終總結(jié)一樣。 include/asm-i386/segment.h
#define GDT_ENTRY_DEFAULT_USER_CS 14 #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3) #define GDT_ENTRY_DEFAULT_USER_DS 15 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3) #define GDT_ENTRY_KERNEL_BASE 12 #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)
把其中的宏替換成數(shù)值,則為:
#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]
方括號(hào)后是這四個(gè)段選擇符的16位二制表示,它們的索引號(hào)和T1字段值也可以算出來了
__USER_CS index= 14 T1=0 __USER_DS index= 15 T1=0 __KERNEL_CS index= 12 T1=0 __KERNEL_DS index= 13 T1=0
T1均為0,則表示都使用了GDT,再來看初始化GDT的內(nèi)容中相應(yīng)的12-15項(xiàng)(arch/i386/head.S):
.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */ .quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */ .quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */ .quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */
按照前面段描述符表中的描述,可以把它們展開,發(fā)現(xiàn)其16-31位全為0,即四個(gè)段的基地址全為0。 這樣,給定一個(gè)段內(nèi)偏移地址,按照前面轉(zhuǎn)換公式,0 + 段內(nèi)偏移,轉(zhuǎn)換為線性地址,可以得出重要的結(jié)論,“在Linux下,邏輯地址與線性地址總是一致(是一致,不是有些人說的相同)的,即邏輯地址的偏移量字段的值與線性地址的值總是相同的。?。?!” 忽略了太多的細(xì)節(jié),例如段的權(quán)限檢查。呵呵。 Linux中,絕大部份進(jìn)程并不例用LDT,除非使用Wine ,仿真Windows程序的時(shí)候。
4.CPU的頁式內(nèi)存管理 CPU的頁式內(nèi)存管理單元,負(fù)責(zé)把一個(gè)線性地址,最終翻譯為一個(gè)物理地址。從管理和效率的角度出發(fā),線性地址被分為以固定長度為單位的組,稱為頁(page),例如一個(gè)32位的機(jī)器,線性地址最大可為4G,可以用4KB為一個(gè)頁來劃分,這頁,整個(gè)線性地址就被劃分為一個(gè)tatol_page[2^20]的大數(shù)組,共有2的20個(gè)次方個(gè)頁。這個(gè)大數(shù)組我們稱之為頁目錄。目錄中的每一個(gè)目錄項(xiàng),就是一個(gè)地址——對(duì)應(yīng)的頁的地址。 另一類“頁”,我們稱之為物理頁,或者是頁框、頁楨的。是分頁單元把所有的物理內(nèi)存也劃分為固定長度的管理單位,它的長度一般與內(nèi)存頁是一一對(duì)應(yīng)的。 這里注意到,這個(gè)total_page數(shù)組有2^20個(gè)成員,每個(gè)成員是一個(gè)地址(32位機(jī),一個(gè)地址也就是4字節(jié)),那么要單單要表示這么一個(gè)數(shù)組,就要占去4MB的內(nèi)存空間。為了節(jié)省空間,引入了一個(gè)二級(jí)管理模式的機(jī)器來組織分頁單元。文字描述太累,看圖直觀一些:

如上圖, 1、分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3寄存器中,是進(jìn)行地址轉(zhuǎn)換的開始點(diǎn)。萬里長征就從此長始了。 2、每一個(gè)活動(dòng)的進(jìn)程,因?yàn)槎加衅洫?dú)立的對(duì)應(yīng)的虛似內(nèi)存(頁目錄也是唯一的),那么它也對(duì)應(yīng)了一個(gè)獨(dú)立的頁目錄地址。——運(yùn)行一個(gè)進(jìn)程,需要將它的頁目錄地址放到cr3寄存器中,將別個(gè)的保存下來。 3、每一個(gè)32位的線性地址被劃分為三部份,面目錄索引(10位):頁表索引(10位):偏移(12位) 依據(jù)以下步驟進(jìn)行轉(zhuǎn)換: 1、從cr3中取出進(jìn)程的頁目錄地址(操作系統(tǒng)負(fù)責(zé)在調(diào)度進(jìn)程的時(shí)候,把這個(gè)地址裝入對(duì)應(yīng)寄存器); 2、根據(jù)線性地址前十位,在數(shù)組中,找到對(duì)應(yīng)的索引項(xiàng),因?yàn)橐肓硕?jí)管理模式,頁目錄中的項(xiàng),不再是頁的地址,而是一個(gè)頁表的地址。(又引入了一個(gè)數(shù)組),頁的地址被放到頁表中去了。 3、根據(jù)線性地址的中間十位,在頁表(也是數(shù)組)中找到頁的起始地址; 4、將頁的起始地址與線性地址中最后12位相加,得到最終我們想要的葫蘆; 這個(gè)轉(zhuǎn)換過程,應(yīng)該說還是非常簡(jiǎn)單地。全部由硬件完成,雖然多了一道手續(xù),但是節(jié)約了大量的內(nèi)存,還是值得的。那么再簡(jiǎn)單地驗(yàn)證一下: 1、這樣的二級(jí)模式是否仍能夠表示4G的地址;頁目錄共有:2^10項(xiàng),也就是說有這么多個(gè)頁表每個(gè)目表對(duì)應(yīng)了:2^10頁;每個(gè)頁中可尋址:2^12個(gè)字節(jié)。還是2^32 = 4GB
2、這樣的二級(jí)模式是否真的節(jié)約了空間;也就是算一下頁目錄項(xiàng)和頁表項(xiàng)共占空間 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么說呢?。?! 紅色錯(cuò)誤,標(biāo)注一下,后文貼中有此討論。。。。。。 值得一提的是,雖然頁目錄和頁表中的項(xiàng),都是4個(gè)字節(jié),32位,但是它們都只用高20位,低12位屏蔽為0——把頁表的低12屏蔽為0,是很好理解的,因?yàn)檫@樣,它剛好和一個(gè)頁面大小對(duì)應(yīng)起來,大家都成整數(shù)增加。計(jì)算起來就方便多了。但是,為什么同時(shí)也要把頁目錄低12位屏蔽掉呢?因?yàn)榘赐瑯拥牡览?,只要屏蔽其?0位就可以了,不過我想,因?yàn)?2>10,這樣,可以讓頁目錄和頁表使用相同的數(shù)據(jù)結(jié)構(gòu),方便。 本貼只介紹一般性轉(zhuǎn)換的
原理,擴(kuò)展分頁、頁的保護(hù)機(jī)制、
PAE模式的分頁這些麻煩點(diǎn)的東東就不啰嗦了……可以參考其它專業(yè)書籍。
5.Linux的頁式內(nèi)存管理 原理上來講,Linux只需要為每個(gè)進(jìn)程分配好所需數(shù)據(jù)結(jié)構(gòu),放到內(nèi)存中,然后在調(diào)度進(jìn)程的時(shí)候,切換寄存器cr3,剩下的就交給硬件來完成了(呵呵,事實(shí)上要復(fù)雜得多,不過偶只分析最基本的流程)。 前面說了i386的二級(jí)頁管理架構(gòu),不過有些CPU,還有三級(jí),甚至四級(jí)架構(gòu),Linux為了在更高層次提供抽像,為每個(gè)CPU提供統(tǒng)一的界面。提供了一個(gè)四層頁管理架構(gòu),來兼容這些二級(jí)、三級(jí)、四級(jí)管理架構(gòu)的CPU。這四級(jí)分別為: 頁全局目錄PGD(對(duì)應(yīng)剛才的頁目錄)頁上級(jí)目錄PUD(新引進(jìn)的)頁中間目錄PMD(也就新引進(jìn)的)頁表PT(對(duì)應(yīng)剛才的頁表)。 整個(gè)轉(zhuǎn)換依據(jù)硬件轉(zhuǎn)換原理,只是多了二次數(shù)組的索引罷了,如下圖:

那么,對(duì)于使用二級(jí)管理架構(gòu)32位的硬件,現(xiàn)在又是四級(jí)轉(zhuǎn)換了,它們?cè)趺茨軌騾f(xié)調(diào)地工作起來呢?嗯,來看這種情況下,怎么來劃分線性地址吧!從硬件的角度,32位地址被分成了三部份——也就是說,不管理軟件怎么做,最終落實(shí)到硬件,也只認(rèn)識(shí)這三位老大。從軟件的角度,由于多引入了兩部份,,也就是說,共有五部份。——要讓二層架構(gòu)的硬件認(rèn)識(shí)五部份也很容易,在地址劃分的時(shí)候,將頁上級(jí)目錄和頁中間目錄的長度設(shè)置為0就可以了。這樣,操作系統(tǒng)見到的是五部份,硬件還是按它死板的三部份劃分,也不會(huì)出錯(cuò),也就是說大家共建了和諧計(jì)算機(jī)系統(tǒng)。 這樣,雖說是多此一舉,但是考慮到64位地址,使用四層轉(zhuǎn)換架構(gòu)的CPU,我們就不再把中間兩個(gè)設(shè)為0了,這樣,軟件與硬件再次和諧——抽像就是強(qiáng)大呀!?。?例如,一個(gè)邏輯地址已經(jīng)被轉(zhuǎn)換成了線性地址,0x08147258,換成二制進(jìn),也就是: 0000100000 0101000111 001001011000 內(nèi)核對(duì)這個(gè)地址進(jìn)行劃分 PGD = 0000100000 PUD = 0 PMD = 0 PT = 0101000111 offset = 001001011000 現(xiàn)在來理解
Linux針對(duì)硬件的花招,因?yàn)橛布究床坏剿^PUD,PMD,所以,本質(zhì)上要求PGD索引,直接就對(duì)應(yīng)了PT的地址。而不是再到PUD和PMD中去查數(shù)組(雖然它們兩個(gè)在線性地址中,長度為0,2^0 =1,也就是說,它們都是有一個(gè)數(shù)組元素的數(shù)組),那么,內(nèi)核如何合理安排地址呢?從軟件的角度上來講,因?yàn)樗捻?xiàng)只有一個(gè),32位,剛好可以存放與PGD中長度一樣的地址指針。那么所謂先到PUD,到到PMD中做映射轉(zhuǎn)換,就變成了保持原值不變,一一轉(zhuǎn)手就可以了。這樣,就實(shí)現(xiàn)了“邏輯上指向一個(gè)PUD,再指向一個(gè)PDM,但在物理上是直接指向相應(yīng)的PT的這個(gè)抽像,因?yàn)橛布静恢烙蠵UD、PMD這個(gè)東西”。 然后交給硬件,硬件對(duì)這個(gè)地址進(jìn)行劃分,看到的是:頁目錄 = 0000100000 PT = 0101000111 offset = 001001011000 嗯,先根據(jù)0000100000(32),在頁目錄數(shù)組中索引,找到其元素中的地址,取其高20位,找到頁表的地址,頁表的地址是由內(nèi)核動(dòng)態(tài)分配的,接著,再加一個(gè)offset,就是最終的物理地址了