1. 程式人生 > >Windows 記憶體詳解(三)Windows記憶體管理

Windows 記憶體詳解(三)Windows記憶體管理

本文主要內容:
1.基本概念:實體記憶體、虛擬記憶體;實體地址、虛擬地址、邏輯地址;頁目錄,頁表
2.Windows記憶體管理
3.CPU段式記憶體管理
4.CPU頁式記憶體管理
 
一、基本概念
1. 兩個記憶體概念
實體記憶體:人盡皆知,就是插在主機板上的記憶體條。他是固定的,記憶體條的容量多大,實體記憶體就有多大(整合顯示卡系統除外)。但是如果程式執行很多或者程式本身很大的話,就會導致大量的實體記憶體佔用,甚至導致實體記憶體消耗殆盡。
虛擬記憶體:簡明的說,虛擬記憶體就是在硬碟上劃分一塊頁面檔案,充當記憶體。當程式在執行時,有一部分資源還沒有用上或者同時開啟幾個程式卻只操作其中一個程式時,系統沒必要將程式所有的資源都塞在實體記憶體中,於是,系統將這些暫時不用的資源放在虛擬記憶體上,等到需要時在調出來用。
2.三個地址概念
實體地址(physical address):用於記憶體晶片級的單元定址,與處理器和CPU連線的地址匯流排相對應。
——這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把實體地址理解成插在機器上那根記憶體本身,把記憶體看成一個從0位元組一直到 最大空量逐位元組的編號的大陣列,然後把這個陣列叫做實體地址,但是事實上,這只是一個硬體提供給軟體的抽像,記憶體的定址方式並不是這樣。所以,說它是“與 地址匯流排相對應”,是更貼切一些,不過拋開對實體記憶體定址方式的考慮,直接把實體地址與物理的記憶體一一對應,也是可以接受的。也許錯誤的理解更利於形而上的抽像。
邏輯地址(logical address):是指由程式產生的與段相關的偏移地址部分。例如,你在進行C語言指標

程式設計中,可以讀取指標變數本身值(&操作),實際上這個值就是邏輯地址,它是相對於你當前程序資料段的地址,不和絕對實體地址相干。只有在Intel真實模式下,邏輯地址才和實體地址相等(因為真實模式沒有分段或分頁機制,Cpu不進行自動地址轉換);邏輯也就是在Intel 保護模式下程式執行程式碼段限長內的偏移地址(假定程式碼段、資料段如果完全一樣)。應用程式設計師僅需與邏輯地址打交道,而分段和分頁機制對您來說是完全透明的,僅由系統程式設計人員涉及。應用程式設計師雖然自己可以直接操作記憶體,那也只能在作業系統給你分配的記憶體段操作。
Intel為了相容,將遠古時代的段式記憶體管理方式保留了下來。邏輯地址指的是機器語言指令中,用來指定一個運算元或者是一條指令的地址。以上例,我們說的聯結器為A分配的0x08111111這個地址就是邏輯地址。
——不過不好意思,這樣說,好像又違背了Intel中段式管理中,對邏輯地址要求,“一個邏輯地址,是由一個段識別符號加上一個指定段內相對地址的偏移量, 表示為 [段識別符號:段內偏移量],也就是說,上例中那個0x08111111,應該表示為[A的程式碼段識別符號: 0x08111111],這樣,才完整一些”
線性地址(linear address)或也叫虛擬地址(virtual address)
跟邏輯地址類似,它也是一個不真實的地址,如果邏輯地址是對應的硬體平臺段式管理轉換前地址的話,那麼線性地址則對應了硬體頁式記憶體的轉換前地址。
-------------------------------------------------------------
每個程序都有4GB的虛擬地址空間
這4GB分3部分 
(1)一部分對映實體記憶體
(2)一部分對映硬碟上的交換檔案
(3)一部分什麼也不做
程式中都是使用4GB的虛擬地址,訪問實體記憶體需要使用實體地址,實體地址是放在定址總線上的地址,以位元組(8位)為單位。
-------------------------------------------------------------
CPU將一個虛擬記憶體空間中的地址轉換為實體地址,需要進行兩步:首先將給定一個邏輯地址(其實是段內偏移量,這個一定要理解!!!),CPU要利用其段式記憶體管理單元,先將為個邏輯地址轉換成一個執行緒地址,再利用其頁式記憶體管理單元,轉換為最終實體地址。
這樣做兩次轉換,的確是非常麻煩而且沒有必要的,因為直接可以把線性地址抽像給程序。之所以這樣冗餘,Intel完全是為了相容而已。
3.頁表、頁目錄概念
使用了分頁機制之後,4G的地址空間被分成了固定大小的頁,每一頁或者被對映到實體記憶體,或者被對映到硬碟上的交換檔案中,或者沒有對映任何東西。對於一般程式來說,4G的地址空間,只有一小部分映射了實體記憶體,大片大片的部分是沒有對映任何東西。實體記憶體也被分頁,來對映地址空間。對於32bit的 Win2k,頁的大小是4K位元組。CPU用來把虛擬地址轉換成實體地址的資訊存放在叫做頁目錄和頁表的結構裡。
實體記憶體分頁,一個物理頁的大小為4K位元組,第0個物理頁從實體地址 0x00000000 處開始。由於頁的大小為4KB,就是0x1000位元組,所以第1頁從實體地址 0x00001000處開始。第2頁從實體地址0x00002000處開始。可以看到由於頁的大小是4KB,所以只需要32bit的地址中高20bit來定址物理頁。
頁目錄:  一個頁目錄大小為4K位元組,放在一個物理頁中。由1024個4位元組的頁目錄項組成。頁目錄項的大小為4 個位元組(32bit),所以一個頁目錄中有1024個頁目錄項。頁目錄中的每一項的內容(每項4個位元組)高20bit用來放一個頁表(頁表放在一個物理頁中)的實體地址,低12bit放著一些標誌。
頁表:  一個頁表的大小為4K位元組,放在一個物理頁中。由1024個4位元組的頁表項組成。頁表項的大小為4個位元組 (32bit),所以一個頁表中有1024個頁表項。頁表中的每一項的內容(每項4個位元組,32bit)高20bit用來放一個物理頁的實體地址,低 12bit放著一些標誌。
對於x86系統,頁目錄的實體地址放在CPU的CR3暫存器中。
4. 虛擬地址轉換成實體地址
一個虛擬地址大小為4位元組,其中包含找到實體地址的資訊
虛擬地址分3部分
(1)31-22位(10位)是頁目錄中的索引
(2)21-12位(10位)是頁表中的索引
(2)11-0位(12位)是頁內偏移

