?
??樊梓慕:個(gè)人主頁(yè)
???個(gè)人專(zhuān)欄:《C語(yǔ)言》《數(shù)據(jù)結(jié)構(gòu)》《藍(lán)橋杯試題》《LeetCode刷題筆記》《實(shí)訓(xùn)項(xiàng)目》《C++》《Linux》
??每一個(gè)不曾起舞的日子,都是對(duì)生命的辜負(fù)
目錄
前言
1.程序地址空間
1.1驗(yàn)證地址空間的排布
?1.2利用fork函數(shù)觀察當(dāng)子進(jìn)程修改某個(gè)共享變量時(shí)父子進(jìn)程分別讀取到的值和地址
2.進(jìn)程地址空間
2.1操作系統(tǒng)是如何建立起進(jìn)程與物理內(nèi)存之間的聯(lián)系的呢??
2.2什么是進(jìn)程地址空間?
2.3為什么有進(jìn)程地址空間和頁(yè)表
2.4malloc和new開(kāi)辟空間的原理
2.5頁(yè)表與寫(xiě)時(shí)拷貝的更多細(xì)節(jié)
前言
在之前學(xué)習(xí)進(jìn)程概念時(shí)我們提到過(guò)fork函數(shù),了解了如何創(chuàng)建進(jìn)程,并且知道了fork之后的父子進(jìn)程代碼共享,當(dāng)父子對(duì)共享的變量做修改時(shí)會(huì)拷貝一份到自己這再做修改(寫(xiě)時(shí)拷貝),但當(dāng)時(shí)對(duì)于一個(gè)變量為什么能有兩個(gè)值我們的講解仍然十分局限,今天在學(xué)習(xí)完進(jìn)程地址空間后,我想你就會(huì)明白原因所在。
?歡迎大家??收藏??以便未來(lái)做題時(shí)可以快速找到思路,巧妙的方法可以事半功倍。
=========================================================================
GITEE相關(guān)代碼:??fanfei_c的倉(cāng)庫(kù)??
=========================================================================?
1.程序地址空間
在之前學(xué)習(xí)內(nèi)存管理時(shí)我相信你一定見(jiàn)過(guò)這張圖:
?當(dāng)時(shí)我們說(shuō)這是底層物理內(nèi)存的分布,那今天我可能要告訴你他其實(shí)并不是,而只是操作系統(tǒng)創(chuàng)造出來(lái)的一個(gè)虛擬的結(jié)構(gòu),而真實(shí)的物理內(nèi)存分布其實(shí)并不是如此。
但正式開(kāi)始之前:我們還是來(lái)驗(yàn)證一下數(shù)據(jù)是不是按如圖所示進(jìn)行排列的呢?
1.1驗(yàn)證地址空間的排布
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{
printf("code addr:\t%p\n", main);//驗(yàn)證正文代碼
printf("init data addr:\t%p\n", &g_val);//驗(yàn)證初始化數(shù)據(jù)(全局)
printf("uninit data addr: %p\n", &g_unval);//驗(yàn)證未初始化數(shù)據(jù)(全局)
char *heap = (char*)malloc(20);//如圖先創(chuàng)建的動(dòng)態(tài)內(nèi)存應(yīng)該在堆底
char *heap1 = (char*)malloc(20);//所以heap的地址應(yīng)為最小
char *heap2 = (char*)malloc(20);//heap3的地址應(yīng)為最大
char *heap3 = (char*)malloc(20);//一會(huì)觀察是否是這樣
printf("heap addr: %p\n", heap);//驗(yàn)證堆區(qū)(動(dòng)態(tài)內(nèi)存)
printf("heap1 addr: %p\n", heap1);
printf("heap2 addr: %p\n", heap2);
printf("heap3 addr: %p\n", heap3);
printf("stack addr: %p\n", &heap);//驗(yàn)證棧區(qū)(指針變量)
printf("stack addr: %p\n", &heap1);//如圖先創(chuàng)建的heap指針應(yīng)該在??臻g中地址最大
printf("stack addr: %p\n", &heap2);//所以&heap應(yīng)為最大
printf("stack addr: %p\n", &heap3);//&heap3應(yīng)為最小
for(int i = 0; argv[i]; i++)//驗(yàn)證命令行參數(shù)
{
printf("argv[%d]=%p\n", i, argv[i]);
}
for(int i = 0; env[i]; i++)//驗(yàn)證環(huán)境變量
{
printf("env[%d]=%p\n", i, env[i]);
}
return 0;
}
?打印出來(lái)看看是不是這樣呢?
補(bǔ)充知識(shí):當(dāng)一個(gè)變量被定義為static變量時(shí),其實(shí)該變量的地址就被放到了全局變量的區(qū)域,他在某種意義上來(lái)講就是全局變量,但是由于編譯器的原因會(huì)對(duì)他進(jìn)行語(yǔ)法上的檢查等,才呈現(xiàn)出了靜態(tài)變量的特性。?
?1.2利用fork函數(shù)觀察當(dāng)子進(jìn)程修改某個(gè)共享變量時(shí)父子進(jìn)程分別讀取到的值和地址
既然我們之前在進(jìn)程概念的學(xué)習(xí)中創(chuàng)建過(guò)子進(jìn)程,那我們剛好可以觀察一下當(dāng)子進(jìn)程修改某一共享變量時(shí),父子進(jìn)程讀取到的該變量的值是否會(huì)發(fā)生改變,該變量的地址又呈現(xiàn)出什么樣的內(nèi)容?
int g_val = 100;
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 0;
//子進(jìn)程
while(1)
{
printf("child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",
getpid(), getppid(),
g_val, &g_val);//獲取子進(jìn)程信息以及變量g_val的值與地址
sleep(1);
cnt++;
if(cnt == 2)//2s后修改全局變量g_val的值為200
{
g_val = 200;
printf("child change g_val: 100->200\n");
}
}
}
else
{
while(1)
{
printf("father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",
getpid(), getppid(),
g_val, &g_val);//獲取父進(jìn)程信息以及變量g_val的值與地址
sleep(1);
}
}
}
解析代碼:2秒之前父子進(jìn)程讀取變量g_val的值,2秒后子進(jìn)程對(duì)該變量進(jìn)行修改,觀察修改之后父子進(jìn)程讀取該變量的值如何變化,并且是否符合我們之前所講的寫(xiě)時(shí)拷貝,是否會(huì)拷貝一份給自己再修改?
我們發(fā)現(xiàn)確實(shí),當(dāng)子進(jìn)程對(duì)變量進(jìn)行修改時(shí),子進(jìn)程對(duì)應(yīng)的g_val發(fā)生了改變,而父進(jìn)程沒(méi)有改變,進(jìn)程之間確實(shí)具有獨(dú)立性。
可是最令人費(fèi)解的是,父子進(jìn)程讀取該變量的地址竟然相同???
這也就證實(shí)了之前我們所學(xué)習(xí)的所謂的內(nèi)存分布圖是假的,打印出來(lái)的地址也是假的,因?yàn)槿绻俏锢韮?nèi)存地址,同一物理地址是不可能存放兩個(gè)值的!!
結(jié)論:
- 我們所有用到的語(yǔ)言上的地址,都不是物理地址,而是虛擬地址(線(xiàn)性地址)。
- 此圖不是物理內(nèi)存分布圖,而是進(jìn)程地址空間分布圖。
2.進(jìn)程地址空間
現(xiàn)在你就知道了文章開(kāi)頭給出的圖片根本不是什么物理內(nèi)存分布圖,而是進(jìn)程地址空間分布圖。
完了,我們之前所學(xué)被顛覆了,那物理內(nèi)存到底在哪里啊,進(jìn)程是如何訪問(wèn)到物理內(nèi)存的?
所以我們繼續(xù)往下看:
2.1操作系統(tǒng)是如何建立起進(jìn)程與物理內(nèi)存之間的聯(lián)系的呢??
首先:每一個(gè)進(jìn)程都會(huì)存在一個(gè)進(jìn)程地址空間,操作系統(tǒng)如何管理這些進(jìn)程地址空間呢?
先描述,再組織。
所以進(jìn)程地址空間本質(zhì)上就是一種數(shù)據(jù)結(jié)構(gòu),PCB中會(huì)有一個(gè)指針指向該數(shù)據(jù)結(jié)構(gòu),該數(shù)據(jù)結(jié)構(gòu)中存儲(chǔ)的就是對(duì)應(yīng)的虛擬地址,所以操作系統(tǒng)對(duì)進(jìn)程地址空間的管理也就變成了對(duì)該數(shù)據(jù)結(jié)構(gòu)的管理。
另外操作系統(tǒng)會(huì)為我們維護(hù)一張映射表:頁(yè)表。
- 該表中存儲(chǔ)的就是虛擬地址與物理地址,通過(guò)虛擬地址就可以找到物理地址,也就建立起來(lái)了進(jìn)程與物理內(nèi)存的聯(lián)系。
?當(dāng)創(chuàng)建子進(jìn)程時(shí),子進(jìn)程會(huì)繼承父進(jìn)程的進(jìn)程地址空間、頁(yè)表等。
所以我們說(shuō)父子進(jìn)程代碼共享,數(shù)據(jù)共享,是因?yàn)樗麄兊捻?yè)表是相同的。
但對(duì)共享的變量進(jìn)行修改時(shí),會(huì)發(fā)生寫(xiě)時(shí)拷貝,拷貝到的代碼和數(shù)據(jù)也是新開(kāi)辟在物理內(nèi)存上的,此時(shí)子進(jìn)程只需要修改頁(yè)表,虛擬地址不變,而物理地址則是新開(kāi)辟的物理地址。
所以才會(huì)出現(xiàn)虛擬地址相同,而物理地址不同的情況。
2.2什么是進(jìn)程地址空間?
每一個(gè)進(jìn)程都會(huì)存在一個(gè)進(jìn)程地址空間,在32位操作系統(tǒng)下,該空間的大小為[0,4]GB。?
上面說(shuō)到:進(jìn)程地址空間其實(shí)就是一個(gè)數(shù)據(jù)結(jié)構(gòu),那該數(shù)據(jù)結(jié)構(gòu)中都存在有哪些內(nèi)容呢?
查看Linux內(nèi)核源碼:
我們找到mm_struct的定義:
struct mm_struct
{
struct vm_area_struct* mmap;
struct rb_root mm_rb;
struct vm_area_struct* mmap_cache;
//....
unsingned long start_code, end_code, start_data, end_data;
//代碼段的開(kāi)始start_code ,結(jié)束end_code,數(shù)據(jù)段的開(kāi)始start_data,結(jié)束end_data
unsigned long start_brk, brk, start_stack;
//start_brk和brk記錄有關(guān)堆的信息,
//start_brk是用戶(hù)虛擬地址空間初始化,
//brk是當(dāng)前堆的結(jié)束地址,
//start_stack是棧的起始地址
unsigned long arg_start, arg_end, env_start, env_end;
//參數(shù)段的開(kāi)始arg_start,結(jié)束arg_end,
//環(huán)境段的開(kāi)始env_start,結(jié)束env_end
}
?那么如何理解各個(gè)數(shù)據(jù)存放的區(qū)域呢,如上面的源碼所示:就是利用首尾的位置信息。
通過(guò)這些信息我們就可以:
- 判斷是否越界
- 可以進(jìn)行擴(kuò)大和縮小范圍?
區(qū)域劃分的本質(zhì)就是區(qū)域內(nèi)的地址我們可以使用。?
可是我們又知道進(jìn)程地址空間是不具備保存實(shí)際的代碼和數(shù)據(jù)的能力的。
這些代碼和數(shù)據(jù)實(shí)際是放置在物理內(nèi)存上的。
所以就需要頁(yè)表的存在來(lái)將虛擬地址轉(zhuǎn)化為實(shí)際的物理內(nèi)存地址。
那轉(zhuǎn)化的工作是誰(shuí)來(lái)做呢?
- 粗淺的說(shuō)是CPU,在轉(zhuǎn)化的過(guò)程中,CPU中的CR3寄存器會(huì)記錄頁(yè)表的地址(注意:CR3中存儲(chǔ)的地址一定是真實(shí)的物理地址,如果是虛擬地址,那CPU還不知道頁(yè)表在哪,那怎么通過(guò)映射關(guān)系找到CR3中虛擬地址映射到實(shí)際的物理地址呢),當(dāng)CPU開(kāi)始執(zhí)行正文代碼時(shí),假設(shè)遇到了a++這樣的指令,那么CPU就會(huì)根據(jù)CR3寄存器中頁(yè)表的地址進(jìn)行查表,從而就得到了物理內(nèi)存地址,也就找到了a的值。
- 準(zhǔn)確的說(shuō),這個(gè)轉(zhuǎn)化工作是由CPU中的硬件單元MMU(內(nèi)存管理單元)完成的。
2.3為什么有進(jìn)程地址空間和頁(yè)表
- 因?yàn)橛辛诉M(jìn)程地址空間和頁(yè)表,物理內(nèi)存空間上不連續(xù)、無(wú)序的空間就可以通過(guò)頁(yè)表這一映射關(guān)系聯(lián)系在一起,讓進(jìn)程以統(tǒng)一的視角看待內(nèi)存。
- 有了進(jìn)程地址空間和頁(yè)表后,每個(gè)進(jìn)程都認(rèn)為自己在獨(dú)占內(nèi)存,這樣能更好的保障進(jìn)程的獨(dú)立性以及合理使用內(nèi)存空間(當(dāng)實(shí)際需要使用內(nèi)存空間的時(shí)候再在內(nèi)存進(jìn)行開(kāi)辟),并能將進(jìn)程管理與內(nèi)存管理進(jìn)行解耦合。
- 地址空間+頁(yè)表的設(shè)計(jì)是保護(hù)內(nèi)存安全的重要手段!
2.4malloc和new開(kāi)辟空間的原理
在之前的學(xué)習(xí)中,我們不知道進(jìn)程地址空間的概念,所以malloc和new開(kāi)辟空間我們總是默認(rèn)為內(nèi)存上的操作,而學(xué)習(xí)完進(jìn)程地址空間后,你會(huì)發(fā)現(xiàn)并不是如此。
當(dāng)代碼執(zhí)行到malloc和new時(shí),OS不一定會(huì)直接將實(shí)際的物理內(nèi)存分配給你,因?yàn)樵撨M(jìn)程可能不會(huì)立即使用該塊內(nèi)存,也就造成了內(nèi)存浪費(fèi),OS一定要確保效率和資源使用率,所以O(shè)S給你分配的實(shí)際上是進(jìn)程地址空間,地址也是虛擬地址,而且并不會(huì)在頁(yè)表上建立有效的映射關(guān)系。
當(dāng)檢測(cè)到該進(jìn)程實(shí)際要使用該塊空間時(shí)(寫(xiě)入修改之類(lèi)的操作,讀取不算),會(huì)發(fā)生缺頁(yè)中斷,然后立即在頁(yè)表中建立映射關(guān)系,此時(shí)該進(jìn)程需要的物理內(nèi)存空間才被申請(qǐng)。
這樣做有什么好處呢?
- 充分保證內(nèi)存的使用率,不會(huì)造成空轉(zhuǎn);
- 提升new或malloc的速度(因?yàn)闆](méi)有實(shí)際在內(nèi)存上開(kāi)辟空間)。
2.5頁(yè)表與寫(xiě)時(shí)拷貝的更多細(xì)節(jié)
?頁(yè)表其實(shí)不光存放虛擬地址和物理內(nèi)存地址,還有其他的屬性,比如會(huì)存放權(quán)限屬性。
什么意思呢?
我們平時(shí)寫(xiě)代碼時(shí)常量不可修改究竟是誰(shuí)決定的?
- 其實(shí)就是操作系統(tǒng)在頁(yè)表中該數(shù)據(jù)的權(quán)限屬性上放置的是'r',當(dāng)你要對(duì)該數(shù)據(jù)進(jìn)行修改時(shí)(寫(xiě)入)時(shí),首先需要進(jìn)行虛擬地址與物理地址的轉(zhuǎn)化,轉(zhuǎn)化的過(guò)程中操作系統(tǒng)發(fā)現(xiàn)權(quán)限為只讀,所以才不可修改不可寫(xiě)入。
那const修飾的數(shù)據(jù)是不是也是由頁(yè)表決定的呢?
- 不是!const與系統(tǒng)沒(méi)有任何關(guān)系,const是編譯器檢查前后語(yǔ)法的問(wèn)題。const的意義是將可能在未來(lái)運(yùn)行時(shí)出現(xiàn)的錯(cuò)誤提前在編譯階段發(fā)現(xiàn)并報(bào)錯(cuò)。所以我們說(shuō)const能加則加,是一種好的編程習(xí)慣,防御性編程。
你知道操作系統(tǒng)是如何知道什么時(shí)候進(jìn)行寫(xiě)時(shí)拷貝的呢?
在父進(jìn)程創(chuàng)建子進(jìn)程時(shí),按之前所學(xué)子進(jìn)程會(huì)繼承父進(jìn)程的進(jìn)程地址空間和頁(yè)表。
并且操作系統(tǒng)還會(huì)將父子進(jìn)程的頁(yè)表中數(shù)據(jù)對(duì)應(yīng)的權(quán)限屬性修改為只讀!
當(dāng)父或子進(jìn)程修改(寫(xiě)入)該數(shù)據(jù)時(shí),會(huì)發(fā)生缺頁(yè)中斷,但其實(shí)缺頁(yè)中斷做的工作不僅會(huì)在物理內(nèi)存上開(kāi)辟空間建立映射關(guān)系,還會(huì)對(duì)我們的訪問(wèn)操作做判斷:
- 操作系統(tǒng)會(huì)判斷,頁(yè)表權(quán)限為只讀,但數(shù)據(jù)所在的進(jìn)程地址空間屬于可讀可寫(xiě)的數(shù)據(jù)區(qū),操作系統(tǒng)明白了,這是要寫(xiě)時(shí)拷貝??!
所以這就是操作系統(tǒng)判斷什么時(shí)候進(jìn)行寫(xiě)時(shí)拷貝的原理,根據(jù)這個(gè)方法,操作系統(tǒng)就能實(shí)現(xiàn)按需拷貝!
誰(shuí)要使用(寫(xiě)入)給誰(shuí)開(kāi)辟新的物理空間,否則就不拷貝,共用物理內(nèi)存空間。?
=========================================================================文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-761889.html
如果你對(duì)該系列文章有興趣的話(huà),歡迎持續(xù)關(guān)注博主動(dòng)態(tài),博主會(huì)持續(xù)輸出優(yōu)質(zhì)內(nèi)容
??博主很需要大家的支持,你的支持是我創(chuàng)作的不竭動(dòng)力??
??~ 點(diǎn)贊收藏+關(guān)注 ~??文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-761889.html
=========================================================================
到了這里,關(guān)于【Linux】進(jìn)程周邊006之進(jìn)程地址空間的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!