IPC——共享内存
概述
管道是OS在物理内存上开辟一段缓存空间,当进程通过read、write等API来共享读写这段空间时,就实现了进程间通信。
消息队列是OS创建的链表,链表的所有节点都是保存在物理内存上的,所以消息队列这个链表其实也是OS在物理内存上所开辟的缓存,当进程调用msgsnd、msgrcv等API来共享读写时,就实现了进程间通信。
共享内存也逃不开同样的套路。共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的。
当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信。
信号、管道、消息队列、共享内存对比
信号:非精确通信
管道:无名管道只用于亲缘进程,命名管道克服了这一缺点。但是这两种管道方式还是不适合网状通信
消息队列:克服了无名管道只用于亲缘进程,无名/命名管道 网状通信若的缺点。但是不能实现大规模数据的通信。
共享内存:继承了消息队列的优点,还克服了其缺点。支持大规模数据通信。
为啥共享内存比消息队列效率高?
前面这4中IPC,其本质都是操作OS提供的一段虚拟内存(在引入虚拟内存机制的情况下),虚拟内存最终还是被OS映射到真实物理内存。信号、管道、消息队列 都要调用各种API,在到达内存之前,经过了多次函数调用,直到最后一个函数,该函数才会通过地址去读写共享的缓存。层层调用势必会严重降低效率。而共享内存就没有这么多麻烦,直接使用地址来读写内存,效率高,那是必须的!
共享内存原理
注:紫线表述有误,共享内存不会占据进程全部虚拟地址空间
每个进程的虚拟内存只严格对应自己的那片物理内存空间,也就是说虚拟空间的虚拟地址,只和自己的那片物理内存空间的物理地址建立映射关系,和其它进程的物理内存空间没有任何的交集,因此进程空间之间是完全独立的。
以两个进程使用共享内存来通信为例,实现的方法就是:
(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。
多个进程能不能映射到同一片空间,然后数据共享呢?
当然是可以的。不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的数据给岔开了。这时往往需要加保护措施,让每个进程在没有操作时不要被别人干扰,等操作完以后,别的进程才能写数据。
共享内存的使用步骤
①进程调用shmget函数创建新的或获取已有共享内存。shm是share memory的缩写。
②进程调用shmat函数,将物理内存映射到自己的进程空间。即让虚拟地址和真实物理地址建立一 一对应的映射关系。建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了。
③shmdt函数,取消映射
④调用shmctl函数释放开辟的那片物理内存空间和消息队列的msgctl的功能是一样的,只不过这个是共享内存的。
多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信。
API
shmget
函数原型
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size, int shmflg);
功能
创建新的,或者获取已有的共享内存
如果key值没有对应任何共享内存:创建一个新的共享内存,创建的过程其实就是OS在物理内存上划出(开辟出)一段物理内存空间出来。
如果key值有对应某一个共享内存:说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创建好了,那就直接获取key所对应的共享内存。
参数
key:用于生成共享内存的标识符,使用方法参考消息队列的key
size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍。一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动帮你补成整数倍。
semflg:与消息队列一样。指定原始权限和IPC_CREAT,比如 0664|IPC_CREAT。只有在创建一个新的共享内存时才会用到,否者不会用到。
返回值
成功:返回共享内存的标识符,以后续操作
失败:返回-1,并且errno被设置。
shmat
函数原型
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
功能
将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始地址(虚拟地址)。有了这个地址后,就可以通过这个地址对共享内存进行读写操作。
参数
shmid:共享内存标识符。
shmaddr:指定映射的起始地址,有两种设置方式
①自己指定映射的起始地址(虚拟地址)。我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。
②NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用。
shmflg:指定映射条件。
0:以可读可写的方式映射共享内存,也就是说映射后,可以读、也可以写共享内存。
SHM_RDONLY:以只读方式映射共享内存,也就是说映射后,只能读共享内存,不能写。
返回值
成功:则返回映射地址
失败:返回(void *)-1,并且errno被设置。
shmdt
函数原型
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
功能
取消建立的映射。
参数
shmaddr:映射的起始地址(虚拟地址)。
返回值
调用成功返回0,失败返回-1,且errno被设置。
shmctl
函数原型
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
功能
根据cmd的要求,对共享内存进行相应控制。
比如:
获取共享内存的属性信息
修改共享内存的属性信息
删除共享内存
等等
删除共享内存是最常见的控制。
参数
shmid:标识符。
cmd:控制选项
①IPC_STAT:从内核获取共享内存属性信息到第三个参数(应用缓存)。
②IPC_SET:修改共享内存的属性。修改方法与消息队列相同。
③IPC_RMID:删除共享内存,不过前提是只有当所有的映射取消后,才能删除共享内存。删除时,用不着第三个参数,所以设置为NULL
buf:buf的类型为struct shmid_ds。
①cmd为IPC_STAT时,buf用于存储原有的共享内存属性,以供查看。
②cmd为IPC_SET时,buf中放的是新的属性设置,用于修改共享内存的属性。
struct shmid_ds结构体
struct shmid_ds { struct ipc_perm shm_perm; /* Ownership and permissions:权限 */ size_t shm_segsz; /* Size of segment (bytes):共享内存大小 */ time_t shm_atime; /* Last attach time:最后一次映射的时间 */ time_t shm_dtime; /* Last detach time:最后一次取消映射的时间 */ time_t shm_ctime; /* Last change time:最后一次修改属性信息的时间 */ pid_t shm_cpid; /* PID of creator:创建进程的PID */ pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) :当前正在使用进程的PID*/ shmatt_t shm_nattch; /* No. of current attaches:映射数量,标记有多少个进程空间映射到了共享内存上,每增加一个映射就+1,每取消一个映射就-1 */ ... };
struct ipc_perm,这个结构体我们在讲消息队列时已经讲过
struct ipc_perm { key_t __key; /* Key supplied to shmget(2) */ uid_t uid; /* UID of owner */ gid_t gid; /* GID of owner */ uid_t cuid; /* UID of creator */ gid_t cgid; /* GID of creator */ unsigned short mode; /* Permissions + SHM_DEST andSHM_LOCKED flags */ unsigned short __seq; /* Sequence number */ };
返回值
调用成功0,失败则返回-1,并且errno被设置。
共享内存示例代码
单向通行
p1.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/types.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <strings.h> #include <string.h> #include <signal.h> #include <errno.h> #define SHM_FILE "./shmfile" #define SHM_SIZE 4096 int shmid = -1; char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ 222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\ ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"}; void print_err(char *estr) { perror(estr); exit(-1); } void create_or_get_shm(void) { int fd = 0; key_t key = -1; fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664); if(fd == -1) print_err("open fail"); key = ftok(SHM_FILE, 'b'); if(key == -1) print_err("ftok fail"); shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT); if(shmid == -1) print_err("shmget fail"); //read(fd, &shmid, sizeof(shmid)); } int main(void) { void *shmaddr = NULL; /* 创建、或者获取共享内存 */ create_or_get_shm(); //建立映射 shmaddr = shmat(shmid, NULL, 0); if(shmaddr == (void *)-1) print_err("shmat fail"); while(1) { memcpy(shmaddr, buf, sizeof(buf)); sleep(1); } return 0; }
p2.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/types.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <strings.h> #include <string.h> #include <signal.h> #include <errno.h> #define SHM_FILE "./shmfile" #define SHM_SIZE 4096 int shmid = -1; void print_err(char *estr) { perror(estr); exit(-1); } void create_or_get_shm(void) { int fd = 0; key_t key = -1; fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664); if(fd == -1) print_err("open fail"); key = ftok(SHM_FILE, 'b'); if(key == -1) print_err("ftok fail"); shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT); if(shmid == -1) print_err("shmget fail"); //read(fd, &shmid, sizeof(shmid)); } int main(void) { void *shmaddr = NULL; /* 创建、或者获取共享内存 */ create_or_get_shm(); //建立映射 shmaddr = shmat(shmid, NULL, 0); if(shmaddr == (void *)-1) print_err("shmat fail"); while(1) { if(strlen((char*)shmaddr)!=0) { printf("%s\n", (char *)shmaddr); bzero(shmaddr, SHM_SIZE); } } return 0; }
这种写法,在Ctrl+C硬件中断进程后,并不会删除共享内存。下面代码改进这一问题
p1.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/types.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <strings.h> #include <string.h> #include <signal.h> #include <errno.h> #define SHM_FILE "./shmfile" #define SHM_SIZE 4096 int shmid = -1; void *shmaddr = NULL; void print_err(char *estr) { perror(estr); exit(-1); } void create_or_get_shm(void) { int fd = 0; key_t key = -1; fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664); if(fd == -1) print_err("open fail"); key = ftok(SHM_FILE, 'b'); if(key == -1) print_err("ftok fail"); shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT); if(shmid == -1) print_err("shmget fail"); //write(fd, &shmid, sizeof(shmid)); } char buf[300] = {"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\ 222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222\ ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff2222222222"}; void signal_fun(int signo) { shmdt(shmaddr); shmctl(shmid, IPC_RMID, NULL); remove("./fifo"); remove(SHM_FILE); exit(-1); } int get_peer_PID(void) { int ret = -1; int fifofd = -1; /* 创建有名管道文件 */ ret = mkfifo("./fifo", 0664); if(ret == -1 && errno != EEXIST) print_err("mkfifo fail"); /* 以只读方式打开管道 */ fifofd = open("./fifo", O_RDONLY); if(fifofd == -1) print_err("open fifo fail"); /* 读管道,获取“读共享内存进程”的PID */ int peer_pid; ret = read(fifofd, &peer_pid, sizeof(peer_pid)); if(ret == -1) print_err("read fifo fail"); return peer_pid; } int main(void) { int peer_pid = -1; /* 给SIGINT信号注册捕获函数,用于删除共享内存、管道、文件等 */ signal(SIGINT, signal_fun); /* 使用有名管道获取读共享内存进程的PID */ peer_pid = get_peer_PID(); /* 创建、或者获取共享内存 */ create_or_get_shm(); //建立映射 shmaddr = shmat(shmid, NULL, 0); if(shmaddr == (void *)-1) print_err("shmat fail"); while(1) { memcpy(shmaddr, buf, sizeof(buf)); kill(peer_pid, SIGUSR1); sleep(1); } return 0; }
p2.c
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/ipc.h> #include <sys/types.h> #include <sys/shm.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <strings.h> #include <string.h> #include <signal.h> #include <errno.h> #define SHM_FILE "./shmfile" #define SHM_SIZE 4096 int shmid = -1; void *shmaddr = NULL; void print_err(char *estr) { perror(estr); exit(-1); } void create_or_get_shm(void) { int fd = 0; key_t key = -1; fd = open(SHM_FILE, O_RDWR|O_CREAT, 0664); if(fd == -1) print_err("open fail"); key = ftok(SHM_FILE, 'b'); if(key == -1) print_err("ftok fail"); shmid = shmget(key, SHM_SIZE, 0664|IPC_CREAT); if(shmid == -1) print_err("shmget fail"); //read(fd, &shmid, sizeof(shmid)); } void signal_fun(int signo) { if(SIGINT == signo) { shmdt(shmaddr); shmctl(shmid, IPC_RMID, NULL); remove("./fifo"); remove(SHM_FILE); exit(-1); } else if(SIGUSR1 == signo) { } } void snd_self_PID(void) { int ret = -1; int fifofd = -1; /* 创建有名管道文件 */ mkfifo("./fifo", 0664); if(ret == -1 && errno != EEXIST) print_err("mkfifo fail"); /* 以只写方式打开文件 */ fifofd = open("./fifo", O_WRONLY); if(fifofd == -1) print_err("open fifo fail"); /* 获取当前进程的PID, 使用有名管道发送给写共享内存的进程 */ int pid = getpid(); ret = write(fifofd, &pid, sizeof(pid));//发送PID if(ret == -1) print_err("write fifo fail"); } int main(void) { /*给SIGUSR1注册一个空捕获函数,用于唤醒pause()函数 */ signal(SIGUSR1, signal_fun); signal(SIGINT, signal_fun); /* 使用有名管道,讲当前进程的PID发送给写共享内存的进程 */ snd_self_PID(); /* 创建、或者获取共享内存 */ create_or_get_shm(); //建立映射 shmaddr = shmat(shmid, NULL, 0); if(shmaddr == (void *)-1) print_err("shmat fail"); while(1) { pause(); printf("%s\n", (char *)shmaddr); bzero(shmaddr, SHM_SIZE); } return 0; }
双向通信