咱們知道,在32位機器上linux操做系統中的進程的地址空間大小是4G,其中0-3G是用戶空間,3G-4G是內核空間。其實,這個4G的地址空間是不存在的,也就是咱們所說的虛擬內存空間。linux
那虛擬內存空間是什麼呢,它與實際物理內存空間又是怎樣對應的呢,爲何有了虛擬內存技術,咱們就能運行比實際物理內存大的應用程序,它是怎麼作到的呢?算法
呵呵,這一切的一切都是個迷呀,下面咱們就一步一步解開心中的謎團吧!shell
咱們來看看,當咱們寫好一個應用程序,編譯後它都有什麼東東?編程
例如:數據結構
用命令size a.out會獲得:函數
其中text是放的是代碼,data放的是初始化過的全局變量或靜態變量,bss放的是未初始化的全局變量或靜態變量spa
因爲歷史緣由,C程序一直由下列幾部分組成:操作系統
A.正文段。這是由cpu執行的機器指令部分。一般,正文段是可共享的,因此即便是常常執行的程序(如文本編輯程序、C編譯程序、shell等)在存儲器中也只須要有一個副本,另外,正文段經常是隻讀的,以防止程序因爲意外事故而修改器自身的指令。設計
B.初始化數據段。一般將此段稱爲數據段,它包含了程序中需賦初值的變量。例如,C程序中任何函數以外的說明:orm
int maxcount = 99;(全局變量)
C.非初始化數據段。一般將此段稱爲bss段,這一名稱來源於早期彙編程序的一個操做,意思是"block started by symbol",在程序開始執行以前,內核將此段初始化爲0。函數外的說明:
long sum[1000];
使此變量存放在非初始化數據段中。
D.棧。自動變量以及每次函數調用時所需保存的信息都存放在此段中。每次函數調用時,其返回地址、以及調用者的環境信息(例如某些機器寄存器)都存放在棧中。而後,新被調用的函數在棧上爲其自動和臨時變量分配存儲空間。經過以這種方式使用棧,C函數能夠遞歸調用。
E.堆。一般在堆中進行動態存儲分配。因爲歷史上造成的慣例,堆位於非初始化數據段頂和棧底之間。
從上圖咱們看到棧空間是下增加的,堆空間是從下增加的,他們會會碰頭呀?通常不會,由於他們之間間隔很大,如:
#include
#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("_____________________________________\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)"頁面調度技巧來公平地選擇哪一個頁能夠從系統中刪除。這種設計系統中每一個頁都有一個"年齡",年齡隨頁面被訪問而改變。頁面被訪問越多它越年輕;被訪問越少越老。年老的頁是用於交換的最佳候選頁