程序的虛拟位址在核心中通過三/四級頁表到達實體位址。 而核心的虛拟位址在NORMAL部分算是邏輯位址隻是線性的映射。
這兩者有什麼關系麼?或者說核心态為什麼還要有虛拟位址存在?
開場白:
按照以前書上,或linux核心2.6 核心的邏輯位址 與 使用者空間邏輯位址 (邏輯位址有時也被叫虛拟位址) 都是位于 0x00000000~0xFFFFFFFF 這段虛拟位址空間 ,其中使用者空間邏輯位址 位于 邏輯位址 0x00000000~ 0xBFFFFFFF ,共3g , 核心邏輯位址是 0xC0000000~0XFFFFFFFF,共1g。而且這個位址空間對于每個程序來說都是獨立的。
這裡解釋一下獨立是什麼含義。
看一下這個圖
每個程序看到的 位址空間都是一樣的,比如.text 都是從0x80048000 開始,然後使用者棧都是從0xBFFFFFFF 向低位址增長,核心位址空間都是0xC0000000~0xFFFFFFFF。
每個程序看到的 位址空間都是一樣的,比如.text 都是從0x80048000 開始,然後使用者棧都是從0xBFFFFFFF 向低位址增長,核心位址空間都是0xC0000000~0xFFFFFFFF。
但是,每個程序的邏輯位址0x08048000 ~ system break 以及stack中對應的内容應該是不一樣的(除非兩個共享位址空間,那就是線程了)。 那問題來了,不同程序 有相同的邏輯位址,但是卻又不同的内容,這怎麼實作呢?
這就要靠 每個程序的的 頁表了。每個程序都有一個自己的頁表,使得 某邏輯位址對應于某個實體記憶體。 正因為 每個程序都有一個自己的頁表,使得相同的邏輯位址映射到 不同的實體記憶體。對于線程 ,它也有自己的頁表,隻是頁表的 邏輯位址 映射到的實體記憶體相同。
那程序的頁表是怎樣的呢?首先,核心本身就有一個頁表了,而且 對于normal_area 都是一一映射到實體記憶體的,具體可查一下網上資料關于低端記憶體和高端記憶體。 這裡可以不用知道到底怎麼映射,隻需要知道 核心中有一個頁表, 能把 核心邏輯位址映射到核心實體位址。
這個核心邏輯位址對于每一個程序來說都應該是一樣的,是以 ,在建立程序表時候,就可以直接拷貝該核心的頁表,作為該程序 的頁表的一部分,另外對于 該程序的使用者部分的頁表,可以簡單地了解為 把邏輯位址 映射到一個 空閑的 實體記憶體區域。
每當切換到另一個程序時,就要設定這個程序的頁表,通過 設定MMU的某些寄存器 ,然後 MMU 就可以把 cpu 發出的邏輯位址 轉化為 實體位址了。
雖然看起來 ,該程序 擁有 0x00000000~0xFFFFFFFF 的 邏輯位址空間 ,但是0xC0000000~0xFFFFFFFF 這段是核心的邏輯位址 ,在使用者态時通路會出錯,權限不夠,如果想通路,需要切換到核心态 ,可以通過 系統調用等。系統調用代表某個程序運作于核心,此時,相當于該程序可以通路0xC0000000~0xFFFFFFFF 這個位址了(但實際上 隻能通路 該程序的某個8KB的核心棧 ,這裡不是很确定,因為每個程序都有自己獨立的8KB的核心棧,你應該是不能通路别的核心的核心棧),此時可以把使用者空間邏輯位址 在 核心邏輯位址 之間 進行記憶體拷貝。
另外 0X00000000 ~0x08048000 是不能給使用者通路的,這裡面是一些C運作庫的内容。通路會報segement fault 錯誤。
另外linux 對隻讀的内容可以共享,在實體記憶體中隻有一份拷貝。這樣,即使在邏輯位址上看起來有很多c庫等運作庫在裡面,但整個記憶體隻有一份拷貝,當然,對于可寫的資料段,每個程序都應該有獨立一份。
步入正題:
1. 現在 Linux 核心是4級頁表結構,3級頁表的時代是10年前了。 X86_64 架構下,無論 Intel 還是 AMD 的 CPU, 都是四級的硬體頁表,是以軟體層面的頁表至少要4級(否則,程序通路的空間将受限, 因為有一級頁表被固定住了,是以3級頁表時代,X86_64 隻能通路 512GB 空間, 而 X86_64 的設計可通路空間達到 131 072( = 2^47) GB。打個比方就是,省,市,區,縣 四級行政規劃,硬要嵌套進三級規劃,隻能表達市,區,縣三級,省一級給固定住了, 通路範圍縮小了)。
2. 不過你會問:i386 隻有三級硬體頁表:PUD -> PMD -> PTE, 怎麼嵌入四級軟體頁表結構? 答案就是虛設一層。打個比方:北京是省級行政機關,如果要按省,市,區,縣結構來表達某縣,就是: 北京(省)北京(市)XX區XX縣, 有一層完全就是占個位而已。有興趣了解 Linux 頁表的變遷曆史,可以看我之前寫過的文章: Linux核心4級頁表的演進
3. 核心空間,使用者空間的位址都是虛拟位址,都要經過 MMU 的翻譯,變成實體位址。使用者空間的虛拟位址,就是按前面所述的走四級頁表來翻譯。 核心空間虛拟位址是所有程序共享的,重要的是,從效率角度看, 如果同樣走四級頁表翻譯的流程,速度太慢;于是,核心在初始化時,就建立核心空間的映射(因為所有程序共享,有一份就夠了),并且,采用的就是線性映射,而不是走頁表翻譯這種類似哈希表的方式。這樣,核心位址的翻譯,簡化為一條偏移加減指令就行,相比走頁表,效率大大提高(不過,核心空間并非完全不用頁表,此處講原理是以簡化,詳細的看尾注).
4. 至于為什麼使用者空間不能也像核心空間這麼做,原因是使用者位址空間是随程序建立才産生的,它的頁面可能散布在不同的實體記憶體中,無法這麼做。另外,走頁表的過程,不止是翻譯的過程,還是一個權限檢查的過程,對于不可控的使用者态位址,這安全性檢查必不可省。而核心空間,隻有一份,所有可以提前固定下來一片連續的實體位址空間,按線性方法來映射。這是很正常的優化方法。
5. 那麼問題來了,在 Linux 剛引入的時候, i386 4G 的程序空間典型的是 3G user + 1G kernel 的劃分,這教科書上都有說。 那按前面的線性方法, 1G 核心空間,隻能映射 1G 實體位址空間,這對核心來說,太掣肘了。是以,折衷方案是, Linux 核心隻對 1G 核心空間的前 896 MB 按前面所說的方法線性映射, 剩下的 128 MB 的核心空間, 采用動态映射[1]的方式,即按需映射的方式 ,這樣,核心态的通路空間更多了。 這個直接映射的部分, 就是題主所說的 NORMAL 區, 就是所謂低端記憶體。到了 64 位時代, 核心空間大大增大, 這種限制就沒了,核心空間可以完全進行線性映射,不過,基于[1]的緣故, 仍保留有動态映射這部分。
[1] 動态映射不全是為了核心空間可以通路更多的實體記憶體,還有一個重要原因: 當核心需要連續多頁面的空間時,如果核心空間全線性映射,那麼,可能會出現核心空間碎片化而滿足不了這麼多連續頁面配置設定的需求。基于此,核心空間也必須有一部分是非線性映射,進而在這碎片化實體位址空間上,用頁表構造連續虛拟位址空間,這就是所謂vmalloc空間。
結束語:
從OS實作的角度做個補充(已上都是從硬體實作的原理為出發點的)。
首先的首先,開啟分頁機制後核心也不能繞過該機制,是以核心也要有虛拟位址。
首先,程序的虛拟位址和核心的虛拟位址有一點不同:核心的虛拟位址如果觸發了缺頁中斷整個系統就panic了,而程序的虛拟位址不是這樣。為什麼這樣設計呢?這是因為如果不做此限制,核心上下文中觸發缺頁中斷後進行中斷處理時還可能繼續發生嵌套的缺頁中斷,如此會一直嵌套。
其次,怎麼避免在核心上下文中不産生缺頁中斷呢?最簡單的方法就是把所有的實體記憶體映射到核心的某段虛拟位址空間,從此段空間内malloc的虛拟位址都已經映射好了不會觸發缺頁中斷。其他的核心的虛拟位址通路産生的缺頁中斷,肯定是代碼錯誤或者是硬體錯誤,隻能panic。而最簡單的映射方法就是實體位址加上固定的偏移就得到虛拟位址,偷懶的做法
最後,實際的OS的實作上(比如FreeBSD)不會按照4k的頁大小把所有的實體位址都預先映射出來,因為這樣需要浪費不少記憶體作為頁表。實際上是按照2M(64位系統)的頁做映射,稱之為direct mapping,需要做4k頁面映射的時候,除了配置設定出一頁,還可能需要申請一頁作為頁表,并把對應的實體位址寫入作為頁表的這一頁的對應PSN處。如果沒有direct mapping,上述做法的實作會很複雜。