轉換過程:
首先通過CR3找到頁目錄所在物理頁-》根據虛擬地址中的31-22找到該頁目錄項-》通過該頁目錄項找到該虛擬地址對應的頁表地址-》根據虛擬地址21-12找到物理頁的實體地址-》更具虛擬地址的11-0位作為偏移加上該物理頁的地址就找到了 該虛擬地址對應的實體地址
CPU把虛擬地址轉換成實體地址:一個虛擬地址,大小4個位元組(32bit),包含著找到實體地址的資訊,分為3個部分:第22位到第31位這10位(最高10位)是頁目錄中的索引,第 12位到第21位這10位是頁表中的索引,第0位到第11位這12位(低12位)是頁內偏移。對於一個要轉換成實體地址的虛擬地址,CPU首先根據CR3 中的值,找到頁目錄所在的物理頁。然後根據虛擬地址的第22位到第31位這10位(最高的10bit)的值作為索引,找到相應的頁目錄項(PDE, page directory entry),頁目錄項中有這個虛擬地址所對應頁表的實體地址。有了頁表的實體地址,根據虛擬地址的第12位到第21位這10位的值作為索引,找到該頁表中相應的頁表項(PTE,page table entry),頁表項中就有這個虛擬地址所對應物理頁的實體地址。最後用虛擬地址的最低12位,也就是頁內偏移,加上這個物理頁的實體地址,就得到了該虛擬地址所對應的實體地址。
-------------------------------------------------------------
一個頁目錄有1024項,虛擬地址最高的10bit剛好可以索引1024項(2的10次方等於1024)。一個頁表也有1024項,虛擬地址中間部分的 10bit,剛好索引1024項。虛擬地址最低的12bit(2的12次方等於4096),作為頁內偏移,剛好可以索引4KB,也就是一個物理頁中的每個位元組。
-------------------------------------------------------------
一個虛擬地址轉換成實體地址的計算過程就是,處理器通過CR3找到當前頁目錄所在物理頁,取虛擬地址的高10bit,然後把這10bit右移2bit(因為每個頁目錄項4個位元組長,右移2bit相當於乘4)得到在該頁中的地址,取出該地址處PDE(4個位元組),就找到了該虛擬地址對應頁表所在物理頁,取虛擬地址第12位到第21位這10位,然後把這10bit右移2bit(因為每個頁表項4個位元組長,右移2bit相當於乘4)得到在該頁中的地址,取出該地址處的PTE(4個位元組),就找到了該虛擬地址對應物理頁的地址,最後加上12bit的頁內偏移得到了實體地址。
-------------------------------------------------------------
32bit的一個指標,可以定址範圍0x00000000-0xFFFFFFFF,4GB大小。也就是說一個32bit的指標可以定址整個4GB地址空間的每一個位元組。一個頁表項負責4K的地址空間和實體記憶體的對映,一個頁表1024項,也就是負責1024*4k=4M的地址空間的對映。一個頁目錄項,對應一個頁表。一個頁目錄有1024項,也就對應著1024個頁表,每個頁表負責4M地址空間的對映。1024個頁表負責1024*4M=4G的地址空間對映。一個程序有一個頁目錄。所以以頁為單位,頁目錄和頁表可以保證4G的地址空間中的每頁和實體記憶體的對映。
-------------------------------------------------------------
每個程序都有自己的4G地址空間,從0x00000000-0xFFFFFFFF。通過每個程序自己的一套頁目錄和頁表來實現。由於每個程序有自己的頁目錄和頁表,所以每個程序的地址空間對映的實體記憶體是不一樣的。兩個程序的同一個虛擬地址處(如果都有實體記憶體對映)的值一般是不同的,因為他們往往對應不同的物理頁。

4G地址空間中低2G,0x00000000-0x7FFFFFFF是使用者地址空間,4G地址空間中高2G,即0x80000000-0xFFFFFFFF 是系統地址空間。訪問系統地址空間需要程式有ring0的許可權。
 
二. windows記憶體原理
 
主要的內容如下:
1.概述
2.虛擬記憶體
3.實體記憶體
4.對映

