1. system-v IPC简介
消息队列、共享内存和信号量统称为system-V IPC,V是罗马数字5,是UNIX的AT&T分支中的一个版本,一般习惯称之为IPC对象。这些对象的操作接口比较相似,在系统中它们都使用一种名为key的值也唯一标识,而且它们是“可持续”资源–它们被创建以后,不会因为进程的退出而消息,而会持续存在,除非调用特殊的函数或者命令来删除。
进程每次“打开”一个IPC对象,就会获得一个表征这个对象的ID,进而再使用这个ID来操作这个对象。IPC对象的key是唯一的,但是ID是可变的。key类似文件的路径名,ID类似文件的描述符
2. 函数ftok()函数介绍
IPC对象的键值key是怎么产生的呢?理论上它就是一个整数,一般用函数ftok()函数来产生,函数ftok()的接口规范如下:
函数项描述 | 函数项说明 | |
---|---|---|
函数功能 | 获取一个当前未用的IPC的key | |
头文件 | #include <sys/types.h> | #include <sys/ipc.h> |
原型 | key_t ftok(const char *pathname, int proj_id); | |
参数1 | pathname | 一个合法的路径,比如/目录或者./当前目录 |
参数2 | proj_id | 工程ID,非0,仅仅使用低8位,通常传入一个unsigned char |
返回值 | 成功:返回合法未用的键值 | 失败:返回-1 |
这个函数需要注意以下几点:
- 如果两个参数相同,那么产生的key值也相同
- 第一个参数一般取进程所在的目录,因为一个项目中需要通信的几个进程通常会出现在一个工程目录中
- 如果同一个目录中的进程需要使用超过一个IPC对象,可以通过第2个参数来标识
- 系统中只有一套key标识,也就是说,不同类型的IPC对象也不能重复
可以使用如下命令来查看或者删除系统中的IPC对象
- 查看消息队列:ipcs -q
- 查看共享内存:ipcs -m
- 查看信号量:ipcs -s
- 查看所有IPC对象:ipcs -a
- 删除指定的消息队列:ipcs -q MSG_ID 或者 ipcrm -Q msg_key
- 删除执行的共享内存:ipcs -m SHM_ID 或者 ipcrm -M shm_key
- 删除指定的信号量:ipcs -s SEM_ID 或者 ipcrm -S sem_key
3. 共享内存SHM介绍
共享内存是效率最高的IPC,因为它摒弃了内核这个“代理人”,直截了当地将一块裸漏的内存暴漏在需要进行通信的进程面前,让进程自己来操作这块内存,这样做的代价是:这些进程必须小心谨慎地操作这块裸露的共享内存,做好诸如同步、互斥等工作,毕竟现在操作系统已经不参与该过程了,一切需要进程们自己动手。也是因为这个原因,共享内存一般不能单独使用,而要配合信号量、互斥锁等协调机制,让各个进程在高下率交换数据的同时,不会发生数据践踏、破坏等意外
共享内存的思想很朴素,进程与进程之间之间虚拟内存本来是相互独立的,不能互相访问,但是可以通过某种方式,使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这样的效果就相当于多个进程的虚拟内存空间部分重叠在一起,如下图所示:
如上图所示,但进程1向其虚拟内存空间区域1写如数据时,进程2就能同时在其虚拟内存空间的区域2看见这些数据,中间没有经过任何转发,效率极高
使用共享内存的一般步骤包括:
- 获取共享内存对象的ID
- 将共享内存映射至本进程的虚拟内存空间的某个区域
- 当不再使用时,解除映射关系
- 当没有任何进程使用这块共享内存时,删除它
4. 共享内存SHM相关接口函数
获取共享内存ID的函数原型:
int shmget(key_t key, size_t size, int shmflg);
共享内存的映射函数原型:
void *shmat(int shmid, const void *shmaddr, int shmflg);
共享内存的解除映射函数原型:
int shmdt(const void *shmaddr);
shmaddr参数:
- 映射函数shmat()的shmaddr参数: a. 如果为NULL,则系统会自动选择一个合适的 b.如果不为NULL,则系统会根据shmaddr来选择一个合适的内存区域
- 解除映射函数shmdt()的shmaddr参数:共享内存的首地址
shmflg参数:
- SHM_RDONLY:以只读方式映射共享内存
- SHM_REMAP:重新映射,此时shmaddr不能为NULL
- SHM_RND:自动选择比shmaddr小的最大页对齐地址
需要注意以下几点:
- 共享内存只能以只读或者可读可写方式映射,无法以只写方式映射
- shmat()第2个参数shmaddr一般设置为NULL,让系统自动寻找合适的地址。但shmaddr确实不为NULL时,那么要求SHM_RND在shmflag必须被设置,这样的话系统会选择比shmaddr小而又最大的页对齐地址(也就是SHMLBA的整数倍)当作共享区域的起始地址,如果没有设置SHM_RND,那么shmflag必须时严格的页对齐地址。总之,将shmaddr设置为NULL是最为明智的做法,因为这样简单,也具有移植性
- 解除映射之后,进程将不再被允许访问共享内存
获取或者设置共享内存相关属性的函数原型:
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid参数:共享内存ID
cmd参数:
- IPC_STAT:获取属性信息,放到buf中
- IPC_SET:设置属性信息为buf中指定的信息
- IPC_RMID:将共享内存标记为“即将移除状态”,此时还未移除
- IPC_LOCK:禁止系统将SHM交换至swap分区
5. 共享内存SHM代码示例
下面的示例代码展示了进程Jack如何通过共享内存SHM向进程Rose发送一段数据的过程。在Rose收到数据后,将数据打印出来,给Jack发送一个信号通知Jack将该SHM删除
Jack向共享内存中发送数据
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHMSZ 1024
#define PROJ_PATH "."
#define PROJ_ID 33
int shmid;
//信号来了,执行删除共享内存ID的动作
void rmid(int sig){
shmctl(shmid, IPC_RMID, NULL);
}
int main(int argc, char ** argv){
signal(SIGINT, rmid); //捕捉一个信号,捕捉后的动作为执行函数指针指向的函数
key_t key = ftok(PROJ_PATH, PROJ_ID); //获取IPC的Key
shmid = shmget(key, SHMSZ, IPC_CREAT | 0666);
char *p = (char *)shmat(shmid, NULL, 0); //将该共享内存映射到当前进程中,如果第二个参数为NULL,则系统指定返回地址
bzero(p, SHMSZ);
pid_t pid = getpid(); //Jack将自身的PID信息放入到SHM的前4个字节
printf("Current Pid = %d\n", pid);
memcpy(p, &pid, sizeof(pid_t));
fgets(p+sizeof(pid_t), (SHMSZ-sizeof(pid_t)), stdin); //从键盘输入数据到共享内存中
pause(); //等待Rose的信号去删除SHM
shmdt(p); //解除共享内存映射
return 0;
}
Rose从共享内存中读数据
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define SHMSZ 1024
#define PROJ_PATH "."
#define PROJ_ID 33
int main(int argc, char** argv){
key_t key = ftok(PROJ_PATH, PROJ_ID); //获取IPC对象的key
int shmid = shmget(key, SHMSZ, 0666); //获取共享内存的id
char *p = shmat(shmid, NULL, 0); //将共享内存映射到当前进程
printf("From Jack : %s\n", p+sizeof(pid_t)); //从共享内存中读取Jack发来的消息
pid_t jack_pid = *((pid_t *)(p)); //获取jack的进程PID
kill(jack_pid, SIGINT); //给Jack发送信号
shmdt(p); //解除映射
return 0;
}
上面是一个比较粗糙的示例,有一个要求是,必须先让写程序运行,而且必须要输入数据,然后读程序才能运行,否则读程序不能读取写程序的数据(如果先运行读程序,会发生段错误,如下图所示)
从代码中可以看到,读程序读完数据之后,需要使用信号通知写程序,因此写程序必须将自己的进程号PID写入共享内存的头4个字节,这样的方式,让人觉得非常笨拙,事实上对SHM的多进程或者多线程同步和互斥的工作,一般并不是采用信号量来协调的,有更好用的工具,比如信号量