文章目錄
前言
本節主要是講解進程地址空間,區分和物理內存地址空間的差別,并且向讀者解釋四個疑問:
怎樣驗證地址空間的排布; 進程地址空間是什么; 進程地址空間和物理內存之間的關系; 為什么要存在地址空間;
而本篇講解時候,是以32位計算機為例.
1. 概念引入
我們在學習C/C++時候,應該學習過內存布局,以及了解各種變量的存儲位置,例如局部變量存儲在棧區,動態申請的內容在堆區,全局變量,常量等在數據常量區,如果用一張圖來表示,如下:
(已初始化和未初始化區指全局變量) (圖1)
上圖是否正確呢?我們可以用以下程序進行驗證數據分布:
#include
#include
int g_unval;
int g_val = 100;
int main(int argc,char* argv[],char* env[])
{
printf("代碼區地址 : %p\n",main);
const char* p = "hello world!";
printf("字符常量區地址 : %p\n",p);
printf("已初始化全局區地址 : %p\n",&g_val);

printf("未初始化全局區地址 : %p\n",&g_unval);
char* q0 = (char*)malloc(10);
char* q1 = (char*)malloc(10);
char* q2 = (char*)malloc(10);
char* q3 = (char*)malloc(10);
char* q4 = (char*)malloc(10);
printf("堆區地址 : %p\n",q0);
printf("堆區地址 : %p\n",q1);
printf("堆區地址 : %p\n",q2);
printf("堆區地址 : %p\n",q3);
printf("堆區地址 : %p\n",q4);
printf("棧區地址 : %p\n",&q0);
printf("棧區地址 : %p\n",&q1);
printf("棧區地址 : %p\n",&q2);
printf("棧區地址 : %p\n",&q3);
printf("棧區地址 : %p\n",&q4);
printf("第一個命令行地址 : %p\n",argv[0]);
printf("最后一個命令行地址 : %p\n",argv[argc-1]);

printf("環境變量地址 : %p\n",env[0]);
return 0;
}
根據運行結果,能夠看出從代碼區地址開始,一直到環境變量地址區域,按照地址類型都是逐漸增大的, 并且能發現堆區地址向上生長,棧區地址向下生長,堆區地址向上生長,所以上圖是正確的
既然我們知道圖1是正確的,那么請問,圖一這個空間是我們的所說的物理內存空間嗎?
答案是否定的,它只是一個虛擬空間,是由人為邏輯而想象出來的,可以通過下面程序進行驗證.
#include
#include
#include
#include
int g_val = 0;
int main()
{
printf("begin <--------> g_val = %d\n",g_val);
pid_t id = fork();
if(id == 0)

{
g_val = 10;
for(int i = 0;i<5;i++)
{
printf("child --->pid:%d--->ppid:%d--->[g_val:%d]-->[&g_val:%p]\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
else if(id > 0)
{
g_val = 100;
for(int i = 0;i<5;i++)
{
printf("parent--->pid:%d--->ppid: %d--->[g_val:%d]-->[&g_val:%p]\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
return 0;

}
在前面進程章節我們提過,fork以后父子進程共享代碼,數據各自一份,而下面的運行結果顯示,數據確實私有,但是g_val的地址父子進程竟然一樣,也就是說同一塊內存空間,竟然存了兩份數據,這明顯是不可能的.
所以圖一根本不是我們所說的物理內存空間,那它是什么呢? — 進程地址空間,所以這也同時解釋了,為何全局變量的生命周期會跟著程序一起結束. 因此這里再澄清一個概念,在我們以前學習的任何語言中,所提到的內存概念其實指的是進程地址空間,說內存是為了讓我們進行程序.
2. CPU和物理內存關系
內存被人為的劃分了很多區域進行編號,稱為地址,為了方便進行查找,就像我們的門牌號一樣.
CPU是通過什么將地址、數據和控制信息傳到內存中的呢?電子計算機能處理、傳輸的信息都是電信號,電信號當然要用導線傳送。在計算機中專門有連接CPU和其他芯片的導線,通常稱為總線。總線從物理上來講,就是一根根導線的集合,根據傳送信息的不同,總線從邏輯上又分為3類,地址總線、控制總線和數據總線。
比如CPU想要獲取地址編號為3的內存的數據,他們的過程如下:
(1)CPU 通過地址線將地址信息3發出。
(2)CPU 通過控制線發出內存讀命令32位程序最大尋址空間,選中存儲器芯片32位程序最大尋址空間,并通知它,將要從中讀取數據。
(3) 存儲器將3號單元中的數據8通過數據線送入CPU。
然而一根線只能發送一個高低信號(表示0或1),因此地址線是有很多根的,我們典型的說計算機是多少位系統,就是說的地址線有多少根.
假設地址線有32根,那么就是說CPU可以訪問2^32個空間單位(一單位是1字節),即可以訪問2^32個字節 = 4GB.
現在我們再回頭過來看一下圖一,最上面博主所空出來的那1G,這里面主要是用來駐OS的.
3.何為進程地址空間
我們知道進程是由PCB控制的,而進程地址空間其實是PCB中的一個結構體(),這個結構體內部定義了存儲區地址的起始區域.
例如棧區
unsigned int stack_start;
unsigned int stack_end;
當進程被創建以后,等就會被存儲起始地址值,用于表示棧區地址的起始范圍,以及表示在該區域只能存儲局部變量等,其他區域同理.由于是32位計算機,這個結構體所記錄的總的起始區域大小就是end-start=2^32.而這個就是進程地址空間,也就是圖一,但是圖一并不存在現實中,是我們所抽象出來的一個概念,而該結構體所記錄的地址還需要一個被稱為頁表的結構通過某種轉換映射到真實的物理內存地址中.他們之間的關系如圖
做個類比可能更好理解這個虛擬的進程地址空間,一位資產只有10億的富翁分別對10個互不相識的人說,我這里有100億,你們幫我做事情,這錢就是你的了,如果在做事情的過程中,你需要一部分錢就給我說,我給你,等事后完成,需要扣除這一部分. 這時候是不是每個人都認為自己有了100億美金,即使并沒有真的得到?
而這個富翁就是物理內存,10億就是內存的真實大小,這多個人就是計算機中的進程.而這個100億就是存在于想象中,是一種抽象,就像進程地址空間,因為進程地址空間本質上只是一個記錄區域的結構體.然后其中一個人說,我需要買工具,你給我點錢,于是富翁就給他一點錢,這個過程就像代碼中的部分變量,需要存儲數據,然后便在這個虛擬的想象內存中要一部分空間,然后頁表把這個空間映射到真實的內存中進行存儲.
因此,進程地址空間和物理內存之間的聯系便是這個頁表.
4. 為什么存在地址空間?
可能讀者會好奇,為什么要多一個進程空間地址呢?直接存放數據不香嗎?
通過前面我們可以知道內存是被劃分為很多個小單元(字節)的,如果我們的進程在內存里面,然后各種數據也在內存里面,在進程空間地址中,能看到其地址是非常有序的,而通過頁表映射到內存中的實際位置時,是亂序的,也就是說物理內存的存儲原則是哪里有空我就存哪里.
如果我們直接存在內存里面,這就會導致一個非常糟糕的問題,尋址麻煩,其次對于指針訪問越界應該怎樣處理?
因此存在地址空間的第一個原因是
保護物理內存不受到任何進程內的地址的直接訪問,出現越界訪問,即保護系統進行合法性檢測
觀察上圖,可能會發現頁表有兩部分,其左邊存放的是虛擬地址,右邊則是標明其所指向內存空間是否具有讀寫權限
而操作系統的任務之一是進行內存管理,如果沒有進程空間地址,我們想創建一個進程,第一步是獲取數據,第二步是獲取內存空閑的位置,第三部是給這些數據進行存儲,并且還要想辦法記住這些地址; 也就是說進程的內存管理將和進程密不可分(強耦合).如果有進程空間地址的話,操作系統只需要給進程一個空間地址和頁表,進程就可以通過頁表自動向內存申請空間,而這些數據存儲的地址用戶角度看來還是順序存儲的.當進程結束時,也只需要通過頁表進行高速內存自行釋放.
因此存在地址空的第二個原因是
將操作系統的內存管理和進程管理進行解耦,管理比較輕松
在計算機中,還有一個外存磁盤,而磁盤中的數據存儲形式就是按照代碼區,數據區,棧區,堆區等有序的形式存著數據,進程空間地址的區域劃分本質和磁盤一樣.也就是說
讓每個進程都可以和磁盤一樣,以同樣的方式看待代碼和數據,方便查找代碼和數據