Linux進(jìn)程地址空間詳解
一、程序地址空間
1、各內(nèi)存區(qū)域的相對(duì)位置
我記得在之前的博文中好像用編譯器粗略定位過(guò)各個(gè)類(lèi)型地址空間的位置,這里我們?cè)衮?yàn)證一下它們的相對(duì)關(guān)系,這里是32位的機(jī)器,存儲(chǔ)空間為2^32byte=4GB

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int g_val_1;
int g_val_2 = 100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr: %p\n", main);//代碼段
const char *str = "hello world";
printf("read only string addr: %p\n", str);//只讀數(shù)據(jù)段
printf("init global value addr: %p\n", &g_val_2);//數(shù)據(jù)段(已初始化)
printf("uninit global value addr: %p\n", &g_val_1);//BBS段(未初始化)
char *mem = (char*)malloc(100);
char *mem1 = (char*)malloc(100);
char *mem2 = (char*)malloc(100);
//malloc在堆上開(kāi)辟空間
printf("heap addr: %p\n", mem);
printf("heap addr: %p\n", mem1);
printf("heap addr: %p\n", mem2);
//臨時(shí)變量在棧上開(kāi)辟空間
printf("stack addr: %p\n", &str);
printf("stack addr: %p\n", &mem);
static int a = 0;
int b;
int c;
//靜態(tài)成員變量在數(shù)據(jù)段
printf("a = stack addr: %p\n", &a);
//臨時(shí)變量在棧區(qū)
printf("stack addr: %p\n", &b);
printf("stack addr: %p\n", &c);
//其實(shí)在棧區(qū)的最大地址處和內(nèi)核空間的最小地址處之間還有一部分
//用來(lái)存放我們的命令行和環(huán)境變量,且環(huán)境變量在大地址處
int i = 0;
for(; argv[i]; i++)
printf("argv[%d]: %p\n", i, argv[i]);
for(i=0; env[i]; i++)
printf("env[%d]: %p\n", i, env[i]);
return 0;
}

從圖中我們可以看到,棧區(qū)和堆區(qū)是相對(duì)而生的,其中間有很大一部分的空間,在它們的中間還有一段內(nèi)存映射段,這里我們后面結(jié)合后面的內(nèi)容來(lái)解釋
2、引入父子進(jìn)程問(wèn)題
- test
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{
//child
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{
//parent
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
- fork_test
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{
//child,子進(jìn)程先修改,完成之后,父進(jìn)程再讀取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{
//parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}
通過(guò)test進(jìn)程現(xiàn)象我們發(fā)現(xiàn),在這里我們的父子進(jìn)程在訪問(wèn)我們的g_val值時(shí)訪問(wèn)同一個(gè)位置同一個(gè)值,但是在我們子進(jìn)程對(duì)gal值進(jìn)行修改然后再讓父進(jìn)程進(jìn)行讀取的時(shí)候,我們發(fā)現(xiàn)父進(jìn)程讀取的值依舊是原來(lái)的g_val值,而子進(jìn)程讀取的值已經(jīng)是修改值,但地址還是相同的,地址相同怎么會(huì)讀取到不同的值呢?首先我們可以肯定的是,這個(gè)地址一定不是物理地址!同一塊物理地址訪問(wèn)到的值一定是一樣的!
結(jié)合我們?cè)谇懊嬷v到的,如果子進(jìn)程修改了數(shù)據(jù),我們會(huì)在另一塊位置重新開(kāi)辟一塊空間用來(lái)存放子進(jìn)程與父進(jìn)程不同的這部分?jǐn)?shù)據(jù),這是為什么呢?這個(gè)實(shí)現(xiàn)的原理是什么呢?我們也可以肯定的是,這個(gè)變量,也就是這個(gè)數(shù)據(jù),內(nèi)容是不一樣的,這是我們觀察到的,父子進(jìn)程輸出的一定不是同一個(gè)變量!下面我們來(lái)討論一下
二、進(jìn)程地址空間
1、頁(yè)表
我們?cè)谥爸v到的程序地址空間的說(shuō)法其實(shí)是錯(cuò)誤的,正確來(lái)說(shuō)應(yīng)該叫進(jìn)程地址空間,上面我們所說(shuō)的地址叫做虛擬地址,也叫做線性地址,既然叫做虛擬地址,那當(dāng)然就不是真實(shí)的物理地址了,虛擬地址和物理地址存在映射關(guān)系,而承載他們映射關(guān)系的,就是頁(yè)表
我整理了一下地址空間、頁(yè)表和物理內(nèi)存的關(guān)系如下圖

