1. 程式人生 > >程式地址空間:虛擬地址原理及發展過程(圖解說明)

程式地址空間:虛擬地址原理及發展過程(圖解說明)

目錄

 

簡單理解的空間佈局圖及驗證:

早期的記憶體管理機制

分段

分頁


簡單理解的空間佈局圖及驗證:

用兩段程式碼測試一下:

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 int val = 100;
  5 int main()
  6 {
  7     pid_t pid = fork();
  8     if(pid < 0)
  9     {
 10         perror("fork");
            return -1;
 11     }
 12     if(pid == 0)
 13     {
 14         printf("child:[%d],%d,%p\n",getpid(),val,&val);
 15     }
 16     else
 17     {
 18         printf("parent:[%d],%d,%p\n",getpid(),val,&val);
 19     }
 20     sleep(1);
 21     return 0;
 22 }

輸出來的變數地址一模一樣,因為子程序按照父程序為模板,父子程序並沒有對變數進行修改

 1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 int val = 100;
  5 int main()
  6 {
  7     pid_t pid = fork();
  8     if(pid < 0)
  9     {
 10         perror("fork");
 11         return -1;
 12     }
 13     if(pid == 0)
 14     {
 15         val = 0;
 16         printf("child:[%d],%d,%p\n",getpid(),val,&val);
 17     }
 18     else
 19     {
 20         sleep(3);
 21         printf("parent:[%d],%d,%p\n",getpid(),val,&val);
 22     }
 23     return 0;
 24 }

父子程序,輸出地址一樣,但是變數內容不一樣

  1. 變數的內容不一樣,所以父子程序輸出的變數不是同一個
  2. 但是地址一樣,說明這絕對不是實體地址
  3. 這是虛擬地址,我們所看到的地址全是虛擬地址,實體地址使用者一概看不到,由OS統一管
  4. OS也必須負責將虛擬地址轉化為實體地址

早期的記憶體管理機制

早期的計算機中要執行一個程式,會把這寫程式直接執行在記憶體上,也就是說程式訪問的記憶體地址都是實際的實體記憶體地址。

當計算機要同時執行多個程式時,必須保證這些程式用到的記憶體總量要小於實際的實體記憶體。

記憶體分配是例項:

  1. 要執行一個程式時,會把這些程式全部裝入庫內
  2. 當計算機要同時執行多個程式時,必須保證這些程式用到的記憶體總量要小於實際的實體記憶體
  3. 程序地址空間不隔離,由於程式都是直接訪問實體記憶體,所以惡意程式可以隨意修改別的程序記憶體資料
  4. 效率低,如果剩下的空間不夠,系統只能將一個執行的程式資料暫時拷貝的硬碟上,釋放出空間才能使用
  5. 執行地址不確定,當剩餘空間滿足程式要求後,系統會在剩餘空間中隨機分配一段連續的空間給程式使用,因為是隨機的,所以不確定地址(程式的起始地址都是實體地址,而實體地址都是在載入後才能確定)

分段

為 瞭解決上述問題,人們想到了一種方法,就是增加一箇中間層,利用一種間接的地址訪問方法訪問實體記憶體。按照這種方法,程式中訪問的記憶體地址不再是實際的實體記憶體地址,而是一個虛擬地址,然後由作業系統將這個虛擬地址對映到適當的實體記憶體地址上。這樣,只要作業系統處理好虛擬地址到實體記憶體地址的映 射,就可以保證不同的程式最終訪問的記憶體地址位於不同的區域,沒有重疊,達到記憶體地址空間隔離的效果。

當建立一個程序時,作業系統會為該程序分配一個4GB的虛擬程序空間地址

(在32位作業系統中,一個指標長度4位元組,定址能力為:0x00000000~0xFFFFFFFF,最大值位4GB的容量)

虛擬地址空間對應的實體地址空間,當建立程序時,每個程序都會有一個自己的4GB虛擬地址空間,(每個程序只能訪問自己的虛擬地址空間中的資料,從而實現了地址隔離)

在windows中,這個虛擬地址被分成了四部分:NULL指標區,使用者區,64KB禁入區,核心區(應用程式只能使用使用者區,2GB左右(最大可以調整成3GB),核心區為2GB,核心區儲存的是系統執行緒排程,記憶體管理,裝置驅動等資料,這部分資料供所有的程序共享,但是應用程式不能直接訪問)


分段的思想就是在虛擬地址空間和實體地址空間一一對映(比如說虛擬地址空間中某個10M 大小的空間對映到實體地址空間中某個10M 大小的空間,可以多對一,就是說可虛擬地址的多個10M大小的空間可以對映到相同的實體地址空間的某個10M)作業系統保證不同的程序地址空間被對映到實體地址空間中不同的區域,這樣每個程序最終訪問到的實體地址空間都是彼此分開的,通過這種方式實現了程序間的地址隔離

假設有兩個程序 A 和 B ,程序 A 所需記憶體大小為 10M ,其虛擬地址空間分佈在 0x00000000 到 0x00A00000 ,程序 B 所需記憶體為 100M ,其虛擬地址空間分佈為 0x00000000 到 0x06400000 。那麼按照分段的對映方法,程序 A 在實體記憶體上對映區域為 0x00100000 到 0x00B00000 ,,程序 B 在實體記憶體上對映區域為 0x00C00000 到 0x07000000 。於是程序 A 和程序 B 分別被對映到了不同的記憶體區間,彼此互不重疊,實現了地址隔離。從應用程式的角度看來,程序 A 的地址空間就是分佈在 0x00000000 到 0x00A00000 ,在做開發時,開發人員只需訪問這段區間上的地址即可。應用程式並不關心程序 A 究竟被對映到實體記憶體的那塊區域上了,所以程式的執行地址也就是相當於說是確定的了。(也即是說虛擬地址雖然重複,但是其對映的實體地址是唯一的)

分段的對映方式雖然解決的部分問題,但是還存在記憶體使用的效率問題,再分段對映中,每次換入換出的記憶體都是整個程式,會造成大量的磁碟訪問操作,導致效率低下。

實際上程式執行有區域性的特性,在某個時間內,只訪問程式的一小部分資料,也就是說大部分在一段時間內不會被用到,所以人們想到了粒度更小的記憶體分隔和對映的方法----分頁

分頁

分頁的基本方法是將地址空間分成許多頁,每頁的大小有cpu決定,然後由作業系統選擇頁的大小。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 頁了。