期的內存分配機制
在早期的計算機中,要運行一個程序,會把這些程序全都裝入內存,程序都是直接運行在內存上的,也就是說程序中訪問的內存地址都是實際的物理內存地址。當計算機同時運行多個程序時,必須保證這些程序用到的內存總量要小於計算機實際物理內存的大小。
那當程序同時運行多個程序時,操作系統是如何為這些程序分配內存 的呢?下面通過實例來說明當時的內存分配方法:
某台計算機總的內存大小是 128M ,現在同時運行兩個程序 A 和 B , A 需占用內存 10M , B 需占用內存 110 。計算機在給程序分配內存時會采取這樣的方法:先將內存中的前 10M 分配給程序 A ,接着再從內存中剩余的 118M 中划分出 110M 分配給程序 B 。這種分配方法可以保證程序 A 和程序 B 都能運行,但是這種簡單的內存分配策略問題很多。
早期的內存分配方法
問題 1 :進程地址空間不隔離。由於程序都是直接訪問物理內存,所以惡意程序可以隨意修改別的進程的內存數據,以達到破壞的目的。有些非惡意的,但是有 bug 的程序也可能不小心修改了其它程序的內存數據,就會導致其它程序的運行出現異常。這種情況對用戶來說是無法容忍的,因為用戶希望使用計算機的時候,其中一個任務失敗了,至少不能影響其它的任務。
問題 2 :內存使用效率低。在 A 和 B 都運行的情況下,如果用戶又運行了程序 C,而程序 C 需要 20M 大小的內存才能運行,而此時系統只剩下 8M 的空間可供使用,所以此時系統必須在已運行的程序中選擇一個將該程序的數據暫時拷貝到硬盤上,釋放出部分空間來供程序 C 使用,然后再將程序 C 的數據全部裝入內存中運行。可以想象得到,在這個過程中,有大量的數據在裝入裝出,導致效率十分低下。
問題 3 :程序運行的地址不確定。當內存中的剩余空間可以滿足程序 C 的要求后,操作系統會在剩余空間中隨機分配一段連續的 20M 大小的空間給程序 C 使用,因為是隨機分配的,所以程序運行的地址是不確定的。
分段
為 了解決上述問題,人們想到了一種變通的方法,就是增加一個中間層,利用一種間接的地址訪問方法訪問物理內存。按照這種方法,程序中訪問的內存地址不再是實際的物理內存地址,而是一個虛擬地址,然后由操作系統將這個虛擬地址映射到適當的物理內存地址上。這樣,只要操作系統處理好虛擬地址到物理內存地址的映射,就可以保證不同的程序最終訪問的內存地址位於不同的區域,彼此沒有重疊,就可以達到內存地址空間隔離的效果。
當創建一個進程時,操作系統會為該進程分配一個 4GB 大小的虛擬進程地址空間。之所以是 4GB ,是因為在 32 位的操作系統中,一個指針長度是 4 字節,而 4 字節指針的尋址能力是從 0x00000000~0xFFFFFFFF,最大值 0xFFFFFFFF 表示的即為 4GB 大小的容量。與虛擬地址空間相對的,還有一個物理地址空間,這個地址空間對應的是真實的物理內存。如果你的計算機上安裝了 512M 大小的內存,那么這個物理地址空間表示的范圍是 0x00000000~0x1FFFFFFF 。當操作系統做虛擬地址到物理地址映射時,只能映射到這一范圍,操作系統也只會映射到這一范圍。當進程創建時,每個進程都會有一個自己的 4GB 虛擬地址空間。要注意的是這個 4GB 的地址空間是“虛擬”的,並不是真實存在的,而且每個進程只能訪問自己虛擬地址空間中的數據,無法訪問別的進程中的數據,通過這種方法實現了進程間的地址隔離。那是不是這 4GB 的虛擬地址空間應用程序可以隨意使用呢?很遺憾,在 Windows 系統下,這個虛擬地址空間被分成了 4 部分: NULL 指針區、用戶區、 64KB 禁入區、內核區。
1)NULL指針區 (0x00000000~0x0000FFFF): 如果進程中的一個線程試圖操作這個分區中的數據,CPU就會引發非法訪問。他的作用是,調用 malloc 等內存分配函數時,如果無法找到足夠的內存空間,它將返回 NULL。而不進行安全性檢查。它只是假設地址分配成功,並開始訪問內存地址 0x00000000(NULL)。由於禁止訪問內存的這個分區,因此會發生非法訪問現象,並終止這個進程的運行。
2)用戶模式分區 ( 0x00010000~0xBFFEFFFF):這個分區中存放進程的私有地址空間。一個進程無法以任何方式訪問另外一個進程駐留在這個分區中的數據 (相同 exe,通過 copy-on-write 來完成地址隔離)。(在windows中,所有 .exe 和動態鏈接庫都載入到這一區域。系統同時會把該進程可以訪問的所有內存映射文件映射到這一分區)。
2)隔離區 (0xBFFF0000~0xBFFFFFFF):這個分區禁止進入。任何試圖訪問這個內存分區的操作都是違規的。微軟保留這塊分區的目的是為了簡化操作系統的現實。
3)內核區 (0xC0000000~0xFFFFFFFF):這個分區存放操作系統駐留的代碼。線程調度、內存管理、文件系統支持、網絡支持和所有設備驅動程序代碼都在這個分區加載。這個分區被所有進程共享。
應用程序能使用的只是用戶區而已,大約 2GB 左右 ( 最大可以調整到 3GB) 。內核區為 2GB ,內核區保存的是系統線程調度、內存管理、設備驅動等數據,這部分數據供所有的進程共享,但應用程序是不能直接訪問的。
人們之所以要創建一個虛擬地址空間,目的是為了解決進程地址空間隔離的問題。但程序要想執行,必須運行在真實的內存上,所以,必須在虛擬地址與物理地址間建立一種映射關系。這樣,通過映射機制,當程序訪問虛擬地址空間上的某個地址值時,就相當於訪問了物理地址空間中的另一個值。人們想到了一種分段(Sagmentation) 的方法,它的思想是在虛擬地址空間和物理地址空間之間做一一映射。比如說虛擬地址空間中某個 10M 大小的空間映射到物理地址空間中某個 10M 大小的空間。這種思想理解起來並不難,操作系統保證不同進程的地址空間被映射到物理地址空間中不同的區域上,這樣每個進程最終訪問到的。
物理地址空間都是彼此分開的。通過這種方式,就實現了進程間的地址隔離。還是以實例說明,假設有兩個進程 A 和 B ,進程 A 所需內存大小為 10M ,其虛擬地址空間分布在 0x00000000 到 0x00A00000 ,進程 B 所需內存為 100M ,其虛擬地址空間分布為 0x00000000 到 0x06400000 。那么按照分段的映射方法,進程 A 在物理內存上映射區域為 0x00100000 到 0x00B00000 ,,進程 B 在物理內存上映射區域為0x00C00000 到 0x07000000 。於是進程 A 和進程 B 分別被映射到了不同的內存區間,彼此互不重疊,實現了地址隔離。從應用程序的角度看來,進程 A 的地址空間就是分布在 0x00000000 到 0x00A00000 ,在做開發時,開發人員只需訪問這段區間上的地址即可。應用程序並不關心進程 A 究竟被映射到物理內存的那塊區域上了,所以程序的運行地址也就是相當於說是確定的了。 下圖顯示的是分段方式的內存映射方法:
分段方式的內存映射方法
這種分段的映射方法雖然解決了上述中的問題一和問題三,但並沒能解決問題二,即內存的使用效率問題。在分段的映射方法中,每次換入換出內存的都是整個程序, 這樣會造成大量的磁盤訪問操作,導致效率低下。所以這種映射方法還是稍顯粗糙,粒度比較大。實際上,程序的運行有局部性特點,在某個時間段內,程序只是訪問程序的一小部分數據,也就是說,程序的大部分數據在一個時間段內都不會被用到。基於這種情況,人們想到了粒度更小的內存分割和映射方法,這種方法就是分頁 (Paging) 。
分頁
分頁的基本方法是,將地址空間分成許多的頁。每頁的大小由 CPU 決定,然后由操作系統選擇頁的大小。目前 Inter 系列的 CPU 支持 4KB 或 4MB 的頁大小,而 PC上目前都選擇使用 4KB 。按這種選擇, 4GB 虛擬地址空間共可以分成 1048576 頁, 512M 的物理內存可以分為 131072 個頁。顯然虛擬空間的頁數要比物理空間的頁數多得多。
在分段的方法中,每次程序運行時總是把程序全部裝入內存,而分頁的方法則有所不同。分頁的思想是程序運行時用到哪頁就為哪頁分配內存,沒用到的頁暫時保留在硬盤上。當用到這些頁時再在物理地址空間中為這些頁分配內存,然后建立虛擬地址空間中的頁和剛分配的物理內存頁間的映射。
下面通過介紹一個可執行文件的裝載過程來說明分頁機制的實現方法。一個可執行文件 (PE 文件 ) 其實就是一些編譯鏈接好的數據和指令的集合,它也會被分成很多頁,在 PE 文件執行的過程中,它往內存中裝載的單位就是頁。當一個 PE 文件被執行時,操作系統會先為該程序創建一個 4GB 的進程虛擬地址空間。前面介紹過,虛擬地址空間只是一個中間層而已,它的功能是利用一種映射機制將虛擬地址空間映射到物理地址空間,所以,創建 4GB 虛擬地址空間其實並不是要真的創建空間,只是要創建那種映射機制所需要的數據結構而已,這種數據結構就是頁目和頁表。
當創建完虛擬地址空間所需要的數據結構后,進程開始讀取 PE 文件的第一頁。在PE 文件的第一頁包含了 PE 文件頭和段表等信息,進程根據文件頭和段表等信息,將 PE 文件中所有的段一一映射到虛擬地址空間中相應的頁 (PE 文件中的段的長度都是頁長的整數倍 ) 。這時 PE 文件的真正指令和數據還沒有被裝入內存中,操作系統只是據 PE 文件的頭部等信息建立了 PE 文件和進程虛擬地址空間中頁的映射關系而已。當 CPU 要訪問程序中用到的某個虛擬地址時,當 CPU 發現該地址並沒有相相關聯的物理地址時, CPU 認為該虛擬地址所在的頁面是個空頁面, CPU 會認為這是個頁錯誤 (Page Fault) , CPU 也就知道了操作系統還未給該 PE 頁面分配內存,CPU 會將控制權交還給操作系統。操作系統於是為該 PE 頁面在物理空間中分配一個頁面,然后再將這個物理頁面與虛擬空間中的虛擬頁面映射起來,然后將控制權再還給進程,進程從剛才發生頁錯誤的位置重新開始執行。由於此時已為 PE 文件的那個頁面分配了內存,所以就不會發生頁錯誤了。隨着程序的執行,頁錯誤會不斷地產生,操作系統也會為進程分配相應的物理頁面來滿足進程執行的需求。
分頁方法的核心思想就是當可執行文件執行到第 x 頁時,就為第 x 頁分配一個內存頁 y ,然后再將這個內存頁添加到進程虛擬地址空間的映射表中 , 這個映射表就相當於一個 y=f(x) 函數。應用程序通過這個映射表就可以訪問到 x 頁關聯的 y 頁了。
邏輯地址、線性地址、物理地址和虛擬地址的區別
邏輯地址(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的段機制自動轉換的。如果沒有開啟分頁管理,則線性地址就是物理地址。如果開啟了分頁管理,那么系統程式需要參和線性地址到物理地址的轉換過程。具體是通過設置頁目錄表和頁表項進行的。