在這個(gè)圖中,我們把父子進(jìn)程以及頁(yè)表分開(kāi)來(lái)畫(huà),因?yàn)樗鼈兪莾蓚€(gè)獨(dú)立的進(jìn)程,但是地址空間的這部分內(nèi)容是共享的,也就是虛擬地址是相同的,我們不是復(fù)制出了兩個(gè)地址空間,這里需要注意
內(nèi)核空間中有父子進(jìn)程的task_struct,它們里面有指向各自頁(yè)表的指針
其中上方是父進(jìn)程的地址空間,下方是子進(jìn)程的地址空間,子進(jìn)程直接復(fù)制父進(jìn)程的地址空間,包括虛擬地址、頁(yè)表等等變量都是相同的,類(lèi)似于一個(gè)淺拷貝的過(guò)程,在子進(jìn)程修改g_val變量時(shí),子進(jìn)程在物理內(nèi)存上新開(kāi)辟一塊空間,用來(lái)存放與父進(jìn)程數(shù)據(jù)不同的量,這個(gè)過(guò)程類(lèi)似于memcpy的過(guò)程,創(chuàng)建并復(fù)制內(nèi)容,然后再將g_val改成100,然后頁(yè)表的物理地址指向該地址,這個(gè)過(guò)程是寫(xiě)時(shí)拷貝,我們前面提到過(guò)
其中MMU起到的作用是負(fù)責(zé)將進(jìn)程虛擬地址轉(zhuǎn)換為物理地址,當(dāng) CPU 需要訪問(wèn)內(nèi)存時(shí),會(huì)將虛擬地址發(fā)送給 MMU,MMU 根據(jù)頁(yè)表等數(shù)據(jù)結(jié)構(gòu)進(jìn)行地址轉(zhuǎn)換,是與頁(yè)表息息相關(guān)的一個(gè)內(nèi)存管理單元
2、深入理解進(jìn)程地址空間
那看到這里有人問(wèn)了,地址空間究竟是什么啊,我們?yōu)槭裁匆M(jìn)行這樣的劃分?
我們一直拿32位的計(jì)算機(jī)舉例,因?yàn)樗粩?shù)少,比64位的計(jì)算機(jī)簡(jiǎn)單一些,這里的32位計(jì)算機(jī)又指的是什么?
在 32 位計(jì)算機(jī)里,地址總線寬度是 32 位,也就是有 32 條線路,每條線路能通過(guò)高低電平的轉(zhuǎn)換來(lái)實(shí)現(xiàn)0和1的變化,所以這 32 條線路能表示的不同地址組合數(shù)量為 2^32個(gè),因?yàn)槊總€(gè)內(nèi)存地址對(duì)應(yīng)一個(gè)字節(jié),所以 32 位計(jì)算機(jī)理論上能直接訪問(wèn)的內(nèi)存空間大小就是 2 ^32字節(jié),而2 ^32字節(jié)換算后等于 4GB,這就意味著 32 位計(jì)算機(jī)的 CPU 可以通過(guò)地址總線直接訪問(wèn)從 0 到 2 ^32 - 1地址范圍內(nèi)的 4GB 物理內(nèi)存
我們的進(jìn)程地址空間就在這樣一個(gè)概念中展開(kāi),而地址空間的劃分實(shí)際上是對(duì)該空間的一種組織,在正常運(yùn)行的情況下互不影響
我們計(jì)算機(jī)中最小的存儲(chǔ)單元就是字節(jié)byte,每個(gè)字節(jié)都會(huì)有一個(gè)地址,這個(gè)地址是可以直接被操作系統(tǒng)使用的,這是可以使用地址找到的最小單位,類(lèi)似于bit這樣的存儲(chǔ)單元是沒(méi)有地址的概念的
所以所謂的進(jìn)程地址空間,本質(zhì)上是一個(gè)描述進(jìn)程可視范圍的大小,地址空間內(nèi)一定要存在各種區(qū)域的劃分,只要對(duì)虛擬地址(線性地址)進(jìn)行區(qū)域劃分即可

