父子進(jìn)程地址相同的變量值不同問(wèn)題
#include<stdio.h>
#include<unistd.h>
int g_val = 100;
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子進(jìn)程
int i = 0;
while (1)
{
printf("I am child process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
i++;
if (i == 5)
{
g_val = 200;
printf("Child process changed g_val success!!!\n");
}
sleep(1);
}
}
else {
while (1)
{
printf("I am parent process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}
運(yùn)行結(jié)果
[yzl@VM-4-5-centos tmp]$ ./proc
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:100, &g_val:0x601054
Child process changed g_val success!!!
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:200, &g_val:0x601054
I am parent process, id:28298, g_val:100, &g_val:0x601054
I am child process, id:28299, g_val:200, &g_val:0x601054
上述代碼為,創(chuàng)建子進(jìn)程,若干秒后,子進(jìn)程改變?nèi)肿兞恐担l(fā)現(xiàn)子進(jìn)程與父進(jìn)程打印此全局變量值時(shí),值不同,且地址相同。
同一個(gè)地址處的值在同一時(shí)刻不可能不同,于是引出了虛擬地址空間的概念。即這里子進(jìn)程與父進(jìn)程打印的地址并非實(shí)際的物理地址,而是一種虛擬地址(線性地址)。
Linux下進(jìn)程虛擬地址空間分布
虛擬地址空間使得每個(gè)進(jìn)程看待內(nèi)存時(shí)都有一個(gè)統(tǒng)一的視角,并且在他們看來(lái),內(nèi)存的分布是井然有序的。具體分布如下圖
-
棧堆相向增長(zhǎng),堆向高地址增長(zhǎng),棧向低地址增長(zhǎng)。這兩個(gè)區(qū)域是動(dòng)態(tài)變化的。
-
虛擬地址空間分為兩個(gè)空間:1. 內(nèi)核空間,在32位下占1G 2. 用戶空間,在32位下占3G即[0, 3GB] 用戶空間 [3GB, 4GB] 內(nèi)核空間
-
static修飾局部變量,本質(zhì)上是將此變量屬性變?yōu)槿謱傩?,存?chǔ)在全局區(qū)。而語(yǔ)法的限制使得此static變量?jī)H能在局部可見(jiàn)。
-
上圖虛擬內(nèi)存分布僅適用于Linux操作系統(tǒng),不適于Windows。
-
一個(gè)有關(guān)堆區(qū)的知識(shí):當(dāng)C語(yǔ)言使用malloc函數(shù)時(shí),申請(qǐng)10字節(jié)空間,實(shí)際在內(nèi)存中會(huì)占用大于10字節(jié)的空間,多出的空間用于存儲(chǔ)一些屬性。這也是為什么free時(shí)傳首地址即可,而不需要傳空間大小。
什么是虛擬地址空間?
-
虛擬地址空間(進(jìn)程地址空間)在操作系統(tǒng)內(nèi)核中是一個(gè)數(shù)據(jù)結(jié)構(gòu),在Linux內(nèi)核中,就是一個(gè)struct結(jié)構(gòu)體
-
在Linux下,進(jìn)程地址空間是一個(gè)名為mm_struct的結(jié)構(gòu)體,主要存儲(chǔ)各區(qū)域(堆,棧,全局?jǐn)?shù)據(jù)區(qū),只讀代碼區(qū)等)的范圍,即start和end,用于劃分各個(gè)區(qū)域。
-
頁(yè)表是和虛擬地址空間結(jié)構(gòu)體配套的內(nèi)核數(shù)據(jù)結(jié)構(gòu),頁(yè)表的作用是:保存對(duì)應(yīng)進(jìn)程中每一個(gè)虛擬地址到物理內(nèi)存中的物理地址的映射關(guān)系。即起一個(gè)映射配對(duì)的作用。 因?yàn)閷?shí)際上數(shù)據(jù),代碼,變量等肯定最終要存儲(chǔ)在物理內(nèi)存中。
-
頁(yè)表起映射作用,就類似于C++中的map數(shù)據(jù)結(jié)構(gòu),key 是虛擬地址, value是對(duì)應(yīng)的物理地址
-
每一個(gè)進(jìn)程都有一份地址空間結(jié)構(gòu)體變量mm_struct和頁(yè)表實(shí)例化對(duì)象。在磁盤中的二進(jìn)制可執(zhí)行程序加載到內(nèi)存中時(shí),要?jiǎng)?chuàng)建對(duì)應(yīng)的PCB結(jié)構(gòu)體,同時(shí),也會(huì)創(chuàng)建對(duì)應(yīng)的地址空間結(jié)構(gòu)體變量和頁(yè)表。
進(jìn)程直接訪問(wèn)物理內(nèi)存(無(wú)虛擬空間)
在早期計(jì)算機(jī)操作系統(tǒng)內(nèi)部,進(jìn)程直接訪問(wèn)物理內(nèi)存。這樣做有很多弊端。在說(shuō)弊端之前,有一個(gè)點(diǎn)需要明確:內(nèi)存本身是可以隨意讀寫的,物理內(nèi)存是不存在只讀的情況的。
比如,最典型的野指針問(wèn)題,一個(gè)進(jìn)程的野指針很容易破壞其他進(jìn)程,甚至影響操作系統(tǒng)內(nèi)的安全數(shù)據(jù)。其次,進(jìn)程直接訪問(wèn)物理內(nèi)存,使得進(jìn)程和物理內(nèi)存耦合度很大,內(nèi)存管理變得不方便,從而內(nèi)存碎片等問(wèn)題也變得更難處理。
基于進(jìn)程直接訪問(wèn)物理內(nèi)存的弊端,衍生出虛擬地址空間。
再述虛擬地址空間!
程PCB,虛擬地址空間,頁(yè)表,物理內(nèi)存的關(guān)系大致如上圖所示。
- PCB中有一個(gè)struct mm_struct* mm指針數(shù)據(jù)成員指向這個(gè)進(jìn)程對(duì)應(yīng)的mm_struct
-
因?yàn)閮?nèi)存本身是隨意讀寫的,所以,在地址空間+頁(yè)表的作用下,可以在某些虛擬地址與物理地址的映射關(guān)系中,用某些數(shù)據(jù)(比如頁(yè)表中存儲(chǔ))表明這個(gè)內(nèi)存是只讀的。以此來(lái)保護(hù)某些數(shù)據(jù)。這些都是地址空間+頁(yè)表的作用,而非使用內(nèi)存的權(quán)限控制。
-
基于第二點(diǎn),地址空間+頁(yè)表可以對(duì)某些內(nèi)存進(jìn)行權(quán)限管理,比如常量代碼區(qū)設(shè)為只讀。同時(shí),對(duì)于某些內(nèi)存的非法訪問(wèn),也可以及時(shí)禁止。從而保護(hù)物理內(nèi)存。
-
我們知道,進(jìn)程是具有獨(dú)立性的,那么,在地址空間+頁(yè)表的作用下,只要使得各個(gè)進(jìn)程的虛擬地址通過(guò)頁(yè)表映射的物理內(nèi)存是不同的,則可以保證進(jìn)程之間互不干擾,即進(jìn)程獨(dú)立性。
虛擬地址空間結(jié)構(gòu)體是如何區(qū)域劃分?
通過(guò)定義棧區(qū),堆區(qū),常量代碼區(qū),全局?jǐn)?shù)據(jù)區(qū)等區(qū)域的start,end。來(lái)對(duì)這些區(qū)域進(jìn)行劃分。
比如棧區(qū),堆區(qū)是動(dòng)態(tài)變化的。那么只需要增大或者減小end,即可對(duì)棧區(qū)堆區(qū)的空間大小進(jìn)行控制。再比如只讀代碼區(qū),在源文件編譯之后,可執(zhí)行程序內(nèi)部已經(jīng)有了虛擬地址。若此文件加載到內(nèi)存中變?yōu)檫M(jìn)程,則mm_struct中的常量代碼區(qū)的start和end即可通過(guò)這些編譯生成的虛擬地址來(lái)確定start和end
Linux內(nèi)核源碼
如圖,為L(zhǎng)inux內(nèi)核源碼中mm_struct的定義,即虛擬地址空間的定義??梢钥吹?,它確實(shí)是通過(guò)定義各個(gè)區(qū)域的start,end來(lái)劃分各個(gè)區(qū)域的。類型是unsigned long
解答最初的問(wèn)題
1 #include<stdio.h>
2 #include<unistd.h>
3 int g_val = 100;
4
5 int main()
6 {
7 pid_t id = fork();
8 if(id == 0)
9 {
10 // 子進(jìn)程
11 int i = 0;
12 while(1)
13 {
14 printf("I am child process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
15 i++;
16 if(i == 5)
17 {
18 g_val = 200;
19 printf("Child process changed g_val success!!!\n");
20 }
21 sleep(1);
22 }
23 }
24 else {
25 while(1)
26 {
27 printf("I am parent process, id:%d, g_val:%d, &g_val:%p\n", getpid(), g_val, &g_val);
28 sleep(1);
29 }
30 }
31 return 0;
32 }
最初,父進(jìn)程與子進(jìn)程打印出同一個(gè)變量地址相同,但是值不同。我們現(xiàn)在知道了,這個(gè)地址其實(shí)是虛擬地址,而非物理地址。
一個(gè)事實(shí):父進(jìn)程創(chuàng)建子進(jìn)程時(shí),除了一些子進(jìn)程獨(dú)有的屬性,比如典型的pid。其余大部分屬性和數(shù)據(jù)都是從父進(jìn)程那里拷貝過(guò)來(lái)的。包括mm_struct 和 頁(yè)表。
所以,起初,在子進(jìn)程執(zhí)行g(shù)_val = 200;之前,也就是修改這個(gè)全局變量之前。因?yàn)樽舆M(jìn)程的mm_struct和頁(yè)表是直接從父進(jìn)程那里拷貝過(guò)來(lái)的。故父子進(jìn)程的g_val的虛擬地址,以及這個(gè)虛擬地址映射的物理內(nèi)存中的數(shù)據(jù)都是一樣的。
這樣做的原因是:如果有某些數(shù)據(jù),父子進(jìn)程都是只讀的,也就是不會(huì)修改,那么這份數(shù)據(jù)在內(nèi)存中只保存一份即可,沒(méi)必要給子進(jìn)程在內(nèi)存中再創(chuàng)建一份相同的,只讀的數(shù)據(jù)。(寫時(shí)拷貝)
而當(dāng)子進(jìn)程執(zhí)行g(shù)_val = 200;時(shí),這是子進(jìn)程對(duì)這個(gè)全局?jǐn)?shù)據(jù)執(zhí)行寫操作。因?yàn)楦缸舆M(jìn)程訪問(wèn)的g_val不應(yīng)該互相干擾。故此時(shí),OS在內(nèi)存中的其他區(qū)域,拷貝了一個(gè)新的,子進(jìn)程的g_val,賦值為200,并改變子進(jìn)程的頁(yè)表的映射關(guān)系即可!(不需要改變g_val的虛擬地址)。
從而當(dāng)子進(jìn)程修改g_val后,父子進(jìn)程打印的這個(gè)全局?jǐn)?shù)據(jù)的虛擬地址相同,但是映射到物理內(nèi)存不同區(qū)域,值不相同。才有了最初的現(xiàn)象。
這種子進(jìn)程寫數(shù)據(jù)時(shí)進(jìn)行拷貝的操作,稱為寫時(shí)拷貝!
延伸問(wèn)題: 一個(gè)pid變量怎么可能保存不同的值?
pid_t pid = fork();
我們知道,fork函數(shù)內(nèi)部的主體邏輯就是創(chuàng)建子進(jìn)程,而當(dāng)fork函數(shù)return之前,則子進(jìn)程已經(jīng)創(chuàng)建好了。
所以有了兩個(gè)進(jìn)程執(zhí)行流,兩個(gè)執(zhí)行流會(huì)執(zhí)行兩次return。其次,return了兩次給pid賦值,也就是子進(jìn)程執(zhí)行流的return給pid本質(zhì)就是對(duì)pid變量進(jìn)行寫操作!會(huì)發(fā)生寫時(shí)拷貝,那么各自就有各自的pid了(雖然虛擬地址相同,頁(yè)表映射到物理內(nèi)存是不同的,看到的是自己的pid變量)
fork return兩次,第二次return發(fā)生了寫時(shí)拷貝,則父子進(jìn)程各自在物理內(nèi)存中,都有屬于自己的id變量空間!
只不過(guò)在用戶層用同一個(gè)變量pid(虛擬地址)來(lái)標(biāo)識(shí)了。
為什么存在虛擬地址空間?(虛擬地址空間的好處)
保護(hù)物理內(nèi)存
- 凡是非法的訪問(wèn)或者映射,OS都會(huì)識(shí)別到,并終止你的進(jìn)程。(頁(yè)表機(jī)制會(huì)將虛擬地址空間會(huì)劃分為多個(gè)頁(yè),通過(guò)設(shè)置頁(yè)表項(xiàng)的權(quán)限位答到保護(hù)物理敏感內(nèi)存的目的)
原因就是:比如const char* p = “abcd”; p指針保存的是虛擬地址,且這個(gè)虛擬地址所映射的物理內(nèi)存不可被寫,這都是基于虛擬地址空間(代碼段不能修改等等)+頁(yè)表的作用。
除了這種保護(hù)只讀的數(shù)據(jù),當(dāng)存在野指針或者非法訪問(wèn)時(shí),虛擬地址空間+頁(yè)表也能以某種方式告訴OS,從而OS可以發(fā)信號(hào)終止這個(gè)進(jìn)程。
樣一來(lái),物理內(nèi)存的訪問(wèn)都在OS的監(jiān)管之下。保護(hù)了物理內(nèi)存,物理內(nèi)存中的數(shù)據(jù),其他進(jìn)程,以及內(nèi)核的相關(guān)有效數(shù)據(jù)。
內(nèi)存管理和進(jìn)程管理低耦合
因?yàn)橛刑摂M虛擬地址空間+頁(yè)表,物理內(nèi)存中的數(shù)據(jù)可以隨意存儲(chǔ)。只要保證虛擬地址可以通過(guò)映射找到對(duì)應(yīng)的數(shù)據(jù)即可
物理內(nèi)存管理 和 進(jìn)程管理因此可以做到關(guān)聯(lián)性很低
內(nèi)存管理模塊和進(jìn)程管理模塊 完成了解耦合。在操作系統(tǒng)層面,這兩個(gè)模塊關(guān)聯(lián)性很低,維護(hù)成本也會(huì)降低(各維護(hù)各的);
延時(shí)分配,提高整機(jī)效率
- 我們?cè)贑語(yǔ)言中進(jìn)行malloc時(shí),申請(qǐng)內(nèi)存本質(zhì)是在虛擬地址空間中申請(qǐng),并不會(huì)立即向物理申請(qǐng)內(nèi)存空間。
原因是:如果我malloc時(shí)就立刻申請(qǐng)物理內(nèi)存,且不立刻使用,則這就是一種內(nèi)存資源浪費(fèi)。
所以,因?yàn)橛械刂房臻g存在,上層申請(qǐng)內(nèi)存,其實(shí)是在虛擬地址空間中申請(qǐng)。
而當(dāng)你進(jìn)行對(duì)物理內(nèi)存的訪問(wèn)時(shí),才執(zhí)行相關(guān)的內(nèi)存管理算法(缺頁(yè)中斷等),幫你申請(qǐng)內(nèi)存,構(gòu)建頁(yè)表映射關(guān)系。然后再讓你進(jìn)行內(nèi)存訪問(wèn)。 (這些是由操作系統(tǒng)完成的,進(jìn)程0感知)
虛擬地址存在,但是物理內(nèi)存中沒(méi)有對(duì)應(yīng)的空間。稱為缺頁(yè)(映射)中斷。
那么,這樣延時(shí)分配的好處就是:確保物理內(nèi)存中的有效使用是100%的,不會(huì)出現(xiàn)物理內(nèi)存中申請(qǐng)空間但不使用的情況。提高整機(jī)效率。
使內(nèi)存分布有序化,實(shí)現(xiàn)進(jìn)程獨(dú)立性
- 地址空間+頁(yè)表的存在,可以使得內(nèi)存分布有序化!每個(gè)進(jìn)程看到的是完整的虛擬地址->實(shí)現(xiàn)進(jìn)程獨(dú)立;
在虛擬地址中,每個(gè)進(jìn)程以完整虛擬地址的角度來(lái)看待內(nèi)存布局,這個(gè)布局是有序的(代碼段,數(shù)據(jù)段,堆棧等分區(qū));
因?yàn)轫?yè)表的存在,可以建立虛擬地址和物理地址的映射關(guān)系,物理內(nèi)存存放的數(shù)據(jù)是隨機(jī)無(wú)序的。
通過(guò)讓不同進(jìn)程看到的完整分布的內(nèi)存虛擬地址 映射 到物理內(nèi)存的任意區(qū)域 -> 實(shí)現(xiàn)進(jìn)程獨(dú)立性
內(nèi)存擴(kuò)展(物理內(nèi)存不夠時(shí),通過(guò)內(nèi)存頁(yè)和硬盤的交換達(dá)到擴(kuò)展(了解)
內(nèi)存共享(允許多進(jìn)程通過(guò)虛擬地址+頁(yè)表等機(jī)制,訪問(wèn)同一個(gè)物理內(nèi)存(eg:fork的寫時(shí)拷貝)(了解)
小結(jié)
綜上:
可以說(shuō)虛擬地址空間是OS內(nèi)核中的一種數(shù)據(jù)結(jié)構(gòu),主要保存各個(gè)數(shù)據(jù)區(qū)的start和end。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-823753.html
32位系統(tǒng)下,虛擬地址空間使得每個(gè)進(jìn)程都認(rèn)為自己獨(dú)占4GB內(nèi)存,它們也看不到其他進(jìn)程的存在。內(nèi)核通過(guò)頁(yè)面的映射等管理手段,從而讓物理內(nèi)存中的進(jìn)程和進(jìn)程之間,進(jìn)程和內(nèi)核之間可以互不干擾;文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-823753.html
到了這里,關(guān)于Linux虛擬地址空間的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!