Linux進(jìn)程地址空間詳解
一、程序地址空間
1、各內(nèi)存區(qū)域的相對(duì)位置
我記得在之前的博文中好像用編譯器粗略定位過各個(gè)類型地址空間的位置,這里我們?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在堆上開辟空間 printf("heap addr: %p\n", mem); printf("heap addr: %p\n", mem1); printf("heap addr: %p\n", mem2); //臨時(shí)變量在棧上開辟空間 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)核空間的最小地址處之間還有一部分 //用來存放我們的命令行和環(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)容來解釋
2、引入父子進(jì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; }
通過test進(jìn)程現(xiàn)象我們發(fā)現(xiàn),在這里我們的父子進(jìn)程在訪問我們的g_val值時(shí)訪問同一個(gè)位置同一個(gè)值,但是在我們子進(jìn)程對(duì)gal值進(jìn)行修改然后再讓父進(jìn)程進(jìn)行讀取的時(shí)候,我們發(fā)現(xiàn)父進(jìn)程讀取的值依舊是原來的g_val值,而子進(jìn)程讀取的值已經(jīng)是修改值,但地址還是相同的,地址相同怎么會(huì)讀取到不同的值呢?首先我們可以肯定的是,這個(gè)地址一定不是物理地址!同一塊物理地址訪問到的值一定是一樣的!
結(jié)合我們?cè)谇懊嬷v到的,如果子進(jìn)程修改了數(shù)據(jù),我們會(huì)在另一塊位置重新開辟一塊空間用來存放子進(jìn)程與父進(jìn)程不同的這部分?jǐn)?shù)據(jù),這是為什么呢?這個(gè)實(shí)現(xiàn)的原理是什么呢?我們也可以肯定的是,這個(gè)變量,也就是這個(gè)數(shù)據(jù),內(nèi)容是不一樣的,這是我們觀察到的,父子進(jìn)程輸出的一定不是同一個(gè)變量!下面我們來討論一下
二、進(jìn)程地址空間
1、頁表
我們?cè)谥爸v到的程序地址空間的說法其實(shí)是錯(cuò)誤的,正確來說應(yīng)該叫進(jìn)程地址空間,上面我們所說的地址叫做虛擬地址,也叫做線性地址,既然叫做虛擬地址,那當(dāng)然就不是真實(shí)的物理地址了,虛擬地址和物理地址存在映射關(guān)系,而承載他們映射關(guān)系的,就是頁表
我整理了一下地址空間、頁表和物理內(nèi)存的關(guān)系如下圖
在這個(gè)圖中,我們把父子進(jìn)程以及頁表分開來畫,因?yàn)樗鼈兪莾蓚€(gè)獨(dú)立的進(jìn)程,但是地址空間的這部分內(nèi)容是共享的,也就是虛擬地址是相同的,我們不是復(fù)制出了兩個(gè)地址空間,這里需要注意
內(nèi)核空間中有父子進(jìn)程的task_struct,它們里面有指向各自頁表的指針
其中上方是父進(jìn)程的地址空間,下方是子進(jìn)程的地址空間,子進(jìn)程直接復(fù)制父進(jìn)程的地址空間,包括虛擬地址、頁表等等變量都是相同的,類似于一個(gè)淺拷貝的過程,在子進(jìn)程修改g_val變量時(shí),子進(jìn)程在物理內(nèi)存上新開辟一塊空間,用來存放與父進(jìn)程數(shù)據(jù)不同的量,這個(gè)過程類似于memcpy的過程,創(chuàng)建并復(fù)制內(nèi)容,然后再將g_val改成100,然后頁表的物理地址指向該地址,這個(gè)過程是寫時(shí)拷貝,我們前面提到過
其中MMU起到的作用是負(fù)責(zé)將進(jìn)程虛擬地址轉(zhuǎn)換為物理地址,當(dāng) CPU 需要訪問內(nèi)存時(shí),會(huì)將虛擬地址發(fā)送給 MMU,MMU 根據(jù)頁表等數(shù)據(jù)結(jié)構(gòu)進(jìn)行地址轉(zhuǎn)換,是與頁表息息相關(guān)的一個(gè)內(nèi)存管理單元
2、深入理解進(jìn)程地址空間
那看到這里有人問了,地址空間究竟是什么啊,我們?yōu)槭裁匆M(jìn)行這樣的劃分?
我們一直拿32位的計(jì)算機(jī)舉例,因?yàn)樗粩?shù)少,比64位的計(jì)算機(jī)簡單一些,這里的32位計(jì)算機(jī)又指的是什么?
在 32 位計(jì)算機(jī)里,地址總線寬度是 32 位,也就是有 32 條線路,每條線路能通過高低電平的轉(zhuǎn)換來實(shí)現(xiàn)0和1的變化,所以這 32 條線路能表示的不同地址組合數(shù)量為 2^32個(gè),因?yàn)槊總€(gè)內(nèi)存地址對(duì)應(yīng)一個(gè)字節(jié),所以 32 位計(jì)算機(jī)理論上能直接訪問的內(nèi)存空間大小就是 2 ^32字節(jié),而2 ^32字節(jié)換算后等于 4GB,這就意味著 32 位計(jì)算機(jī)的 CPU 可以通過地址總線直接訪問從 0 到 2 ^32 - 1地址范圍內(nèi)的 4GB 物理內(nèi)存
我們的進(jìn)程地址空間就在這樣一個(gè)概念中展開,而地址空間的劃分實(shí)際上是對(duì)該空間的一種組織,在正常運(yùn)行的情況下互不影響
我們計(jì)算機(jī)中最小的存儲(chǔ)單元就是字節(jié)byte,每個(gè)字節(jié)都會(huì)有一個(gè)地址,這個(gè)地址是可以直接被操作系統(tǒng)使用的,這是可以使用地址找到的最小單位,類似于bit這樣的存儲(chǔ)單元是沒有地址的概念的
所以所謂的進(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)存
我們以頁表這樣的形式用來過渡,保證了我們所訪問的虛擬地址(線性地址)是線性的,我們的進(jìn)程不管要做什么,我們只要知道它做的事情的性質(zhì),我們就知道它大概存儲(chǔ)在哪個(gè)線性地址區(qū)域,并且因?yàn)橛辛隧摫淼拇嬖?,我?strong>不必再關(guān)心物理內(nèi)存的實(shí)際布局以及其他進(jìn)程的存在,我們本進(jìn)程只做好本進(jìn)程自己的事情就好了,其他的我并不關(guān)心
不同進(jìn)程的虛擬地址空間是相互隔離的,一個(gè)進(jìn)程無法直接訪問另一個(gè)進(jìn)程的虛擬地址空間,這就保證了進(jìn)程之間的獨(dú)立性和安全性,一個(gè)進(jìn)程的錯(cuò)誤或惡意操作不會(huì)影響到其他進(jìn)程的正常運(yùn)行
(二)保護(hù)物理內(nèi)存
增加進(jìn)程虛擬地址空間可以讓我們?cè)L問內(nèi)存的時(shí)候,增加一個(gè)轉(zhuǎn)換的過程,在這個(gè)轉(zhuǎn)換的過程中,可以對(duì)我們的尋址請(qǐng)求進(jìn)行審查,所以如果訪問異常,就可以直接攔截,請(qǐng)求不會(huì)到達(dá)物理內(nèi)存,從而很好的保護(hù)了物理內(nèi)存不被攻擊
(三)進(jìn)程管理模塊和內(nèi)存管理模塊低耦合
我們通過頁表這個(gè)結(jié)構(gòu),很好地將進(jìn)程管理和內(nèi)存管理解耦合,互不影響,我們進(jìn)程所看到的只有虛擬地址,并不在乎物理地址如何如何,而我們的內(nèi)存也不需要在乎有多少進(jìn)程,進(jìn)程的作用是什么,而是只在需要的時(shí)候開辟和回收空間就可以了,這樣我們?cè)谶M(jìn)程出現(xiàn)問題的時(shí)候不會(huì)影響到內(nèi)存管理,很好地阻斷了可能出現(xiàn)的一系列崩盤的問題
4、頁表的其他內(nèi)容
頁表除了我們上面提到的作用以外,還存在類似讀寫權(quán)限這樣的功能,我們?cè)谥皩W(xué)習(xí)的時(shí)候,我們知道在只讀數(shù)據(jù)段中的數(shù)據(jù)是只可讀不可寫的,那么它相對(duì)應(yīng)的映射到物理內(nèi)存上,物理內(nèi)存上又沒有限制條件,它是怎么實(shí)現(xiàn)的只讀呢?其實(shí)是頁表的某一項(xiàng)屬性控制了該變量的讀寫,分為不可讀寫、可讀不可寫、可寫不可讀、可讀可寫,在映射的同時(shí)將該性質(zhì)傳遞回去,就只可讀了
其他的還有對(duì)應(yīng)代碼和數(shù)據(jù)是否已經(jīng)加載到內(nèi)存等等一系列的其他屬性
頁表的本質(zhì)屬于進(jìn)程的硬件上下文,在進(jìn)程切換的時(shí)候會(huì)帶走這些信息,被存儲(chǔ)在CPU寄存器中,task_struct中有指向頁表地址的指針
缺頁中斷
在虛擬內(nèi)存系統(tǒng)里,程序運(yùn)行時(shí)使用的是虛擬地址,虛擬地址空間會(huì)被劃分為多個(gè)頁面。物理內(nèi)存則被劃分為與虛擬頁大小相同的頁框。當(dāng)程序訪問一個(gè)虛擬地址,而該地址對(duì)應(yīng)的頁面不在物理內(nèi)存中,也就是沒有被加載到物理內(nèi)存的頁框里時(shí),就會(huì)觸發(fā)缺頁中斷,這是一種特殊的中斷,它會(huì)暫停當(dāng)前程序的執(zhí)行,轉(zhuǎn)而去處理頁面加載的問題
進(jìn)程剛開始運(yùn)行時(shí),它的代碼和數(shù)據(jù)所在的頁面可能都還沒有被加載到物理內(nèi)存中,當(dāng)進(jìn)程第一次訪問某個(gè)頁面時(shí),就會(huì)因?yàn)樵擁撁娌辉趦?nèi)存而產(chǎn)生缺頁中斷;或者由于物理內(nèi)存資源有限,操作系統(tǒng)會(huì)使用頁面置換算法將一些暫時(shí)不用的頁面從物理內(nèi)存換出到磁盤的交換空間,當(dāng)進(jìn)程后續(xù)又需要訪問這些被換出的頁面時(shí),就會(huì)觸發(fā)缺頁中斷
當(dāng)缺頁中斷發(fā)生時(shí),CPU 會(huì)保存當(dāng)前進(jìn)程的現(xiàn)場信息,包括程序計(jì)數(shù)器、寄存器等內(nèi)容,以便在中斷處理完成后能恢復(fù)進(jìn)程的執(zhí)行,操作系統(tǒng)根據(jù)引發(fā)缺頁中斷的虛擬地址,查找該頁面在磁盤上的位置,這通常需要借助頁表等數(shù)據(jù)結(jié)構(gòu)來確定頁面的磁盤地址,如果物理內(nèi)存中有空閑的頁框,操作系統(tǒng)會(huì)直接分配一個(gè)頁框;若沒有空閑頁框,就需要使用頁面置換算法選擇一個(gè)當(dāng)前在物理內(nèi)存中的頁面換出到磁盤,為即將要加載的頁面騰出空間,然后發(fā)出磁盤 I/O 請(qǐng)求,將所需的頁面從磁盤讀取到分配好的物理頁框中,頁面加載完成后,操作系統(tǒng)會(huì)更新頁表,將該虛擬頁與新分配的物理頁框建立映射關(guān)系,并設(shè)置相應(yīng)的標(biāo)志位,表示該頁面現(xiàn)在已經(jīng)在物理內(nèi)存中,最后,操作系統(tǒng)恢復(fù)之前保存的進(jìn)程現(xiàn)場,讓進(jìn)程從產(chǎn)生缺頁中斷的指令處繼續(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)境搭建),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01linux系統(tǒng)下使用tcpdump進(jìn)行抓包方法
在本篇文章中小編給大家分享了關(guān)于linux系統(tǒng)下使用tcpdump進(jìn)行抓包的方法和相關(guān)知識(shí)點(diǎn),需要的朋友們學(xué)習(xí)下。2019-04-04Ubuntu18.04.2下安裝 RTX2080 Nvidia顯卡驅(qū)動(dòng)的方法
這篇文章主要介紹了Ubuntu18.04.2下安裝 RTX2080 Nvidia顯卡驅(qū)動(dòng)的方法,本文圖文并茂給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-07-07Linux低電量自動(dòng)關(guān)機(jī)的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于Linux低電量自動(dòng)關(guān)機(jī)的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用linux具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-11-11