🌟🌟作者主頁:ephemerals__
🌟🌟所屬專欄:Linux
目錄
前言
一、什么是程序地址空間
二、深入理解程序地址空間
1. 引例
2. 理解地址轉化
3. 再談程序地址空間
4. 補充知識
總結
前言
????????在現代操作系統中,進程的地址空間管理是實現多任務和內存保護的核心機制。對于Linux操作系統而言,理解程序的地址空間布局不僅對于開發高效、穩定的應用至關重要,也是調試和優化程序不可或缺的基礎。本文將簡要介紹Linux程序在內存中的地址空間結構,幫助讀者更好地理解進程如何利用和管理內存資源。
一、什么是程序地址空間
????????程序地址空間是指操作系統為一個程序分配的內存范圍。在現代操作系統中,每個程序在運行時都有自己的程序地址空間。
程序地址空間通常分為以下幾個部分:
1. 內核空間:供操作系統使用,與其他部分形成隔離,程序無法訪問。
2. 棧區:存儲函數的局部變量和函數調用的上下文等。
3. 內存映射段(共享區):為支持文件的高效訪問或多進程的數據共享而劃分的一塊區域,通常包含文件映射、動態庫和匿名映射等。
4. 堆區:動態內存分配所用區域,需要手動管理。
5. BSS段:存儲未初始化的全局變量和靜態變量。
6. 數據段(靜態區):存儲已經初始化的全局變量和靜態變量。
7. 代碼段(常量區):存放程序的可執行代碼和只讀常量,該區域通常只讀,不可修改。
在32位系統下,程序地址空間一共有2^32個地址,大小為4GB;
在64位系統下,程序地址空間一共有2^64個地址,大小約為17179PB(是理論極限,實際比它小得多,并且整個內存大小遠遠達不到如此)?。
二、深入理解程序地址空間
1. 引例
? ? ? ? ?首先我們寫一段程序:
#include <stdio.h>
#include <unistd.h>int main()
{int m = 0;pid_t id = fork();if(id == 0) // 子進程{m = 10;printf("子進程修改了m\n");sleep(1);printf("子進程:m = %d;&m = %p\n", m, &m);}else // 父進程{sleep(1);printf("父進程:m = %d;&m = %p\n", m, &m);}return 0;
}
這里我們使用fork創建了一個子進程,并讓子進程修改m的值,此時會發生寫時拷貝,兩個進程的“m”應該不再表示同一份資源。接下來打印它們的值和地址。程序運行結果如下:
可以看到,出現了怪事:m的值不同,為什么它們的地址還相同呢?
只能說明:這個地址是假的,不是真實的內存地址(物理地址)。?
實際上,我們使用C/C++編程時使用到的地址,全部都是虛擬地址,并不表示真實的內存地址。為了保護物理內存,真實的內存地址是由操作系統統一管理的,用戶無法直接訪問。
2. 理解地址轉化
? ? ? ? 因此,剛才的例子當中,兩個m的虛擬地址是相同的,但在物理層面,它們的物理地址是不同的。這就意味著兩個進程的虛擬地址空間是不同的,并且操作系統必須將用戶使用的虛擬地址轉化為物理地址,然后進行訪問等操作。負責虛擬地址和物理地址轉化的東西,叫做頁表。
? ? ? ? 我們可以暫時將頁表理解為一個映射表,表的左邊是虛擬地址,右邊是物理地址,通過左邊的虛擬地址就可以進行頁表轉化,找到真實的物理地址進行訪問。
總結:
1. 一個進程具有一個程序地址空間
2. 一個進程具有一個頁表,用于虛擬地址和物理地址的轉化
3.?程序執行時,操作系統根據數據或代碼的虛擬地址,通過頁表找出對應的真實內存地址,進行訪問。
3. 再談程序地址空間
? ? ? ? 因此,再說“程序地址空間”就不太準確了,我們稱其為“進程地址空間”或“虛擬地址空間”更為恰當。
? ? ? ? 實際上,不僅進程地址空間的地址是虛擬的,其“大小”也是虛擬的。進程地址空間就像是操作系統給進程畫的一張大餅,讓進程以為自己獨占4GB的內存(32位系統下)。但由于其他進程可需要訪問物理內存,物理內存總共就4GB,怎么可能被一個進程獨占呢?所以雖說進程地址空間大小是4GB,但實際根本不可能有這么多。
? ? ? ? 因此,進程在創建時,操作系統按照程序代碼和數據的大小,創建地址空間,并設定各個區域的范圍和內存大小,然后加載程序,申請相應的物理內存,再創建頁表,支持將物理地址轉化為虛擬地址,供上層用戶使用。
? ? ? ? 這樣,我們就可以解釋剛才的程序:
1. 子進程在創建之后,拷貝了父進程的進程地址空間以及頁表,因此,父子進程的地址映射關系是相同的,具體表現就是虛擬地址相同,且維護的是同一塊物理內存。
2. 此時子進程修改變量m的值,操作系統檢測到變量要被修改,于時發生寫時拷貝,在物理內存中對該變量進行拷貝,并修改子進程的頁表映射關系,使原來的虛擬地址映射到別的物理地址處。這樣,父子進程就各自維護一個m,它們表現出的虛擬地址相同,但物理地址不同,頁表映射關系也不同,因此出現了地址相同,值不相同的情況。
那么為什么不支持直接訪問物理內存,而這樣大費周折地搞出一個什么進程地址空間和頁表呢?有三點原因:
1. 如果每個進程都可以直接訪問物理內存,那么就意味著這個程序可以直接修改另一個程序甚至操作系統的數據和代碼,如果有惡意病毒程序,就會造成不可預料的后果。
2. 假設進程A需要申請地址為1~100的內存區域,并且代碼中訪問了地址為“50”的內存單元。下一次啟動進程A時,如果已經有進程申請了1~100的內存區域,那么進程A如何申請內存呢?尚且申請101~200,但是代碼中訪問“50”已經寫死了,沒法修改,這導致每次程序執行的結果都是不確定的。
3. 如果直接使用物理內存,那么一個進程就是一整塊內存區域,如果需要執行掛起或其他操作,就只能將整個區域都進行拷貝,效率降低。
進程地址空間的出現,就完美地解決了這些問題:
1. 地址轉換過程中,可以對操作的合法性進行判定(頁表中,針對虛擬地址空間中的各個區域,有明確的權限劃分,假如在程序中修改常量數據,就會出現頁表權限攔截,發生崩潰),從而保護物理內存,確保程序的正常執行。
2. 有了虛擬地址,物理內存申請就不必連續,經過頁表可以轉化成連續的虛擬地址,方便上層使用。
3. 除此之外,程序地址空間還使得操作系統對進程的管理和內存的管理之間進行一定的解耦合(用戶申請內存根本無需關心物理內存的具體情況)。
4. 補充知識
1. 虛擬地址空間的本質是一個數據結構,在Linux下叫做mm_struct,其地址存放在task_struct對象當中,mm_struct維護著一個個vm_area_struct,這些vm_area_struct存儲著進程地址空間每一個區域的起始和結束地址。要調整區域劃分,只需對其中變量進行加減操作。
2. 一個進程具有一個進程地址空間,意味著一個進程維護一個mm_struct,操作系統要將這些mm_struct組織起來統一管理。當進程數量較少時,mm_struct是用單鏈表結構組織起來的;較多時,利用紅黑樹結構進行組織。
總結
? ? ? ? 本篇文章,我們學習了進程地址空間的基本概念,并深入理解了頁表轉化以及進程地址空間的本質。學習進程地址空間,為我們后續學習理解進程控制相關操作至關重要。如果你覺得博主講的還不錯,就請留下一個小小的贊在走哦,感謝大家的支持???