Linux进程间通信【消息队列、信号量】
✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器
文章目录
🌇前言
在 System V
通信标准中,还有一种通信方式:消息队列
,以及一种实现互斥的工具:信号量
;随着时代的发展,这些陈旧的标准都已经较少使用了,但作为 IPC
中的经典知识,我们可以对其做一个简单了解,扩展 IPC
的知识栈,尤其是 信号量
,可以通过它,为以后多线程学习中 POSIX
信号量的学习做铺垫
🏙️正文
1、消息队列
1.1、什么是消息队列?
消息队列(Message Queuing
)是一种比较特殊的通信方式,它不同于管道与共享内存那样借助一块空间进行数据读写,而是 在系统中创建了一个队列,这个队列的节点就是数据块,包含类型和信息
- 假设现在进程
A
、B
想要通过消息队列进行通信,首先创建一个消息队列 - 然后进程
A
将自己想要发送给进程B
的信息打包成数据块(其中包括发送方的信息),将数据块添加至消息队列队尾处 - 进程
B
同样也可以向消息队列中添加数据块,同时也会从消息队列中捕获其他进程的数据块,解析后进行读取,这样就完成了通信
遍历消息队列时,存数据块 还是 取数据块 取决于 数据块中的类型 type
注意: 消息队列跟共享内存一样,是由操作系统创建的,其生命周期不随进程,因此在使用结束后需要删除
因为消息队列比陈旧且较少使用了,所以这里就不详细讲解原理,关于消息队列更详细的介绍可以看看这两篇文章:
1.2、消息队列的数据结构
同属于 System V
标准,消息队列也有属于自己的数据结构
注:msg
表示 消息队列
struct msqid_ds
{
struct ipc_perm msg_perm; /* Ownership and permissions */
time_t msg_stime; /* Time of last msgsnd(2) */
time_t msg_rtime; /* Time of last msgrcv(2) */
time_t msg_ctime; /* Time of last change */
unsigned long __msg_cbytes; /* Current number of bytes in queue (nonstandard) */
msgqnum_t msg_qnum; /* Current number of messages in queue */
msglen_t msg_qbytes; /* Maximum number of bytes allowed in queue */
pid_t msg_lspid; /* PID of last msgsnd(2) */
pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
和 共享内存 一样,其中 struct ipc_perm
中存储了 消息队列的基本信息,具体包含内容如下:
struct ipc_perm
{
key_t __key; /* Key supplied to msgget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
可以通过 man msgctl
查看函数使用手册,其中就包含了 消息队列 的数据结构信息
1.3、消息队列的相关接口
论标准的重要性,消息队列的大小接口风格与共享内存一致,都是出自 System V
标准
1.3.1、创建
使用 msgget
函数创建 消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
关于 msgget
函数
组成部分 | 含义 |
---|---|
返回值 int | 创建成功返回消息队列的 msqid ,失败返回 -1 |
参数1 key_t key | 创建共享内存时的唯一 key 值,通过函数计算获取 |
参数2 int msgflg | 位图,可以设置消息队列的创建方式及创建权限 |
与 共享内存 的 shmget
可以说是十分相似了,关于 ftok
函数计算 key
值,这里就不再阐述,可以在这篇文章中学习 《Linux进程间通信【共享内存】》
简单使用函数 msgget
创建 消息队列,并使用 ipcs -q
指令查看资源情况
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
using namespace std;
int main()
{
//创建消息队列
int n = msgget(ftok("./", 668), IPC_CREAT | IPC_EXCL | 0666);
if(n == -1)
{
cerr << "msgget fail!" << endl;
exit(1);
}
return 0;
}
程序运行后,创建出了一个 msqid
为 0
的消息队列
因为此时并 没有使用消息队列进行通信,所以已使用字节 used-bytes
和 消息数 messages
都是 0
注意:
- 消息队列在创建时,也需要指定创建方式:
IPC_CREAT
、IPC_EXCL
、权限
等信息 - 消息队列创建后,
msqid
也是随机生成的,大概率每次都不一样 - 消息队列生命周期也是随操作系统的,并不会因进程的结束而释放
1.3.2、释放
消息队列也有两种释放方式:通过指令释放、通过函数释放
释放指令:ipcrm -q msqid
释放消息队列,其他 System V
通信资源也可以这样释放
ipcrm -m shmid
释放共享内存ipcrm -s semid
释放信号量集
释放函数:msgctl(msqid, IPC_RMID, NULL)
释放指定的消息队列,跟 shmctl
删除共享内存一样
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
关于 msgctl
函数
组成部分 | 含义 |
---|---|
返回值 int | 成功返回 0 ,失败返回 -1 |
参数1 int msqid | 待控制的消息队列 id |
参数2 int cmd | 控制消息队列的具体动作,同样是位图 |
参数3 struct msqid_ds *buf | 用于获取或设置所控制消息队列的数据结构 |
简单回顾下参数2部分可传递参数:
IPC_RMID
表示删除共享内存IPC_STAT
用于获取或设置所控制共享内存的数据结构IPC_SET
在进程有足够权限的前提下,将共享内存的当前关联值设置为buf
数据结构中的值
同样的,消息队列 = 消息队列的内核数据结构(struct msqid_ds
) + 真正开辟的空间
1.3.3、发送
利用消息队列发送信息,即 将信息打包成数据块,入队尾,所使用函数为 msgsnd
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
关于 msgsnd
函数
组成部分 | 含义 |
---|---|
返回值 int | 成功返回 0 ,失败返回 -1 |
参数1 int msqid | 待发送数据块的消息队列 id |
参数2 const void *msgp | 待发送的数据块 |
参数3 size_t msgsz | 待发送数据块大小 |
参数4 int msgflg | 表示发送数据块的方式,一般默认为 0 |
参数2 表示待发送的数据块,这显然是一个结构体类型,需要自己定义,结构如下:
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
mtype
就是传说中数据块类型,据发送方而设定;mtex
是一个比较特殊的东西:柔性数组,其中存储待发送的 信息,因为是 柔性数组,所以可以根据 信息 的大小灵活调整数组的大小
关于 柔性数组 的详细介绍可以看看这篇文章 《C语言进阶——动态内存管理》
1.3.4、接收
消息发送后,总得接收吧,既然发送是往队尾中添加数据块,那么接收就是 从队头中取数据块,假设所取数据块为自己发送的,那么就不进行操作,其他情况则取出数据块,使用 msgrcv
函数接收信息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
关于 msgrcv
函数
组成部分 | 含义 |
---|---|
返回值 int | 成功返回实际从 mtext 数组中读取的字节数,失败返回 -1 |
参数1 int msqid | 待接收数据块的消息队列 id |
参数2 void *msgp | 接收到的数据块,是一个输出型参数 |
参数3 size_t msgsz | 要接收数据块的大小 |
参数4 long msgtyp | 要接收数据块的类型 |
参数5 int msgflg | 表示接收数据块的方式,一般默认为 0 |
同样的,接收的数据结构如下所示,也包含了 类型 和 柔性数组
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};
1.4、消息队列小结
消息队列 的大部分接口都与 共享内存 近似,所以掌握 共享内存 后,即可快速上手 消息队列
但是如你所见,System V
版的 消息队列 使用起来比较麻烦,并且过于陈旧,现在已经较少使用了,所以我们不必对其进行深究,知道个大概就行了,如果实际中真遇到了,再查文档也不迟
2、信号量
2.1、什么是信号量?
信号量(semaphore
)一种特殊的工具,主要用于实现 同步和互斥
信号量 又称 信号灯,是各大高校《操作系统》课程中老师提及的高频知识点,往往伴随着 P、V
操作出现,但大多数老师都只是提及了基本概念,并未对 信号量 的本质及使用场景作出详细讲解
在正式学习 信号量 相关知识前,需要先简单了解下 互斥相关四个概念,为后续 多线程中信号量的学习作铺垫(重点)
2.2、互斥相关概念
1、并发
是指系统中同时存在多个独立的活动单元
- 比如在多线程中,多个执行流可以同时执行代码,可能访问同一份共享资源
2、互斥
是指同一时刻只允许一个活动单元使用共享资源
- 即在任何一个时刻,都只允许一个执行流进行共享资源的访问(可以通过加锁实现)
3、临界资源
与 临界区
,多执行流环境中的共享资源就是 临界资源
,涉及 临界资源
操作的代码区间即 临界区
- 在多线程环境中,全局变量就是
临界资源
,对全局变量的修改、访问代码属于临界区
4、原子性
:只允许存在 成功 和 失败 两种状态
- 比如对变量的修改,要么修改成功,要么修改失败,不会存在修改一半被切走的状态
所以 互斥 是为了解决 临界资源 在多执行流环境中的并发访问问题,需要借助 互斥锁 或 信号量 等工具实现 原子操作,实现 互斥
关于互斥锁(mutex
) 的相关知识在 多线程 中介绍,现在先来学习 信号量,搞清楚它是如何实现 互斥 的
2.3、信号量的感性理解
将整个程序看作现实世界,形色各异的人看作 执行流,电影院 等公共资源看作 临界区,而单场电影的电影票看作 临界资源,主角 信号量 就是电影院中单场电影余票的 计数器,即余票越多,计数器值越大,当有人买票时,计数器 -1
,当有人看完电影时,计数器 +1
当电影票卖完时,计数器归零,其他想看电影的人也无法购票观看本场电影
下面这些情况应运而生:
- 当你购票成功后,计数器
-1
,你必然可以去看这场电影,其他人也无法与你争夺,因为那个位置当电影放映之时就是属于你一个人的 - 如果你买票晚了,票已告罄,计数器为
0
,你就无法购票观看这场电影,即使自己偷偷溜进去也不行,会被保安叉出去,这是规定 - 得益于计数器的控制,电影院在放映电影时,有效划分了电影票这个
临界资源
的所属权限,从而保证了在电影放映时,绝对不会发生位置冲突、位置爆满、非法闯入等各种情况
信号量 的设计初衷也是如此,就是为了避免 因多执行流对临界资源的并发访问,而导致程序运行出现问题
因为电影院一次能容纳几十个人,所以可能不太好理解 互斥 这个概念,将场景特殊化,现在有一个 顶级VIP放映室,每天饮料零食随便吃,但 一次只允许一个人看电影,与普通电影院一样,这个 顶级VIP放映室 也有自己的售票系统,其本质同样是 计数器,但此时 计数器初始值为 1
所以:当一群人都想进这个顶级VIP放映室看电影时,必须等到 计数器 为 1
时,才能进行抢票,才有资格进去看电影,当然一次只能放一个人进去,同时计数器是否恢复 1
,取决于上一个看电影的人是否出了放映室 -> 看电影结束 -> 计数器 +1
规定:只允许一个人看电影
透过现象看本质,在 顶级VIP看电影 不就是代码中 多个执行流对同一个临界资源的互斥访问吗? 此时的 信号量 可以设为 1
,确保 只允许一个执行流进行访问,这种 信号量 被称为 二元信号量,常用来实现 互斥
综上所述,信号量本质上就是 计数器 count
,所谓的 P
操作(申请)就是在对 count--
,V
操作(归还)则是在对 count++
2.4、信号量的数据结构
下面来看看 信号量 的数据结构,通过 man semctl
进行查看
注:sem
表示 信号量
struct semid_ds
{
struct ipc_perm sem_perm; /* Ownership and permissions */
time_t sem_otime; /* Last semop time */
time_t sem_ctime; /* Last change time */
unsigned long sem_nsems; /* No. of semaphores in set */
};
System V
家族基本规矩,struct ipc_perm
中存储了 信号量的基本信息,具体包含内容如下:
struct ipc_perm
{
key_t __key; /* Key supplied to semget(2) */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions */
unsigned short __seq; /* Sequence number */
};
显然,无论是 共享内存、消息队列、信号量,它们的 ipc_perm 结构体中的内容都是一模一样的,结构上的统一可以带来管理上的便利,具体原因可以接着往下看
2.5、信号量的相关接口
2.5.1、创建
信号量的申请比较特殊,一次可以申请多个信息量,官方称此为 信号量集,所使用函数为 semget
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
关于 semget
函数
组成部分 | 含义 |
---|---|
返回值 int | 创建成功返回信号量集的 semid ,失败返回 -1 |
参数1 key_t key | 创建信号量集时的唯一 key 值,通过函数 ftok 计算获取 |
参数2 int nsems | 待创建的信号量个数,这也正是 集 的来源 |
参数3 int semflg | 位图,可以设置消息队列的创建方式及创建权限 |
除了参数2,其他基本与另外俩兄弟一模一样,实际传递时,一般传 1
,表示只创建一个 信号量
使用函数创建 信号量集,并通过指令 ipcs -s
查看创建的 信号量集 信息
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
using namespace std;
int main()
{
//创建一个信号量
int n = semget(ftok("./", 668), 1, IPC_CREAT | IPC_EXCL | 0666);
if(n == -1)
{
cerr << "semget fail!" << endl;
exit(1);
}
return 0;
}
程序运行后,创建了一个 信号量集,nsems
为 1
,表示在当前 信号量集 中只有一个 信号量
注意:
- 信号量集在创建时,也需要指定创建方式:
IPC_CREAT
、IPC_EXCL
、权限
等信息 - 信号量集创建后,
semid
也是随机生成的,大概率每次都不一样 - 信号量集生命周期也是随操作系统的,并不会因进程的结束而释放
2.5.2、释放
老生常谈的两种释放方式:指令释放、函数释放
指令释放:直接通过指令 ipcrm -s semid
释放信号量集
通过函数释放:semctl(semid, semnum, IPC_RMID)
,信号量中的控制函数有一点不一样
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...);
关于 semctl
函数
组成部分 | 含义 |
---|---|
返回值 int | 成功返回 0 ,失败返回 -1 |
参数1 int semid | 待控制的信号量集 id |
参数2 int semnum | 表示对信号量集中的第 semnum 个信号量作操作 |
参数3 int cmd | 控制信号量的具体动作,同样是位图 |
参数4 ... | 可变参数列表,不止可以获取信号量的数据结构,还可以获取其他信息 |
注意:
- 参数2 表示信号量集中的某个信号量编号,从
1
开始编号 - 参数3 中可传递的动作与共享内存、消息队列一致
- 参数4 就像
printf
和scanf
中最后一个参数一样,可以灵活使用
2.5.3、操作
信号量的操纵比较ex,也比较麻烦,所以仅作了解即可
使用 semop
函数对 信号量 进行诸如 +1
、-1
的基本操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, unsigned nsops);
关于 semop
函数
组成部分 | 含义 |
---|---|
返回值 int | 成功返回 0 ,失败返回 -1 |
参数1 int semid | 待操作的信号量集 id |
参数2 struct sembuf *sops | 一个比较特殊的参数,需要自己设计结构体 |
参数3 unsigned nsops | 可以简单理解为信号量编号 |
重点在于参数2,这是一个结构体,具体成员如下:
unsigned short sem_num; /* semaphore number */
short sem_op; /* semaphore operation */
short sem_flg; /* operation flags */
其中包含信号量编号、操作等信息,需要我们自己设计出一个结构体,然后传给 semop
函数使用
可以简单理解为:sem_op
就是要进行的操作,如果将 sem_op
设为 -1
,表示信号量 -1
(申请),同理 +1
表示信号量 +1
(归还)
sem_flg
是设置动作,一般设为默认即可
当然这些函数我们不必深入去研究,知道个大概就行了
2.6、信号量小结
信号量 是实现 互斥 的其中一种方法,具体表现为:资源申请,计数器 -1
,资源归还,计数器 +1
,只有在计数器不为 0
的情况下,才能进行资源申请,可以设计 二元信号量 实现 互斥
System V
中的 信号量 操作比较麻烦,但 信号量 的思想还是值得一学的,等后面学习 多线程 时,也会使用 POSIX
中的 信号量 实现 互斥,相比之下,POSIX
版的信号量操作要简单得多,同时应用也更为广泛
因为 信号量 需要被多个独立进程看到,所以 信号量 本身也是 临界资源,不过它是 原子 的,所以可以用于 互斥
- 多个独立进程看到同一份资源,这就是
IPC
的目标,所以 信号量 被划分至进程间通信中
3、深入理解 System V 通信方式
不难发现,共享内存、消息队列、信号量的数据结构基本一致,并且都有同一个成员 struct ipc_perm
,所以实际对于 操作系统 来说,对 System V
中各种方式的描述管理只需要这样做:
- 将 共享内存、消息队列、信号量对象描述后,统一存入数组中
- 再进行指定对象创建时,只需要根据
ipc_id_arr[n]->__key
进行比对,即可当前对象是否被创建! - 因为
struct shmid_ds
与struct ipc_perm shm_perm
的地址一致(其他对象也一样),所以可以对当前位置的指针进行强转:((struct shmid_ds)ipc_id_arr[0])
即可访问shmid_ds
中的成员,这不就是多态中的虚表吗?
这样一来,操作系统可以只根据一个地址,灵活访问 两个结构体中的内容,比如 struct ipc_perm shm_perm
和 struct shmid_ds
,并且操作系统还把多种不同的对象,描述融合入了一个 ipc_id_arr
指针数组中,真正做到了 高效管理
注:默认 ipc_id_arr[n]
访问的是 struct ipc_perm
中的成员
注:上述图示只是一个草图,目的是为了辅助理解原理,并非操作系统中真实样貌
操作系统在进行比较判断时,如何判断类型呢?
- 这就是操作系统设计的巧妙之处了,
ipc_id_arr
没那么简单,它会存储对象的相应类型信息
通过下标(id
) 访问对象,这与文件系统中的机制不谋而合,不过实现上略有差异,间接导致 System V
的管理系统被边缘化(历史选择了文件系统)
shmid
、msqid
和 semid
都是 ipc_id_arr
的下标,为什么值很大呢?
- 在进行查找时,会将这些
id % 数组大小
进行转换,确保不会发生越界,事实上,这个值与开机时间有关,开机越长,值越大,当然到了一定程度后,会重新轮回
将内核中的所有
ipc
资源统一以数组的方式进行管理
- 假设想访问具体
ipc
中的资源,可以通过ipc_id_arr[n]
强转为对应类型指针,再通过->
访问其中的其他资源以上方法就是 多态,通过父类指针,访问成员
🌆总结
以上就是本次关于 Linux
进程间通信【消息队列、信号量】的全部内容了,消息队列和信号量相对来说不怎么重要,因此本文主要以理论为主,并未涉及很多实操代码;本文中最重要的内容莫过于理解 互斥 相关概念与 信号量 实现互斥的原理,最后关于操作系统对 System V
通信相关资源的封装也算得上是精彩绝伦
相关文章推荐
Linux进程间通信【共享内存】
Linux进程间通信【命名管道】
Linux进程间通信【匿名管道】
Linux基础IO【软硬链接与动静态库】
Linux基础IO【深入理解文件系统】
Linux【模拟实现C语言文件流】
Linux基础IO【重定向及缓冲区理解】
更多推荐
所有评论(0)