“ 進(jìn)程、線程有什么區(qū)別?虛擬地址和物理地址有什么區(qū)別?讓我們用一只青蛙的視角,來解讀它們背后的秘密”
進(jìn)程、線程、虛擬地址、物理地址,這些名詞既熟悉也陌生!似乎無論看多少資料,都很難準(zhǔn)確地弄清楚它們之間的差異和存在的意義。今天我們用CPU的視角,再次會會這個老朋友,看看你是否有新的啟發(fā)?
奇怪的內(nèi)存
先寫一個最簡單的程序。這個程序只做兩件事情:定義一個全局變量 a,并賦值為:1;然后打印出 a 的地址和數(shù)值
#include <stdio.h>
#include <unistd.h>
int a = 1;
int main()
{
printf("address: %p, value: %dn", &a, a);
sleep(10000000);
}
將上面的代碼,編輯成可執(zhí)行程序:p1;接著,修改一下代碼:給全局變量 a 賦值為:2,再次編譯出可執(zhí)行程序:p2。
好了,兩個程序:p1,p2 都準(zhǔn)備好了,讓我們再以兩個進(jìn)程的形式,運(yùn)行它們:左邊的窗口進(jìn)程,運(yùn)行p1;右邊的窗口進(jìn)程,運(yùn)行p2

發(fā)現(xiàn)問題了嗎?無論是 p1 還是 p2,它們輸出的 a 的地址都是一樣的,但 a 的值卻不一樣!難道同一個內(nèi)存地址里面,既能存放 1,也能存放 2?
當(dāng)然不是,原來0x555555558010是虛擬地址,并不是真實的物理內(nèi)存地址,進(jìn)程p1的0x555555558010,跟進(jìn)程p2的0x555555558010,沒有任何關(guān)系!
虛擬地址,只在進(jìn)程內(nèi),是有意義的;可以用來指示不同變量,所對應(yīng)的不同內(nèi)存地址;但一旦跳出單一進(jìn)程,在進(jìn)程之間比較虛擬地址,就沒有任何意義了!
MMU
是什么軟件,擁有如此的魔力?能讓進(jìn)程p1和進(jìn)程p2的內(nèi)存空間完全隔離?答案不是軟件,而是硬件 — 現(xiàn)代CPU的協(xié)處理器:MMU
MMU的工作原理,未來我們還會詳細(xì)闡述,這里只說結(jié)論:無論是進(jìn)程p1,還是進(jìn)程p2的,它們的變量 a 的地址都是虛擬地址,看上去是同一個地址,但實際上,已經(jīng)被 MMU 映射到了不同的物理地址上去了。這就是“進(jìn)程”最顯著的特點(diǎn):空間獨(dú)立性。

舉個例子,進(jìn)程就像一只井底之蛙,它固執(zhí)地認(rèn)為:自己已經(jīng)擁有整個天空;但它永遠(yuǎn)不知道:天空到底有多大。更不知道:周圍還有很多跟它有一樣想法的井底之蛙,而 MMU 就是束縛這些青蛙視野的井,每一口井,就是一個:進(jìn)程空間。


進(jìn)程 vs 線程
那看來全是 MMU 惹的禍,不要 MMU 行嗎?當(dāng)然可以,但一旦沒有井的束縛,所有的青蛙,都跳到地面上,它們都可以看到一個完整的天空,所以,沒有 MMU,進(jìn)程也就不存在了,進(jìn)程被降級成了:線程。
這樣的例子很多,例如:在沒有 MMU 的單片機(jī)。你就只會遇到線程或者叫:task,根本沒有“進(jìn)程”的概念。
在 MMU 出現(xiàn)之前,計算機(jī)的世界里面,只有“線程”,在 MMU 出現(xiàn)之后,“進(jìn)程”才真正落地,因為沒有 MMU,就沒有辦法實現(xiàn):內(nèi)存空間的隔離,也就根本無法實現(xiàn)“進(jìn)程”要求的:空間獨(dú)立性。
至于“進(jìn)程”中的多“線程”就很容易理解了。就是:一堆青蛙,都放在一個井里。而且,它們都認(rèn)為自己擁有整個天空。

因為,這些“線程”都處于同一個“進(jìn)程”空間中,大家可以相互訪問,完全沒有任何限制。這使得用“線程”實現(xiàn)多任務(wù)編程,會非常便利。

