作者:程姚根,華清遠見嵌入式培訓中心講師。
我們知道,在32位機器上linux作業系統中的進程的地址空間大小是4G,其中0-3G是使用者空間,3G-4G是核心空間。其實,這個4G的地址空間是不存在的,也就是我們所說的虛擬記憶體空間。
那虛擬記憶體空間是什麼呢,它與實際實體記憶體空間又是怎樣對應的呢,為什麼有了虛擬記憶體技術,我們就能運行比實際實體記憶體大的應用程式,它是怎麼做到的呢?呵呵,這一切的一切都是個迷呀,下面我們就一步一步解開心中的謎團吧!
我們來看看,當我們寫好一個應用程式,編譯後它都有什麼東東?
例如:
用命令size a.out會得到:
其中text是放的是代碼,data放的是初始化過的全域變數或靜態變數,bss放的是未初始化的全域變數或靜態變數。
由於曆史原因,C程式一直由下列幾部分組成:
A、本文段。這是由cpu執行的機器指令部分。通常,本文段是可共用的,所以即使是經常執行的程式(如文本編輯程式、C編譯器、shell等)在儲存空間中也只需要有一個副本,另外,本文段常常是唯讀,以防止程式由於意外事故而修改器自身的指令。
B、初始化資料區段。通常將此段稱為資料區段,它包含了程式中需賦初值的變數。例如,C程式中任何函數之外的說明:
int maxcount = 99;(全域變數)
C、非初始化資料區段。通常將此段稱為bss段,這一名稱來源於早期組譯工具的一個操作,意思是"block started by symbol",在程式開始執行之前,核心將此段初始化為0。函數外的說明:
long sum[1000];
使此變數存放在非初始化資料區段中。
D、棧。自動變數以及每次函數調用時所需儲存的資訊都存放在此段中。每次函數調用時,其返回地址、以及調用者的環境資訊(例如某些機器寄存器)都存放在棧中。然後,新被調用的函數在棧上為其自動和臨時變數分配儲存空間。通過以這種方式使用棧,C函數可以遞迴調用。
E、堆。通常在堆中進行動態儲存裝置分配。由於曆史上形成的慣例,堆位於非初始化資料區段頂和棧底之間。
從我們看到棧空間是下增長的,堆空間是從下增長的,他們會會碰頭呀?一般不會,因為他們之間間隔很大,如:
#include <stdio.h>
#include <stdlib.h>
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("_____________________________________\n");
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("_____________________________________\n");
printf("BSS location:\n");
printf("\tAddress of bss_var:%p\n",&bss_var);
printf("_____________________________________\n");
printf("Heap location:\n");
char *p = (char *)malloc(10);
printf("\tAddress of head_var:%p\n",p);
return 0;
}
運行結果如下:
呵呵,這裡我們看到地址了,這個地址是虛擬位址,這些地址時怎麼來的呢?其實在我們編譯的時候,這些地址就已經確定了,如中紅線。
也就是說,我們不論我們運行a.out程式多少次這些地址都是一樣的。我們知道,linux作業系統每個進程的地址空間都是獨立的,其實這裡的獨立說得是物理空間上得獨立。那相同的虛擬位址,不同的物理地址,他們之間是怎樣聯絡起來的呢?我們繼續探究....
在linux作業系統中,每個進程都通過一個task_struct的結構體描敘,每個進程的地址空間都通過一個mm_struct描敘,c語言中的每個段空間都通過vm_area_struct表示,他們關係如下 :
當運行一個程式時,作業系統需要建立一個進程,這個進程和程式之間都幹了些什麼呢?
當一個程式被執行時,該程式的內容必須被放到進程的虛擬位址空間,對於可執行程式的共用庫也是如此。可執行程式並非真正讀到實體記憶體中,而只是連結到進程的虛擬記憶體中。
當一個可執行程式映射到進程虛擬位址空間時,一組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中;
注意:這裡沒有對應 的頁目錄表項建立頁表,更不存在設定頁表項了。
假設現在程式中有一條指令需要讀取上面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]在記憶體中分配一個頁表,並讓該表項指向它,如所示:
注意:這裡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位移處的內容讀入到該物理頁面中,如所示:
①分配實體記憶體頁面;
②從磁碟檔案中將內容讀取到實體記憶體頁面中
從上面我們可以知道,在進程建立的過程中,程式內容被映射到進程的虛擬記憶體空間,為了讓一個很大的程式在有限的實體記憶體空間運行,我們可以把這個程式的開始部分先載入到實體記憶體空間運行,因為作業系統處理的是進程的虛擬位址,如果在進行虛擬到物理地址的轉換工程中,發現物理地址不存在時,這個時候就會發生缺頁異常(nopage),接著作業系統就會把磁碟上還沒有載入到記憶體中的資料載入到實體記憶體中,對應的進程頁表進行更新。也許你會問,如果此時實體記憶體滿了,作業系統將如何處理?
下面我們看看linux作業系統是如何處理的:
如果一個進程想將一個虛擬頁裝入實體記憶體,而又沒有可使用的空閑物理頁,作業系統就必須淘汰實體記憶體中的其他頁來為此頁騰出空間。
在linux作業系統中,物理頁的描敘如下:
struct mem_map
{
1、本頁使用計數,當該頁被許多進程共用時計數將大於1
2、age描敘本頁的年齡,用來判斷該頁是否為淘汰或交換的好候選
3、map_nr描敘物理頁的頁幀號
}
如果從實體記憶體中被淘汰的頁來自於一個映像或資料檔案,並且還沒有被寫過,則該頁不必儲存,它可以丟掉。如果有進程在需要該頁時就可以把它從映像或資料檔案中取回記憶體。
然而,如果該頁被修改過,作業系統必須保留該頁的內容以便晚些時候在被訪問。這種頁稱為"髒(dirty)頁",當它被從記憶體中刪除時,將被儲存在一個稱為分頁檔的特殊檔案中。
相對於處理器和實體記憶體的速度,訪問分頁檔要很長時間,作業系統必須在將頁寫到磁碟以及再次使用時取回記憶體的問題上花費心機。
如果用來決定哪一頁被淘汰或交換的演算法不夠高效的話,就可能出現稱為"抖動"的情況。在這種情況下,頁面總是被寫到磁碟又讀回來,作業系統忙於此而不能進行真正的工作。
linux使用"最近最少使用(Least Recently Used ,LRU)"頁面調度技巧來公平地選擇哪個頁可以從系統中刪除。這種設計系統中每個頁都有一個"年齡",年齡隨頁面被訪問而改變。頁面被訪問越多它越年輕;被訪問越少越老。年老的頁是用於交換的最佳候選頁。
嵌入式及3G相關資源及學習請點擊:嵌入式開發視頻 android開發視頻 android培訓 3G培訓 QT培訓 QT開發視頻 物聯網培訓 物聯網技術視頻 嵌入式學習