Linux中的進程地址空間用法解讀
1.進程地址空間分布
進程地址空間分布圖

棧區(qū)是向地址減小方向開空間(棧是先使用高地址),而堆區(qū)是向地址增長方向申請空間(堆是先使用低地址),堆棧之間的共享區(qū),主要用來加載動態(tài)庫。
我們通過如下代碼來驗證一下是否符合上面的地址空間分布:
#include<stdio.h>
#include<stdlib.h>
int g_unval;//未初始化
int g_val = 100;//初始化
int main(int argc, char* argv[], char* env[])
{
const char* p = "hello bit";//p是指針變量(棧區(qū)),p指向字符常量h(字符常量區(qū))
char* q = (char*)malloc(10);//q 存放堆區(qū)地址,&q 指的是棧區(qū)地址
printf("env addr: %p\n", env[0]);//環(huán)境變量
printf("args addr %p\n", argv[0]);//命令行參數(shù)
printf("stack addr: %p\n", &p);//p先定義,先入棧
printf("stack addr: %p\n", &q);//棧區(qū)
printf("heap addr: %p\n", q); //堆區(qū)
printf("global val: %p\n", &g_val); //全局初始化
printf("global uninit val: %p\n", &g_unval); //全局未初始化
printf("read only : %p\n", p); //字符常量區(qū)
printf("code addr: %p\n", main);//代碼區(qū)起始地址
return 0;
}運行結果如下:

地址自上而下,可以看到它的地址分布嚴格遵守上面的進程地址分布圖
進程地址空間,會在進程的整個生命周期內一直存在,直到進程退出,所以全局變量會一直存在。
2. 地址空間是虛擬的
進程地址空間不是實際的物理內存而是一個虛擬的地址內存
我們如何驗證這個觀點呢??通過如下代碼:
#include<stdio.h>
#include<unistd.h>
int g_val = 10;
int main()
{
printf("剛開始時的g_val:%d\n", g_val);
pid_t id = fork();
if (id == 0)
{
//子進程
g_val = 100;
printf("子進程中的g_val:%d\n", g_val);
printf("子進程中g_val的地址:%p\n", &g_val);
}
else if (id > 0)
{
//父進程
printf("父進程中的g_val:%d\n", g_val);
printf("父進程中g_val的地址:%p\n", &g_val);
}
return 0;
}運行結果如下:

可以看到這里父子進程中g_val的地址是相同的,而g_val的值卻是不同的。
根據(jù)常識我們都知道,一個地址不可能存放有兩個不同的數(shù)據(jù),因此我們可以得出進程地址空間中的地址并不是真實的物理內存地址,而是一個虛擬地址??!
虛擬地址是由操作系統(tǒng)提供的,由馮諾依曼體系結構我們知道任何數(shù)據(jù)在啟動時必須加載到物理內存,所以肯定需要將虛擬地址轉化成物理地址。因此操作系統(tǒng)需要將虛擬地址轉化成物理地址。
進程地址空間是由操作系統(tǒng)虛擬出來的內存, 那么操作系統(tǒng)是如何劃分進程地址空間區(qū)域的??
進程地址空間本質上是一種數(shù)據(jù)結構,是多個區(qū)域的集合。在Linux內核中,有這樣一個結構體:struct mm_struct,在這個結構體里面進行區(qū)域的劃分(棧區(qū)堆區(qū)等區(qū)域)。
struct mm_struct
{
//...
unsigned long heap_start;//堆區(qū)
unsigned long heap_end;
unsigned long stack_start;//棧區(qū)
unsigned long stack_end;
unsigned long uninit_start;//未初始化區(qū)
unsigned long uninit_end;
unsigned long init_start;//初始化區(qū)
unsigned long init_end;
unsigned long code_start;//代碼區(qū)
unsigned long code_end;
//...
}這里還存在一個疑問??假設物理內存是4G的情況下,那么每一個進程中都有一個4G的進程地址空間,我們都知道一個內存可以加載多個進程,那么物理內存足夠給每一個進程中的進程地址空間的地址數(shù)據(jù)進行映射存放下來嗎?
答案是夠的,每一個進程都認為他們會使用4G的物理內存,但實際上他們所使用的空間遠遠小于4G的內存,因此內存中才能加載多個進程。
3.虛擬地址和物理地址的映射
每一個進程都會有一個PCB(task_struct結構體),而PCB中有一個指針指向各自進程的地址空間,進程將自己的代碼和數(shù)據(jù)首先放在虛擬地址空間的對應的區(qū)域,在這其中會有一種表結構,叫做頁表,頁表的核心工作就是完成虛擬地址到物理地址之間的映射。之后,我們的代碼和數(shù)據(jù)通過頁表的映射加載到實際的物理內存中。
因此通過頁表建立的虛擬與物理地址之間的對應關系成功將進程中的代碼和數(shù)據(jù)存入到了內存中

