
在多进程操作系统中,进程间通信(IPC, Inter-Process Communication)是实现不同进程之间数据交换和协作的关键技术。Linux作为开源的类Unix操作系统,提供了多种机制来实现进程间的高效通信。本文将聚焦于其中两种重要的通信方式:消息队列和信号量,详细探讨它们的工作原理、应用场景,并通过具体代码示例帮助理解。
消息队列(Message Queuing)是一种比较特殊的通信方式,它不同于管道与共享内存那样借助一块空间进行数据读写,而是 在系统中创建了一个队列,这个队列的节点就是数据块,包含类型和信息
进程 B 同样也可以向消息队列中添加数据块,同时也会从消息队列中捕获其他进程的数据块,解析后进行读取,这样就完成了通信

遍历消息队列时,存数据块 还是 取数据块 取决于 数据块中的类型 type
注意: 消息队列跟共享内存一样,是由操作系统创建的,其生命周期不随进程,因此在使用结束后需要删除
因为消息队列比陈旧且较少使用了,所以这里就不详细讲解原理,关于消息队列更详细的介绍可以看看这两篇文章:
同属于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查看函数使用手册,其中就包含了 消息队列 的数据结构信息
论标准的重要性,消息队列的大小接口风格与共享内存一致,都是出自 System V 标准
使用msgget函数创建 消息队列

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);关于 msgget 函数

与 共享内存 的 shmget 可以说是十分相似了,关于 ftok 函数计算key值,这里就不再阐述
简单使用函数 msgget 创建 消息队列,并使用ipcs -q指令查看资源情况

程序运行后,创建出了一个msqid为 0 的消息队列
因为此时并 没有使用消息队列进行通信,所以已使用字节 used-bytes 和 消息数 messages 都是 0
注意:
IPC_CREAT、IPC_EXCL、权限 等信息msqid也是随机生成的,大概率每次都不一样消息队列也有两种释放方式:通过指令释放、通过函数释放
释放指令: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函数

简单回顾下参数2部分可传递参数:
IPC_RMID 表示删除共享内存IPC_STAT 用于获取或设置所控制共享内存的数据结构IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值同样的,消息队列 = 消息队列的内核数据结构(struct msqid_ds) + 真正开辟的空间
利用消息队列发送信息,即 将信息打包成数据块,入队尾,所使用函数为 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函数

参数2 表示待发送的数据块,这显然是一个结构体类型,需要自己定义,结构如下:
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};mtype 就是传说中数据块类型,据发送方而设定;-mtex 是一个比较特殊的东西:柔性数组,其中存储待发送的 信息,因为是 柔性数组,所以可以根据 信息 的大小灵活调整数组的大小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函数

同样的,接收的数据结构如下所示,也包含了 类型 和 柔性数组
struct msgbuf
{
long mtype; /* message type, must be > 0 */
char mtext[1]; /* message data */
};消息队列 的大部分接口都与 共享内存 近似,所以掌握 共享内存 后,即可快速上手 消息队列
但是如你所见,System V 版的 消息队列 使用起来比较麻烦,并且过于陈旧,现在已经较少使用了,所以我们不必对其进行深究,知道个大概就行了,如果实际中真遇到了,再查文档也不迟
信号量(semaphore)一种特殊的工具,主要用于实现 同步和互斥
信号量 又称 信号灯,是各大高校《操作系统》课程中老师提及的高频知识点,往往伴随着 P、V 操作出现,但大多数老师都只是提及了基本概念,并未对 信号量 的本质及使用场景作出详细讲解
在正式学习 信号量 相关知识前,需要先简单了解下 互斥相关四个概念,为后续 多线程中信号量的学习作铺垫(重点)
1、并发 是指系统中同时存在多个独立的活动单元
2、互斥 是指同一时刻只允许一个活动单元使用共享资源
3、临界资源 与 临界区,多执行流环境中的共享资源就是 临界资源,涉及 临界资源 操作的代码区间即 临界区
4、原子性:只允许存在 成功 和 失败 两种状态
所以 互斥 是为了解决 临界资源 在多执行流环境中的并发访问问题,需要借助 互斥锁 或 信号量 等工具实现 原子操作,实现 互斥