1.概述:
windows中 我們一般程式設計時接觸的都是線性地址 也就是我們所說的虛擬地址,然而很不幸在我不斷成長的過程中發現原來線性地址是作業系統自己意淫出來的,根本就不是我們資料真實存在的地方.換句話說我們在0x80000000(虛擬地址)的地方寫入了"UESTC"這個字串,但是我們這個字串並不真實存在於實體地址的0x80000000這裡.再說了真實的實體地址是利用一段N長的陣列來定位的(額~看不懂這句話沒關係,一會看到實體地址那你就明白了).但是為什麼windows乃至linux都需要採取這種方式來定址呢?原因很簡單 為了安全.聽說過保護模式吧?顧名思義就是這個模式下加入了保護系統安全的措施,同樣採用線性地址也是所謂的安全措施之一.
    我們假設下如果沒有使用線性地址,那麼我們可以直接訪問實體地址,但是這樣的話當我們往記憶體寫東西的時候作業系統無法檢查這塊記憶體是否可寫,換句話說作業系統無法實現對頁面訪問控制.這點是很可怕的事情,就如win9x那樣沒事在應用態往核心地址寫東西,還有沒有天理了~~
    由於作業系統的安全需要,催生了虛擬地址的應用.在CPU中有個叫MMU(應該是Memory Manage Unit 記憶體管理單元)的東西,專門負責線性地址和實體地址之間的轉化.我們每次讀寫記憶體,從CPU的結構看來不是都要經過ALU麼,ALU拿到虛擬地址後扔給MMU轉化成實體地址後再把資料讀入暫存器中.過程就是如此.

2.虛擬記憶體
    我們程式設計時所面對的都是虛擬地址,對於每個程序來說都擁有4G的虛擬記憶體(小補充: 4G虛擬記憶體中,高2G記憶體屬於核心部分,是所有程序共有的,低2G記憶體資料是程序獨有的,每個程序低2G記憶體都不一樣),但注意的是虛擬地址是作業系統自己意淫出來的,打個比方就是說想法還未付諸實踐,所以不構成任何資源損失.比如我們要在0x80000000的地方寫個"UESTC"的時候,作業系統就會將這個虛擬地址對映到一塊實體地址A中,你寫這塊虛擬地址就相當於寫實體地址A.但是加入我們只申請了一段1KB的虛擬記憶體空間,並未讀寫,系統是不會分配任何實體記憶體的,只有當虛擬記憶體要使用時系統才會分配相應的物理空間.
    下面要說些細節點的了,其實說白了虛擬記憶體的管理是由一堆資料結構來實現的,下面給出這些資料結構:
(懶得打那麼多 就只打出重要的部分~~)
在EPROCESS中有個資料結構如下:
typedef struct _MADDRESS_SPACE
{
    PMEMORY_AREA MemoryAreaRoot ; //這個指標指向一顆二叉排序樹,想必學過資料結構的朋友都曉得吧~~嘿嘿~~ 主要是這個情況下采用二叉排序樹能加快記憶體的搜尋速度 
    ...
    ...
    ...
}MADDRESS_SPACE , *PMADDRESS_SPACE ;

然而這顆二叉排序樹的節點結構結構是這樣的:
typedef struct _MEMORY_AREA
{
    PVOID StartingAddress ; //虛擬記憶體段的起始地址
    PVOID EndingAddress ;   //虛擬記憶體段的結束地址
    struct _MEMORY_AREA *Parent ; //該節點的老爹
    struct _MEMORY_AREA *LeftChild ; //該節點的左兒子
    struct _MEMORY_AREA *RrightChild ; //該節點的左兒子
    ...
    ...
    ...

}MEMORY_AREA , *PMEMORY_AREA ;
    這個節點內主要記錄了已分配的虛擬記憶體空間,如果要申請虛擬記憶體空間就跑來這裡建立個節點就好了,如果要刪除空間同樣把對應節點刪除就好了.不過說來簡單,其實還會涉及到很多操作,比如要平衡這棵樹什麼的.
    那麼我們在分配虛擬記憶體空間時系統會跑去找到這顆樹,然後通過一定演算法遍歷這顆樹,找到符合條件的記憶體空隙(未分配的記憶體空間),建立個節點掛到這顆樹上,返回起始地址就完成了.


3.實體記憶體
    接下來就到實體記憶體的東東了,其實呢 實體記憶體的管理是基於一個數組來管理的,聽說過分頁機制吧,下面說下分頁.windows下分頁是4kb一頁 那麼假設我們實體記憶體有4GB 那麼windows會將這4GB空間分頁,分成4GB/4KB = 1M頁    那麼每一頁(就是4KB)的物理空間都由一個叫PHYSICAL_PAGE的資料結構管理,這個資料結構就不寫啦....一來我寫的手痠 二來看的人也累~~說說思路就好了.
    接著以上假設 4GB的記憶體 作業系統就會相應產生一個PHYSICAL_PAGE陣列,這個陣列有多少個元素呢?有1M個 正好覆蓋了4GB的實體地址.這就是所謂的分頁機制的原型.說白了就是把記憶體按4k劃分來管理.那麼實體地址就好定位了,所謂的實體記憶體地址就可以直接以陣列下標來表示,其實這裡的實體記憶體地址指的是實體記憶體地址的頁面號... 具體地址還是要根據虛擬記憶體地址的低12位和實體記憶體的頁面號一起確定的 
     再說下實體記憶體的管理吧,在核心下有3個佇列 這些佇列內的元素就是上邊所說的PHYSICAL_PAGE結構 
