本文是对http://www.cnblogs.com/andtt/articles/2136279.html中共享内存(上)的进一步阐释说说明
1 共享内存的实现原理
共享内存是linux进程间通讯的一种方式;顾名思义,共享内存就是说两个不同的进程A、B可以共同享有一块内存区域,A和B可以按照约定的规则读写该内存区域,达到进程间通讯的目的。那么问题来了,我们都知道linux下使用虚拟内存技术,使得每个进程都是自己独立的进程空间,不能相互访问;那么如何实现共享内存区域呢,且看下图
进程正常访问文件是将文件读到内存按页存储,然后访问内存页;
共享内存的处理流程是进程A和进程B都将该页映射到自己的地址空间, 当进程A第一次访问该页中的数据时, 它生成一个缺页中断. 内核此时读入这一页到内存并更新页表使之指向它.以后, 当进程B访问同一页面而出现缺页中断时, 该页已经在内存, 内核只需要将进程B的页表登记项指向次页即可;这样的话,两个进程同时就可以同时访问该页,达到进程A和B通信的目的
2 相关的系统调用
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
函数作用:
mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存,进程可以像访问普通内存一样对文件进行访问,而不必调用read(),write()函数等操作;
可以看出来,mmap系统调用并不是完全为了共享内存而设计的,也可在一个进程内将文件映射到内存,而通过访问内存的方式访问文件;
参数说明:
Fd :要映射到进程空间的文件描述符;
Offset :要映射到内存的文件的偏移位置,一般设置为0,即从文件开头开始映射
Addr :文件映射到内存的起始地址,一般设为null,有系统自动分配
Len : 映射到调用进程地址空间的字节数,它从被映射文件开头的offset个字节开始算
Prot :这个参数描述共享内存的访问权限(不可与open文件的权限冲突);它可以是PROT_NONE或者PROT _READ、PROT _WRITE、PROT _EXEC的组合
Flag : 此参数确定某个进程映射区域的更新是否被其他进程课件,和该更新是否同步到底层文件,通常设置为MAP_SHARED
<span style="font-size:12px;">int munmap(void *addr, size_t length);</span>
函数作用:
解除映射关系
参数说明:
Addr : 文件映射到内存的起始地址,即mmap函数的返回值;
Length: 映射的进程地址空间的长度
int msync(void *addr, size_t length, intflags);
函数作用:
通常情况下,mmap建立映射后,对共享内存的访问是对内存的操作,并没有同步到文件,当执行munmap函数后,才将内存的内容同步到文件,msync是指将内存的数据立即同步内存映射数据到文件
3 共享内存的两种实现方式
匿名文件映射:适用于有亲缘关系的进程时间;由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用fork,那么在fork之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap返回的地址;这样,父子进程就可以通过映射区域进程通信了。对于具有亲缘关系的进程共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必制定具体的文件,只要设置相应的标志即可
4 共享内存使用实例及解释
实例1:两个进程通过映射不同文件实现共享内存通信
实例1包含两个子程序:map_normalfile1.c和map_normalfile2.c。编译两个程序,分别生成可执行文件map_normalfile1和map_normalfile2;两个程序通过命令行制定同一文件实现共享内存方式的进程通信;map_normalfile2试图打开命令行参数指定的一个普通文件,把该文件映射到进程的地址空间,并对映射后的地址空间进行写操作。map_normalfile1把命令行参数指定的文件映射到进程地址空间,然后对映射后的地址空间执行读操作。这样,两个进程通过命令行参数指定同一个文件来实现共享内存方式的进程间通信;
// map_normalfile1.c #include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <string.h> typedef struct{ char name[4]; int age; }people; int main(int argc, char** argv) { int fd,i; people *p_map; char temp; //打开映射文件 fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); printf("%lu ", sizeof(people)*5-1); //设置映射文件大小 lseek(fd,sizeof(people)*5-1,SEEK_SET); write(fd,"",1); //将文件映射到进程空间,返回进程空间地址 p_map = (people*) mmap( NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0 ); close( fd ); temp = 'a'; //以固定的格式,向该内存中写入数据 for(i=0; i<10; i++) { temp += 1; memcpy( ( *(p_map+i) ).name, &temp,2 ); ( *(p_map+i) ).age = 20+i; } printf("initialize over "); //写操作完成,等待10秒钟 sleep(10); //解除映射关系 munmap( p_map, sizeof(people)*10 ); printf( "umap ok " ); return 0; }该文件定义一个people数据结构,该结构需要和共享内存的另一个进程协商决定;首先打开一个或者创建一个文件,并把文件长度设置为5个people结构大小,然后使用mmap函数,把该文件映射到进程空间,len设置为10个people结构的大小,请注意此时文件大小和映射到进程空间的地址并不一样,这是被允许的,进程空间设置的大小受文件大小的影响但不一定要等于文件的大小,以指定的格式向该映射内存中写入10个people的数据;睡眠10s等待另一个进程读取数据;最后解除映射关系,打印;
#include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; int main(int argc, char** argv) // map a normal file as shared mem: { int fd,i; people *p_map; //打开映射文件 fd=open( argv[1],O_CREAT|O_RDWR,00777 ); //将文件映射到进程空间 p_map = (people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED,fd,0); //读取共享内存中的数据 for(i = 0;i<10;i++) { printf( "name: %s age %d; ",(*(p_map+i)).name, (*(p_map+i)).age ); } //解除映射关系 munmap( p_map,sizeof(people)*10 ); return 0; }该文件首先打开映射文件,然后将该文件映射到进程空间,此时,就可以从内存空间中读取上个文件中写入的内容,最后解除映射关系
测试1:运行./map_normalfile1.out /tmp/1.txt;在initialize over之后,umap ok之前,运行./map_normalfile2.out /tpm/1.txt;结果如下:
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;测试2:运行./map_normalfile1.out /tmp/1.txt;在initialize over和umap ok之后,运行./map_normalfile2.out /tpm/1.txt;结果如下:
name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24; name: age 0; name: age 0; name: age 0; name: age 0; name: age 0;结果分析
对于测试1,file_normalfile1.out在解除映射前,file_normalfile1.out读取共享内存中的数据,实现了真正意义上的进程间通讯
对于测试2,这个比较简单,当file_normalfile1.out进程结束后,会将映射内存中的数据同步到文件上,但是由于映射内存的大小是10个people结构,而文件大小只有5个people结构,所以只把前5个内存中的数据保存到文件;当运行map_normalfile2.out的时候,把文件中的5个people结构映射到内存进行读取,结构只显示前5个结构的内容;可以简单的理解为file_normalfile1.out把数据写入/tmp/1.txt中,file_normalfile2.out从/tmp/1.txt中读取数据
相关问题如下:
1 file_normalfile1.c中为什么要执行lseek和write操作,不执行可以吗(前提是此文件是由进程创建的)?
不可以,lseek函数是定位文件读写位置,执行完lseek函数后,读写位置定位到39个字节处,然而此时文件的大小是没有变化的,还是0字节,write一个字节后,此时文件大小是40个字节;经过测试,lseek函数是可以删除的,只是删除后测试2不能正常工作,但是write函数不可以删除,否则会出现bus error,具体原因,我们下一节详细介绍
2 为什么我在机器上进程测试,测试2的结果与测试1结果完全相同?
由于file_normalfile1.out进程在解除映射关系后,映射内存中的数据并没有改变,此时运行file_normalfile2.out,建立映射关系,恰好mmap()返回的地址file_normaifile1.out的映射内存的首地址,所以显示结果相同;为了的到上述测试中的结果,可以将1.txt和file_normalfile2.out拷贝到另一台机器上运行或者重启本机运行
3 文件大小和映射的进程空间的大小有什么关系
进程空间 <= 文件大小+单位页大小
4 测试1文件大小是5个people结构的大小,为什么file_normalfile2.out会读取到10个people结构的大小?
这是理解的误区,其实当文件映射到进程后,不管是file_normalfile1.out还是file_normalfile2.out直接访问的都是映射的内存,此时和文件并没有关系,所以不会收到文件大小的影响。只有调用了munmap()后或者msync()时,才把内存中的相应内容写回磁盘文件,所写内容仍然不能超过文件的大小
实例2 父子进程通过匿名映射实现共享内存
#include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4]; int age; }people; main(int argc, char** argv) { int i; people *p_map; char temp; p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS,-1,0); if(fork() == 0) { sleep(2); for(i = 0;i<5;i++) printf("child read: the %d people's age is %d ",i+1,(*(p_map+i)).age); (*p_map).age = 100; munmap(p_map,sizeof(people)*10); //实际上,进程终止时,会自动解除映射。 exit(); } temp = 'a'; for(i = 0;i<5;i++) { temp += 1; memcpy((*(p_map+i)).name, &temp,2); (*(p_map+i)).age=20+i; } sleep(5); printf( "parent read: the first people,s age is %d ",(*p_map).age ); printf("umap "); munmap( p_map,sizeof(people)*10 ); printf( "umap ok " ); }程序输出结果如下
child read: the 1 people's age is 20 child read: the 2 people's age is 21 child read: the 3 people's age is 22 child read: the 4 people's age is 23 child read: the 5 people's age is 24 parent read: the first people,s age is 100 umap umap ok结果分析: 父进程建立映射关系后,子进程继承映射内存的首地址。父进程想映射内存中写入数据;子进程从映射内存中读取数据;子进程读取完成后,修改了people结构中的一个字段。父进程读取到该进程也被修改,说明父子进程使用的同一块内存
5 对mmap()返回地址的访问
linux采用的是页式管理机制。对于用mmap()映射普通文件来说,进程会在自己的地址空间新增一块空间,空间大小有mmap()的len参数指定;然后进程并不一定能够对全部新增的空间都能进行访问,而是受到文件被映射部分的影响;简单的说,能够容纳文件被映射部分大小的最少页面个数决定了进程从mmap()返回的地址开始,能够有效访问的地址空间的大小。超过这个空间大小,内核会根据超过的严重程度的返回发送不同的信号给进程,如图
现在我们解释下前面的问题1,为什么lseek能够去掉,而write不能去,就是因为lseek去掉后,文件大小是1个字节,但是进程可访问的空间是一个页的大小,在我的机器上是4096个字节;那么为什么测试2不能正常运行?那是因为文件大小只有1个字节,file_normalfile1.out退出后,只能同步一个字节到文件,file_normailfile2.out自然读取不到数据
我们通过下面这个实例来深入理解
#include <sys/mman.h> #include <sys/types.h> #include <fcntl.h> #include <unistd.h> typedef struct{ char name[4] int age; }people; main(int argc, char** argv) { int fd,i; int pagesize,offset; people *p_map; pagesize = sysconf(_SC_PAGESIZE); printf("pagesize is %d ",pagesize); fd = open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777); lseek(fd,pagesize*2-100,SEEK_SET); write(fd,"",1); offset = 0; //此处offset = 0编译成版本1;offset = pagesize编译成版本2 p_map = (people*)mmap(NULL,pagesize*3,PROT_READ|PROT_WRITE,MAP_SHARED,fd,offset); close(fd); for(i = 1; i<10; i++) { (*(p_map+pagesize/sizeof(people)*i-2)).age = 100; printf("access page %d over ",i); (*(p_map+pagesize/sizeof(people)*i-1)).age = 100; printf("access page %d edge over, now begin to access page %d ",i, i+1); (*(p_map+pagesize/sizeof(people)*i)).age = 100; printf("access page %d over ",i+1); } munmap(p_map,sizeof(people)*10); }
根据offset的数值不同,编译成两个不同的版本1和版本2
版本1结果如下: <p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">pagesize is 4096</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 1 over</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 1 edge over, now begin to access page 2</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 2 over</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 2 over</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 2 edge over, now begin to access page 3</p>Bus error
版本2结果如下 <p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">pagesize is 4096</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 1 over</p><p style="margin-top: 5px; margin-right: auto; margin-bottom: 5px; margin-left: auto; text-indent: 0px;">access page 1 edge over, now begin to access page 2</p>Bus error结果分析:
版本1中映射到内存的文件大小是pagesize*2-100+1=pagesize*2-99;所以进程能够访问到映射到内存的空间大小是2个pagesize;for循环体中访问的分别是第i个页倒数第二个people结构体、第i个页倒数第一个people结构体,和下一个页的开头;因为进程只能访问2个页,所以当访问第3个页的时候,出现bus error
版本2中把offset的大小设置为pagesize的大小,所以它映射到内存的文件大小变成了pagesize-99,即进程只能访问一个页; 所以当进程访问第二个页的时候,会出现bus error