消息队列与信号量:System V 进程间通信的基础
目录
1. 消息队列
1.1 什么是消息队列?
编辑1.2 消息队列的数据结构
1.3 消息队列的相关接口
1.3.1 创建消息队列
1.3.2 发送消息
1.3.3 接收消息
1.3.4 删除消息队列
2. 信号量
2.1 什么是信号量?
2 .2信号量的类型
二值信号量(Binary Semaphore)
计数信号量(Counting Semaphore)
2.3 信号量的基本操作
2.4 信号量的数据结构
2.5、信号量的相关接口
2.5.1、创建
2.5.2、释放
2.5.3、操作
前言:在操作系统的多进程编程中,进程间通信(IPC) 是一个关键问题。System V 标准为我们提供了几种进程间通信的方式,其中最常用的包括 消息队列 和 信号量。这两种机制不仅可以让进程进行高效的协作,还能确保资源的安全使用。
本文将重点介绍消息队列和信号量的概念、数据结构以及如何在 C 语言中使用这些机制来实现进程间通信。
1. 消息队列
1.1 什么是消息队列?
消息队列(Message Queue)是一种进程间通信机制,与管道和共享内存相比,它采用一种队列的方式来传递数据。消息队列通过**先进先出(FIFO)**的方式来管理消息,发送方将消息添加到队列尾部,而接收方则从队列头部读取消息。
消息队列的特点:
-
每个消息有类型(
mtype
),可以按类型区分消息。区分发送方和接受方的不同。 -
发送和接收消息都是异步的,不需要发送方和接收方同时存在。
-
消息队列的生命周期由操作系统管理,进程退出时不会自动删除。
1.2 消息队列的数据结构
消息队列的数据结构通常定义为 msqid_ds
,它包含了消息队列的各种管理信息,具体结构如下
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; // 上次发送消息的进程 PIDpid_t msg_lrpid; // 上次接收消息的进程 PID
};
其中,msg_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 消息队列的相关接口
1.3.1 创建消息队列
消息队列的创建使用 msgget()
函数。该函数需要提供一个唯一的键值(key
)来标识消息队列,并返回消息队列的标识符。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);
-
返回值:成功时返回消息队列的标识符
msqid
,失败时返回 -1。 -
参数1:
key
是消息队列的唯一标识符,通常通过ftok()
函数计算。 -
参数2:
msgflg
是标志位,用于设置消息队列的创建方式及权限。
共享内存 的
shmget
可以说是十分相似了,关于ftok
函数计算key
值,这里就不再阐述
#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 发送消息
消息的发送通过 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);
-
参数1:
msqid
是消息队列的标识符。 -
参数2:
msgp
是指向消息的指针,消息通常是一个结构体,包含消息类型和数据。 -
参数3:
msgsz
是消息的数据大小。 -
参数4:
msgflg
是消息发送标志。一般默认为0
参数2 表示待发送的数据块,这显然是一个结构体类型,需要自己定义,结构如下:
struct msgbuf
{long mtype; /* message type, must be > 0 */char mtext[]; /* message data */
};
mtype
就是传说中数据块类型,据发送方而设定;mtex
是一个比较特殊的东西:柔性数组,其中存储待发送的 信息,因为是 柔性数组,所以可以根据 信息 的大小灵活调整数组的大小。
struct msgbuf *msg=(struct msgbuf*)malloc(sizof(struct msbuf)+100); 创建mtext 大小为100
1.3.3 接收消息
消息的接收通过 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);
-
参数1:
msqid
是消息队列的标识符。 -
参数2:
msgp
是指向接收消息的缓冲区。接收到的数据块,是一个输出型参数 -
参数3:
msgsz
是缓冲区大小。 -
参数4:
msgtyp
是消息的类型。 -
参数5:
msgflg
是接收消息的标志。一般默认为0
1.3.4 删除消息队列
消息队列在使用结束后需要被删除,可以使用 msgctl()
函数来删除消息队列。
参数1:msqid 是消息队列的标识符。参数2:cmd 是要执行的命令,IPC_RMID 表示删除消息队列。参数3:buf 是指向 msqid_ds 结构体的指针,用于获取或设置消息队列的状态。
消息队列也有两种释放方式:通过指令释放、通过函数释放
释放指令:ipcrm -q msqid
释放消息队列,其他 System V
通信资源也可以这样释放
ipcrm -m shmid
释放共享内存ipcrm -s semid
释放信号量集
消息队列 的大部分接口都与 共享内存 近似,所以掌握 共享内存 后,即可快速上手 消息队列
但是如你所见,System V
版的 消息队列 使用起来比较麻烦,并且过于陈旧,现在已经较少使用了,所以我们不必对其进行深究,知道个大概就行了,如果实际中真遇到了,再查文档也不迟。
2. 信号量
2.1 什么是信号量?
信号量(Semaphore)是一种用于实现 同步 和 互斥 的机制,它通过一个计数器来管理资源的访问。在多进程或多线程环境中,信号量用于避免多个进程同时访问共享资源,从而避免数据竞争和冲突。
在正式学习 信号量 相关知识前,需要先简单了解下 互斥相关四个概念,为后续 多线程中信号量的学习作铺垫(重点
-
并发 是多个活动单元(如线程)在同一时段内执行的概念,可能是交替进行的。
-
互斥 是一种同步机制,用于确保同一时刻只有一个线程或进程访问临界资源。
-
临界资源 是并发环境中需要被多个线程共享的资源,而 临界区 是访问这些资源的代码区域。
-
原子性 确保操作要么成功,要么失败,不会出现部分执行的情况。
-
通过 互斥锁 和 信号量 等机制,可以有效地解决并发环境下的资源访问问题,确保程序的正确性和稳定性
2 .2信号量的类型
二值信号量(Binary Semaphore)
二值信号量的计数值仅有 0 和 1,用于实现 互斥锁。它表示某个共享资源是否被占用:
-
0:资源被占用。
-
1:资源可用。
常用的二值信号量就是 互斥锁,确保在任意时刻只有一个线程能访问共享资源。
计数信号量(Counting Semaphore)
计数信号量的计数值可以是 任意非负整数,通常用来表示一个资源池的容量。例如,表示数据库连接池中剩余的连接数。计数信号量的值可以大于 1,表示有多个资源可以同时被访问。
当计数信号量的值为 0 时,表示所有资源都被占用;而当计数信号量的值大于 0 时,表示有可用资源。
2.3 信号量的基本操作
信号量的操作通常包括 P 操作(等待操作)和 V 操作(释放操作)。这些操作通常是原子的,即不可中断,确保了对共享资源的正确访问。
P 操作是 等待操作,它用于请求资源并将信号量的值减 1:
-
如果信号量的值大于 0,P 操作成功,信号量减 1。
-
如果信号量的值为 0,进程或线程将被阻塞,直到信号量的值大于 0。
V 操作是 释放操作,它用于释放资源并将信号量的值加 1:
-
如果有进程或线程在等待该信号量,V 操作会唤醒一个等待的进程或线程。
-
V 操作不会阻塞进程,它只负责更新信号量的值并可能唤醒其他进程。
信号量的用途
信号量通常用于以下两种情况:
-
互斥控制:保证同一时刻只有一个线程或进程可以访问临界资源。
-
同步控制:确保多个线程或进程按照特定的顺序执行,常用于生产者-消费者问题等。
在 System V 和 POSIX 标准中,信号量都提供了一些相关的操作函数:
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 是设置动作,一般设为默认即可
总结:操作系统可以使用 统一的方法 来管理不同类型的 IPC 对象,如 struct shmid_ds
(共享内存描述符)、struct semid_ds
(信号量描述符)、struct msgid_ds
(消息队列描述符),因为这些结构体都包含一个公共部分,即 struct ipc_perm
。
关键点:
-
struct ipc_perm
是所有 System V IPC 对象 的公共部分,定义了这些对象的基本元数据和权限(例如对象的键、UID、GID、访问权限等)。 -
通过将这些具有相同结构(即包含
struct ipc_perm
)的不同类型的 IPC 对象存储在一个统一的数组或数据结构中,操作系统可以用相同的机制来管理它们。
具体说明:
-
struct ipc_perm
的公共部分:
每个 IPC 对象(共享内存、信号量、消息队列)都包含一个struct ipc_perm
,该结构体用于存储关于该对象的权限、标识符和其他元数据。例如:
-
共享内存:
struct shmid_ds
中包含struct ipc_perm shm_perm
,用于描述共享内存的权限信息。 -
信号量:
struct semid_ds
中包含struct ipc_perm sem_perm
,用于描述信号量集合的权限信息。 -
消息队列:
struct msgid_ds
中包含struct ipc_perm msg_perm
,用于描述消息队列的权限信息。
-
-
统一管理的机制:
操作系统可以通过统一的 管理方法(比如指针数组、哈希表等)来处理不同类型的 IPC 对象。由于它们的权限部分具有相同的结构(struct ipc_perm
),操作系统可以通过这个公共结构来管理这些对象。具体来说:
例如,操作系统维护一个名为
ipc_id_arr[]
的数组,其中每个元素对应一个 IPC 对象。每个 IPC 对象的第一个部分是struct ipc_perm
,因此操作系统只需要根据这个公共部分来访问和管理不同的对象类型。-
存储在同一数组或数据结构:操作系统将所有的 IPC 对象存储在一个数组或类似的数据结构中,每个对象通过一个标识符(如
shmid
、semid
、msgid
)来唯一标识。 -
访问和管理对象:通过公共的
struct ipc_perm
部分,操作系统可以以统一的方式进行权限检查、资源释放等操作。
-
-
访问和操作的统一性:
通过 强制类型转换(type casting),操作系统可以灵活地处理不同类型的 IPC 对象。例如,假设ipc_id_arr[n]
是一个通用的指针,操作系统可以通过强制转换来访问特定类型的结构体:-
shmid_ds
(共享内存)通过强制转换访问ipc_id_arr[n]
:struct shmid_ds *shm = (struct shmid_ds *)ipc_id_arr[n];
-
semid_ds
(信号量)通过强制转换访问ipc_id_arr[n]
:struct semid_ds *sem = (struct semid_ds *)ipc_id_arr[n];
-
通过这种方式,操作系统只需要一个通用的管理接口来处理不同类型的 IPC 对象。