它們分別是:
A.已分配的記憶體佇列 :存放正被使用的記憶體
B.待清理的記憶體佇列 :存放已被釋放的記憶體,但是這些記憶體未被清理(清零)
C.空閒佇列 :存放可使用的空閒記憶體

系統管理流程如下:
1).每隔一段時間,系統會自動從B佇列中提取佇列元素進行清理,然後放入空閒佇列中.
2).每當釋放實體記憶體時,系統會將要釋放的記憶體從A佇列提取出來,放入B佇列中.
3).申請記憶體時,系統將要分配的記憶體從C佇列中提取出來,放入A佇列中

4.對映 
    說到對映得先從虛擬記憶體的32位地址說起.在CPU中存在個CR3暫存器,裡面存放著每個程序的頁目錄地址

我們可以把轉換過程分成幾步看
1.根據CR3(注:CR3中的值是實體地址)的值我們可以定位到當前程序的頁目錄基址,然後通過虛擬地址的高10位做偏移量來獲得指定的PDE(Page Directory Entry),PDE內容有4位元組,高20位部分用來做頁表基址,剩下的位元位用來實現許可權控制之類的東西了.系統只要檢測相應的位元位就可以實現記憶體的許可權控制了.
2.通過PDE提供的基址加上虛擬記憶體的中10位(21-12)做偏移量就找到了頁表PTE(Page Table Entry)地址,然後PTE的高20位就是實體記憶體的基址了(其實就是那個PHYSICAL_PAGE陣列的下標號....),剩下的位元位同樣用於訪問控制之類的.
3.通過虛擬記憶體的低12位加上PTE中高20位做基址就可以確定唯一的實體記憶體了.

   
三. CPU段式記憶體管理,邏輯地址如何轉換為線性地址
一個邏輯地址由兩部份組成,段識別符號: 段內偏移量。段識別符號是由一個16位長的欄位組成,稱為段選擇符。其中前13位是一個索引號。後面3位包含一些硬體細節,如圖:

Windows記憶體管理 - 嘯百川 - 嘯百川的部落格

最後兩位涉及許可權檢查,本貼中不包含。

索引號,或者直接理解成陣列下標——那它總要對應一個數組吧,它又是什麼東東的索引呢?這個東東就是“段描述符(segment descriptor)”,呵呵,段描述符具體地址描述了一個段(對於“段”這個字眼的理解,我是把它想像成,拿了一把刀,把虛擬記憶體,砍成若干的截—— 段)。這樣,很多個段描述符,就組了一個數組,叫“段描述符表”,這樣,可以通過段識別符號的前13位,直接在段描述符表中找到一個具體的段描述符,這個描 述符就描述了一個段,我剛才對段的抽像不太準確,因為看看描述符裡面究竟有什麼東東——也就是它究竟是如何描述的,就理解段究竟有什麼東東了,每一個段描 述符由8個位元組組成,如下圖:

Windows記憶體管理 - 嘯百川 - 嘯百川的部落格

這些東東很複雜,雖然可以利用一個數據結構來定義它,不過,我這裡只關心一樣,就是Base欄位,它描述了一個段的開始位置的線性地址。

Intel設計的本意是,一些全域性的段描述符,就放在“全域性段描述符表(GDT)”中,一些區域性的,例如每個程序自己的,就放在所謂的“區域性段描述符表 (LDT)”中。那究竟什麼時候該用GDT,什麼時候該用LDT呢?這是由段選擇符中的T1欄位表示的,=0,表示用GDT,=1表示用LDT。

GDT在記憶體中的地址和大小存放在CPU的gdtr控制暫存器中,而LDT則在ldtr暫存器中。

好多概念,像繞口令一樣。這張圖看起來要直觀些:

Windows記憶體管理 - 嘯百川 - 嘯百川的部落格

首先,給定一個完整的邏輯地址[段選擇符:段內偏移地址],
1、看段選擇符的T1=0還是1,知道當前要轉換是GDT中的段,還是LDT中的段,再根據相應暫存器,得到其地址和大小。我們就有了一個數組了。
2、拿出段選擇符中前13位,可以在這個陣列中,查詢到對應的段描述符,這樣,它了Base,即基地址就知道了。
3、把Base + offset,就是要轉換的線性地址了。

還是挺簡單的,對於軟體來講,原則上就需要把硬體轉換所需的資訊準備好,就可以讓硬體來完成這個轉換了。OK,來看看Linux怎麼做的。

Linux的段式管理
Intel要求兩次轉換,這樣雖說是相容了,但是卻是很冗餘,呵呵,沒辦法,硬體要求這樣做了,軟體就只能照辦,怎麼著也得形式主義一樣。
另一方面,其它某些硬體平臺,沒有二次轉換的概念,Linux也需要提供一個高層抽像,來提供一個統一的介面。所以,Linux的段式管理,事實上只是“哄騙”了一下硬體而已。

按照Intel的本意,全域性的用GDT,每個程序自己的用LDT——不過Linux則對所有的程序都使用了相同的段來對指令和資料定址。即使用者資料段,用 戶程式碼段,對應的,核心中的是核心資料段和核心程式碼段。這樣做沒有什麼奇怪的,本來就是走形式嘛,像我們寫年終總結一樣。
[Copy to clipboard] [ - ]
CODE:
#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)
把其中的巨集替換成數值,則為:
[Copy to clipboard] [ - ]
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欄位值也可以算出來了
[Copy to clipboard] [ - ]
CODE:
__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的內容中相應的12-15項(arch/i386/head.S):
[Copy to clipboard] [ - ]
CODE:
.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 */

