共享内存可以说是最有用的进程间通信方式,也是最快的 IPC 形式。两个不同进程 A、B 共享内存的意思是,同一块物理内存被映射到进程 A、B 各自的进程地址空间。进程 A 可以即时看到进程 B 对共享内存中数据的更新,反之亦然。由于多个进程共享同一块内存区域,必然需要某种同步机制,互斥锁和信号量都可以。
采用共享内存通信的一个显而易见的好处是效率高,因为进程可以直接读写内存,而不需要任何数据的拷贝。对于像管道和消息队列等通信方式,则需要在内核和用户空间进行四次的数据拷贝,而共享内存则只拷贝两次数据[1]:一次从输入文件到共享内存区,另一次从共享内存区到输出文件。
实际上,进程之间在共享内存时,并不总是读写少量数据后就解除映射,有新的通信时,再重新建立共享内存区域。而是保持共享区域,直到通信完毕为止,这样,数据内容一直保存在共享内存中,并没有写回文件。共享内存中的内容往往是在解除映射时才写回文件的。因此,采用共享内存的通信方式效率是非常高的。
mmap()系统调用
1:两个进程通过映射普通文件实现共享内存通信
#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){
int fd,i;
people *p_map;
char temp;
fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);
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\n");
sleep(10);
munmap(p_map,sizeof(people)*10);
printf("umap ok\n");
return 0;
}
#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){
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("%d,name:%s,age:%d\n",i+1,(*(p_map+i)).name,(*(p_map+i)).age);
}
munmap(p_map,sizeof(people)*10);
return 0;
}
2:父子进程通过匿名映射实现共享内存
#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 i;
people *p_map;
char temp;
pid_t pid;
p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANONYMOUS,-1,0);
if((pid=fork())<0){
printf("fork error\n");
}else if(pid>0){
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("P read:%d\n",(*p_map).age);
printf("umap\n");
munmap(p_map,sizeof(people)*10);
printf("umap ok\n");
exit(0);
}else{
sleep(2);
for(i=0;i<5;i++){
printf("c read %d,name:%s,age:%d\n",i+1,(*(p_map+i)).name,(*(p_map+i)).age);
}
(*p_map).age=100;
munmap(p_map,sizeof(people)*10);
exit(0);
}
return 0;
}
System V 共享内存
放共享数据的物理内存页面。
系统调用 mmap()通过映射一个普通文件实现共享内存。
system V 则是通过映射特殊文件系统 shm 中的文件实现进程间的共享内存通信。也就是说,每个共享内存区域对应特殊文件系统shm 中的一个文件。
system_v_shm_w.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct {
char name[4];
int age;
}people;
int main(){
int shm_id,i;
key_t key;
char temp;
people* p_map;
char* name="/home/archermind/Desktop/ritter";
char name2[40];
strcpy(name2,"/dev/shm/ritter");
if((key=ftok("/dev/shm/myshm",1))==-1){
printf("ftok error\n");
exit(0);
}
if((shm_id=shmget(key,4096,IPC_CREAT))<0){
printf("shmget error\n");
exit(0);
}
p_map=(people*)shmat(shm_id,NULL,0);
temp='a';
for(i=0;i<5;i++){
temp+=1;
memcpy((*(p_map+i)).name,&temp,1);
(*(p_map+i)).age=20+i;
}
if(shmdt(p_map)<0){
printf("detach error\n");
exit(0);
}
return 0;
}
system_v_shm_r.c
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
typedef struct{
char name[4];
int age;
}people;
int main(){
int shm_id,i;
key_t key;
people* p_map;
char *name="/dev/shm/ritter";
if((key=ftok(name,0))<0){
printf("ftok error\n");
exit(0);
}
if((shm_id=shmget(key,4096,IPC_CREAT))<0){
printf("shmget error\n");
exit(0);
}
p_map=(people*)shmat(shm_id,NULL,0);
for(i=0;i<5;i++){
printf("%d,name:%s,age:%d\n",i+1,(*(p_map+i)).name,(*(p_map+i)).age);
}
if(shmdt(p_map)<0){
printf("detach error\n");
}
return 0;
}
参考郑彦兴《Linux进程通信》
消息队列和管道基本上都是4次拷贝,而共享内存(mmap, shmget)只有两次。
4次:1,由用户空间的buf中将数据拷贝到内核中。2,内核将数据拷贝到内存中。3,内存到内核。4,内核到用户空间的buf.
2次: 1,用户空间到内存。 2,内存到用户空间。
消息队列和管道都是内核对象,所执行的操作也都是系统调用,而这些数据最终是要存储在内存中执行的。因此不可避免的要经过4次数据的拷贝。但是共享内存不同,当执行mmap或者shmget时,会在内存中开辟空间,然后再将这块空间映射到用户进程的虚拟地址空间中,即返回值为一个指向一个内存地址的指针。当用户使用这个指针时,例如赋值操作,会引起一个从虚拟地址到物理地址的转化,会将数据直接写入对应的物理内存中,省去了拷贝到内核中的过程。当读取数据时,也是类似的过程,因此总共有两次数据拷贝。