地址是假的?
在C語言中,指針變量的值就是一個內存地址,&運算符的作用也是取變量的內存地址,請看下面的代碼:
#include <stdio.h>
#include <stdlib.h>
int a = 1, b = 255;
int main(){
int *pa = &a;
printf("pa = %#X, &b = %#X\n", pa, &b);
system("pause");
return 0;
}
在 C-Free 5.0 下運行,結果為:
pa = 0X402000, &b = 0X402004
代碼中的 a、b 是全局變量,它們的內存地址在鏈接時就已經決定了,以后再也不能改變,該程序無論在何時運行,結果都是一樣的。
那么問題來了,如果物理內存中的這兩個地址被其他程序占用了怎么辦,我們的程序豈不是無法運行了?
幸運的是,這些內存地址都是假的,不是真實的物理內存地址,而是虛擬地址。虛擬地址通過CPU的轉換才能對應到物理地址,而且每次程序運行時,操作系統都會重新安排虛擬地址和物理地址的對應關系,哪一段物理內存空閑就使用哪一段。如下圖所示:

虛擬地址
虛擬地址的整個想法是這樣的:把程序給出的地址看做是一種虛擬地址(Virtual Address),然后通過某些映射的方法,將這個虛擬地址轉換成實際的物理地址。這樣,只要我們能夠妥善地控制這個虛擬地址到物理地址的映射過程,就可以保證程序每次運行時都可以使用相同的地址。
例如,上面代碼中變量 a 的地址是 0X402000,第一次運行時它對應的物理內存地址可能是 0X12ED90AA,第二次運行時可能又對應 0XED90,而我們的程序不需要關心這些,這些繁雜的內存管理工作交給操作系統處理即可。
讓我們回到程序的運行本質上來。用戶程序在運行時不希望介入到這些復雜的內存管理過程中,作為普通的程序,它需要的是一個簡單的執行環境,有自己的內存,有自己的CPU,好像整個程序占有整個計算機而不用關心其他的程序。
除了在編程時可以使用固定的內存地址,給程序員帶來方便外,使用虛擬地址還能夠使不同程序的地址空間相互隔離,提高內存使用效率。
使不同程序的地址空間相互隔離
如果所有程序都直接使用物理內存,那么程序所使用的地址空間不是相互隔離的。惡意程序可以很容易改寫其他程序的內存數據,以達到破壞的目的;有些非惡意、但是有 Bug 的程序也可能會不小心修改其他程序的數據,導致其他程序崩潰。
這對於需要安全穩定的計算機環境的用戶來說是不能容忍的,用戶希望他在使用計算機的時候,其中一個任務失敗了,至少不會影響其他任務。
使用了虛擬地址后,程序A和程序B雖然都可以訪問同一個地址,但它們對應的物理地址是不同的,無論如何操作,都不會修改對方的內存。
提高內存使用效率
使用虛擬地址后,操作系統會更多地介入到內存管理工作中,這使得控制內存權限成為可能。例如,我們希望保存數據的內存沒有執行權限,保存代碼的內存沒有修改權限,操作系統占用的內存普通程序沒有讀取權限等。
另外,當物理內存不足時,操作系統能夠更加靈活地控制換入換出的粒度,磁盤 I/O 是非常耗時的工作,這能夠從很大程度上提高程序性能。
以上兩點我們將在《內存分頁機制》和《內存分頁機制的實現》中進行詳細講解。這里先放一張圖,這個就是內存分頁機制的基本原理了。

中間層思想
在計算機中,為了讓操作更加直觀、易於理解、增強用戶體驗,開發者經常會使用一件法寶——增加中間層,即使用一種間接的方式來屏蔽復雜的底層細節,只給用戶提供簡單的接口。虛擬地址是使用中間層的一個典型例子。
實際上,計算機的整個發展過程就是不斷引入新的中間層:
計算機的早期,程序都是直接運行在硬件之上,自己負責硬件的管理工作;程序員也使用二進制進行編程,需要處理各種邊界條件和安全問題。
后來人們不能忍受了,於是開發出了操作系統,讓它來管理各種硬件,同時發明了匯編語言,減輕程序員的負擔。
隨着軟件規模的不斷增大,使用匯編語言編程開始變得捉襟見肘,不僅學習成本高,開發效率也很低,於是C語言誕生了。C語言編譯器先將C代碼翻譯為匯編代碼,再由匯編器將匯編代碼翻譯成機器指令。
隨着計算機的發展,硬件越來越強大,軟件越來越復雜,人們又不滿足於使用C語言了,於是 C++、Java、C#、PHP 等現代化的編程語言誕生了。