1. 程式人生 > 其它 >linux程序地址空間和虛擬記憶體

linux程序地址空間和虛擬記憶體

虛擬地址

在早期的計算機中,程式是直接執行在實體記憶體上的,也就是說,程式在執行時所訪問的地址都是實體地址,這種情況下只要程式所需要的記憶體空間不超過實體記憶體的大小就不會有問題。但是大多數情況下我們必須同時執行多個程式這樣必定會造成記憶體空間的重疊現象,並且程式去直接操作實體記憶體也是十分危險的,那麼我們如何將計算機有限的實體記憶體分配給多個程式使用呢?

我們在這裡加入了一箇中間層,即使用一種間接的地址訪問方法。我們把程式給出的地址看作是一種虛擬地址,然後通過某些對映的方法,將這個虛擬地址轉換成實際的實體地址。這就多個程式可以同時執行且各個程式之間能夠訪問的實體記憶體區域不重疊,也杜絕了程式直接操作地址的現象,同時也提高實體地址的使用效率。這種呈現出比實際擁有的地址空間大得多的記憶體我們叫做虛擬記憶體。

這裡有一個形象的比喻:就像你不需要很長的軌道就可以讓一列火車從上海開到北京。你只需要足夠長的鐵軌(比如說3公里)就可以完成這個任務。採取的方法是把後面的鐵軌立刻鋪到火車的前面,只要你的操作足夠快並能滿足要求,列車就能象在一條完整的軌道上執行。這也就是虛擬地址管理需要完成的任務。

程序與虛擬地址

32位系統下每個程序都會分配4G的虛擬記憶體空間,而其實所有程序都共享著同一實體記憶體,每個程序只把自己目前需要的虛擬記憶體空間對映並存儲到實體記憶體上。每次訪問記憶體空間的某個地址,都需要把地址翻譯為實際實體記憶體地址。
這時我們需要一個東西它就是MMU(記憶體管理單元),它的主要作用就是完成地址的對映,也就是頁表的建立、對映過程。頁表就是用來記錄程序中哪些記憶體地址上的資料在實體記憶體上以及它們所在的位置的一個結構。每個程序都有一個頁表,當程序需要訪問某個虛擬地址時,就會去訪問頁表,頁表實現從頁號到物理塊號的地址對映。

一、虛擬記憶體

先來看一張圖(來自《Linux核心完全剖析》),如下:

分段機制:即分成程式碼段,資料段,堆疊段。每個記憶體段都與一個特權級相關聯,即0~3,0具有最高特權級(核心),3則是最低特權級(使用者),每當程式試圖訪問(許可權又分為可讀、可寫和可執行)一個段時,當前特權級CPL就會與段的特權級進行比較,以確定是否有許可權訪問。每個特權級都有自己的程式棧,當程式從一個特權級切換到另一個特權級上執行時,堆疊段也隨之改換到新級別的堆疊中。

段選擇符:每個段都有一個段選擇符。段描述符指明段的大小、訪問許可權和段的特權級、段型別以及段的第一個位元組線上性地址空間中的位置(稱為段的基地址)。而段選擇符用於在描述符表中進行索引找到段描述符。

虛擬地址:虛擬地址的偏移量部分加上段的基地址上就可以定位段中某個位元組的位置,即形成線性地址空間中的地址。

分頁機制:當使用分頁機制時,每個段被劃分成頁面(通常每頁在4KB大小),頁面會被儲存於物理記憶體或硬碟上。如果禁用分頁機制,那麼線性地址空間就是實體地址空間。

當程式試圖訪問線性地址空間上的一個地址位置時,發生以下操作:

C++ Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
if(資料在實體記憶體中)
{
虛擬地址轉換成實體地址
讀資料
}
else
{
if(資料在磁碟中)
{
if(實體記憶體還有空閒)
{
把資料從磁碟中讀到實體記憶體
虛擬地址轉換成實體地址
讀資料
}
else
{
把實體記憶體中某頁的資料存入磁碟
把要讀的資料從磁碟讀到該頁的實體記憶體中
虛擬地址轉換成實體地址
讀資料
}
}
else
{
報錯
}
}
其中MMU負責虛擬地址到實體地址的轉換工作,分段和分頁操作都使用駐留在記憶體中的段表和頁表來指定他們各自的交換資訊。如果使用者程式想要訪問一個虛擬地址,經MMU檢查無權訪問(特權級),MMU產生一個異常,CPU從使用者模式切換到特權模式,跳轉到核心程式碼中執行異常服務程式,核心把這個異常解釋為段錯誤,把引發異常的程序終止掉。

二、linux程序地址空間

由前面可得知,程序有4G的定址空間,其中第一部分為“使用者空間”,用來對映其整個程序空間(0x0000 0000-0xBFFF FFFF)即3G位元組的虛擬地址;第二部分為“系統空間”,用來對映(0xC000 0000-0xFFFF FFFF)1G位元組的虛擬地址。如下圖

將其更加詳細地展示如下:

程式路徑:完整的絕對路徑字串如 “/home/simba/code/asm/simple”

環境變數:類似linux下的PATH,HOME等的環境變數,子程序會繼承父程序的環境變數。

命令列引數:類似ls -l 中-l 就是命令列引數,而ls 就是可執行程式。

棧:就是堆疊,程式執行時需要在這裡做資料運算,儲存臨時資料,開闢函式棧等。在Linux下,棧是高地址往低地址增長的。

對於函式棧來說,函式執行完畢就釋放記憶體,舉例遞迴來說,一直開闢向下函式棧,然後由下往上收復,所以遞迴太多層的話很可能造成棧溢位。

區域性變數(不包含靜態變數);區域性可讀變數(const)都分配在棧上。

共享庫和mmap記憶體對映區:比如很多程式都會用到的printf,函式共享庫 printf.o 固定在某個實體記憶體位置上,讓許多程序對映共享。mmap是個系統函式,可以把磁碟檔案的一部分直接對映到記憶體,這樣檔案中的位置直接就有對應的記憶體地址,對檔案的讀寫可以直接用指標來做而不需要read/write函式。此外,呼叫malloc 時正常是呼叫brk 系統呼叫分配記憶體,特定條件下是呼叫mmap 來對映實體記憶體到程序地址空間。

堆:即malloc申請的記憶體,使用free釋放,如果沒有主動釋放,在程序執行結束時也會被釋放。

Text Segment: 可執行程式(二進位制)(.text);全域性初始化只讀變數(const)(.rodata);字串常量(.rodata);均在這裡分配。

Data Segment: 全域性變數(初始化的在.data,未初始化的在.bss);靜態變數(全域性和區域性)(初始化的在.data,未初始化的在.bss);全域性未初始化只讀變數(.bss);均在這裡分配。