关于互斥锁(mutex) 的相关知识在 多线程 中介绍,现在先来学习 信号量,搞清楚它是如何实现 互斥 的
将整个程序看作现实世界,形色各异的人看作 执行流,电影院 等公共资源看作 临界区,而单场电影的电影票看作 临界资源,主角 信号量 就是电影院中单场电影余票的 计数器,即余票越多,计数器值越大,当有人买票时,计数器-1,当有人看完电影时,计数器 +1
当电影票卖完时,计数器归零,其他想看电影的人也无法购票观看本场电影
下面这些情况应运而生:
-1,你必然可以去看这场电影,其他人也无法与你争夺,因为那个位置当电影放映之时就是属于你一个人的0,你就无法购票观看这场电影,即使自己偷偷溜进去也不行,会被保安叉出去,这是规定临界资源 的所属权限,从而保证了在电影放映时,绝对不会发生位置冲突、位置爆满、非法闯入等各种情况信号量 的设计初衷也是如此,就是为了避免 因多执行流对临界资源的并发访问,而导致程序运行出现问题
因为电影院一次能容纳几十个人,所以可能不太好理解 互斥 这个概念,将场景特殊化,现在有一个 顶级VIP放映室,每天饮料零食随便吃,但 一次只允许一个人看电影,与普通电影院一样,这个 顶级VIP放映室 也有自己的售票系统,其本质同样是 计数器,但此时 计数器初始值为 1
所以:当一群人都想进这个顶级VIP放映室看电影时,必须等到 计数器 为 1 时,才能进行抢票,才有资格进去看电影,当然一次只能放一个人进去,同时计数器是否恢复 1,取决于上一个看电影的人是否出了放映室 -> 看电影结束 -> 计数器 +1
规定:只允许一个人看电影
透过现象看本质,在 顶级VIP看电影 不就是代码中 多个执行流对同一个临界资源的互斥访问吗? 此时的 信号量 可以设为 1,确保 只允许一个执行流进行访问,这种 信号量 被称为 二元信号量,常用来实现 互斥
综上所述,信号量本质上就是 计数器 count,所谓的 P 操作(申请)就是在对 count--,V 操作(归还)则是在对 count++
下面来看看 信号量 的数据结构,通过 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 结构体中的内容都是一模一样的,结构上的统一可以带来管理上的便利,具体原因可以接着往下看
信号量的申请比较特殊,一次可以申请多个信息量,官方称此为 信号量集,所使用函数为 semget

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);关于 semget 函数

除了参数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也是随机生成的,大概率每次都不一样老生常谈的两种释放方式:指令释放、函数释放
指令释放:直接通过指令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 函数

注意:
参数2 表示信号量集中的某个信号量编号,从 1 开始编号参数3 中可传递的动作与共享内存、消息队列一致参数4 就像 printf 和 scanf 中最后一个参数一样,可以灵活使用信号量的操作比较麻烦,所以仅作了解即可
使用 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函数

重点在于参数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 是设置动作,一般设为默认即可
当然这些函数我们不必深入去研究,知道个大概就行了
信号量 是实现 互斥 的其中一种方法,具体表现为:资源申请,计数器 -1,资源归还,计数器 +1,只有在计数器不为 0 的情况下,才能进行资源申请,可以设计 二元信号量 实现 互斥
System V 中的 信号量 操作比较麻烦,但 信号量 的思想还是值得一学的,等后面学习多线程时,也会使用 POSIX 中的 信号量 实现 互斥,相比之下,POSIX 版的信号量操作要简单得多,同时应用也更为广泛
信号量 需要被多个独立进程看到,所以信号量本身也是 临界资源,不过它是 原子 的,所以可以用于 互斥
多个独立进程看到同一份资源,这就是 IPC 的目标,所以 信号量 被划分至进程间通信中
不难发现,共享内存、消息队列、信号量的数据结构基本一致,并且都有同一个成员 struct ipc_perm,所以实际对于 操作系统 来说,对 System V 中各种方式的描述管理只需要这样做:
共享内存、消息队列、信号量对象描述后,统一存入数组中 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内核这座庞大而精密的交响乐团中,
消息队列与信号量不过是两个独立却又密切协作的乐器。它们以不同的节奏演奏着同步与通信的旋律,为无数进程之间的协同作业提供了可能。我们在这场穿越内核迷雾的旅途中,看见了结构的优雅,也触摸到机制背后那隐秘的复杂。
正如乐章的终止并不意味着音乐的结束,掌握了消息队列与信号量,不过是理解操作系统交响曲的起点。在共享内存的低语、管道的絮语、Socket 的远鸣中,还有更多值得我们探索的旋律。
本篇关于消息队列和信号量的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!