采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。

1. 共享内存的通信原理

在Linux中,每个进程都有属于自己的进程控制块(PCB)和地址空间(Addr Space),并且都有一个与之对应的页表,负责将进程的虚拟地址与物理地址进行映射,通过内存管理单元(MMU)进行管理。两个不同的虚拟地址通过页表映射到物理空间的同一区域,它们所指向的这块区域即共享内存。
image.png 当两个进程通过页表将虚拟地址映射到物理地址时,在物理地址中有一块共同的内存区,即共享内存,这块内存可以被两个进程同时看到。这样当一个进程进行写操作,另一个进程读操作就可以实现进程间通信。但是,我们要确保一个进程在写的时候不能被读,因此我们使用信号量来实现同步与互斥。

2. System V共享内存

进程间需要共享的数据被放在一个叫做IPC共享内存区域的地方,所有需要访问该共享区域的进程都要把该共享区域映射到本进程的地址空间中去。系统V共享内存通过shmget获得或创建一个IPC共享内存区域,并返回相应的标识符。内核在保证shmget获得或创建一个共享内存区,初始化该共享内存区相应的shmid_kernel结构的同时,还将在特殊文件系统shm中,创建并打开一个同名文件,并在内存中建立起该文件的相应dentry及inode结构,新打开的文件不属于任何一个进程(任何进程都可以访问该共享内存区)。所有这一切都是系统调用shmget完成的。
注:每一个共享内存区都有一个控制结构struct shmid_kernel,shmid_kernel是共享内存区域中非常重要的一个数据结构,它是存储管理和文件系统结合起来的桥梁,定义如下:

  1. struct shmid_kernel /* private to the kernel */
  2. {
  3. struct kern_ipc_perm shm_perm;
  4. struct file * shm_file;
  5. int id;
  6. unsigned long shm_nattch;
  7. unsigned long shm_segsz;
  8. time_t shm_atim;
  9. time_t shm_dtim;
  10. time_t shm_ctim;
  11. pid_t shm_cprid;
  12. pid_t shm_lprid;
  13. };

该结构中最重要的一个域应该是shm_file,它存储了将被映射文件的地址。每个共享内存区对象都对应特殊文件系统shm中的一个文件,一般情况下,特殊文件系统shm中的文件是不能用read()、write()等方法访问的,当采取共享内存的方式把其中的文件映射到进程地址空间后,可直接采用访问内存的方式对其访问。
以下一段不理解,先摘抄下来,后续等知识扩展以后,再研究。
start。。。
这里我们采用[1]中的图表给出与系统V共享内存相关数据结构:
image.png
正如消息队列和信号灯一样,内核通过数据结构struct ipc_ids shm_ids维护系统中的所有共享内存区域。上图中的shm_ids.entries变量指向一个ipc_id结构数组,而每个ipc_id结构数组中有个指向kern_ipc_perm结构的指针。到这里读者应该很熟悉了,对于系统V共享内存区来说,kern_ipc_perm的宿主是shmid_kernel结构,shmid_kernel是用来描述一个共享内存区域的,这样内核就能够控制系统中所有的共享区域。同时,在shmid_kernel结构的file类型指针shm_file指向文件系统shm中相应的文件,这样,共享内存区域就与shm文件系统中的文件对应起来。
在创建了一个共享内存区域后,还要将它映射到进程地址空间,系统调用shmat()完成此项功能。由于在调用shmget()时,已经创建了文件系统shm中的一个同名文件与共享内存区域相对应,因此,调用shmat()的过程相当于映射文件系统shm中的同名文件过程,原理与mmap()大同小异。
end。。。

2.1 system V共享内存API

