天天看點

【Linux系統程式設計】程序位址空間和虛拟位址空間

00. 目錄

文章目錄

    • 01. 早期的記憶體配置設定機制
    • 02. 分段
    • 03. 分頁
    • 04. 位址比較
    • 05. 附錄

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

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

某台計算機總的記憶體大小是 128M ,現在同時運作兩個程式 A 和 B , A 需占用記憶體 10M , B 需占用記憶體 110 。計算機在給程式配置設定記憶體時會采取這樣的方法:先将記憶體中的前 10M 配置設定給程式 A ,接着再從記憶體中剩餘的 118M 中劃分出 110M 配置設定給程式 B 。這種配置設定方法可以保證程式 A 和程式 B 都能運作,但是這種簡單的記憶體配置設定政策問題很多。

【Linux系統程式設計】程式位址空間和虛拟位址空間

早期的記憶體配置設定方法

問題 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 究竟被映射到實體記憶體的那塊區域上了,是以程式的運作位址也就是相當于說是确定的了。 下圖顯示的是分段方式的記憶體映射方法:

【Linux系統程式設計】程式位址空間和虛拟位址空間

分段方式的記憶體映射方法

這種分段的映射方法雖然解決了上述中的問題一和問題三,但并沒能解決問題二,即記憶體的使用效率問題。在分段的映射方法中,每次換入換出記憶體的都是整個程式, 這樣會造成大量的磁盤通路操作,導緻效率低下。是以這種映射方法還是稍顯粗糙,粒度比較大。實際上,程式的運作有局部性特點,在某個時間段内,程式隻是通路程式的一小部分資料,也就是說,程式的大部分資料在一個時間段内都不會被用到。基于這種情況,人們想到了粒度更小的記憶體分割和映射方法,這種方法就是分頁 (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根位址總線尋址)。

繼續閱讀