共享内存之——mmap内存映射
共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所以是最快的一种进程间通信机制。共享内存可以通过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; }