对于系统V共享内存,主要有以下几个API:shmget()、shmat()、shmdt()及shmctl()。
#include
#include
int shmget(key_t key, size_t size, int shmflg);
参数:
key:由ftok生成的key标识,标识系统的唯一IPC资源。
size:需要申请共享内存的大小。在操作系统中,申请内存的最小单位为页,一页是4k字节,为了避免内存碎片,我们一般申请的内存大小为页的整数倍。
shmflg:如果要创建新的共享内存,需要使用IPC_CREAT,IPC_EXCL,如果是已经存在的,可以使用IPC_CREAT或直接传0。
返回值:
成功时返回一个新建或已经存在的的共享内存标识符,取决于shmflg的参数。失败返回-1并设置错误码。
void shmat(int shmid, const void shmaddr, int shmflg);
参数:
int shmid:共享存储段的标识符。
const voidshmaddr:shmaddr = 0,则存储段连接到由内核选择的第一个可以地址上(推荐使用)。
shmflg:若指定了SHM_RDONLY位,则以只读方式连接此段,否则以读写方式连接此段。
返回值:
成功返回共享存储段的指针(虚拟地址),并且内核将使其与该共享存储段相关的shmid_ds结构中的shm_nattch计数器加1(类似于引用计数);出错返回-1。
int shmdt(const void
shmaddr);
参数:
shmaddr:连接以后返回的地址。
返回值:
成功返回0,并将shmid_ds结构体中的 shm_nattch计数器减1;出错返回-1。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
shmid:共享存储段标识符。
cmd:指定的执行操作,设置为IPC_RMID时表示可以删除共享内存。
buf:设置为NULL即可。
返回值:
成功返回0,失败返回-1。
shmget()用来获得共享内存区域的ID,如果不存在指定的共享区域就创建相应的区域。shmat()把共享内存区域映射到调用进程的地址空间中去,这样,进程就可以方便地对共享区域进行访问操作。shmdt()调用用来解除进程对共享内存区域的映射。shmctl实现对共享内存区域的控制操作。这里我们不对这些系统调用作具体的介绍,读者可参考相应的手册页面,后面的范例中将给出它们的调用方法。
注:shmget的内部实现包含了许多重要的系统V共享内存机制;shmat在把共享内存区域映射到进程空间时,并不真正改变进程的页表。当进程第一次访问内存映射区域访问时,会因为没有物理页表的分配而导致一个缺页异常,然后内核再根据相应的存储管理机制为共享内存映射区域分配相应的页表。

2.2 system V共享内存限制

在/proc/sys/kernel/目录下,记录着系统V共享内存的一下限制,如一个共享内存区的最大字节数shmmax,系统范围内最大共享内存区标识符数shmmni等,可以手工对其调整,但不推荐这样做。

2.3 system V共享内存使用案例

  1. /***** testwrite.c *******/
  2. #include <sys/ipc.h>
  3. #include <sys/shm.h>
  4. #include <sys/types.h>
  5. #include <unistd.h>
  6. typedef struct{
  7. char name[4];
  8. int age;
  9. } people;
  10. int main(int argc, char** argv)
  11. {
  12. int shm_id,i;
  13. key_t key;
  14. char temp;
  15. people *p_map;
  16. char* name = "/dev/null";
  17. key = ftok(name,0x6666);
  18. printf("key = %d", key);
  19. if(key==-1)
  20. {
  21. perror("ftok error");
  22. return 0;
  23. }
  24. shm_id=shmget(key,4096,IPC_CREAT | 0666);
  25. if(shm_id==-1)
  26. {
  27. perror("shmget error");
  28. return 0;
  29. }
  30. p_map=(people*)shmat(shm_id,NULL,0);
  31. temp='a';
  32. for(i = 0;i<10;i++)
  33. {
  34. temp+=1;
  35. memcpy((*(p_map+i)).name,&temp,1);
  36. (*(p_map+i)).age=20+i;
  37. }
  38. if(shmdt(p_map)==-1)
  39. perror(" detach error ");
  40. return 0;
  41. }
  42. /********** testread.c ************/
  43. #include <sys/ipc.h>
  44. #include <sys/shm.h>
  45. #include <sys/types.h>
  46. #include <unistd.h>
  47. typedef struct{
  48. char name[4];
  49. int age;
  50. } people;
  51. int main(int argc, char** argv)
  52. {
  53. int shm_id,i;
  54. key_t key;
  55. people *p_map;
  56. char* name = "/dev/null";
  57. key = ftok(name,0x6666);
  58. if(key == -1)
  59. {
  60. perror("ftok error");
  61. return 0;
  62. }
  63. shm_id = shmget(key,4096,IPC_CREAT | 0666);
  64. if(shm_id == -1)
  65. {
  66. perror("shmget error");
  67. return 0;
  68. }
  69. p_map = (people*)shmat(shm_id,NULL,0);
  70. for(i = 0;i<10;i++)
  71. {
  72. printf( "name:%s\n",(*(p_map+i)).name );
  73. printf( "age %d\n",(*(p_map+i)).age );
  74. }
  75. if(shmdt(p_map) == -1)
  76. perror(" detach error ");
  77. return 0;
  78. }