但也因為這種對安全的忽視,一旦任何一個“線程”崩潰

所有的“線程”都不能幸免,大家一榮俱榮,一損俱損!

所以,網(wǎng)絡(luò)服務(wù)器一般都會使用“多進(jìn)程”,而很少使用“多線程”。這樣即使某一個用戶的服務(wù)進(jìn)程崩潰了,其他“進(jìn)程”還能繼續(xù)正常工作。這樣,就不會因為某個用戶的訪問失敗,而導(dǎo)致其他用戶也無法訪問服務(wù)器。

好了,這可能就是你永遠(yuǎn)弄不清楚:“線程”和“進(jìn)程”的原因,因為,這不僅僅是:一個軟件問題,更是一個 MMU 的問題。以后你再跟人討論“線程”、“進(jìn)程”的時候,一定要先問一下:有 MMU 嗎?
總結(jié)
- 進(jìn)程就像一只井底之蛙,雖然看上去,它可以讀/寫整個64位(假設(shè)CPU是64位的)的虛擬內(nèi)存空間:void write_memory()
{char*p = 0;
for(unsigned long offset = 0; offset<= 0xFFFFFFFFFFFFFFFF; offset++)
{*(p + offset) = 0x55;
}} 但天到底有多大,真實的物理內(nèi)存空間到底有多少?除了操作系統(tǒng)和MMU,沒有人能知道。 - 進(jìn)程間的空間隔離,讓進(jìn)程之間的信息共享沒有線程那么方便,但也大大提高了整個系統(tǒng)的安全性;再也不會因為某一個應(yīng)用程序的崩潰,導(dǎo)致計算機(jī)重啟。還記得紅白機(jī)上的reset按鍵嗎?

任何一個游戲程序的崩潰,都需要通過reset來重啟/恢復(fù)系統(tǒng)。
3. 進(jìn)程間的空間隔離,讓惡意程序無法再掃描整其他程序的內(nèi)存或整個物理內(nèi)存,任何程序只能在自己的一畝三分地里面干活。通過游戲修改器,玩《仙劍奇?zhèn)b傳》的日子,不會再有了。

這樣看來:生活在“井”里,也沒有什么不好。
熱點(diǎn)問題
Q1:誰在分配:虛擬地址?
A1:程序員在代碼中編寫的:任何變量、函數(shù)、數(shù)據(jù)結(jié)構(gòu),在編譯過程中,都會被編譯器安排了一個內(nèi)存地址,這個地址就是虛擬地址。
Q2:虛擬內(nèi)存的好處?
A2:好處是:編譯器在生成可執(zhí)行程序時,可以更加自由的分配:代碼中變量的內(nèi)存地址,不用關(guān)心它在實際運(yùn)行環(huán)境中,應(yīng)該是多少?也不用關(guān)心運(yùn)行環(huán)境的內(nèi)存是否夠用。
相同的 exe文件,同時運(yùn)行2次,也不用擔(dān)心它們同名的變量或函數(shù),會在內(nèi)存中重疊在一起。因為它們都用的是虛擬地址,無論外表多么相似,都可能被分配在不同的“物理地址”上。
此外,當(dāng)程序所需的內(nèi)存大于計算機(jī)的實際內(nèi)存時,虛擬內(nèi)存機(jī)制可以用硬盤來給內(nèi)存“擴(kuò)容”。
Q3:我電腦的內(nèi)存有8GB,那跑在該電腦上的程序,其虛擬地址空間,也只能有8GB嗎?
A3:虛擬地址的空間大小,取決于CPU的位數(shù),32位CPU對應(yīng)的虛擬地址空間為:0~0xFFFFFFFF(4GB),64位CPU對應(yīng)的虛擬地址空間為:0~0xFFFFFFFFFFFFFFFF(16384PB),它們跟真實的物理內(nèi)存大小,沒有關(guān)系。
Q4:為什么我執(zhí)行實例代碼的時候,輸出的 a 的內(nèi)存地址總是在變化?
A4:新版本的linux對虛擬內(nèi)存進(jìn)行了保護(hù),作了隨機(jī)化處理,為了達(dá)到文章所述的效果,需要取消這種隨機(jī)化處理。請在運(yùn)行程序之前,在命令行中輸入如下命令:
echo 0 > /proc/sys/kernel/randomize_va_space