按照前面段描述符表中的描述,可以把它們展開,發現其16-31位全為0,即四個段的基地址全為0。
這樣,給定一個段內偏移地址,按照前面轉換公式,0 + 段內偏移,轉換為線性地址,可以得出重要的結論,“在Linux下,邏輯地址與線性地址總是一致(是一致,不是有些人說的相同)的,即邏輯地址的偏移量欄位的值與線性地址的值總是相同的。!!!”
忽略了太多的細節,例如段的許可權檢查。呵呵。
Linux中,絕大部份程序並不例用LDT,除非使用Wine ,模擬Windows程式的時候。

四.CPU頁式記憶體管理

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

Windows記憶體管理 - 嘯百川 - 嘯百川的部落格

如上圖,
1、分頁單元中,頁目錄是唯一的,它的地址放在CPU的cr3暫存器中,是進行地址轉換的開始點。萬里長征就從此長始了。
2、每一個活動的程序,因為都有其獨立的對應的虛似記憶體(頁目錄也是唯一的),那麼它也對應了一個獨立的頁目錄地址。——執行一個程序,需要將它的頁目錄地址放到cr3暫存器中,將別個的儲存下來。
3、每一個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 + 2^10 * 4 + 2 ^10 * 2 ^10 *4) = 4100KB+4Byte)

值得一提的是,雖然頁目錄和頁表中的項,都是4個位元組,32位,但是它們都只用高20位,低12位遮蔽為0——把頁表的低12遮蔽為0,是很好理解的,因 為這樣,它剛好和一個頁面大小對應起來,大家都成整數增加。計算起來就方便多了。但是,為什麼同時也要把頁目錄低12位遮蔽掉呢?因為按同樣的道理,只要 遮蔽其低10位就可以了,不過我想,因為12>10,這樣,可以讓頁目錄和頁表使用相同的資料結構,方便。

本貼只介紹一般性轉換的原理,擴充套件分頁、頁的保護機制、PAE模式的分頁這些麻煩點的東東就不囉嗦了……可以參考其它專業書籍。
 
 
 
 Win32通過一個兩層的表結構來實現地址對映,因為每個程序都擁有私有的4G的虛擬記憶體空間,相應的,每個程序都有自己的層次表結構來實現其地址對映。
      第一層稱為頁目錄,實際就是一個記憶體頁,Win32的記憶體頁有4KB大小,這個記憶體頁以4個位元組分為1024項,每一項稱為“頁目錄項”(PDE);
      第二層稱為頁表,這一層共有1024個頁表,頁表結構與頁目錄相似,每個頁表也都是一個記憶體頁,這個記憶體頁以4KB的大小被分為1024項,頁表的每一項被稱為頁表項(PTE),易知共有1024×1024個頁表項。每一個頁表項對應一個實體記憶體中的某一個“記憶體頁”,即共有1024×1024個實體記憶體頁,每個實體記憶體頁為4KB,這樣就可以索引到4G大小的虛擬實體記憶體。
如下圖所示(注下圖中的頁目錄項和頁表項的大小應該是4個位元組,而不是4kB):

      Win32提供了4GB大小的虛擬地址空間。因此每個虛擬地址都是一個32位的整數值,也就是我們平時所說的指標,即指標的大小為4B。它由三部分組成,如下圖:

      這三個部分的第一部分,即前10位為頁目錄下標,用來定址頁目錄項,頁目錄項剛好1024個。找到頁目錄項後,找對頁目錄項對應的的頁表。第二部分則是用來在頁表內定址,用來找到頁表項,共有1024個頁表項,通過頁表項找到實體記憶體頁。第三部分用來在實體記憶體頁中找到對應的位元組,一個頁的大小是4KB,12位剛好可以滿足定址要求。
具體的例子:
假設一個執行緒正在訪問一個指標(Win32的指標指的就是虛擬地址)指向的資料,此指標指為0x2A8E317F,下圖表示了這一個過程:

0x2A8E317F的二進位制寫法為0010101010_0011100011_000101111111,為了方便我們把它分為三個部分。
首先按照0010101010定址,找到頁目錄項。因為一個頁目錄項為4KB,那麼先將0010101010左移兩位,001010101000(0x2A8),用此下標找到頁目錄項,然後根據此頁目錄項定位到下一層的某個頁表。
然後按照0011100011定址,在上一步找到頁表中尋找頁表項。定址方法與上述方法類似。找到頁表項後,就可以找到對應的實體記憶體頁。
最後按照000101111111定址,尋找頁內偏移。
      上面的假設的是此資料已在實體記憶體中,其實判斷訪問的資料是否在記憶體中也是在地址對映過程中完成的。Win32系統總是假設資料已在實體記憶體中,並進行地址對映。頁表項中有一位標誌位,用來標識包含此資料的頁是否在實體記憶體中,如果在的話,就直接做地址對映,否則,丟擲缺頁中斷,此時頁表項也可標識包含此資料的頁是否在調頁檔案中(外存),如果不在則訪問違例,程式將會退出,如果在,頁表項會查出此資料頁在哪個調頁檔案中,然後將此資料頁調入實體記憶體,再繼續進行地址對映。為了實現每個程序擁有私有4G的虛擬地址空間,也就是說每個程序都擁有自己的頁目錄和頁表結構,對不同程序而言,即使是相同的指標(虛擬地址)被不同的程序對映到的實體地址也是不同的,這也意味著在程序之間傳遞指標是沒有意義的。