testwrite.c创建一个系统V共享内存区,并在其中写入格式化数据;testread.c访问同一个系统V共享内存区,读出其中的格式化数据。分别把两个程序编译为testwrite及testread,先后执行./testwrite及./testread 则./testread输出结果如下:

  1. name: b age 20; name: c age 21; name: d age 22; name: e age 23; name: f age 24;
  2. name: g age 25; name: h age 26; name: I age 27; name: j age 28; name: k age 29;

3.mmap函数共享内存

3.1mmap基础概念

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
image.png 由上图可以看出,进程的虚拟地址空间,由多个虚拟内存区域构成。虚拟内存区域是进程的虚拟地址空间中的一个同质区间,即具有同样特性的连续地址范围。上图中所示的text数据段(代码段)、初始数据段、BSS数据段、堆、栈和内存映射,都是一个独立的虚拟内存区域。而为内存映射服务的地址空间处在堆栈之间的空余部分。
linux内核使用vm_area_struct结构来表示一个独立的虚拟内存区域,由于每个不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个vm_area_struct结构来分别表示不同类型的虚拟内存区域。各个vm_area_struct结构使用链表或者树形结构链接,方便进程快速访问,如下图所示:
image.png
vm_area_struct结构中包含区域起始和终止地址以及其他相关信息,同时也包含一个vm_prot指针,其内部可引出所有针对这个区域可以使用的系统调用函数。这样,进程对某一虚拟内存区域的任何操作需要用要的信息,都可以从vm_area_struct中获得。mmap函数就是要创建一个新的vm_area_struct结构,并将其与文件的物理磁盘地址相连。

3.2 mmap内存映射原理

mmap内存映射的实现过程,总的来说可以分为三个阶段:
(一)进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
1、进程在用户空间调用库函数mmap,原型:void mmap(void start, size_t length, int prot, int flags, int fd, off_t offset);
2、在当前进程的虚拟地址空间中,寻找一段空闲的满足要求的连续的虚拟地址
3、为此虚拟区分配一个vm_area_struct结构,接着对这个结构的各个域进行了初始化
4、将新建的虚拟区结构(vm_area_struct)插入进程的虚拟地址区域链表或树中
二)调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系
5、为映射分配了新的虚拟地址区域后,通过待映射的文件指针,在文件描述符表中找到对应的文件描述符,通过文件描述符,链接到内核“已打开文件集”中该文件的文件结构体(struct file),每个文件结构体维护着和这个已打开文件相关各项信息。
6、通过该文件的文件结构体,链接到file_operations模块,调用内核函数mmap,其原型为:int mmap(struct file filp, struct vm_area_struct vma),不同于用户空间库函数。
7、内核mmap函数通过虚拟文件系统inode模块定位到文件磁盘物理地址。
8、通过remap_pfn_range函数建立页表,即实现了文件地址和虚拟地址区域的映射关系。此时,这片虚拟地址并没有任何数据关联到主存中。
(三)进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。
9、进程的读或写操作访问虚拟地址空间这一段映射地址,通过查询页表,发现这一段地址并不在物理页面上。因为目前只建立了地址映射,真正的硬盘数据还没有拷贝到内存中,因此引发缺页异常。
10、缺页异常进行一系列判断,确定无非法操作后,内核发起请求调页过程。
11、调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中。
12、之后进程即可对这片主存进行读或者写的操作,如果写操作改变了其内容,一定时间后系统会自动回写脏页面到对应磁盘地址,也即完成了写入到文件的过程。
注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

