18-IPC之共享内存
共享内存:共享内存的API与消息队列的API非常相似
与回顾管道、消息队列
1.管道
管道是OS在物理内存上开辟一段缓存空间,当进程通过read、write等API来共享读写这段空间时,就实现了进程间通信。
2.消息队列
消息队列是OS创建的链表,链表的所有节点都是保存在物理内存上的,所以消息队列这个链表其实也是OS在物理内存上所开辟的缓存,当进程调用msgsnd、msgrcv等API来 共享读写时,就实现了进程间通信。
3 .共享内存
共享内存也逃不开同样的套路
共享内存就是OS在物理内存中开辟一大段缓存空间,不过与管道、消息队列调用read、write、msgsnd、
msgrcv等API来读写所不同的是,使用共享内存通信时,进程是直接使用地址来共享读写的
当然不管使用那种方式,只要能够共享操作同一段缓存,就都可以实现进程间的通信
不过如果直接使用地址来读写缓存时,效率会更高,但是如果是调用API来读写的话,中间必须经过重重
的OS函数调用之后,直到调用到最后一个函数时,该函数才会通过地址去读写共享的缓存,中间的调用过程会降低效率。
对于小数据量的通信来说,使用管道和消息队列这种使用API读写的通信方式很合适,但是如果进程涉及到
超大量的数据通信时,必须使用“共享内存”这种直接使用地址操作的通信方式,如果使用API来读写的话,
效率会非常的低
(2)共享内存的原理
每个进程的虚拟内存只严格对应自己的那片物理内存空间,也就是说虚拟空间的虚拟地址,只和自己的那片物理内存空间的物理地址建立映射关系,
和其它进程的物理内存空间没有任何的交集
因此进程空间之间是完全独立的。
mmap1.png
共享内存的实现原理很简单,进程空间不是没有交集吗,让他们的空间有交集就可以了
以两个进程使用共享内存来通信为例,实现的方法就是:
(1)调用API,让OS在物理内存上开辟出一大段缓存空间。
(2)让各自进程空间与开辟出的缓存空间建立映射关系
就让虚拟地址和物理内存的实际物理地址建立一对一的对应关系,使用虚拟地址读写缓存时,虚拟地址最终
是要转为物理地址的,转换时就必须参考这个映射关系
总之建立映射关系后,每个进程都可以通过映射后的虚拟地址来共享操作实现通信了。、
mmap2.png
多个进程也能映射到同一片空间,然后数据共享
不过当多个进程映射并共享同一个空间时,在写数据的时候可能会出现相互干扰,
比如A进程的数据刚写了一半没写完,结果切换到B进程后,B进程又开始写,A的数据就被中间B的数据
给岔开了这时往往需要加保护措施(信号量或者文件锁),让每个进程在没有操作时不要被别人干扰,等操作完以后,别的进程才能写数据。
3共享内存的使用步骤
(1)进程调用shmget函数创建新的或获取已有共享内存shm是share memory的缩写
(2)进程调用shmat函数,将物理内存映射到自己的进程空间
说白了就是让虚拟地址和真实物理地址建议一一对应的映射关系
建立映射后,就可以直接使用虚拟地址来读写共享的内存空间了
(3)shmdt函数,取消映射
(4)调用shmctl函数释放开辟的那片物理内存空间
和消息队列的msgctl的功能是一样的,只不过这个是共享内存的
多个进程使用共享内存通信时,创建者只需要一个,同样的,一般都是谁先运行谁创建,其它后运行的
进程发现已经被创建好了,就直接获取共享使用,大家共享操作同一个内存,即可实现通信
4.共享内存的函数
shmget函数
(1)函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
1)功能:创建新的,或者获取已有的共享内
· 如果key值没有对应任何共享内存
创建一个新的共享内存,创建的过程其实就是os在物理内存上划出(开辟出)一段物理内存空间出来
· 如果key值有对应某一个共享内存
说明之前有进程调用msgget函数,使用该key去创建了某个共享内存,既然别人之前就创建好了,
那就直接获取key所对应的共享内存
2)返回值
(a)成功:返回共享内存的标识符,以后续操作
(b)失败:返回-1,并且errno被设置
int shmget(key_t key, size_t size, int shmflg);
3)参数
(a)key:用于生成共享内存的标识符
可以有三种设置:
· IPC_PRIVATE:指定这个后,每次调用shmget时都会创建一个新共享内存
· 自己指定一个长整型数
· 使用ftok函数,通过路径名和一个8位的整形数来生成key值,最常用的
(b)size:指定共享内存的大小,我们一般要求size是虚拟页大小的整数倍
一般来说虚拟页大小是4k(4096字节),如果你指定的大小不是虚拟页的整数倍,也会自动帮你补成整数倍
(c)semflg:与消息队列一样
指定原始权限和IPC_CREAT,比如0664|IPC_CREAT
只有在创建一个新的共享内存时才会用到,否者不会用到
(2)代码演示
写一个例子程序,使用共享内存实现将A进程数据发送给B进程。
我这里只实现单向的通信,至于双向通信,后面会实现
(a)使用ipcs命令即可查看创建的共享内存:
- a 或者 什么都不跟:消息队列、共享内存、信号量的信息都会显示出来
- m:只显示共享内存的信息
- q:只显示消息队列的信息
- s:只显示信号量的信息
(b)共享内存的删除
进程结束时,system v ipc不会自动删除,进程结束后,使用ipcs依然能够查看到
· 方法1:重启OS,很麻烦
· 方法2:进程结束时,调用相应的API来删除,后面再讲
· 方法3:使用ipcrm命令删除
- 删除共享内存
+ M:按照key值删除
ipcrm -M key
+ m:按照标识符删除
ipcrm -m semid
- 删除消息队列
+ Q:按照key值删除
+ q:按照标识符删除
- 删除信号量
+ S:按照key值删除
+ s:按照标识符删除
shmat
(1)函数原型
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
1)功能
将shmid所指向的共享内存空间映射到进程空间(虚拟内存空间),并返回影射后的起始
地址(虚拟地址)有了这个地址后,就可以通过这个地址对共享内存进行读写操作
2)参数
(a)shmid:共享内存标识符
(b)shmaddr:指定映射的起始地址
有两种设置方式
1. 自己指定映射的起始地址(虚拟地址)
我们一般不会这么做,因为我们自己都搞不清哪些虚拟地址被用了,哪些没被用。
2. NULL:表示由内核自己来选择映射的起始地址(虚拟地址)。
这是最常见的方式,也是最合理的方式,因为只有内核自己才知道哪些虚拟地址可用,哪些不可用
(c)shmflg:指定映射条件
· 0:以可读可写的方式映射共享内存,也就是说映射后,可以读、也可以写共享内存
· SHM_RDONLY:以只读方式映射共享内存,也就是说映射后,只能读共享内存,不能写
. 1:只写
3)返回值
(a)成功:则返回映射地址
(b)失败:返回(void *)-1,并且errno被设置
shmdt函数
(1)函数原型
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
1)功能:取消建立的映射。
2)返回值:调用成功返回0,失败返回-1,且errno被设置
3)参数
shmaddr:映射的起始地址(虚拟地址)
shmctl函数
(1)函数原型
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
(a)功能:根据cmd的要求,对共享内存进行相应控制。
比如:
· 获取共享内存的属性信息
· 修改共享内存的属性信息
· 删除共享内存
· 等等
删除共享内存是最常见的控制。
(b)参数
· 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 */ };
4)、返回值
调用成功0,失败则返回-1,并且errno被设置
(3)代码改进
1)读共享内存的代码存缺陷
while(1) { if(strlen((char *)shmaddr) != 0) { printf("%s\n", (char *)shmaddr); bzero(shmaddr, SHM_SIZE); } }
(a)缺陷1:strlen函数只能用于判断字符串
如果对方通过共享内存发送不是字符串,而是结构体、整形、浮点型数据,
strlen将无法正确判断
(b)缺陷2:没有数据时,cpu会一直循环的判断
这样会让cpu一直做好无意义的事情,非常浪费cpu资源。
2)改进
保证写完后再读数据,当共享内存没有数据时,读进程休眠,当写进程把数据写完后,将读进程唤醒
说白了就是多个进程在操作时,涉及到一个谁先谁后的问题,其实就是同步问题,所谓同步就是
保持一个谁先谁后的统一步调
这就好比我踩一个脚步你跟着踩一个脚步,统一踩脚步的步调,这就是同步,否者我踩我的,你踩
你的,各自的步调不一致,这就是异步。
实现同步的方法:
(a)方法1:使用信号
shm_upgrade.h
#include <sys/ipc.h> #include <sys/shm.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> #include <strings.h> #include <string.h> #include <signal.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #define SHM_FILE "./shmfile" #define SHM_SIZE 4096 #define FIFO_FILE "./fifo" int shmid= -1; void *shmaddr = NULL; char buf[300]={0}; 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 err"); } int get_peer_pid(void){ int ret =-1; int fifofd =-1; //创建有名管道文件 ret =mkfifo(FIFO_FILE,0664); if(ret == -1 && errno != EEXIST) print_err("mkfifo err"); printf("begin open fifo ....\n"); //以只读方式打开管道 fifofd = open(FIFO_FILE,O_RDONLY);//打开时阻塞 if(fifofd == -1) print_err("open fifo fail"); //读管道 获取"读共享内存进程" 的PID int peer_pid; printf("begin read fifo ....\n"); ret = read(fifofd,&peer_pid,sizeof(peer_pid)); if(ret ==-1) print_err("read fifo fail"); printf("get peer_pid %d\n",peer_pid); return peer_pid; } void signal_fun(signum){ if(SIGINT == signum){ shmdt(shmaddr); shmctl(shmid,IPC_RMID,NULL); remove(SHM_FILE); remove(FIFO_FILE); exit(-1); }else if(SIGUSR1 == signum){ printf("recv signum=%d\n",signum ); } } void snd_self_pid(void){ int ret =-1; int fifofd =-1; //创建有名管道文件 ret =mkfifo(FIFO_FILE,0664); if(ret == -1 && errno != EEXIST) print_err("mkfifo err"); //以只写方式打开管道文件 fifofd = open(FIFO_FILE,O_WRONLY); if(fifofd == -1) print_err("open fifo fail"); //获取当前进程的pid,使用有名管道发送给 写共享内存的进程 pid_t pid=getpid(); ret = write(fifofd,&pid,sizeof(pid)); if(ret ==-1) print_err("write fifo fail"); }
shm1_upgrade.c
#include "shm.h" int main(int argc, char const *argv[]) { signal(SIGINT,signal_fun); pid_t peer_pid; peer_pid =get_peer_pid(); create_or_get_shm(); //建立映射 shmaddr=shmat(shmid,NULL,0); if(shmaddr == (void *) -1) print_err("shmaddr error"); while(1){ scanf("%s",buf); memcpy(shmaddr,buf,sizeof(buf)); printf("send signal to %d\n",peer_pid ); kill(peer_pid,SIGUSR1); sleep(1); } return 0; }
shm2_upgrade.c
#include "shm.h" int main(int argc, char const *argv[]) { signal(SIGINT,signal_fun); //注册一个空捕获,用于唤醒pause函数 signal(SIGUSR1,signal_fun); //使用有名管道,将当前进程的pid 发送给写共享内存的进程 snd_self_pid(); //创建获取共享内存 create_or_get_shm(); //建立映射 shmaddr=shmat(shmid,NULL,0); if(shmaddr == (void *) -1) print_err("shmaddr error"); while(1){ pause(); printf("recv data:%s\n",(char *)shmaddr); bzero(shmaddr,SHM_SIZE); } return 0; }
(b)方法2:使用信号量实现