1 代碼感受
在正式講程序[地址空間]前我們先來看一段簡單的代碼來分析分析:
1#include2#include 3usingnamespacestd; 4 5intg_val=100; 6 7intmain() 8{ 9pid_tid=fork(); 10if(id==0) 11{ 12//child 13while(true) 14{ 15cout<<"我是一個子進程,我的pid是:"<
大家可以自己先分析一下結果。
我們來運行一下結果:
大家看前面幾行可能就會立馬發現問題:我們定義的 g_val 是全局變量,當子進程修改 g_val 的值時我們發現父進程的 g_val 是不受影響的,那么說明父子進程所用的 g_val 并不是同一個變量(這個很好理解,之前的我們說過父子進程是相互獨立的,互相不受干擾的),但是問題出現在最后一列,我們驚奇的發現居然父子進程的 g_val 變量的地址居然是相同的,前面不是說父子進程的 g_val 不是同一個變量嗎?這里為啥打印出來的地址會是相同的呢?
這里就說明我們打印出來的地址并不是真正的[物理地址],我們語言層面打印出的地址叫做虛擬地址或者線性地址。我們在用C/C++語言所看到的地址,全部都是虛擬地址!而物理地址,用戶一概看不到,由OS統一管理 。OS必須負責將虛擬地址轉化成物理地址。
2 進程地址空間
首先我們來講一個故事:從前有一個企業家很有錢,他的家產大概有一億美金左右的樣子。他有 4 個私生子,并且這四個私生子互相并不知道對方的存在。第一個私生子是個學霸,在國內頂尖學校上學,這個富豪便對他說,你要好好讀書,將來我這一億美金全部都是你的;第二個私生子是一個三線演員,富豪便對他說,我幫你打開你紅的渠道,你不要辜負了我對你的期望,好好努力,將來我這一億美金都是你的;第 3 個私生子是個女兒,當的是小學老師,富豪便對他說,你也不用太過努力工作,我就你這一個女兒,等我老了這一億美金就是你的了;第四個私生子是一個初中的小混混,富豪對他說,你只要好好聽我的話,這一億美金就是你的了。
富豪給每個私生子都做出了承諾要將一億美金給他們,但是實際富豪并沒有那么多的錢給每個私生子一億美金,而這一億美金就是富豪給私生子們畫的一張大餅,但是它的私生子們卻信以為真。
那這個故事與我們講的知識有什么關系呢?其實操作系統就是那個富豪,私生子們就是一個一個的進程,而那一億美金就是進程地址空間。
PS: 我們在生活中要盡量少畫餅。
操作系統給進程畫了一張大餅,操作系統的資源是有限的,所以他就得要好好的把這張餅給管理起來,不讓這些進程亂來,而如何管理呢?
那就要先描述,再組織,Linux 中用的是一種叫做 mm_struct 的內核數據結構來管理的。
我們來用一張圖帶大家來看看程序地址空間:
這張圖相信大家多多少少也不會陌生,在 C 語言的學習中我們也見到了很多次。
那么程序地址空間如何編碼的呢?(32 位的平臺下[虛擬地址]空間大概是 4GB)
ps: 下面圖每個小空格代表著一個字節。
所以從這里我們也不難看出為啥虛擬地址也叫做線性地址。那么我們究竟是如何管理虛擬地址空間的每個區的呢?
我們可以用下面這種方式來描述管理:
structmm_struct { longcode_start; longcode_end; longinit_start; longinit_end; ………… longbrk_start; longbrk_end; longstack_start; longstack_end; }
而_start 和_end 限定的區域就是叫做虛擬地址(線性地址)
那么問題來了,既然上面我們講了那么多虛擬地址,真正的物理地址又在哪里呢?
我們畫一個圖方便大家理解:
通過這張圖大家并不難發現,我們在語言層面上的地址是地址空間的虛擬地址,而虛擬地址要與物理地址建立映射,就需要一張頁表(頁表的工作原理我們將放到后面來講)。
我們在學習 C 語言時大家在書上看到這樣的一句代碼:const char* str="hello world";
這時書上會告訴大家這句 str 指向的內容是只讀的,不可修改的,但是這時為什么呢?這時我們就可以自己來分析分析:str 指向的內容是在常量字符區,當常量字符區通過頁表與物理地址建立映射時在頁表中就將該數據設置為只讀,當我們后續有修改操作時就會直接報錯。
有了上面的基礎我們就可以來解釋解釋為啥開頭我們的 g_val 是同一個地址,但是指向的內容卻不相同的問題了:
當不修改數據時就不會發生寫時拷貝,父子進程指向的是同一塊物理空間 (為了節約資源);當要修改數據時就會發生寫時拷貝,父子進程指向的是不同的物理空間,但是虛擬地址空間是相等的。
我們再來回答為啥 fork 會有兩個返回值的問題就很容易了,就是因為父子進程的返回值是不同的,所以肯定會發生寫時拷貝將不同的返回值用相同的虛擬地址來進行返回,雖然虛擬地址是相同的,但是他們通過頁表建立映射的關系卻是不一樣的。
到目前為止,程序地址空間的基本內容已經 ok, 接下來給出一些擴展。
3 擴展
首先引出一個問題:假如沒有程序地址空間,OS 是如何工作的?
我們知道如果沒有了地址空間,那么 cpu 將直接跟物理地址打交道,這樣做的后果是什么?
我們不難知道假如 cpu 直接跟物理地址打交道的話那么當我們從 cpu 中讀到非法地址時那就壞了,通過非法地址將我們程序中其他變量的值給修改了那不就扯淡了嗎。所以我們要通過一層屏障來保護數據,而這一層保護就是通過程序地址空間來進行的,當我們訪問的數據非法時通過頁表的映射就會拒絕你的非法操作。
所以我們得出了程序地址空間的第一個好處:防止地址隨意訪問,保護物理內存和其他進程。
在向大家提出一個小問題:當我們在堆上 new 空間時 OS 是立馬就把空間給你,還是等你需要的時候再給你?
這個問題大家應該都能夠答對,與我們想得一樣,OS 會在我們需要該空間的時候再去在堆上申請。
而頁表暫時沒有與物理內存建立映射關系稱作頁表中斷,當我們需要空間的時候再與 · 物理內存建立映射。大家從這張圖看出來沒有,當我們通過頁表建立映射時將進程管理與內存管理給解耦合了。我進程管理不需要關心你是怎樣在內存上申請空間的,內存管理也不需要關心進程是如何管理起來的,這樣下來維護成本就會變得更低,維護效率會更加高效一些。
所以我們得出了程序地址空間的第二個好處:將進程管理與內存管理進行解耦合。
再提出一個問題:程序在被編譯的時候沒有被加載到內存,那么程序內有沒有地址呢?
答案是有的。源代碼再被編譯的時候就是按照虛擬地址空間的方式將對應的代碼和數據進行編制,編譯器也會遵守虛擬地址的規則。
當我們把程序加載到內存,程序里保存的地址(虛擬地址,并不是程序本身在內存中的物理地址) 就會被 cpu 讀取, cpu 通過虛擬地址找到對應的虛擬地址空間,然后虛擬地址空間又通過頁表映射到物理內存中,這樣就將程序的整個運轉給聯系起來了。
所以我們得出了程序地址空間的第三個好處:可以讓進程以統一的視角看待自己的代碼和數據。
最近很多小伙伴找我要一些程序員必備資料,于是我翻出了壓箱底的寶藏,免費分享給大家!
審核編輯:湯梓紅
-
內核
+關注
關注
3文章
1372瀏覽量
40290 -
Linux
+關注
關注
87文章
11304瀏覽量
209502 -
操作系統
+關注
關注
37文章
6825瀏覽量
123331 -
程序
+關注
關注
117文章
3787瀏覽量
81043 -
代碼
+關注
關注
30文章
4788瀏覽量
68612
原文標題:Linux:程序地址空間--原來操作系統也喜歡畫大餅
文章出處:【微信號:良許Linux,微信公眾號:良許Linux】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論