文章目錄
- Linux進程內存布局圖:
- 內存布局的驗證
- 進程地址空間
- 寫時拷貝
Linux進程內存布局圖:
地址空間的范圍,在32位機器上是2^32比特位,也就是[0,4G]。
內存布局的驗證
- 代碼驗證內存布局: 驗證代碼:
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>int init=10;int uninit;int main(){printf("code addr:%p\n",&main);printf("init addr:%p\n",&init);printf("uninit addr:%p\n",&uninit);char* heap = (char* )malloc(20);printf("heap addr:%p\n",heap);printf("stack addr:%p\n",&heap);return 0; }
運行結果及分析:根據下圖運行結果分析,驗證了上圖的內存分布。
- 驗證堆向上增長與棧向下增長:
驗證代碼:
char* heap1 = (char* )malloc(20);char* heap2 = (char* )malloc(20);char* heap3 = (char* )malloc(20);char* heap4 = (char* )malloc(20);char* heap5 = (char* )malloc(20);printf("heap1 addr:%p\n",heap1);printf("heap2 addr:%p\n",heap2);printf("heap3 addr:%p\n",heap3);printf("heap4 addr:%p\n",heap4);printf("heap5 addr:%p\n",heap5);printf("stack1 addr:%p\n",&heap1);printf("stack2 addr:%p\n",&heap2);printf("stack3 addr:%p\n",&heap3);printf("stack4 addr:%p\n",&heap4);printf("stack5 addr:%p\n",&heap5);
運行結果:堆向上增長,棧向下減小,與內存分布圖一樣。
結論:堆棧相對而生。
- 驗證命令行參數與環境變量:
驗證代碼:
int main(int argc,char* argv[],char* env[]){for(int i = 0;argv[i];i++){printf("&argv[%d]:%p \n",i,argv+i);}for(int i = 0;env[i];i++){printf("&env[%d]:%p \n",i,env+i);}return 0;}
運行結果及分析:環境變量與命令行參數這兩張表(不是表指向的內容),比棧區大,其中,是先有命令行參數這張表,才有環境變量這張表。
-
驗證表指向的內容的地址存放:
注意區分下面代碼與上面代碼的不同!
驗證代碼:
int main(int argc,char* argv[],char* env[]){for(int i = 0;argv[i];i++){printf("argv[%d]:%p \n",i,argv[i]);}for(int i = 0;env[i];i++){printf("env[%d]:%p \n",i,env[i]);}return 0;}
結果+分析:無論是表還是表指向的項目,都在棧上部的。
- 驗證靜態變量在內存分布中的位置:
這里就不驗證了,直接得出結論:靜態變量是存放在初始化數據與未初始化數據之間的。靜態變量默認是會被初始化的,哪怕用戶定義出來沒有賦值,編譯器也會初始化。例如int 類型的靜態變量,會被編譯器初始化為0;
看看一個這樣的代碼
代碼:
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>int g_val = 1000;int main(){pid_t id = fork();if(id==0){//子進程while(1){printf("child pid:%d ppid:%d g_val=%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);sleep(1);}}//父進程 else{while(1){printf("father pid:%d ppid:%d g_val=%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);sleep(1);}}return 0;}
運行結果:符合我們預期的,數據本來就是父子進程共享的,除非要寫入,進程之間時具有獨立性的,寫入的時候需要寫時拷貝。
奇怪的現象:
測試代碼:
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/types.h>int g_val = 1000;int main(){pid_t id = fork();if(id==0){//子進程int cnt = 0;while(1){printf("child pid:%d ppid:%d g_val=%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);sleep(1);cnt++; if(cnt==3){printf("child change g_val\n");g_val=2000;}}//父進程else{while(1){ printf("father pid:%d ppid:%d g_val=%d &g_val:%p\n",getpid(),getppid(),g_val,&g_val);sleep(1);}}return 0;}
運行結果+分析:奇怪的現象(如下圖):同一個變量,子進程嘗試對g_val進行寫入的時候,會進行寫時拷貝,但是為什么地址一樣,但是值卻不一樣呢?
解釋上面的現象:
- 地址一樣卻值不一樣,所以這個地址肯定不是物理地址。
- 如果是物理地址,絕對不可能在一個地址中存放的內容不一樣。
這個地址叫做虛擬地址/線性地址。
結論:我們平時用到的語言的地址全部都不是物理地址,是虛擬地址。所以下面這個圖的空間排布的情況不是物理內存,它叫做進程地址空間。
進程地址空間
每一個進程都有一個task_struct(PCB),PCB里面有該進程的進程地址空間,進程地址空間和內存之間是用一張表(叫做頁表:里面存放的是虛擬地址與物理地址)建立關系的,如下圖,頁表對應一個映射關系,是虛擬地址與物理地址之間的關系。根據虛擬地址可以找到對應的物理地址。下面的結構都是操作系統內部在維護的。
- 說明:上面的圖就足矣說名問題,同一個變量,地址相同,其實是虛擬地址相同,內容不同其實是被映射到了不同的物理地址!
其中,父進程創建子進程后,子進程也會有一個這樣的結構,也會有進程地址空間,頁表,并且父進程PCB的大部分屬性都會被子進程繼承下來,頁表也會被繼承下來(類似淺拷貝),這時父子進程都指向同一個物理內存。以上面的示例分析:當子進程嘗試對g_val進行修改時,操作系統會在內存中重新開一個空間,將修改后的值放在這個空間里,再改變頁表中g_val的虛擬地址對應的物理地址,注意:改的是物理地址,虛擬地址沒有改變,所以上面示例的結果打印出來的地址(虛擬地址)沒有改變。如下圖:
根據上面的解釋,也能夠很好的解釋fork()返回值問題了!
什么是進程地址空間?
進程地址空間是數據結構,具體到進程中,是有特定的數據結構對象。
如下圖所示:在進程的PCB中,有一個指針,指向自己的進程地址空間,進程地址空間里面,包含一個結構體,結構體里面有很多start和end,劃分區域。
為什么要有地址空間和頁表?
- 在進程看來,有了頁表,可以將物理內存從無徐變為有序,因為頁表是有序的。讓進程以統一的視角,看待內存;
- 將進程管理和內存管解耦合,進程管理與內存管理互不干擾。
- 地址空間+頁表是保護內存安全的重要手段(攔截非法:例如:野指針,越界問題)。
內存申請問題(malloc/new)
申請內存,本質是進程的地址空間中申請。
這樣:可以充分保證:
- 內存使用率,不會空轉。
- 提升new/malloc的速度。
寫時拷貝
- 為什么需要寫時拷貝?
答:進程之間要做到獨立性。 - 創建子進程的時候,為什么不直接將父進程的代碼和數據拷貝一份給子進程呢?
答:因為子進程并不是會對父進程的所有數據都要進行寫入操作,如果fork()創建子進程的時候,直接拷貝一份代碼和數據,會降低fork()的效率。 - 為什么是要拷貝呢,只開空間不拷貝行不行?
答:因為子進程不一定是對這個數據直接進行覆蓋式的寫入,可以只是對該數據進行局部修改或則是基于之前的值進行操作。
如何做到寫時拷貝的?
前面所說的頁表,不只是有虛擬地址與物理地址的轉換的,還可以帶很多選項的,如下圖(介紹其中一個:權限):
下圖代碼字符串"hello Linux"是具有常屬性的,不能被修改,當我們嘗試去修改的時候,會報錯(運行報錯)。
是因為在頁表有權限,虛擬地址映射到物理地址的時候,會做權限審核,如下圖所示,當只有可讀權限,沒有修改的權限的時候,嘗試去修改,就會報錯。
寫時拷貝的細節:
當要進行寫時拷貝的時候,會將父子進程頁表里大部分內容的映射權限設置為只讀權限,當父子進程任何一方要去進行嘗試寫入的時候,操作系統會進行判斷,如果是數據段,對數據進行寫入時合理的,就會引發缺頁中斷,操作系統會將權限改為讀寫,然后寫時拷貝后,再把頁表對應的條目改為讀寫。