共享內存允許兩個或多個進程共享一給定的存儲區,因為數據不需要來回復制,所以是最快的一種進程間通信機制。共享內存可以通過mmap()映射普通文件 (特殊情況下還可以采用匿名映射)機制實現,也可以通過systemV共享內存機制實現。應用接口和原理很簡單,內部機制復雜。為了實現更安全通信,往往還與信號燈等同步機制共同使用。 這一篇詳解mmap內存文件映射原理及其案例,system V共享內存 以及他們的區別將在后面的隨筆中討論。
非原創,內容源於互聯網
mmap內存文件映射
一、傳統文件訪問
unix訪問文件的傳統方法使用open打開他們,如果有多個進程訪問一個文件,則每一個進程在再記得地址空間都包含有該文件的副本,這不必要地浪費了存儲空間。下面說明了兩個進程同時讀一個文件的同一頁的情形,系統要將該頁從磁盤讀到高速緩沖區中,每個進程再執行一個內存期內的復制操作將數據從高速緩沖區讀到自己的地址空間。
二、共享內存映射
現在考慮林一種處理方法:進程A和進程B都將該頁映射到自己的地址空間,當進程A第一次訪問該頁中的數據時,它生成一個缺頁終端,內核此時讀入這一頁到內存並更新頁表使之指向它,以后,當進程B訪問同一頁面而出現缺頁中斷時,該頁已經在內存,內核只需要將進程B的頁表登記項指向次頁即可。
三、mmap及其相關系統調用
mmap()系統調用使得進城之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間后,進程可以像訪問普通內存一樣對文件進行訪問,不必再調用read,write
等操作。
mmap()系統調用形式如下:
#include<sys/mman.h> void mmap(void *addr, size_t len, int prot,int flags, int fildes, off_t off) int msync(void *addr, size_t len, int flags); int munmap(void *addr, size_t len);
mmap的作用是映射文件描述符和指定文件的(off_t off)區域至調用進程的(addr,addr *len)的內存區域,如下圖所示:
參數:
fd:為即將映射到進程空間的文件描述字,一般由open()返回,同時,fd可以指定為-1,此時須指定flags參數中的MAP_ANON,表明進行的是匿名映射(不涉及具體的文件名,避免了文件的創建及打開,很顯然只能用於具有親緣關系的進程間進行通信)。
len:是映射到調用進程地址空間的字節數,它從被映射文件開頭offset個字節開始算起。
prot:指定空想內存的訪問權限。可取如下幾個值的或:PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執行)、PROT_NONE(不可訪問)。
flag:由以下幾個常值指定:MAP_SHARED、MAP_PRIVATE、MAP_FIXED,其中,MAP_SHARED,MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用。
offset:一般設為0,表示從文件頭開始映射。
addr:指定文件應被映射到進程空間的起始地址,一般被指定一個空指針,此時選擇起始地址的任務留給內核來完成。
函數的返回值為最后文件映射到進程空間的地址,進程可直接操作起始地址為該值的有效地址。
四、mmap基礎用例
1 //測試文件 data.txt 后面的程序也要用到 2 aaaaaaaaa 3 bbbbbbbbb 4 cccccccccccc 5 ddddddddd
1、通過共享內存映射的方式修改文件
1 #include <sys/mman.h> 2 #include <sys/stat.h> 3 #include<fcntl.h> 4 #include<stdio.h> 5 #include<stdlib.h> 6 #include<unistd.h> 7 #include<error.h> 8 9 int main(int argc, char * argv[]) 10 { 11 int fd, nread; 12 struct stat sb; 13 char *mapped; 14 15 //打開文件 16 if((fd = open(argv[1], O_RDWR)) < 0){ 17 perror("open") ; 18 } 19 20 //獲取文件的屬性 21 if((fstat(fd, &sb)) == -1 ){ 22 perror("fstat") ; 23 } 24 25 26 //將文件映射至進程的地址空間 27 if((mapped = mmap(NULL, sb.st_size, PROT_READ|\ 28 PROT_WRITE, MAP_SHARED, fd, o)) ==(void*) -1){ 29 perror("mmap") ; 30 } 31 32 //修改一個字符,同步到磁盤文件 33 mapped[20] = '9'; 34 if((msync((void *)mapped, sb.st_size, MS_SYNC)) == -1){ 35 perror("msync") ; 36 37 //釋放存儲映射區 38 if((munmap((void *)mapped,sb.st_size)) == -1){ 39 perror("munmap"); 40 } 41 42 return 0; 43 } 44
2 私有映射無法修改文件
1 //將文件私有映射到進程的地址空間 2 if((mapped = (char *)mmap(NULL,sb.st_size,PROT_READ| 3 PROT_WRITE, MAP_PRIVATE, fd, 0))==(void *)-1){ 4 perror("mmap");
五、使用共享內存映射實現兩個進程之間的通信
兩個程序映射到同一個文件到自己的地址空間,進程A先運行,每個兩秒讀取映射區域,看是否發生變化,進程B后運行,它修改映射區域,然后退出,此時進程A能夠觀察到存儲映射區的變化
進程A的代碼:
1 #include <sys/mman.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <stdlib.h> 6 #include <unistd.h> 7 #include <error.h> 8 9 int main(int argc, char **argv) 10 { 11 int fd, nread; 12 struct stat sb; 13 char *mapped; 14 15 16 /* 打開文件 */ 17 if ((fd = open(argv[1], O_RDWR)) < 0) { 18 perror("open"); 19 } 20 21 /* 獲取文件的屬性 */ 22 if ((fstat(fd, &sb)) == -1) { 23 perror("fstat"); 24 } 25 26 /* 將文件映射至進程的地址空間 */ 27 if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) { 28 perror("mmap"); 29 } 30 31 /* 文件已在內存, 關閉文件也可以操縱內存 */ 32 close(fd); 33 34 /* 每隔兩秒查看存儲映射區是否被修改 */ 35 while (1) { 36 printf("%s\n", mapped); 37 sleep(2); 38 } 39 40 return 0; 41 }
進程B的代碼
1 #include <sys/mman.h> 2 #include <sys/stat.h> 3 #include <fcntl.h> 4 #include <stdio.h> 5 #include <stdlib.h> 6 #include <unistd.h> 7 #include <error.h> 8 9 int main(int argc, char **argv) 10 { 11 int fd; 12 struct stat sb; 13 char *mapped; 14 15 /* 打開文件 */ 16 if ((fd = open(argv[1], O_RDWR)) < 0) { 17 perror("open"); 18 } 19 20 /* 獲取文件的屬性 */ 21 if ((fstat(fd, &sb)) == -1) { 22 perror("fstat"); 23 } 24 /* 私有文件映射將無法修改文件 */ 25 if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ 26 |PROT_WRITE,MAP_PRIVATE, fd, 0)) == (void*)-1) { 27 perror("mmap"); 28 } 29 30 /* 映射完后, 關閉文件也可以操縱內存 */ 31 close(fd); 32 33 /* 修改一個字符 */ 34 mapped[20] = '9'; 35 36 return 0; 37 }
六、通過匿名映射實現父子進程通信
1 #include <sys/mman.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 6 #define BUF_SIZE 100 7 8 int main(int argc, char** argv) 9 { 10 char *p_map; 11 12 /* 匿名映射,創建一塊內存供父子進程通信 */ 13 p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); 14 15 if(fork() == 0) { 16 sleep(1); 17 printf("child got a message: %s\n", p_map); 18 sprintf(p_map, "%s", "hi, dad, this is son"); 19 munmap(p_map, BUF_SIZE); //實際上,進程終止時,會自動解除映射。 20 exit(0); 21 } 22 23 sprintf(p_map, "%s", "hi, this is father"); 24 sleep(2); 25 printf("parent got a message: %s\n", p_map); 26 27 return 0; 28 }
七、對mmap()返回地址的訪問
linux采用的是頁式管理機制。對於用mmap()映射普通文件來說,進程會在自己的地址空間新增一塊空間,空間大小由mmap()的len參數指定,注意,進程並不一定能夠對全部新增空間都能進行有效訪問。進程能夠訪問的有效地址大小取決於文件被映射部分的大小。簡單的說,能夠容納文件被映射部分大小的最少頁面個數決定了進程從mmap()返回的地址開始,能夠有效訪問的地址空間大小。超過這個空間大小,內核會根據超過的嚴重程度返回發送不同的信號給進程。可用如下圖示說明:
總結一下就是,文件大小,mmap()的參數len都不能決定進程能訪問的大小,而是容納文件被映射部分的最小頁面數決定進程能訪問的大小,下面看一個實例:
#include <sys/mman.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> int main(int argc, char** argv) { int fd,i; int pagesize,offset; char *p_map; struct stat sb; /* 取得page size */ pagesize = sysconf(_SC_PAGESIZE); printf("pagesize is %d\n",pagesize); /* 打開文件 */ fd = open(argv[1], O_RDWR, 00777); fstat(fd, &sb); printf("file size is %zd\n", (size_t)sb.st_size); offset = 0; p_map = (char *)mmap(NULL, pagesize * 2, PROT_READ|PROT_WRITE, MAP_SHARED, fd, offset); close(fd); p_map[sb.st_size] = '9'; /* 導致總線錯誤 */ p_map[pagesize] = '9'; /* 導致段錯誤 */ munmap(p_map, pagesize * 2); return 0; }