1. 基本概念及相關術語
1.1 基本概念
虛擬內存使得應用程序認為它擁有連續的可用的內存(一個連續完整的地址空間),而實際上,它通常是被分隔成多個物理內存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進行數據交換。即將不完整,不連續的物理內存映射為連續的虛擬內存。虛擬內存主要有以下三個作用:
(1) 它將主存看成是磁盤的一個高速緩存,只在主存中保存活動區域(通常一個進程只有執行上下文被加載到主存,其余的在磁盤中,隨用隨加載);
(2) 為每個進程提供一致的地址空間,簡化了內存管理;
(3) 它保護每個進程的地址空間不被其他進程破壞(在頁表的PTE條目中加入額外控制信息實現內存保護)。
虛擬內存有兩個重要的地址,虛擬地址(virtual address, VA)和物理地址(physical address)。在訪問某個對象時,CPU給出虛擬地址,通過查詢計算得到物理地址,然后訪問物理地址上的對象。整個過程如下圖:
圖1 CPU訪問主存
1.2 相關術語
在表述虛擬內存相關概念時,有些約定的縮寫和表達方式
- N=2n:虛擬地址數量,n表示虛擬地址位數;
- M=2m:物理地址數量,m表示物理地址位數;
- P=2p:頁大小,p表示頁偏移量的位數;
- VPO(virtual page offset):虛擬地址頁偏移;
- VPN(virtual page number):虛擬地址頁號;
- PPO(physical page offset):物理地址頁偏移;
- PPN(physical page number):物理地址頁號。
- 頁表(Page Table, PT):記錄虛擬地址到物理地址的映射的表
- 頁表項(Page Table Entry, PTE):頁表中一行,PTE的索引即VPN;
- 頁表項地址(PTEA):在CPU中有個頁表基址寄存器,記錄頁表起始地址,頁表基址寄存器+PTE索引=PTEA;
- MMU(Memory Management Unit):內存管理單元,用於虛擬地址到物理地址尋址的硬件。
一個頁表的常見結構如下圖:
圖2 頁表常見結構(有效位表示該PTE是否有VP到PP的映射)
eg: 給定一個32位虛擬地址空間和一個24位物理地址空間,,對於下面的頁大小,確定VPN,VPO,PPN,PPO的位數。
P | VPN位數 | VPO位數 | PPN位數 | PPO位數 |
1KB | 22 | 10 | 14 | 10 |
4KB | 20 | 12 | 12 | 12 |
注:VPO表示對象在頁中的偏移,VPO=PPO,VPO位數=log2(P),VPN表示虛擬頁號,對應PTE表索引,PPN表示物理頁號。
一個虛擬地址翻譯成物理地址,方法如下圖:
圖3 虛擬地址翻譯為物理地址
地址翻譯時,給定虛擬地址,低p位表示頁偏移,其中VPO=PPO,高n-p位表示虛擬頁號,即PTE的索引號,找到對應PTE記錄,得到物理頁號PPN,跟PPO組合得到物理地址。所以訪問一個對象,首先訪問頁表,從虛擬地址轉化為物理地址,再從訪問物理地址得到對象。由於頁表和物理地址都在內存中,因此存在兩次內存訪問。
1.3 地址翻譯加速
從1.2中得知為了訪問對象,需要兩次內存訪問,每次內存訪問一般幾十到幾百個周期,為了加快地址翻譯,減少內存訪問次數,有兩種輔助設備:SRAM緩存和TLB緩存。
SRAM緩存:在CPU和主存(DRAM)之間,還有L1, L2, L3三級高速緩存(SRAM)。因此,可以將部分PTE條目和對象存到SRAM中,減少內存訪問次數,添加了SRAM的訪問機制如下圖。
圖4 加入SRAM的對象訪問過程
可見,在訪問時,優先訪問SRAM獲取PTE和數據,沒有再訪問主存,還沒有則引起缺頁中斷。SRAM的訪問通常幾個時間周期。
TLB緩存:在MMU中的虛擬地址緩存器,稱為翻譯后備寄存器(Translantion Lookaside Buffer)。每一行由一個或多個PTE條目組成,其中TLBI用於行號索引,TLBT用於同一行某個PTE的選擇。
圖5 虛擬地址在TLB中的含義
比如某一時刻,TLB中的快照如下:
圖6 TLB快照,四組,四路組相聯
頁面大小64字節,虛擬地址長度14位,物理地址長度為12位。給定虛擬地址0x03d4,其二進制表示為0b 00 0011 1101 0100,低6位0b 01 0100為VPO,因為四路組相聯,所以第6-7位為TLBI(TLB索引),為0b 11,剩余為TLBT(TLB標記),TLBI表示TLB表的行號,找到TLBT為0x03的位置,得到PPN為0D。結合VPO,得到物理地址為0b 0011 0101 0100,即0x0354。加入TLB之后的對象訪問過程如下:
圖7 加入TLB的對象訪問過程
2. Linux虛擬內存
2.1 Linux虛擬內存組織機制
Linux系統為每個進程維護一個單獨的地址空間,如圖8(a)所示,同時為每個進程維護一個結構體,其中包含虛擬內存相關信息,如圖8(b)所示。
(a) Linux進程的虛擬內存 (b)管理虛擬內存的結構體
圖8 Linux虛擬內存
其中vm_prot描述虛擬內存頁的讀寫權限,vm_flags記錄該虛擬頁是共享還是私有等其他常見信息。
2.2 內存映射
Linux系統將虛擬內存和一個磁盤對象關聯起來,以初始化虛擬內存區域的內容,稱為內存映射。有兩種類型的內存映射:
(1) 映射到Linux文件系統中的普通文件;
(2) 映射到匿名文件,匿名文件是由內核創建的全是二進制0的文件,CPU第一次使用該虛擬頁面時,內核就選擇一個物理頁面進行覆蓋(整個過程沒有跟磁盤發生數據交互)。
一個對象映射到虛擬內存中,要么以共享對象存在,要么以私有對象存在。不論哪一種模式,在物理內存中只有一份副本。共享對象一個進程的寫操作,其他進程都可見,並且能反映到磁盤上;私有對象一個進程的寫操作,其他進程不可見,並且不能反映到磁盤上。
(a) 內存映射到共享區域 (b) 內存映射到私有區域
圖9 多個進程映射同一對象
對於多個進程內存映射到私有區域時,物理內存只有一份副本,此時采用一種"寫時復制"策略。即進程在寫時,復制修改的部分到內存其他區域。這樣對其他進程來說,對象沒有修改過。
2.3 mmap函數
mmap函數提供用戶級的內存映射,該函數能夠把某個磁盤文件映射到內存中,函數的主要格式如下:
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset); start:內存起始地址,通常為NULL,讓系統自己選擇 length:內存的長度 prot: PROT_READ:數據可讀 PROT_WRITE:數據可寫 PROT_EXEC:數據可執行 PROT_NONE:數據不可訪問 flags: MAP_SHARED:共享對象,進程間可察覺修改,並能反映到磁盤 MAP_PRIVATE:私有對象,一切操作只在本進程可見,修改不會寫入磁盤 MAP_FIXED:基本不用 fd:映射的文件的描述符,通常應先打開文件,再調用mmap,此后關閉文件映射仍然存在 offset:文件偏移量,一般為0
該函數返回內存中對應的地址
調用mmap之后,內存與磁盤文件之間就建立了映射關系,如下圖所示:
munmap用於解除映射關系
int munmap(void* start,size_t length);
使用mmap的作用主要有以下兩個:
(1) 將磁盤文件映射到內存中,這樣所有讀寫均針對內存讀寫(可以使用memcpy等內存操作函數,而不是read,write等IO操作函數),加快訪問速度;
(2) 在無親緣關系的進程間提供共享內存。
使用mmap函數,需要注意以下問題:
(1) 在文件映射之前,必須打開該文件,而且mmap的prot權限不能超過打開的權限。比如open打開時只設置了讀文件,那么prot就不能設置PROT_WRITE;
(2) 內存映射通常都是按虛擬內存的頁為基本單位的。比如一個頁512字節,但是映射的文件只有12字節。那么剩下的500字節會自動填充為零,即時修改了后面的500字節,也不會寫入到文件(所以較好的操作是直到文件大小,直接加長文件);
(3) 如果試圖訪問不存在的映射關系,比如頁面大小512字節,實際文件大小為12字節,用mmap映射的時候映射1000個字節,那么實際可操作的結果如下:
(4) 將內存寫入磁盤的操作通常由頁守護進程完成,如果想人為控制將內存數據寫入磁盤,可以調用以下函數:
#include <sys/mman.h> int msync(void *addr,size_t len,int flags); flags: MS_ASYC:異步寫入 MS_SYC:同步寫入,等待寫入之后才會返回
(5) 進程終止或調用munmap時解除映射關系,關閉文件描述符不會解除映射關系。
下面舉一個簡單的例子:父子進程同時修改一個文件寫入數據:
1 #include <sys/mman.h> 2 #include <stdio.h> 3 #include <fcntl.h> 4 #include <unistd.h> 5 #include <sys/wait.h> 6 #include <sys/types.h> 7 #include <stdlib.h> 8 #include <string.h> 9 10 #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH) 11 12 int main() 13 { 14 int fd; 15 if((fd=open("map.txt",O_RDWR|O_CREAT|O_TRUNC,FILE_MODE))<0) 16 { 17 printf("open file failed\n"); 18 exit(1); 19 } 20 21 22 if(ftruncate(fd,50)<0) //文件大小50字節 23 { 24 printf("ftruncate error\n"); 25 exit(1); 26 } 27 28 char *buf;//起始地址 29 30 buf=(char*)mmap(NULL,50,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); 31 close(fd); 32 pid_t pid; 33 if((pid=fork())<0) 34 printf("fork error\n"); 35 36 char* msg="hello world\n"; 37 char* msg1= "good news"; 38 if(pid==0) //子進程 39 { 40 memcpy(buf,msg,strlen(msg)); 41 exit(0); 42 } 43 else 44 { 45 int stat; 46 wait(&stat); 47 memcpy(buf+strlen(msg),msg1,strlen(msg1)); 48 } 49 return 0; 50 51 }
第22-26行就是申請文件大小為50字節,那么實際內存可修改的部分就是buf~(buf+49)。注釋該段再執行就會報SIGBUS錯誤。
執行結果是當前目錄多了map.txt,其內容為:
hello world
good news
2.4 Linux進程分配內存的方式
關於此部分詳細介紹參考博文:https://www.cnblogs.com/vinozly/p/5489138.html
簡單來說,當我們調用分配內存的函數時(如malloc),底層通過調用brk()或mmap()實現。當遇到小於128KB的內存時,調用brk()函數將數據段堆的_edata地址往高地址推(即圖8a中brk指向的指針,此時只分配虛擬內存,沒有物理內存。當產生缺頁中斷時,才調用物理內存)。當申請內存大於128KB時,調用mmap()在堆棧之間的共享區域分配內存(此部分內存可以單獨釋放)。