虛擬內存與mmap,brk
- 基本概念及相關術語
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§,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的對象訪問過程
- 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()在堆棧之間的共享區域分配內存(此部分內存可以單獨釋放)。
標簽: 虛擬內存 , 計算機基礎 , 操作系統 , mmap
好文要頂 關注我 收藏該文 微信分享
晨楓1
粉絲 - 2 關注 - 0
+加關注
00
升級成為會員
? 上一篇: QT下多線程調用TCP的問題及可能的解決方案
? 下一篇: epoll,select,poll的區別
posted @ 2020-05-18 12:17 晨楓1 閱讀(1291) 評論(0) 編輯 收藏 舉報