這里要注意的是,棧的start是高地址處,其他用戶空間都是start為低地址處
3、進(jìn)程地址空間這樣組織的優(yōu)勢(shì)
(一)讓進(jìn)程以一個(gè)統(tǒng)一的視角看待內(nèi)存
我們以頁(yè)表這樣的形式用來(lái)過(guò)渡,保證了我們所訪問(wèn)的虛擬地址(線性地址)是線性的,我們的進(jìn)程不管要做什么,我們只要知道它做的事情的性質(zhì),我們就知道它大概存儲(chǔ)在哪個(gè)線性地址區(qū)域,并且因?yàn)橛辛隧?yè)表的存在,我們不必再關(guān)心物理內(nèi)存的實(shí)際布局以及其他進(jìn)程的存在,我們本進(jìn)程只做好本進(jìn)程自己的事情就好了,其他的我并不關(guān)心
不同進(jìn)程的虛擬地址空間是相互隔離的,一個(gè)進(jìn)程無(wú)法直接訪問(wèn)另一個(gè)進(jìn)程的虛擬地址空間,這就保證了進(jìn)程之間的獨(dú)立性和安全性,一個(gè)進(jìn)程的錯(cuò)誤或惡意操作不會(huì)影響到其他進(jìn)程的正常運(yùn)行
(二)保護(hù)物理內(nèi)存
增加進(jìn)程虛擬地址空間可以讓我們?cè)L問(wèn)內(nèi)存的時(shí)候,增加一個(gè)轉(zhuǎn)換的過(guò)程,在這個(gè)轉(zhuǎn)換的過(guò)程中,可以對(duì)我們的尋址請(qǐng)求進(jìn)行審查,所以如果訪問(wèn)異常,就可以直接攔截,請(qǐng)求不會(huì)到達(dá)物理內(nèi)存,從而很好的保護(hù)了物理內(nèi)存不被攻擊
(三)進(jìn)程管理模塊和內(nèi)存管理模塊低耦合