這里我們回答幾個問題。
1.不同進程的虛擬地址可以一樣嗎??
可以的,因為不同進程之間的虛擬地址可以是一樣的,每一個進程都有獨屬于自己的頁表,他們通過頁表映射到不同的物理地址。
2.不同進程的虛擬地址在頁表映射的物理地址是否會重復??
答案是不會的。但是存在著一種特殊情況,當父進程創(chuàng)造子進程,子進程會以父進程的地址空間和頁表的映射等為模板,創(chuàng)造屬于自己的進程地址和頁表,當子進程不對代碼和數(shù)據(jù)做改變時,子進程的頁表還是會指向和父進程一樣的物理地址。
但是當子進程對數(shù)據(jù)修改時,操作系統(tǒng)會在物理內存中進行寫實拷貝,開辟出一個新的物理地址,里面存放子進程修改后的數(shù)據(jù),并且此時頁表會更新映射的物理地址。(這就回答了上面父子進程中為什么g_val的值不同,而進程地址相同的原因)

4.地址空間和頁表存在的意義
1.保護物理內存,維持進程的獨立性
如果進程直接訪問物理內存,若進程中存在代碼問題(如指針越界等等),那么這個進程很可能會訪問到別的進程的數(shù)據(jù)并對該數(shù)據(jù)進行修改,這就破壞了進程的獨立性。而有了虛擬地址之后,通過頁表只能訪問自己映射到的物理內存保證了進程的獨立性。
2.頁表可以進行越界行為的檢查
- 第一種檢查,通常為指針越界的檢查 :
當發(fā)生越界行為,系統(tǒng)會檢查越界后的地址地址是否在對應的區(qū)域(比如指針原本是棧區(qū),越界后依然在棧區(qū)),編譯器會通過mm_struct結構體里面該區(qū)域的范圍比對,如果還在對應的區(qū)域那么編譯器會認為合法,如果不在則非法。
- 第二種檢查,檢查是否對常量區(qū)的數(shù)據(jù)進行修改:
其實頁表也有一種權限管理判斷是否可以讀寫,當你對數(shù)據(jù)區(qū)進行映射時,數(shù)據(jù)區(qū)是可以讀寫的,相應的在頁表中的映射關系中的權限就是可讀可寫,但是當你對代碼區(qū)和字符常量區(qū)進行映射時,因為這兩個區(qū)域是只讀的,相應的在頁表中的映射關系中的權限就是只讀,如果你對這段區(qū)域進行了寫,通過頁表當中的權限管理,操作系統(tǒng)就直接就將這個進程干掉。

3.降低內存和進程管理的耦合
- 若沒有進程地址空間,當進程退出時,內存管理需要盡快對該進程回收釋放,而有進程加載到內存時,內存管理又要及時分配資源,耦合度太高。
- 當有了進程地址空間后,一個進程需要資源的時候,通過頁表映射去要就即可。
- 內存管理就只需要知道哪些內存區(qū)域(配置)是無效的,哪些是有效的(被頁表映射的就是有效的,沒有被頁表映射的就是無效的)。
- 當進程退出,頁表也隨之退出,沒有了映射關系后,物理內存將該進程的數(shù)據(jù)映射的物理地址設置為無效即可。同理有進程進入時設置為有效。
物理內存如何知道某個地址是否被映射(有效)??
可以理解為在每個物理內存的地址出有一個計數(shù)器,當計數(shù)器為1,即該處被映射,為0時則沒被映射。