3.3 mmap和常规文件操作的区别

常规文件系统操作(调用read/fread等类函数)中,函数的调用过程:
1、进程发起读文件请求。
2、内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode。
3、inode在address_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。
4、如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。
总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。
总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

3.4 mmap相关函数

void mmap(void start, size_t length, int prot, int flags,int fd, off_t offset);
返回说明
成功执行时,mmap()返回被映射区的指针。失败时,mmap()返回MAP_FAILED[其值为(void *)-1]。
参数
start: 建立映射区的首地址,由Linux内核指定。使用时,直接传递NULL
length: 欲创建映射区的大小
prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE
flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区)
MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上(看简单测试)。
MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。
fd:用来建立映射区的文件描述符,由open提供
offset:映射文件的偏移(4k的整数倍)

int munmap( void * addr, size_t len )
成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;
当映射关系解除后,对原来映射地址的访问将导致段错误发生。

int msync( void *addr, size_t len, int flags )
一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

3.5 mmap共享内存的使用案例

1、简单测试
首先打开本地文件,获取文件描述符,用stat函数获取文件大小,然后将这些传入mmap中,建立映射,最后通过返回的指针,在该指针上写入一句话。
该用例需要注意几个点:
a)假如本地文件大小为0,则传入mmap的时候会包错,argument invalid。
b)无论flag选用MAP_SHARED还是MAP_PRIVATE,在本地文件中,都不会增加写入的内容,即映射区所做的修改不会反映到物理设备。

  1. #include <stdlib.h>
  2. #include <sys/mman.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. #include <sys/stat.h>
  6. #include <stdio.h>
  7. #include <fcntl.h>
  8. int main()
  9. {
  10. int fd = open("./mmap.txt", O_RDWR);
  11. char* p;
  12. struct stat filestat;
  13. stat("./mmap.txt", &filestat);
  14. int len = filestat.st_size;
  15. printf("len = %d", len);
  16. p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  17. if (p == MAP_FAILED)
  18. {
  19. perror("map error!\n");
  20. return -1;
  21. }
  22. memcpy(p + len, "this is mmap test\n", len);
  23. printf("%s", p);
  24. //msync(p, len * 2, MAP_SHARED);
  25. munmap(p, len);
  26. close(fd);
  27. return 0;
  28. }

2 创建新文件作为mmap的参数
首先创建文件,设置文件大小,作为参数传入mmap,然后通过返回的指针写入到文件,映射区所做的修改可以反映到物理设备上,但是该文件后面会有乱码。

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <unistd.h>
  4. #include <sys/mman.h>
  5. #include <string.h>
  6. #include <fcntl.h>
  7. int main()
  8. {
  9. char* p = NULL;
  10. int fd = open("mmap_test.txt", O_RDONLY|O_CREAT|O_TRUNC, 0644);
  11. if (fd < 0)
  12. {
  13. printf("open file error!\n");
  14. return -1;
  15. }
  16. /*
  17. 表头文件:#include <unistd.h>
  18. 函数原型:int ftruncate(int fd, off_t length)
  19. 函数说明:ftruncate()会将参数fd指定的文件大小改为参数length指定的大小。参数fd为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件件大小比参数length大,则超过的部分会被删去
  20. 返 回 值:0、-1
  21. 错误原因:errno
  22. EBADF 参数fd文件描述词为无效的或该文件已关闭
  23. EINVAL 参数fd为一socket并非文件,或是该文件并非以写入模式打开
  24. */
  25. int len = ftruncate(fd, 20);
  26. if (len < 0)
  27. {
  28. printf("ftruncate error!\n");
  29. return -1;
  30. }
  31. if (MAP_FAILED == (p = mmap(NULL, 20, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)))
  32. {
  33. printf("mmap error!\n");
  34. return -2;
  35. }
  36. memcpy(p, "abc", 4);
  37. printf("%s", p);
  38. printf("\n");
  39. close(fd);
  40. if (MAP_FAILED == munmap(p, 4))
  41. {
  42. printf("mumap failed\n");
  43. return -1;
  44. }
  45. return 0;
  46. }

