目录
system-V IPC 简介
消息队列、共享内存和信号量被统称为 system-V IPC,V 是罗马数字 5,是 Unix 的 AT&T 分支的其中一个版本,一般习惯称呼他们为 IPC 对象,这些对象的操作接口都比较 类似,在系统中他们都使用一种叫做 key 的键值来唯一标识,而且他们都是“持续性”资 源——即他们被创建之后,不会因为进程的退出而消失,而会持续地存在,除非调用特殊的函数或者命令删除他们。
跟文件类型,进程每次“打开”一个 IPC 对象,就会获得一个表征这个对象的 ID,进 而再使用这个 ID 来操作这个对象。IPC 对象的 key 是唯一的,但是 ID 是可变的。key 类 似于文件的路径名,ID 类似于文件的描述符
api ftok
注意
- 如果两个参数相同,那么产生的 key 值也相同
- 第一个参数一般取进程所在的目录,因为在一个项目中需要通信的几个进程通常会 出现在同一个目录当中。
- 如果同一个目录中的进程需要超过 1 个 IPC 对象,可以通过第二个参数来标识。
- 系统中只有一套 key 标识,也就是说,不同类型的 IPC 对象也不能重复
使用命令来查看或删除当前系统中的 IPC 对象:
- 查看消息队列:ipcs -q
- 查看共享内存:ipcs -m
- 查看信号量:ipcs -s
- 查看所有的 IPC 对象:ipcs -a
- 删除指定的消息队列:ipcrm -q MSG_ID 或者 ipcrm -Q msg_key
- 删除指定的共享内存:ipcrm -m SHM_ID 或者 ipcrm -M shm_key
- 删除指定的信号量:ipcrm -s SEM_ID 或者 ipcrm -S sem_key
消息队列(MSG)
消息队列提供一种带有数据标识的特殊管道,使得每一段被写入的数据都变成带标识的 消息,读取该段消息的进程只要指定这个标识就可以正确地读取,而不会受到其他消息的干 扰,从运行效果来看,一个带标识的消息队列,就像多条并存的管道一样
使用方法
- 发送者
- 获取消息队列的 ID
- 将数据放入一个附带有标识的特殊的结构体,发送给消息队列。
- 接收者
- 获取消息队列的 ID
- 将指定标识的消息读出。
当发送者和接收者都不再使用消息队列时,及时删除它以释放系统资源
api msgget
注意
- 选项 msgflg 是一个位屏蔽字,因此 IPC_CREAT、IPC_EXCL 和权限 mode 可以 用位或的方式叠加起来,比如:msgget(key, IPC_CREAT | 0666); 表示如果 key 对应 的消息队列不存在就创建,且权限指定为 0666,若已存在则直接获取 ID
- 权限只有读和写,执行权限是无效的,例如 0777 跟 0666 是等价的
- 当 key 被指定为 IPC_PRIVATE 时,系统会自动产生一个未用的 key 来对应一个 新的消息队列对象。一般用于线程间通信
api msgsnd msgrcv
注意
- 发送消息时,消息必须被组织成以下形式
struct msgbuf { long mtype; // 消息的标识 char mtext[1]; // 消息的正文 };
也就是说:发送出去的消息必须以一个 long 型数据打头,作为该消息的标识,后 面的数据则没有要求
- 消息的标识可以是任意长整型数值,但不能是 0L
- 参数 msgsz 是消息中正文的大小,不包含消息的标识
api msgctl
注意
- IPC_STAT 获得的属性信息被存放在以下结构体中:
struct msqid_ds { struct ipc_perm msg_perm; /* 权限相关信息 */ time_t msg_stime; /* 最后一次发送消息的时间 */ time_t msg_rtime; /* 最后一次接收消息的时间 */ time_t msg_ctime; /* 最后一次状态变更的时间 */ unsigned long __msg_cbytes; /* 当前消息队列中的数据尺寸 */ msgqnum_t msg_qnum; /* 当前消息队列中的消息个数 */ msglen_t msg_qbytes; /* 消息队列的最大数据尺寸 */ pid_t msg_lspid; /* 最后一个发送消息的进程 PID */ pid_t msg_lrpid; /* 最后一个接收消息的进程 PID */ };
权限相关的信息结构体
struct ipc_perm { key_t __key; /* 当前消息队列的键值 key */ uid_t uid; /* 当前消息队列所有者的有效 UID */ gid_t gid; /* 当前消息队列所有者的有效 GID */ uid_t cuid; /* 当前消息队列创建者的有效 UID */ gid_t cgid; /* 当前消息队列创建者的有效 GID */ unsigned short mode; /* 消息队列的读写权限 */ unsigned short __seq; /* 序列号 */ };
- 当使用 IPC_INFO 时,需要定义一个如下结构体来获取系统关于消息队列的限制值 信息,并且将这个结构体指针强制类型转化为第三个参数的类型
struct msginfo { int msgpool; /* 系统消息总尺寸(千字节为单位)最大值 */ int msgmap; /* 系统消息个数最大值 */ int msgmax; /* 系统单个消息尺寸最大值 */ int msgmnb; /* 写入消息队列字节数最大值 */ int msgmni; /* 系统消息队列个数最大值 */ int msgssz; /* 消息段尺寸 */ int msgtql; /* 系统中所有消息队列中的消息总数最大值 */ unsigned short int msgseg; /* 分配给消息队列的数据段的最大值 */ };
- 当使用选项 MSG_INFO 时,跟 IPC_INFO 一样也是获得一个 msginfo 结构体的 信息,但是有如下几点不同
- 成员 msgpool 记录的是系统当前存在的 MSG 的个数总和
- 成员 msgmap 记录的是系统当前所有 MSG 中的消息个数总和
- 成员 msgtql 记录的是系统当前所有 MSG中的所有消息的所有字节数总和
例子
进程 Jack 如何使用消息队列给另一个进程 Rose 发送消息的过 程,以及如何使用 msgctl( )函数,删除不再使用的消息队列
head4msg.h
1 #ifndef _HEAD4MSG_H_
2 #define _HEAD4MSG_H_
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <unistd.h>
7 #include <string.h>
8 #include <strings.h>
9 #include <errno.h>
10
11 #include <sys/types.h>
12 #include <sys/ipc.h>
13 #include <sys/msg.h>
14
15 #define MSGSIZE 64 // 单个消息最大字节数
16
17 #define PROJ_PATH "." // 使用当前路径来产生消息队列的键值 key
18 #define PROJ_ID 1
19
20 #define J2R 1L // Jack 发送给 Rose 的消息标识
21 #define R2J 2L // Rose 发送给 Jack 的消息标识
22
23 struct msgbuf // 带标识的消息结构体
24 {
25 long mtype;
26 char mtext[MSGSIZE];
27 };
28
29 #endif
Rose.c
1 #include <signal.h>
2 #include "head4msg.h" 3
4 int main(int argc, char **argv)
5 {
6 key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取消息队列的key
7 int msgid = msgget(key, IPC_CREAT | 0666); // 获取消息队列 ID
8
9 struct msgbuf buf;// 发送消息的结构体
10 bzero(&buf, sizeof(buf));// 清0
11
12 if(msgrcv(msgid, &buf, MSGSIZE, J2R, 0) == -1) // 等待接收消息标识为J2R的消息
13 {
14 perror("msgrcv() error");
15 exit(1);
16 }
17 printf("from msg: %s", buf.mtext);
18
19 msgctl(msgid, IPC_RMID, NULL); // 删除消息队列
20 return 0;
21 }
Jack.c
1 #include "head4msg.h" 2
3 int main(int argc, char **argv)
4 {
5 key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取消息队列的key
6 int msgid = msgget(key, IPC_CREAT | 0666); // 获取消息队列 ID
7
8 struct msgbuf message;// 发送消息的结构体
9 bzero(&message, sizeof(message));
10
11 message.mtype = J2R; // 指定该消息标识
12 strncpy(message.mtext, "I love you! Rose.\n", MSGSIZE);
13 // 发送该消息
14 if(msgsnd(msgid, &message, strlen(message.mtext), 0) != 0)
15 {
16 perror("msgsnd() error");
17 exit(1);
18 }
19
20 return 0;
21 }
他跟管道一样,都是需要“代理人”的进程通信机 制:内核充当了这个代理人,内核为使用者分配内存,检查边界,设置阻塞,以及各种权限 监控,使得我们用起来非常省心省力,但是任何事情都是有代价的:代理人机制使得他们的 效率都不高,因为两个进程的数据传递并不是直接了当的,而是要经过内核的辗转接力的, 因而他们都不适合用来传输海量数据
共享内存(SHM)
共享内存是效率最高的 IPC,因为他抛弃了内核这个“代理人”,直截了当地将一块裸 露的内存放在需要数据传输的进程面前,让他们自己搞,这样的代价是:这些进程必须小心 谨慎地操作这块裸露的共享内存,做好诸如同步、互斥等工作,毕竟现在没有人帮他们来管 理了,一切都要自己动手。也因为这个原因,共享内存一般不能单独使用,而要配合信号量、 互斥锁等协调机制,让各个进程在高效交换数据的同时,不会发生数据践踏、破坏等意外
共享内存的思想很朴素,进程与进程之间虚拟内存空间本来相互独立,不能互相访问的, 但是可以通过某些方式,使得相同的一块物理内存多次映射到不同的进程虚拟空间之中,这 样的效果就相当于多个进程的虚拟内存空间部分重叠在一起
使用方法
- 获取共享内存对象的ID
- 将共享内存映射至本进程虚拟内存空间的某个区域
- 当不再使用时,解除映射关系
- 当没有进程再需要这块共享内存时,删除它
api shmget
所谓的“大页面”指的是内核为了提高程序性能,对内存实行分页管理时,采用比默认 尺寸(4KB)更大的分页,以减少缺页中断。Linux 内核支持以 2MB 作为物理页面分页的 基本单位
api shmat shmdt
注意
- 共享内存只能以只读或者可读写方式映射,无法以只写方式映射
- shmat( )第二个参数 shmaddr 一般都设为 NULL,让系统自动找寻合适的地址。 但当其确实不为空时,那么要求 SHM_RND 在 shmflg 必须被设置,这样的话系统将会选 择比 shmaddr 小而又最大的页对齐地址(即为 SHMLBA 的整数倍)作为共享内存区域的 起始地址。如果没有设置 SHM_RND,那么 shmaddr 必须是严格的页对齐地址。 总之,映射时将 shmaddr 设置为 NULL 是更明智的做法,因为这样更简单,也更具移植性
- 解除映射之后,进程不能再允许访问 SHM
api shmctl
注意
- IPC_STAT 获得的属性信息被存放在以下结构体中
struct shmid_ds { struct ipc_perm shm_perm; /* 权限相关信息 */ size_t shm_segsz; /* 共享内存尺寸(字节) */ time_t shm_atime; /* 最后一次映射时间 */ time_t shm_dtime; /* 最后一个解除映射时间 */ time_t shm_ctime; /* 最后一次状态修改时间 */ pid_t shm_cpid; /* 创建者 PID */ pid_t shm_lpid; /* 最后一次映射或解除映射者 PID */ shmatt_t shm_nattch; /* 映射该 SHM 的进程个数 */ };
权限信息结构体
struct ipc_perm { key_t __key; /* 该 SHM 的键值 key */ uid_t uid; /* 所有者的有效 UID */ gid_t gid; /* 所有者的有效 GID */ uid_t cuid; /* 创建者的有效 UID */ gid_t cgid; /* 创建者的有效 GID */ unsigned short mode; /* 读写权限 + SHM_DEST + SHM_LOCKED 标记 */ unsigned short __seq; /* 序列号 */ };
- 当使用 IPC_RMID 后,上述结构体 struct ipc_perm 中的成员 mode 将可以检测 出 SHM_DEST,但 SHM 并不会被真正删除,要等到 shm_nattch 等于 0 时才会被真正 删除。IPC_RMID 只是为删除做准备,而不是立即删除
- 当使用 IPC_INFO 时,需要定义一个如下结构体来获取系统关于共享内存的限制值 信息,并且将这个结构体指针强制类型转化为第三个参数的类型
- 使用选项 SHM_INFO 时,必须保证宏_GNU_SOURCE 有效。获得的相关信息被 存放在如下结构体当中
struct shm_info { int used_ids; /* 当前存在的 SHM 个数 */ unsigned long shm_tot; /* 所有 SHM 占用的内存页总数 */ unsigned long shm_rss; /* 当前正在使用的 SHM 内存页个数 */ unsigned long shm_swp; /* 被置入交换分区的 SHM 个数 */ unsigned long swap_attempts; /* 已废弃 */ unsigned long swap_successes; /* 已废弃 */ };
- 选项 SHM_LOCK 不是锁定读写权限,而是锁定 SHM 能否与 swap 分区发 生交换。一个 SHM 被交换至 swap 分区后如果被设置了 SHM_LOCK,那么任何访问这个 SHM 的进程都将会遇到页错误。进程可以通过 IPC_STAT 后得到的 mode 来检测 SHM_LOCKED 信息
例子
进程 Jack 如何通过 SHM 给进程 Rose 发送一段数据的过程。 在 Rose 将数据打印出来之后,给 Jack 发送一个信号通知 Jack 将该 SHM 删除
head4shm.h
1 #ifndef _HEAD4SHM_H_
2 #define _HEAD4SHM_H_
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <unistd.h>
7 #include <string.h>
8 #include <signal.h>
9 #include <strings.h>
10
11 #include <sys/types.h>
12 #include <sys/ipc.h>
13 #include <sys/shm.h>
14
15 #define SHMSZ 1024
16
17 #define PROJ_PATH "."
18 #define PROJ_ID 100
19
20 #endif
Jack.c
1 #include "head4shm.h" 2 #include <signal.h>
3
4 int shmid;
5
6 void rmid(int sig)
7 {
8 shmctl(shmid, IPC_RMID, NULL); // 信号来了就把 SHM 删除掉
9 }
10
11 int main(int argc, char **argv)
12 {
13 signal(SIGINT, rmid);
14
15 key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取key
16 shmid = shmget(key, SHMSIZE, IPC_CREAT|0666);// 获取共享内存的ID
17
18 char *p = shmat(shmid, NULL, 0);// 对共享内存进行映射
19 bzero(p, SHMSIZE);// 清空共享内存
20
21 pid_t pid = getpid(); // Jack 将自身的 PID 放入 SHM 的前 4 字节里
22 memcpy(p, &pid, sizeof(pid_t));
23
24 fgets(p+sizeof(pid_t), SHMSZ, stdin); // 从键盘将数据填入 SHM
25 pause(); // 等到 Rose 的信号去删除 SHM
26
27 return 0;
28 }
Rose.c
1 #include "head4shm.h" 2
3 int main(int argc, char **argv)
4 {
5 key_t key = ftok(PROJ_PATH, PROJ_ID);// 获取key
6 int shmid = shmget(key, SHMSIZE, 0666);//获取共享内存的ID
7
8 char *p = shmat(shmid, NULL, 0);
9 printf("from SHM: %s", p+sizeof(pid_t)); // 打印 Jack 的信息
10
11 kill(*((pid_t *)p), SIGINT); // 数据已经读完,发信号给 Jack
12 shmdt(p);//解除映射
13
14 return 0;
15 }
必须先运行 Jack,而且必须输入数据,然后 Rose 才能运行,否则 Rose 不能获取 Jack 的信息