4.內存管理如何將一些大型數(shù)據(jù)加載到物理內存
內存管理是通過延遲加載的方式加載到物理內存的。一個大型進程,內存管理首先會給你加載小一部分先供你使用,當你使用完時,會先將進程置為睡眠狀態(tài),然后將進程再喚醒,再加載一部分數(shù)據(jù)代碼,進程再繼續(xù)使用,依次反復。對于用戶來說,唯一感覺到的是我的游戲運行的慢了。

頁表中還有一個管理權限判斷判斷進程的代碼和數(shù)據(jù)是否已經加載到了內存,0為沒有加載到內存,而1則相反,這只是一個簡化理解
磁盤會將這個大型可執(zhí)行程序的數(shù)據(jù)和代碼一點點的放入到內存當中,而操作系統(tǒng)對物理內存的分配是:進程用到哪里就加載多少數(shù)據(jù)代碼原則。
進程所有的代碼和數(shù)據(jù)都有對應的虛擬地址,因此對于那些還不需要用到的數(shù)據(jù)代碼,頁表會在對應權限標0表示沒有加載到內存,當需要這個數(shù)據(jù)代碼的時候,操作系統(tǒng)會在物理內存里開辟空間,并且訪問磁盤里對應的代碼和數(shù)據(jù)。
這些理解都是一些比較淺的理解,對于頁表的具體理解,需要后面進行更深的理解。
最后還有兩個問題,CPU如何訪問進程的頁表??CPU對頁表做出修改后,進程如何保持更新呢??
頁表的地址屬于進程的上下文。
CPU中有一個叫做cr3的寄存器存放著頁表的地址(可以把頁表想象成unordered_map結構的對象),CPU也是通過該地址訪問頁表的,當進程運行完后,進程會帶走自己的臨時數(shù)據(jù),也就是進程的上下文,所以進程也會帶走頁表的地址,這樣就做到了保持更新。

總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Linux+php+apache+oracle環(huán)境搭建之CentOS下安裝Apache
Linux環(huán)境是在VMware虛擬機中搭建的,重在學習,安裝的系統(tǒng)是CentOS6.5-64bit。本文主要講訴在CentOS中如何安裝APACHE。2014-08-08
Apache配置SSLCARevocationCheck吊銷檢查的實現(xiàn)
Apache的SSLCARevocationCheck指令用于啟用客戶端證書吊銷狀態(tài)檢查,配置包括CRL和OCSP模式,三種檢查模式,建議生產環(huán)境采用CRL本地緩存和OCSP實時校驗的雙保險策略,配合自動化更新腳本,取得安全與可用性之間的平衡2026-04-04
Ubuntu?24.04中優(yōu)雅地修改主機名的具體步驟和方法
在使用?Ubuntu?的過程中,有時我們會想要修改主機名,例如為了統(tǒng)一命名規(guī)范、更好地識別主機,或者只是因為安裝系統(tǒng)時隨便填了個名字,本文將帶你一步一步地,在?Ubuntu?24.04?系統(tǒng)中?優(yōu)雅且安全地修改主機名,需要的朋友可以參考下2025-09-09
Linux (Ubuntu 18.04) 下安裝vim編輯器的方法
這篇文章主要介紹了Linux (Ubuntu 18.04) 下安裝vim編輯器的方法,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2019-07-07
詳解如何在Linux(CentOS)下重置MySQL根(Root)密碼
本篇文章主要介紹了詳解如何在Linux(CentOS)下重置MySQL根(Root)密碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-03-03

