1. 程式人生 > >Linux系統程式設計——淺談程序地址空間與虛擬儲存空間

Linux系統程式設計——淺談程序地址空間與虛擬儲存空間

早期的記憶體分配機制

在早期的計算機中,要執行一個程式,會把這些程式全都裝入記憶體,程式都是直接執行在記憶體上的,也就是說程式中訪問的記憶體地址都是實際的實體記憶體地址。當計算機同時執行多個程式時,必須保證這些程式用到的記憶體總量要小於計算機實際實體記憶體的大小。

那當程式同時執行多個程式時,作業系統是如何為這些程式分配記憶體 的呢?下面通過例項來說明當時的記憶體分配方法:

某臺計算機總的記憶體大小是 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 的地址空間是“虛擬”的,並不是真實存在的,而且每個程序只能訪問自己虛擬地址空間中的資料,無法訪問別的程序中的資料,通過這種方法實現了程序間的地址隔離(解決問題1)。那是不是這 4GB 的虛擬地址空間應用程式可以隨意使用呢?很遺憾,在 Windows 系統下,這個虛擬地址空間被分成了 4 部分: NULL 指標區、使用者區、 64KB 禁入區、核心區。

1)NULL指標區 (0x00000000~0x0000FFFF): 如果程序中的一個執行緒試圖操作這個分割槽中的資料,CPU就會引發非法訪問。他的作用是,呼叫 malloc 等記憶體分配函式時,如果無法找到足夠的記憶體空間,它將返回 NULL。而不進行安全性檢查。它只是假設地址分配成功,並開始訪問記憶體地址 0x00000000(NULL)。由於禁止訪問記憶體的這個分割槽,因此會發生非法訪問現象,並終止這個程序的執行。


2)使用者模式分割槽 ( 0x00010000~0xBFFEFFFF):這個分割槽中存放程序的私有地址空間。一個程序無法以任何方式訪問另外一個程序駐留在這個分割槽中的資料 (相同 exe,通過 copy-on-write 來完成地址隔離)。(在windows中,所有 .exe 和動態連結庫都載入到這一區域。系統同時會把該程序可以訪問的所有記憶體對映檔案對映到這一分割槽)。


3)隔離區 (0xBFFF0000~0xBFFFFFFF):這個分割槽禁止進入。任何試圖訪問這個記憶體分割槽的操作都是違規的。微軟保留這塊分割槽的目的是為了簡化作業系統的現實。


4)核心區 (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的段機制自動轉換的。如果沒有開啟分頁管理,則線性地址就是實體地址。如果開啟了分頁管理,那麼系統程式需要參和線性地址到實體地址的轉換過程。具體是通過設定頁目錄表和頁表項進行的。