程序地址空間
以32位機器為例
程式地址空間
地址空間描述的基本空間大小是位元組,每個位元組都要有為一的地址,所以在32位的機器下就會有2^32次方個地址,也就是4G的空間範圍。這些空間被劃分成為了一個個區域。範圍是 0x00000000 - 0xFFFFFFFF
我們知道,在一個程式中變數或函式的地址分佈情況如下圖所示:
那這些地址是實體地址嗎?我們用下面的程式碼來驗證一下。
#include <stdio.h>
#include <unistd.h>
int global_value = 100;
int main()
{
pid_t id = fork();
if (id < 0)
{
printf("error\n");
return 1;
}
else if(id == 0)
{
int cnt = 0;
while(1)
{
printf("子程序,pid:%d, ppid:%d | global_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
sleep(1);
cnt++;
if (cnt == 6)
{
global_value = 600;
printf("子程序更改了global_val!!!!!!\n");
}
}
}
else
{
while(1)
{
printf("父程序,pid:%d, ppid:%d | global_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
sleep(2);
}
}
sleep(1);
return 0;
}
執行結果
在子程序更改global_value
之前,他們的地址都是一樣的。這個現象很好理解,因為在建立子程序的時候,子程序是以父程序為模板建立出來的。那麼變數的地址當然是一樣的。但是當子程序修改了global_value
後,他們的值不一樣了,但是地址還是一樣的。這就有點反常了,多程序在讀取同一個地址的時候怎麼可能會出現不同的結果。那麼就說明這裡的地址就絕對不是實體地址,既然不是實體地址,那麼到底是什麼地址呢?請接著往下看。
虛擬地址
它是一種邏輯上的地址,並不直接對應實體記憶體中的實際儲存單元。這意味著不同程序可能使用相同的虛擬地址,但這些虛擬地址會被作業系統對映到不同的實體地址,從而實現程序之間的記憶體隔離。
在Linux中,虛擬地址本質上就是一個核心資料結構物件。如下圖:
其中start_code
,end_code
等表示了一個區域的開始和結束,透過這種方式來將4G的空間劃分為不同的區域。虛擬地址空間被程序的pcb
管理著,每個程序都會有一個程序pcb
,因此每個程序都會有一個虛擬地址空間(也就是程序地址空間),也就是說每個程序都認為自己獨享記憶體資源。
再來看下面這張圖
當程式在編譯形成可執行檔案的時候,就是按照虛擬地址空間的編址方式對程式碼和資料進行編址的。所以當可執行程式沒有被載入到記憶體的時候,他自己本身就有了一套地址。當載入到記憶體後,程式則又多出了一套實體地址。此時我們就有了兩套地址,一是:標識物理存在中程式碼和資料的地址,二是:在程式內部相互跳轉的時候用的虛擬地址。
由於程式的內部就有地址(虛擬地址),當CPU在執行我們的程式的時候,它所使用的就是虛擬地址。它透過虛擬地址找到對應在mm_struct
的區域,再透過頁表對映的方式找到程式碼和資料實際存放實體記憶體的位置。從這可以看出CPU從始至終都沒有見到過實體地址,它所見到的都只是虛擬地址。
寫時複製
有了這些瞭解讓我們再來看下之前給出的問題,為什麼多程序在讀取同一個地址的時候會出現不同的結果。
繼續看圖:
由於程序具有獨立性,當一個程序嘗試修改共享的資料時,並不是直接修改,而是在記憶體中重新開闢一塊空間,將資料複製到新開闢的空間中,然後再將頁表中對應的實體地址修改成新開闢空間的地址,而對應的虛擬地址不變。這就是寫時複製。
為什麼要有虛擬地址
-
安全
若是沒有虛擬地址空間,那麼程序就會直接的訪問實體記憶體。如果程序在訪問實體記憶體的時候進行了越界訪問,這就會破壞其他程序正在使用的記憶體資料,導致系統崩潰或資料丟失。
而有了虛擬地址空間,那麼程序想要訪問實體記憶體則必須透過頁表對映的方式由虛擬地址找到實體地址,若地址是非法的,那麼頁表就直接會進行攔截,從而保證了實體地址不被錯誤的訪問。 -
獨立性
虛擬地址空間的存在,可以更方便的進行程序和程序的資料程式碼的解耦,保證了程序獨立性。
若一個程序對被共享的資料做修改,如果影響了其他程序,則不能稱之為獨立性,因此在任何一方嘗試對被共享的資料做修改時,作業系統先進行資料的複製,更改頁表對映,然後才讓程序對資料進行修改。
-
簡化記憶體管理
讓程序以統一的視角來看待程序對應的程式碼和資料等各個區域,方便使用編譯器也以統一的視角來進行編譯。
不僅僅作業系統會遵守虛擬地址空間的規則,編譯器也要遵守。編譯器編譯程式碼的時候,就是按照虛擬地址空間的方式對程式碼和資料進行編址的。因此在程式被載入到記憶體之前,它在磁碟中就已經有了一套虛擬地址,用來在程式內部互相跳轉。而在被載入到記憶體後,它又有了一套實體地址,這個實體地址是為了標識程式碼和資料的地址。
當
cpu
讀取指令的時候,指令內部就已經有了一套虛擬地址;cpu
內部的暫存器同樣使用的是虛擬地址。因此當程式執行的時候,cpu
從始至終都沒有見到過實體記憶體。