??
? 本篇文章會圍繞三個問題(什么是地址空間?地址空間是如何設計的?為什么要有地址空間?)進行展開講述。其中主要是了解虛擬地址和物理地址的區(qū)別。希望本篇文章會對你有所幫助。
文章目錄
一、什么是地址空間?
1、1 驗證地址空間
1、2 地址空間是指的物理內(nèi)存嗎?
1、3 地址空間解釋
二、進程訪問地址
2、1 歷史的程序尋址
2、2 進程地址空間映射到物理內(nèi)存
2、3 解釋相同地址打印出不同數(shù)據(jù)
三、為什么要有地址空間
3、1 保護物理內(nèi)存
3、2 內(nèi)存管理和進程管理完成解耦合
3、3 將無序的物理內(nèi)存有序化
四、總結
???♂??作者:@Ggggggtm????♂?
???專欄:Linux從入門到精通? ??
???標題:進程地址空間??
????寄語:與其忙著訴苦,不如低頭趕路,奮路前行,終將遇到一番好風景?????
一、什么是地址空間?
? ?什么是地址空間呢?我們在學C語言時,經(jīng)常說到程序的變量存儲在棧區(qū)、靜態(tài)區(qū)
堆區(qū)等。這些綜合起來就是地址空間。通俗來講,地址空間就是表示計算機系統(tǒng)中內(nèi)存的總體范圍。它是可用于存儲和訪問數(shù)據(jù)的內(nèi)存地址的集合。
1、1 驗證地址空間
? 我們了解地址空間后,不妨來驗證一下我們之前所學的是否正確。我們之前學的地址空間如下圖:
??
? 我們通過下段代碼來驗證我們之前所學的是否正確:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_unval; int g_val = 100; int main(int argc, char *argv[], char *env[]) { // int a = 10; //字面常量 const char *str = "helloworld"; // 10; // 'a'; printf("code addr: %p\n", main); printf("init global addr: %p\n", &g_val); printf("uninit global addr: %p\n", &g_unval); char *heap_mem = (char*)malloc(10); char *heap_mem1 = (char*)malloc(10); printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1) printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1) printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1) printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1) printf("read only string addr: %p\n", str); int i; for(i = 0 ;i < argc; 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ū),我們再來看運行結果:
? 上圖正是在Linux下運行的結果。在windows下運行的結果所得出的結論也是相同的。我們看到上圖的運行結果后是符合我們所學的地址空間的規(guī)律。
? 棧和堆之間有大量空間是空著的。其次堆和棧是相向而生的。細心的小伙伴可能發(fā)現(xiàn),總共的內(nèi)存空間是4G,而用戶空間只占用3G,那剩下的1G呢?其實完整的地址空間如下:
1、2 地址空間是指的物理內(nèi)存嗎?
? 我們之前在學C語言時,經(jīng)常會提到 ‘‘地址’等詞匯。例如,我們隨查看的臨時變量所存儲的地址。那么我們經(jīng)常所說的這些地址是指的物理內(nèi)存(物理內(nèi)存是指由于安裝內(nèi)存條而獲得的臨時儲存空間。主要作用是在計算機運行時為操作系統(tǒng)和各種程序提供臨時儲存。)中的地址嗎?
? 答案不確定時,我們看看如下代碼:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int g_val = 10; int main() { pid_t id = fork(); if(id < 0){ perror("fork"); return 0; } else if(id == 0){ //child,子進程肯定先跑完,也就是子進程先修改,完成之后,父進程再讀取 while(1) { printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val); sleep(1); } }else{ //parent while(1) { printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val); sleep(1); g_val=100; } } sleep(1); return 0; }
? 上述代碼就是區(qū)分父子進程,打印同一個變量的值。結果如下圖:
? 我們驚奇的發(fā)現(xiàn),同一個變量(地址是相同的),他們的值竟然不一樣!難道是一個變量可以存儲兩個不同的值的原因嗎?答案是不是的。
? 我們知道物理內(nèi)存中的地址表示唯一一塊空間,那上述的運行結果證明了,我們所說的地址空間并不是物理地址的!而是存儲在虛擬地址(虛擬內(nèi)存是計算機系統(tǒng)內(nèi)存管理的一種技術。它使得應用程序認為它擁有連續(xù)可用的內(nèi)存,它通常是被分隔成多個物理內(nèi)存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數(shù)據(jù)交換)中。?
1、3 地址空間解釋
? 地址空間本質就是一種內(nèi)核數(shù)據(jù)結構,在Linux當中,叫做struct mm_struct(linux內(nèi)核當中的地址空間結構體)包含了一些區(qū)域信息(先描述),能夠實現(xiàn)區(qū)域劃分(本質就是在一定的范圍內(nèi)定義start和end)。??
struct mm_struct { unsigned long code_start; unsigned long code_end; unsigned long init_start; unsigned long init_end; unsigned long uninit_start; unsigned long uninit_end; unsigned long heap_start; unsigned long heap_end; unsigned long stack_start; unsigned long stack_end; //...等不同的區(qū)域劃分 }
? ?每個進程都會有自己的地址空間,同時進程控制塊(PCB)中也包含了 *mm_struct 指針,可使我們直接找到自己所對應的進程地址空間(后組織)。
? 上述講述的這么多,我們可以理解為進程地址空間就是操作系統(tǒng)給進程花了一個大餅。
? 這個大餅就是指的每個進程都會有4GB的連續(xù)的空間(0x00000000~0xFFFFFFFF)。實際上呢,這4GB的的空間是虛擬內(nèi)存,虛擬內(nèi)存對應的實際物理內(nèi)存,可能只對應的分配了一點點的物理內(nèi)存,實際使用了多少內(nèi)存,就會對應多少物理內(nèi)存。
??這4G虛擬內(nèi)存是一個連續(xù)的地址空間(這也只是進程認為),而實際上,它的數(shù)據(jù)是存儲在多個物理內(nèi)存碎片的,還有一部分存儲在外部磁盤存儲器上,在需要時將數(shù)據(jù)交換進物理內(nèi)存。
二、進程訪問地址
2、1 歷史的程序尋址
? 在虛擬地址出現(xiàn)之前,程序的尋址都是直接尋找的物理地址。但是這樣會有很多的不足:
- 直接訪問物理內(nèi)存不安全。例如我們假如使用了野指針,對內(nèi)存中的數(shù)據(jù)進行了修改,那么這個時就會影響到其他的進程;
- 因為物理內(nèi)存是有限的,當有多個進程要執(zhí)行的時候,對每個進程都要分配4G內(nèi)存,很顯然你內(nèi)存若小一點,這很快就分配完了,于是沒有得到分配資源的進程就只能等待。當一個進程執(zhí)行完后,再將等待的進程裝入內(nèi)存。這種頻繁的裝入內(nèi)存的操作是很沒效率的。
- 因為內(nèi)存是隨機分配的,所以程序運行的地址也是不正確的。
? 由于上述的三個直接原因,后來就產(chǎn)生了虛擬地址。
2、2 進程地址空間映射到物理內(nèi)存
? 當有了虛擬內(nèi)存的概念后,上述的問題就得到了很好的解決。當我們訪問物理內(nèi)存中的數(shù)據(jù)時,需要先訪問進程地址空間上的地址。然后把虛擬地址空間上的地址通過頁表映射到對應的物理內(nèi)存上。具體如下圖:
? ?地址空間和頁表是每個進程都獨有的一份,只要保證每一個進程的頁表,能夠映射到不同區(qū)域的物理內(nèi)存,就能夠做到進程之間互不干擾。這就是我們所說的進程所具有獨立性。
? 映射是由誰來完成的呢?答案是操作系統(tǒng)!操作系統(tǒng)通過地址轉換機制將虛擬地址映射到物理地址,以實現(xiàn)對內(nèi)存的訪問。這種映射通常在頁表或段表等數(shù)據(jù)結構上實現(xiàn),其中存儲了虛擬地址與物理地址之間的映射關系。?
2、3 解釋相同地址打印出不同數(shù)據(jù)
? 我們在上述的 1、2 中看到了相同的地址打印出不同的數(shù)據(jù)。注意,我們所訪問到的地址都是虛擬地址。并不是物理地址。在上述的 1、2 中我們創(chuàng)建了一個子進程,子進程本身是繼承了父進程的數(shù)據(jù)和代碼。在沒有對數(shù)據(jù)進行修改之前,子進程和父進程共享了一份數(shù)據(jù)。一但對子進程或者父進程的數(shù)據(jù)進行修改,就會發(fā)生寫時拷貝。對修改的數(shù)據(jù)進行深拷貝,從而達到對彼此不產(chǎn)生干擾,實現(xiàn)進程獨立性。
? 那就對相同地址打印出不同數(shù)據(jù)的現(xiàn)象不難理解了。當我們對父進程的數(shù)據(jù)進行修改時,父進程發(fā)生了寫時拷貝,在內(nèi)存中開辟了空間。但他們都有自己的地址空間(虛擬地址),所以地址相同也是正?,F(xiàn)象(子進程繼承父進程的代碼和數(shù)據(jù))。即使虛擬地址一樣,但是可通過頁表映射到不同的物理內(nèi)存中。具體如下圖:
三、為什么要有地址空間
3、1 保護物理內(nèi)存
? 可能還有一些疑惑:即使有了虛擬地址,那我要是對野指針進行了訪問修改,頁表對野指針映射后,還不是對物理內(nèi)存進行了非法的訪問修改嗎?地址空間的設置不就多此一舉了嗎?
? 事實并非上述一樣。凡是非法的訪問或者映射,操作系統(tǒng)都會識別到的。一但你進行了非法的訪問或者映射,操作系統(tǒng)就會終止掉你的程序。舉個例子,當我們對野指針進行訪問修改時,你的程序就會崩潰,這不就是程序終止退出嗎!??!
? 地址空間有效的保護了物理內(nèi)存。因為地址空間和頁表是操作系統(tǒng)創(chuàng)建并且維護的。這也就意味著地址空間和頁表進行映射時需要操作系統(tǒng)進行監(jiān)管!
3、2 內(nèi)存管理和進程管理完成解耦合
? 因為有了地址空間和頁表,所以我們的數(shù)據(jù)可以在物理內(nèi)存中的任何合法位置加載。因為他們之間有映射。物理內(nèi)存的分配和進程的管理可以做到?jīng)]有關系!
? 所以進程模塊和內(nèi)存模塊只需要各自完成各自的事情,最后通過頁表的映射將他們連接起來,產(chǎn)生關系。降低了他們之間互相的影響度。
3、3 將無序的物理內(nèi)存有序化
? 由于數(shù)據(jù)可以在物理內(nèi)存中理論上可以加載任何位置,那么是不是物理內(nèi)存中幾乎所有的數(shù)據(jù)和代碼在內(nèi)存中都是亂序的。
? 但是,也為頁表的存在,它可以將地址空間上的虛擬地址和物理地址進行映射,那么在進程的視角所有內(nèi)存分布就是有序的!
四、總結
? 我們平常所訪問到的地址均為虛擬地址。地址空間并不是物理地址,而是虛擬地址。通過頁表映射訪問物理地址。?每個進程都有自己的地址空間和頁表。
? 頁表是一種數(shù)據(jù)結構,它存儲了虛擬地址與物理地址之間的映射關系。在進行地址轉換時,操作系統(tǒng)根據(jù)進程的頁表查找對應的物理地址,然后將虛擬地址轉換為物理地址,以便進行實際的內(nèi)存訪問。文章來源:http://www.zghlxwxcb.cn/news/detail-499037.html
? 通過使用虛擬地址,操作系統(tǒng)可以為每個進程提供獨立的地址空間,使得多個進程可以并發(fā)運行,彼此之間相互隔離,互不干擾。虛擬地址還提供了更高的靈活性和保護性,使得操作系統(tǒng)可以有效地管理和分配內(nèi)存資源,提高系統(tǒng)的性能和安全性。文章來源地址http://www.zghlxwxcb.cn/news/detail-499037.html
到了這里,關于【Linux從入門到精通】進程地址空間(虛擬地址 vs 物理地址)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!