linux进程间通信——共享内存、信号量、消息队列
1、system v IPC对象简介
1.1 system v IPC对象有哪些?IPC对象用来干什么?
system v 的IPC对象有共享内存、信号量、消息队列。在linux下可以使用IPC对象进程进程间通信。IPC对象存在于内核中,作为桥梁供多进程操作进行数据通信。
注意:IPC对象的作用范围是整个系统内,因此创建IPC对象当不再使用他们时应该将他们删除,否则他们会驻留在内核里。为此,系统提供了一些命令用于维护IPC对象。
1.2与IPC对象相关的命令
(1)创建IPC对象命令
ipcmk [-M size][-S nsems][-O]
- -M size 创建共享内存IPC对象,size 指定大小
- -S nsms 创建信号量,nsms 指信号量的大小
- -O 创建消息队列
(2)查看IPC对象命令
ipcs [-argms]
常用参数:
- -a:查看全部IPC对象信息
- -q:查看消息队列IPC对象信息
- -m:查看共享内存IPC对象信息
- -s:查看信号量IPC对象信息
(3)删除IPC对象命令
ipcrm -[gms] ID
ipcrm -[OMS] key
- -q或-O:删除消息队列 IPC对象
- -m或-M:删除共享内存 IPC对象
- -s或-S:删除信号量IPC对象
1.3 IPC标识
三种IPC 对象在内核中都尤其对应的数据结构,内核用一个非负整数标识这样的数据结构,这个非负整数被称为IPC 标识。
注意:IPC标识类似与文件描述符,但是其与文件描述符不同的是,IPC标识的实现为一个系统全局的流水号,当达到一个最大值时归零。(即:不断递增,全局有效,文件描述符是进程内有效的。)。
1.4 IPC键
IPC标识相当于系统内部识别的符号,类似与于文件描述符,而IPC键的作用则相当于文件的pathname,即文件路径名。同类型的IPC对象,都有一个唯一的名字,称之为键(相当于文件的路径名)。通过键,多个进程能够识别同一个IPC对象。
键的数据类型被定义为:key_t,实质上是一个非负的长整数。
注意:不同类型的IPC 对象的key 可以相同。
1.5 键的获取方式
- 方式一:指定键为:IPC_PRIVATE。
- 方式二:通过ftok() 函数获得键。
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
函数参数:
pathname: 路径名
proj_id: 非0整数
注意:
- 在使用这个函数时,路径名可以指定为
.
即当前目录。非零整数可以指定为例如像 'a’这样的字符。这两个参数,准备进行相互通信的两个不同进程应该事先约定好。两方应该使用相同的key 取创建(另一个进程通过key得到)得到我们创建好的IPC对象。 - 通过方式一这种获得key,只能用于亲缘进程间通信。
1.6 为什么需要用一个函数来获取key呢,key的实质不就是一个数字吗,为什么不能共使用认为指定的方式指定一个可以?
因为共享内存是全局的,为了保证不同进程间的通信不混乱,我们不能够使用已经被用过的key,但是作为程序员来讲,我们不知道哪些数字被用了,哪些每被用,因此我们通过一个函数来获得key,以保证得到的key是没有正在被使用的。这两进程间的通信就不会混乱了。
1.7 IPC对象的创建函数
当获取key以后,可以将key传递到下列函数中,获得相应的IPC对象:
- shmget函数创建共享内存IPC对象
- semget信号量IPC对象
- msgget消息队列IPC对象
函数的具体使用,见后面内容。
1.8 进程间使用IPC对象通信的示意图
2、共享内存
2.1 共享内存的实现原理
每个进程有独立的内存空间,在这块内存空间中,为了在多个进程间交换信息,专门留出了一块内存区,可以由需要访问的进程将其映射到自己的私有地址空间。如下图
在堆栈中间的share memory 所示内存块就是我们专门留出来的内存空间,用来进行进程间的通信。共享内存实际上就是将虚拟空间的某些页映射到同一个物理页,这样就实现了共享内存。如下图:
2.2共享内存的特点
- 由于两个进程的一块虚拟地址空间映射到了同一块物理内存上,因此,操作数据时,直接操作的是物理内存中的数据。因此,大大提高了通信的效率。
- 但是,由于多个进程共享一段内存,因此也需要依靠某种同步机制,如互斥锁和信号量等 。(共享内存是临界资源)
2.3共享内存与其他进程间通信的方式的对比
像管道、FIFO、消息队列,这三种通信方式,需要将一个进程物理内存中的内容先拷贝到管道呀这些中间通信文件中,然后另一个进程通过管道另一端再读取,拷贝一份到自己的内存空间中。因此,效率非常低。而共享内存,实际上共享的是同一块物理内存,因此省去了中间的拷贝环节,因此效率非常的高。
2.4 共享内存的操作流程
- 创建/打开共享内存shmget()函数
- 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问
- 撤销共享内存映射(两进程不再通信了,就解除共享内存映射。
- 删除共享内存对象 (如果不删除,它将一直驻留在操作系统中。
2.5 与共享内存相关的API
(1)打开、创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 函数功能:
获取一块共享内存,即创建一个共享内存的IPC对象。 - 返回值:
成功 :返回共享内存的IPC标识符(shmid)。
失败 :返回-1。 - 参数说明:
第一个参数:Key: 前面介绍过,多个进程可以通过ftok函数获得相同的key从而获取相同的shmid。或者key取IPC_PRIVATE。
第二个参数:size:是要建立共享内存的长度。所有的内存条 分配操作都是以页为单位的。所以如果一个进程只申请一块只有一个字节的内存,内存也会分配整整一页(在i386机器中一页的缺省大小PACE_SIZE = 4096字节)。
第三个参数: shmflg:有效的标志包括IPC_CREAT 和IPC_EXCL,他们的功能与open()的O_CREAT和O_EXCL相当。
第三个参数的选项:
- 1)IPC_CREAT:如果共享内存不存在,则创建一个共享内存,否则直接打开已存在的
- 2)IPC_EXCL:只有在共享内存不存在的时候,新的共享内存才建立,否则就产生错误◦
- 3)还包含了共享内存的权限位,可以使用八进制表示
- 例如,第三个参数整体可以是:(O_CREAT|O_EXCL |0666)
总结:Xget的使用方法用来确定如何创建一个IPC对象。Xget函数都有两个类似的参数,key 和 flag。如果满足下列两个条件之一,则在内核中创建一个新的IPC结构。
- Key是IPC_PRIVATE,这种方式创建的一般用于具有亲缘关系的进程间通信。
- Key不是IPC_PRIVATE,比如由ftok+ flag=IPC_CREAT创建并且该Key未与一个已存在的IPC对象绑定。
(2)映射共享内存的函数
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 函数功能:
将标识号为shmid共享内存映射到调用进程的地址空间中。
- 返回值:
成功: 返回映射后的地址
失败: (void *)-1 - 参数说明:
第一个参数 shmid : 要映射的共享内存区标识符
第二个参数shmaddr :指定共享内存映射到的虚拟地址(若为NULL,则表示由系统自动完成映射) 一般shmaddr填写为NULL/0,让系统内核替我们选择地址。除非你写的程序特定于某种需要手工指定共享地址(VM)。
第三个参数shmflg : SHM_RDONLY 共享内存只读。默认0:共享内存可读写。
(3)取消映射
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
-
函数功能:
取消共享内存与用户进程之间的映射 -
返回值:
成功:返回0
失败:返回-1 -
参数说明:
参数shmaddr:shmat映射成功放回的地址。
注意:当一个进程不再需要共享内存段时,它将调用shmdt()系统调用取消这个段,但是,这并不是从内核真正地删除这个段,而是把相关shmid_ds结构的shm_nattch域的值减1,当这个值为0时,内核才能从物理上删除这个共享段。
(4)删除共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
-
函数功能:
控制共享内存 -
函数参数:
参数1: shmid 共享内存标识ID
参数2: cmd
选项1: IPC_STAT得到共享内存的状态(如果获取的话就需要第三个参数,将获取的状态存到结构体中)
选项2:IPC_SET改变共享内存的状态(如果获取的话就需要第三个参数,认为将结构写好,传递出去)
选项3: IPC_RMID删除共享内存参数3: buf 是一个结构体指针。IPC_STAT的时候,取得的状态放在这个结构体中。如果要改变共享内存的状态,用这个结构体指定;
注意:
- cmd为IPC_RMID命令实际上不从内核删除一个段,而是仅仅把这个段标记为删除,实际删除过程是发生在最后一个使用该共享内存的进程退出或者是解除映射之后。若是在此之前删除,则不管此共享内存是否还在被使用,键值(key)都会被清0。
- 当cmd为IPC_RMID时,第三个参数应为NULL。呵呵,大部分我们都是这样做,用这个函数删除共享内存。