天天看點

linux 程序位址空間的一步步探究

我們知道,在32位機器上linux作業系統中的程序的位址空間大小是4G,其中0-3G是使用者空間,3G-4G是核心空間。其實,這個4G的位址空間是不存在的,也就是我們所說的虛拟記憶體空間。

那虛拟記憶體空間是什麼呢,它與實際實體記憶體空間又是怎樣對應的呢,為什麼有了虛拟記憶體技術,我們就能運作比實際實體記憶體大的應用程式,它是怎麼做到的呢?

呵呵,這一切的一切都是個迷呀,下面我們就一步一步解開心中的謎團吧!

我們來看看,當我們寫好一個應用程式,編譯後它都有什麼東東?

例如:

linux 程式位址空間的一步步探究

用指令size a.out會得到:

linux 程式位址空間的一步步探究

其中text是放的是代碼,data放的是初始化過的全局變量或靜态變量,bss放的是未初始化的全局變量或靜态變量

由于曆史原因,C程式一直由下列幾部分組成:

linux 程式位址空間的一步步探究

A.正文段。這是由cpu執行的機器指令部分。通常,正文段是可共享的,是以即使是經常執行的程式(如文本編輯程式、C編譯程式、shell等)在存儲器中也隻需要有一個副本,另外,正文段常常是隻讀的,以防止程式由于意外事故而修改器自身的指令。

B.初始化資料段。通常将此段稱為資料段,它包含了程式中需賦初值的變量。例如,C程式中任何函數之外的說明:

int maxcount = 99;(全局變量)

C.非初始化資料段。通常将此段稱為bss段,這一名稱來源于早期彙程式設計式的一個操作,意思是"block started by symbol",在程式開始執行之前,核心将此段初始化為0。函數外的說明:

long  sum[1000];

使此變量存放在非初始化資料段中。

D.棧。自動變量以及每次函數調用時所需儲存的資訊都存放在此段中。每次函數調用時,其傳回位址、以及調用者的環境資訊(例如某些機器寄存器)都存放在棧中。然後,新被調用的函數在棧上為其自動和臨時變量配置設定存儲空間。通過以這種方式使用棧,C函數可以遞歸調用。

E.堆。通常在堆中進行動态存儲配置設定。由于曆史上形成的慣例,堆位于非初始化資料段頂和棧底之間。

從上圖我們看到棧空間是下增長的,堆空間是從下增長的,他們會會碰頭呀?一般不會,因為他們之間間隔很大,如:

#include

int bss_var;

int data_var0 = 1;

int main()

{

printf("Test location:\n");

printf("\tAddress of main(Code Segment):%p\n",main);

printf("_____________________________________\n");

int stack_var0 = 2;

printf("Stack location:\n");

printf("\tInitial end of stack:%p\n",&stack_var0);

int stack_var1 = 3;

printf("\tNew end of stack:%p\n",&stack_var1);

printf("Data location:\n");

printf("\tAddress of data_var(Data Segment):%p\n",&data_var0);

static int data_var1 = 4;

printf("\tNew end of data_var(Data Segment):%p\n",&data_var1);

printf("BSS location:\n");

printf("\tAddress of bss_var:%p\n",&bss_var);

printf("Heap location:\n");

char *p = (char *)malloc(10);

printf("\tAddress of head_var:%p\n",p);

return 0;

}

運作結果如下:

linux 程式位址空間的一步步探究

呵呵,這裡我們看到位址了,這個位址是虛拟位址,這些位址時怎麼來的呢?其實在我們編譯的時候,

這些位址就已經确定了,如下圖中紅線。

linux 程式位址空間的一步步探究

也就是說,我們不論我們運作a.out程式多少次這些位址都是一樣的。我們知道,linux作業系統每個程序的位址空間都是獨立的,其實這裡的獨立說得是實體空間上得獨立。那相同的虛拟位址,不同的實體位址,他們之間是怎樣聯系起來的呢?我們繼續探究....

在linux作業系統中,每個程序都通過一個task_struct的結構體描叙,每個程序的位址空間都通過一個mm_struct描叙,c語言中的每個段空間都通過vm_area_struct表示,他們關系如下 :

linux 程式位址空間的一步步探究

當運作一個程式時,作業系統需要建立一個程序,這個程序和程式之間都幹了些什麼呢?

當一個程式被執行時,該程式的内容必須被放到程序的虛拟位址空間,對于可執行程式的共享庫也是如此。可執行程式并非真正讀到實體記憶體中,而隻是連結到程序的虛拟記憶體中。