Linux的頁式記憶體管理
原理上來講,Linux只需要為每個程序分配好所需資料結構,放到記憶體中,然後在排程程序的時候,切換暫存器cr3,剩下的就交給硬體來完成了(呵呵,事實上要複雜得多,不過偶只分析最基本的流程)。

前面說了i386的二級頁管理架構,不過有些CPU,還有三級,甚至四級架構,Linux為了在更高層次提供抽像,為每個CPU提供統一的介面。提供了一個四層頁管理架構,來相容這些二級、三級、四級管理架構的CPU。這四級分別為:

頁全域性目錄PGD(對應剛才的頁目錄)
頁上級目錄PUD(新引進的)
頁中間目錄PMD(也就新引進的)
頁表PT(對應剛才的頁表)。

整個轉換依據硬體轉換原理,只是多了二次陣列的索引罷了,如下圖:

Windows記憶體管理 - 嘯百川 - 嘯百川的部落格

那麼,對於使用二級管理架構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,就是最終的實體地址了。
 
 
五. 儲存方式
保護模式現代作業系統的基礎,理解他是我們要翻越的第一座山。保護模式是相對真實模式而言的,他們是處理器的兩種工作方式。很久以前大家使用的dos就是執行在真實模式下,而現在的windows作業系統則是執行在保護模式下。兩種執行模式有著較大的不同,
真實模式由於是由8086/8088發展而來因此他更像是一個執行微控制器的簡單模式,計算機啟動後首先進入的就是真實模式,通過8086/8088只有20根地址線所以它的定址範圍只有2的20次冪,即1M。記憶體的訪問方式就是我們熟悉的seg:offset邏輯地址方式,例如我們給出地址邏輯地址它將在cpu內轉換為20的實體地址,即將seg左移4位再加上offset值。例如地址1000h:5678h,則實體地址為10000h+5678h=15678h。真實模式在後續的cpu中被保留了下來,但真實模式的侷限性是很明顯的,由於使用seg:offset邏輯地址只能訪問1M多一點的記憶體空間,在擁有32根地址線的cpu中訪問1M以上的空間則變得很困難。而且隨著計算機的不斷髮展真實模式的工作方式越來越不能滿足計算機對資源(儲存資源和cpu資源等等)的管理,由此產生了新的管理方式——保護模式。
80386及以上的處理器功能要大大超過其先前的處理器,但只有在保護模式下,處理器才能發揮作用。在保護模式下,全部32根地址線有效,可定址4G的實體地址空間;擴充的儲存分段機制和可選的儲存器分頁機制,不僅為儲存器共享和保護提供了硬體支援,而且為實現虛擬儲存器提供了硬體支援;支援多工;4個特權級和完善的特權級檢查機制,實現了資料的安全和保密。計算機啟動後首先進入的就是真實模式,通過設定相應的暫存器才能進入保護模式(以後介紹)。保護模式是一個整體的工作方式,但分步討論由淺入深更利於學習。

儲存方式主要體現在記憶體訪問方式上,由於相容和IA32框架的限制,保護模式在記憶體訪問上延用了真實模式下的seg:offset的形式(即:邏輯地址),其實seg:offset的形式在保護模式下只是一個軀殼,內部的儲存方式與真實模式截然不同。在保護模式下邏輯地址並不是直接轉換為實體地址,而是將邏輯地址首先轉換為線性地址,再將線性地址轉換為實體地址。

線性地址是個新概念,但大家不要把它想的過於複雜,簡單的說他就是0000000h~ffffffffh(即0~4G)的線性結構,是32個bite位能表示的一段連續的地址,但他是一個概念上的地址,是個抽象的地址,並不存在在現實之中。線性地址地址主要是為分頁機制而產生的。處理器在得到邏輯地址後首先通過分段機制轉換為線性地址,線性地址再通過分頁機制轉換為實體地址最後讀取資料。
 
分段機制是必須的,分頁機制是可選的,當不使用分頁的時候線性地址將直接對映為實體地址,設立分頁機制的目的主要是為了實現虛擬儲存(分頁機制在後面介紹)。先來介紹一下分段機制,以下文字是介紹如何由邏輯地址轉換為線性地址。
分段機制在保護模式中是不能被繞過得,回到我們的seg:offset地址結構,在保護模式中seg有個新名字叫做“段選擇子”(seg..selector)。段選擇子、GDT、LDT構成了保護模式的儲存結構,GDT、LDT分別叫做全域性描述符表和區域性描述符表,描述符表是一個線性表(陣列),表中存放的是描述符。
“描述符”是保護模式中的一個新概念,它是一個8位元組的資料結構,它的作用主要是描述一個段(還有其他作用以後再說),用描述表中記錄的段基址加上邏輯地址(sel:offset)的offset轉換成線性地址。描述符主要包括三部分:段基址(Base)、段限制(Limit)、段屬性(Attr)。一個任務會涉及多個段,每個段需要一個描述符來描述,為了便於組織管理,80386及以後處理器把描述符組織成表,即描述符表。在保護模式中存在三種描述符表 “全域性描述符表”(GDT)、“區域性描述符表”(LDT)和中斷描述符表(IDT)(IDT在以後討論)。
(1)全域性描述符表GDT(Global Descriptor Table)在整個系統中,全域性描述符表GDT只有一張,GDT可以被放在記憶體的任何位置,但CPU必須知道GDT的入口,也就是基地址放在哪裡,Intel的設計者門提供了一個暫存器GDTR用來存放GDT的入口地址,程式設計師將GDT設定在記憶體中某個位置之後,可以通過LGDT指令將GDT的入口地址裝入此積存器,從此以後,CPU就根據此暫存器中的內容作為GDT的入口來訪問GDT了。GDTR中存放的是GDT在記憶體中的基地址和其表長界限。