针对该用例,有以下几个点需要注意:
a) 当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!! mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
b) 如果p++,mummap不能成功,munmap传入的地址必须是mmap返回的地址
c) 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。如果open时O_RDONLY, mmap时PROT参数指定PROT_READ|PROT_WRITE,则会报错,permission denied
d) 创建映射区的过程中,隐含着一次对映射文件的读操作。
e) 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
f) 如果文件有偏移量,必须为4K的整数倍。
g) mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。

3.4 mmap父子进程通信

父子等有血缘关系的进程之间也可以通过mmap建立的映射区来完成数据通信。但相应的要在创建映射区的时候指定对应的标志位参数flags:
MAP_PRIVATE: (私有映射) 父子进程各自独占映射区;
MAP_SHARED: (共享映射) 父子进程共享映射区;
测试结果如下:
参数为MAP_PRIVATE:
child var = 1000, p = 1000
father var = 100, p = 0
参数为MAP_SHARED:
child var = 1000, p = 1000
father var = 100, p = 1000
从结果可以看出,父子进程共享:1. 打开的文件 2. mmap建立的映射区(但必须要使用MAP_SHARED)。

  1. #include <stdlib.h>
  2. #include <stdio.h>
  3. #include <sys/mman.h>
  4. #include <fcntl.h>
  5. int var = 100;
  6. int main()
  7. {
  8. int* p;
  9. int fd = open("temp", O_CREAT | O_RDWR | O_TRUNC, 0644);
  10. if (fd < 0 )
  11. {
  12. perror("open failed!\n");
  13. return -1;
  14. }
  15. unlink("temp"); //删除临时文件目录项,使之具备被释放条件.
  16. int len = ftruncate(fd, 4);
  17. if (len < 0)
  18. {
  19. printf("ftruncate error!\n");
  20. return -1;
  21. }
  22. p = (int*)mmap(NULL, 4, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  23. if (p == MAP_FAILED)
  24. {
  25. perror("mmap failed!\n");
  26. return -1;
  27. }
  28. close(fd); //映射区创建完毕,即可关闭文件
  29. pid_t pid = fork();
  30. if (pid < 0 )
  31. {
  32. perror("fork error!\n");
  33. return -1;
  34. }
  35. else if (pid == 0) //child
  36. {
  37. var = 1000;
  38. *p = 1000;
  39. printf("child var = %d, p = %d\n", var, *p);
  40. }
  41. else
  42. {
  43. sleep(1);
  44. printf("father var = %d, p = %d\n", var, *p);
  45. wait(NULL);
  46. int ret = munmap(p, 20);
  47. if (ret == MAP_FAILED)
  48. {
  49. perror("munmap falied\n");
  50. return -1;
  51. }
  52. }
  53. return 0;
  54. }

3.5 匿名映射

通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。其实Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。同样需要借助标志位参数flags来指定。
使用MAP_ANONYMOUS (或MAP_ANON), 如:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
“4”随意举例,该位置表大小,可依实际需要填写。
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在类Unix系统中如无该宏定义,可使用如下两步来完成匿名映射区的建立。
① fd = open(“/dev/zero”, O_RDWR); //也可以用/dev/null
② p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
经测试,在MacOS X 13.6的系统中,也有相应的宏,但采用上面两步骤中,会报错误,mmap error: Operation not supported by device
示例:

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <fcntl.h>
  5. #include <sys/mman.h>
  6. int main(void)
  7. {
  8. int *p;
  9. pid_t pid;
  10. p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0); //MAP_ANONYMOUS
  11. if(p == MAP_FAILED)
  12. {
  13. perror("mmap error");
  14. exit(1);
  15. }
  16. pid = fork();//创建子进程
  17. if(pid == 0)
  18. {
  19. *p = 2000;
  20. printf("child, *p = %d\n", *p);
  21. }
  22. else
  23. {
  24. sleep(1);
  25. printf("parent, *p = %d\n", *p);
  26. }
  27. munmap(p, 4);//释放映射区
  28. return 0;
  29. }