當一個可執行程式映射到程序虛拟位址空間時,一組vm_area_struct資料結構将被産生。每個vm_area_struct資料結構表示可執行印象的一部分;是可執行代碼,或是初始化的資料,以及未初始化的資料等。

linux作業系統是通過sys_exec對可執行檔案進行映射以及讀取的,有如下幾步:

1.建立一組vm_area_struct

2.圈定一個虛拟使用者空間,将其起始結束位址(elf段中已設定好)儲存到vm_start和vm_end中。

3.将磁盤file句柄儲存在vm_file中

4.将對應段在磁盤file中的偏移值(elf段中已設定好)儲存在vm_pgoff中;

5.将操作該磁盤file的磁盤操作函數儲存在vm_ops中

注意:這裡沒有對應 的頁目錄表項建立頁表,更不存在設定頁表項了。

linux 程式位址空間的一步步探究

假設現在程式中有一條指令需要讀取上面vm_start--vm_end之間的某内容

例如:mov [0x08000011],%eax,那麼将會執行如下序列:

1.cpu依據CR3(current->pgd)找到0x08000011位址對應的pgd[i],由于該pgd[i]内容保持為初始化狀态即為0,導緻cpu異常.

2.do_page_fault被調用,在該函數中,為pgd[i]在記憶體中配置設定一個頁表,并讓該表項指向它,如下圖所示:

linux 程式位址空間的一步步探究

    注意:這裡i為0x08000011高10位,j為其中間10位,此時pt表項全部為0(pte[j]也為0);

3.為pte[j]配置設定一個真正的實體記憶體頁面,依據vm_area_struct中的vm_file、vm_pgoff和vm_ops,調用filemap_nopage将磁盤file中vm_pgoff偏移處的内容讀入到該實體頁面中,如下圖所示:

linux 程式位址空間的一步步探究

①.配置設定實體記憶體頁面;

 ②.從磁盤檔案中将内容讀取到實體記憶體頁面中

從上面我們可以知道,在程序建立的過程中,程式内容被映射到程序的虛拟記憶體空間,為了讓一個很大的程式在有限的實體記憶體空間運作,我們可以把這個程式的開始部分先加載到實體記憶體空間運作,因為作業系統處理的是程序的虛拟位址,如果在進行虛拟到實體位址的轉換工程中,發現實體位址不存在時,這個時候就會發生缺頁異常(nopage),接着作業系統就會把磁盤上還沒有加載到記憶體中的資料加載到實體記憶體中,對應的程序頁表進行更新。也許你會問,如果此時實體記憶體滿了,作業系統将如何處理?

下面我們看看linux作業系統是如何處理的:

如果一個程序想将一個虛拟頁裝入實體記憶體,而又沒有可使用的空閑實體頁,作業系統就必須淘汰實體記憶體中的其他頁來為此頁騰出空間。

在linux作業系統中,實體頁的描叙如下:

struct mem_map

    1.本頁使用計數,當該頁被許多程序共享時計數将大于1.

    2.age描叙本頁的年齡,用來判斷該頁是否為淘汰或交換的好候選

    3.map_nr描叙實體頁的頁幀号

    如果從實體記憶體中被淘汰的頁來自于一個映像或資料檔案,并且還沒有被寫過,則該頁不必儲存,它可以丢掉。如果有程序在需要該頁時就可以把它從映像或資料檔案中取回記憶體。

    然而,如果該頁被修改過,作業系統必須保留該頁的内容以便晚些時候在被通路。這種頁稱為"髒(dirty)頁",當它被從記憶體中删除時,将被儲存在一個稱為交換檔案的特殊檔案中。

    相對于處理器和實體記憶體的速度,通路交換檔案要很長時間,作業系統必須在将頁寫到磁盤以及再次使用時取回記憶體的問題上花費心機。

  如果用來決定哪一頁被淘汰或交換的算法不夠高效的話,就可能出現稱為"抖動"的情況。在這種情況下,頁面總是被寫到磁盤又讀回來,作業系統忙于此而不能進行真正的工作。

linux使用"最近最少使用(Least Recently Used ,LRU)"頁面排程技巧來公平地選擇哪個頁可以從系統中删除。這種設計系統中每個頁都有一個"年齡",年齡随頁面被通路而改變。頁面被通路越多它越年輕;被通路越少越老。年老的頁是用于交換的最佳候選頁。