我們通過(guò)頁(yè)表這個(gè)結(jié)構(gòu),很好地將進(jìn)程管理和內(nèi)存管理解耦合,互不影響,我們進(jìn)程所看到的只有虛擬地址,并不在乎物理地址如何如何,而我們的內(nèi)存也不需要在乎有多少進(jìn)程,進(jìn)程的作用是什么,而是只在需要的時(shí)候開(kāi)辟和回收空間就可以了,這樣我們?cè)谶M(jìn)程出現(xiàn)問(wèn)題的時(shí)候不會(huì)影響到內(nèi)存管理,很好地阻斷了可能出現(xiàn)的一系列崩盤(pán)的問(wèn)題
4、頁(yè)表的其他內(nèi)容
頁(yè)表除了我們上面提到的作用以外,還存在類(lèi)似讀寫(xiě)權(quán)限這樣的功能,我們?cè)谥皩W(xué)習(xí)的時(shí)候,我們知道在只讀數(shù)據(jù)段中的數(shù)據(jù)是只可讀不可寫(xiě)的,那么它相對(duì)應(yīng)的映射到物理內(nèi)存上,物理內(nèi)存上又沒(méi)有限制條件,它是怎么實(shí)現(xiàn)的只讀呢?其實(shí)是頁(yè)表的某一項(xiàng)屬性控制了該變量的讀寫(xiě),分為不可讀寫(xiě)、可讀不可寫(xiě)、可寫(xiě)不可讀、可讀可寫(xiě),在映射的同時(shí)將該性質(zhì)傳遞回去,就只可讀了
其他的還有對(duì)應(yīng)代碼和數(shù)據(jù)是否已經(jīng)加載到內(nèi)存等等一系列的其他屬性
頁(yè)表的本質(zhì)屬于進(jìn)程的硬件上下文,在進(jìn)程切換的時(shí)候會(huì)帶走這些信息,被存儲(chǔ)在CPU寄存器中,task_struct中有指向頁(yè)表地址的指針
缺頁(yè)中斷
在虛擬內(nèi)存系統(tǒng)里,程序運(yùn)行時(shí)使用的是虛擬地址,虛擬地址空間會(huì)被劃分為多個(gè)頁(yè)面。物理內(nèi)存則被劃分為與虛擬頁(yè)大小相同的頁(yè)框。當(dāng)程序訪問(wèn)一個(gè)虛擬地址,而該地址對(duì)應(yīng)的頁(yè)面不在物理內(nèi)存中,也就是沒(méi)有被加載到物理內(nèi)存的頁(yè)框里時(shí),就會(huì)觸發(fā)缺頁(yè)中斷,這是一種特殊的中斷,它會(huì)暫停當(dāng)前程序的執(zhí)行,轉(zhuǎn)而去處理頁(yè)面加載的問(wèn)題
進(jìn)程剛開(kāi)始運(yùn)行時(shí),它的代碼和數(shù)據(jù)所在的頁(yè)面可能都還沒(méi)有被加載到物理內(nèi)存中,當(dāng)進(jìn)程第一次訪問(wèn)某個(gè)頁(yè)面時(shí),就會(huì)因?yàn)樵擁?yè)面不在內(nèi)存而產(chǎn)生缺頁(yè)中斷;或者由于物理內(nèi)存資源有限,操作系統(tǒng)會(huì)使用頁(yè)面置換算法將一些暫時(shí)不用的頁(yè)面從物理內(nèi)存換出到磁盤(pán)的交換空間,當(dāng)進(jìn)程后續(xù)又需要訪問(wèn)這些被換出的頁(yè)面時(shí),就會(huì)觸發(fā)缺頁(yè)中斷
當(dāng)缺頁(yè)中斷發(fā)生時(shí),CPU 會(huì)保存當(dāng)前進(jìn)程的現(xiàn)場(chǎng)信息,包括程序計(jì)數(shù)器、寄存器等內(nèi)容,以便在中斷處理完成后能恢復(fù)進(jìn)程的執(zhí)行,操作系統(tǒng)根據(jù)引發(fā)缺頁(yè)中斷的虛擬地址,查找該頁(yè)面在磁盤(pán)上的位置,這通常需要借助頁(yè)表等數(shù)據(jù)結(jié)構(gòu)來(lái)確定頁(yè)面的磁盤(pán)地址,如果物理內(nèi)存中有空閑的頁(yè)框,操作系統(tǒng)會(huì)直接分配一個(gè)頁(yè)框;若沒(méi)有空閑頁(yè)框,就需要使用頁(yè)面置換算法選擇一個(gè)當(dāng)前在物理內(nèi)存中的頁(yè)面換出到磁盤(pán),為即將要加載的頁(yè)面騰出空間,然后發(fā)出磁盤(pán) I/O 請(qǐng)求,將所需的頁(yè)面從磁盤(pán)讀取到分配好的物理頁(yè)框中,頁(yè)面加載完成后,操作系統(tǒng)會(huì)更新頁(yè)表,將該虛擬頁(yè)與新分配的物理頁(yè)框建立映射關(guān)系,并設(shè)置相應(yīng)的標(biāo)志位,表示該頁(yè)面現(xiàn)在已經(jīng)在物理內(nèi)存中,最后,操作系統(tǒng)恢復(fù)之前保存的進(jìn)程現(xiàn)場(chǎng),讓進(jìn)程從產(chǎn)生缺頁(yè)中斷的指令處繼續(xù)執(zhí)行
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解Linux中PostgreSQL和PostGIS的安裝和使用
這篇文章主要介紹了詳解Linux中PostgreSQL和PostGIS的安裝和使用,并把需要注意點(diǎn)做了分析和解釋,需要的朋友學(xué)習(xí)下。2018-02-02
基于ubuntu16 Python3 tensorflow(TensorFlow環(huán)境搭建)
這篇文章主要介紹了基于ubuntu16 Python3 tensorflow(TensorFlow環(huán)境搭建),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
linux系統(tǒng)下使用tcpdump進(jìn)行抓包方法
在本篇文章中小編給大家分享了關(guān)于linux系統(tǒng)下使用tcpdump進(jìn)行抓包的方法和相關(guān)知識(shí)點(diǎn),需要的朋友們學(xué)習(xí)下。2019-04-04
Ubuntu18.04.2下安裝 RTX2080 Nvidia顯卡驅(qū)動(dòng)的方法
這篇文章主要介紹了Ubuntu18.04.2下安裝 RTX2080 Nvidia顯卡驅(qū)動(dòng)的方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-07-07
Linux低電量自動(dòng)關(guān)機(jī)的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于Linux低電量自動(dòng)關(guān)機(jī)的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用linux具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11
Linux YUM倉(cāng)庫(kù)及NFS共享服務(wù)方式
YUM(Yellowdog Updater Modified)是基于RPM包的軟件包管理器,專門(mén)用于解決軟件包的依賴關(guān)系,支持通過(guò)FTP、HTTP服務(wù)或本地目錄從集中的YUM軟件倉(cāng)庫(kù)獲取軟件包,YUM能夠自動(dòng)處理包依賴問(wèn)題,簡(jiǎn)化了軟件安裝和更新過(guò)程2024-09-09