3.6 mmap无血缘关系进程间通信

实质上mmap是内核借助文件帮我们创建了一个映射区,多个进程之间利用该映射区完成数据传递。由于内核空间多进程共享,因此无血缘关系的进程间也可以使用mmap来完成通信。只要设置相应的标志位参数flags即可。若想实现共享,当然应该使用MAP_SHARED了。

  1. /*************************** mmap_w.c *******************/
  2. #include <stdio.h>
  3. #include <sys/stat.h>
  4. #include <sys/types.h>
  5. #include <fcntl.h>
  6. #include <unistd.h>
  7. #include <stdlib.h>
  8. #include <sys/mman.h>
  9. #include <string.h>
  10. struct STU {
  11. int id;
  12. char name[20];
  13. char sex;
  14. };
  15. void sys_err(char *str)
  16. {
  17. perror(str);
  18. exit(1);
  19. }
  20. int main(int argc, char *argv[])
  21. {
  22. int fd;
  23. struct STU student = {10, "xiaoming", 'm'};
  24. char *mm;
  25. if (argc < 2) {
  26. printf("./a.out file_shared\n");
  27. exit(-1);
  28. }
  29. fd = open(argv[1], O_RDWR | O_CREAT, 0664);
  30. ftruncate(fd, sizeof(student));
  31. mm = mmap(NULL, sizeof(student), PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
  32. if (mm == MAP_FAILED)
  33. sys_err("mmap");
  34. close(fd);
  35. while (1) {
  36. memcpy(mm, &student, sizeof(student));
  37. student.id++;
  38. sleep(1);
  39. }
  40. munmap(mm, sizeof(student));
  41. return 0;
  42. }
  43. /*************************** mmap_r.c *******************/
  44. #include <stdio.h>
  45. #include <sys/stat.h>
  46. #include <fcntl.h>
  47. #include <unistd.h>
  48. #include <stdlib.h>
  49. #include <sys/mman.h>
  50. #include <string.h>
  51. struct STU {
  52. int id;
  53. char name[20];
  54. char sex;
  55. };
  56. void sys_err(char *str)
  57. {
  58. perror(str);
  59. exit(-1);
  60. }
  61. int main(int argc, char *argv[])
  62. {
  63. int fd;
  64. struct STU student;
  65. struct STU *mm;
  66. if (argc < 2) {
  67. printf("./a.out file_shared\n");
  68. exit(-1);
  69. }
  70. fd = open(argv[1], O_RDONLY);
  71. if (fd == -1)
  72. sys_err("open error");
  73. mm = mmap(NULL, sizeof(student), PROT_READ, MAP_SHARED, fd, 0);
  74. if (mm == MAP_FAILED)
  75. sys_err("mmap error");
  76. close(fd);
  77. while (1) {
  78. printf("id=%d\tname=%s\t%c\n", mm->id, mm->name, mm->sex);
  79. sleep(2);
  80. }
  81. munmap(mm, sizeof(student));
  82. return 0;
  83. }

4. system V共享内存与mmap共享内存对比

通过对试验结果分析,对比系统V与mmap()映射普通文件实现共享内存通信,可以得出如下结论:
1、 系统V共享内存中的数据,从来不写入到实际磁盘文件中去;而通过mmap()映射普通文件实现的共享内存通信可以指定何时将数据写入磁盘文件中。 注:前面讲到,系统V共享内存机制实际是通过映射特殊文件系统shm中的文件实现的,文件系统shm的安装点在交换分区上,系统重新引导后,所有的内容都丢失。
2、 系统V共享内存是随内核持续的,即使所有访问共享内存的进程都已经正常终止,共享内存区仍然存在(除非显式删除共享内存),在内核重新引导之前,对该共享内存区域的任何改写操作都将一直保留。
3、 通过调用mmap()映射普通文件进行进程间通信时,一定要注意考虑进程何时终止对通信的影响。而通过系统V共享内存实现通信的进程则不然。 注:这里没有给出shmctl的使用范例,原理与消息队列大同小异。