(2)段選擇子(Selector)由GDTR訪問全域性描述符表是通過“段選擇子”(真實模式下的段暫存器)來完成的,如圖三①步。段選擇子是一個16位的暫存器(同真實模式下的段暫存器相同)

段選擇子包括三部分:描述符索引(index)、TI、請求特權級(RPL)。他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由這個位置再根據在GDTR中儲存的描述符表基址就可以找到相應的描述符(如圖三①步)。然後用描述符表中的段基址加上邏輯地址(SEL:OFFSET)的OFFSET就可以轉換成線性地址(如圖三②步),段選擇子中的TI值只有一位0或1,0代表選擇子是在GDT選擇,1代表選擇子是在LDT選擇。請求特權級(RPL)則代表選擇子的特權級,共有4個特權級(0級、1級、2級、3級)。例如給出邏輯地址:21h:12345678h轉換為線性地址
a. 選擇子SEL=21h=0000000000100 0 01b 他代表的意思是:選擇子的index=4即100b選擇GDT中的第4個描述符;TI=0代表選擇子是在GDT選擇;左後的01b代表特權級RPL=1
b. OFFSET=12345678h若此時GDT第四個描述符中描述的段基址(Base)為11111111h,則線性地址=11111111h+12345678h=23456789h
(3)區域性描述符表LDT(Local Descriptor Table)區域性描述符表可以有若干張,每個任務可以有一張。我們可以這樣理解GDT和LDT:GDT為一級描述符表,LDT為二級描述符表。如圖五

LDT和GDT從本質上說是相同的,只是LDT巢狀在GDT之中。LDTR記錄區域性描述符表的起始位置,與GDTR不同LDTR的內容是一個段選擇子。由於LDT本身同樣是一段記憶體,也是一個段,所以它也有個描述符描述它,這個描述符就儲存在GDT中,對應這個表述符也會有一個選擇子,LDTR裝載的就是這樣一個選擇子。LDTR可以在程式中隨時改變,通過使用lldt指令。如圖五,如果裝載的是Selector 2則LDTR指向的是表LDT2。舉個例子:如果我們想在表LDT2中選擇第三個描述符所描述的段的地址12345678h。
1. 首先需要裝載LDTR使它指向LDT2 使用指令lldt將Select2裝載到LDTR
2. 通過邏輯地址(SEL:OFFSET)訪問時SEL的index=3代表選擇第三個描述符;TI=1代表選擇子是在LDT選擇,此時LDTR指向的是LDT2,所以是在LDT2中選擇,此時的SEL值為1Ch(二進位制為11 1 00b)。OFFSET=12345678h。邏輯地址為1C:12345678h
3. 由SEL選擇出描述符,由描述符中的基址(Base)加上OFFSET可得到線性地址,例如基址是11111111h,則線性地址=11111111h+12345678h=23456789h
4. 此時若再想訪問LDT1中的第三個描述符,只要使用lldt指令將選擇子Selector 1裝入再執行2、3兩步就可以了(因為此時LDTR又指向了LDT1)
由於每個程序都有自己的一套程式段、資料段、堆疊段,有了區域性描述符表則可以將每個程序的程式段、資料段、堆疊段封裝在一起,只要改變LDTR就可以實現對不同程序的段進行訪問。
儲存方式是保護模式的基礎,學習他主要注意與真實模式下的儲存模式的對比,總的思想就是首先通過段選擇子在描述符表中找到相應段的描述符,根據描述符中的段基址首先確定段的位置,再通過OFFSET加上段基址計算出線性地址。

相關推薦

Windows 記憶體Windows記憶體管理

本文主要內容: 1.基本概念:實體記憶體、虛擬記憶體;實體地址、虛擬地址、邏輯地址;頁目錄,頁表 2.Windows記憶體管理 3.CPU段式記憶體管理 4.CPU頁式記憶體管理   一、基本概念 1. 兩個記憶體概念 實體記憶體:人盡皆知,就是插在主機板上的記憶體條。他

Windows記憶體OD記憶體斷點初步分析

記憶體斷點原理:        記憶體斷點原理,通過將記憶體斷點所在記憶體頁的屬性修改為記憶體斷點屬性(non-access or non-writable),程式執行時,對目標記憶體頁中所有資料的訪問或寫,都會丟擲異常,OD通過截獲此異常,然後對比,儲存在某一記憶體的

Redis底層 記憶體管理

一、記憶體分配概述         redis 的記憶體分配,實質上是對 tcmalloc / jemalloc 的封裝。記憶體分配本質就是給定需要分配的大小,以位元組為單位,然後返回一個指向一段分配好的連續的記憶體空間的首指標。   &n

Java 多線程------線程的同步

