程序地址空間與虛擬儲存空間的理解
在進入正題前先來談談作業系統記憶體管理機制的發展歷程,瞭解這些有利於我們更好的理解目前作業系統的記憶體管理機制。
一 早期的記憶體分配機制
在 早期的計算機中,要執行一個程式,會把這些程式全都裝入記憶體,程式都是直接執行在記憶體上的,也就是說程式中訪問的記憶體地址都是實際的實體記憶體地址。當計算機同時執行多個程式時,必須保證這些程式用到的記憶體總量要小於計算機實際實體記憶體的大小。那當程式同時執行多個程式時,作業系統是如何為這些程式分配記憶體 的呢?下面通過例項來說明當時的記憶體分配方法:
某臺計算機總的記憶體大小是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禁入區、核心區。應用程式能使用的只是使用者區而已,大約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頁了。
總結:
32位的CPU的定址空間是4G,所以虛擬記憶體的最大值為4G,而windows作業系統把這4G分成2部分,即2G的使用者空間和2G的系統空間,系統空間是各個程序所共享的,他存放的是作業系統及一些核心物件等,而使用者空間是分配給各個程序使用的,使用者空間包括用:程式程式碼和資料,堆,共享庫,棧。