alt 來看 監聽 介紹 創建進程 java 多線程 system ima 關鍵字 Java 多線程詳解(一)------概念的引入:http://www.cnblogs.com/ysocean/p/6882988.html Java 多線程詳解(二)------如何創建進

elastic-job:Job的手動觸發功能

方法 idt image blog per tle cnblogs ack display elastic-job的任務都是使用quartz來觸發的,quartz表達式一般都是定期執行。但有時候一些周期較長的任務,比如一天一次,幾小時一次的任務,我們需要等待很久才能觸發一次

10.5-全棧Java筆記:常見流

java上節我們講到「Java中常用流:緩沖流」,本節我們學習數據流和對象流~ 數據流數據流將“基本數據類型變量”作為數據源,從而允許程序以與機器無關方式從底層輸入輸出流中操作java基本數據類型。 DataInputStream和DataOutputStream提供了可以存取與機器無關的所有Java基礎類

SpringMVC------基於註解的入門實例

frame hello text 1.0 har ret doc 4.0 進行   前兩篇博客我們講解了基於XML 的入門實例,以及SpringMVC運行的詳細流程。但是我們發現基於 XML 的配置還是比較麻煩的,而且,每個 Handler 類只能有一個方法,在實際開發中肯

Maven------ Maven工程目錄介紹

詳細講解 com tid pom.xml imp 工程目錄 在哪裏 根據 cat   上一章我們配置並安裝好了 Maven,那麽這一章我們介紹如何用eclipse創建一個 Maven 工程,然後介紹 Maven 工程的目錄結構。 1、eclipse 創建 Maven 工程

C++: I/O流——串流

name namespace 轉換 pac end 成員 col logs nbsp 一、串流 串流類是 ios 中的派生類 C++的串流對象可以連接string對象或字符串 串流提取數據時對字符串按變量類型解釋;插入數據時把類型 數據轉換成字符串 串流I/O具有格式化功能

Zookeeper:Zookeeper中的Znode特性

zookeeper數據模型 znode 節點數據 數據模型ZK擁有一個命名空間就像一個精簡的文件系統,不同的是它的命名空間中的每個節點擁有它自己或者它下面子節點相關聯的數據。ZK中必須使用絕對路徑也就是使用“/”開頭。Znode:ZK目錄樹中每個節點對應一個Znode。每個Znode維護這一個屬性

Splay

.html rotate cqoi2014 org tps 線段樹 .cn html highlight 前言 上一節我們學習了splay所能解決的基本問題,這節我來講一下splay怎麽搞區間問題 實現 splay搞區間問題非常簡單,比如我們要在區間$l,r$上搞事情

編碼原理---量化

進一步 mark 新的 dct 說明 一點 註意 cto water 本節開始介紹編碼過程中的量化環節。還記得上一篇的變換嗎?變換之後得到了一個新的矩陣,一個經過從空域變換到頻域的一個矩陣。那麽,量化呢,就是基於變換後得到的矩陣,再做進一步的處理,本質也就是進一步的壓縮。

大數據入門第八天——MapReduce

大數 blog eve 分享圖片 shuf open src hid span 1/mr的combiner 2/mr的排序 3/mr的shuffle 4/mr與yarn 5/mr運行模式 6/mr實現join 7/mr全局圖

常見圖片格式---JPEG

JPEG 圖片格式 編碼 解碼 壓縮 JPEG簡介 JPEG是一種比較成熟的有損的圖像壓縮格式,經過JPEG壓縮,圖像質量會有所損失,但是,人眼不容易分辨出來這種差別。jpeg圖像在質量和存儲空間得到了一個相對平衡的狀態。不過jpeg文件在組織方式上略顯復雜,詳細請向下看。 JPEG文

JavaScript的事件、DOM模型、事件流模型以及內置對象

dde function n) 事件冒泡 字符 nds rep == 防止 JS中的事件 JS中的事件分類   1.鼠標事件:     click/dbclick/mouseover/mouseout   2.HTML事件:     onload/onunload

Redis

redis codis twemproxy redis集群 redis-trib.rb 一、Redis集群介紹 Clustering:redis 3.0之後進入生產環境分布式數據庫,通過分片機制來進行數據分布,clustering 內的每個節點,僅有數據庫的一部分數據;去中心化的集群:re

CentOS 7.4 Tengine安裝配置

location、echo、fancy九、根據HTTP響應狀態碼自定義錯誤頁:1、未配置前訪問一個不存在的頁面:http://192.168.1.222/abc/def.html,按F12後刷新頁面2、在server{}配置段中新增如下location:server {listen 80;server_nam

Keepalived

集群 scrip 網絡異常 可用 size ont 監控 spa 就是 Keepalived基礎功能應用實例: 1.Keepalived基礎HA功能演示: 在默認情況下,Keepalived可以實現對系統死機、網絡異常及Keepalived本身進

HAProxy

客戶端 apr centos watermark ges -o text acl 方式 一.基於虛擬主機的HAProxy負載均衡系統配置實例 1.通過HAProxy的ACL規則配置虛擬主機: 下面將通過HAProxy的ACL功能配置一套基於虛擬主

HAProxy:基於虛擬主機的HAProxy負載均衡系統配置實例【轉】

ise onf sysconf proxy配置 ffffff 規則設置 library 版本信息 論壇 一.基於虛擬主機的HAProxy負載均衡系統配置實例 1.通過HAProxy的ACL規則配置虛擬主機: 下面將通過HAProxy的AC