🔥 消息队列(Message Queue) 是一种进程间通信(IPC)机制,它允许不同进程或线程之间通过发送和接收消息来交换数据。
🔥 消息队列提供了一个先入先出(FIFO)结构,消息被放入队列后,接收者按顺序取出。消息队列广泛用于分布式系统、并发程序设计以及需要可靠异步通信的场景。
消息队列 VS 管道:消息队列基于消息,而管道则基于字节流
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
参数:
S_IRUSR
、S_IWUSR
的权限位来设置对消息队列的访问权限。返回值
-1
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数
cmd
参数指定了 msgctl 要执行的操作类型。它有以下几种常用的值:
这些命令定义了对消息队列的不同操作。
msqid_ds
结构体的指针,用于存储消息队列的状态或提供修改信息。NULL
,因为删除操作不需要额外的数据msqid_ds 结构体定义如下:
struct msqid_ds {
struct ipc_perm msg_perm; // 消息队列的权限
size_t msg_qnum; // 队列中的消息数量
size_t msg_qbytes; // 队列的最大字节数
pid_t msg_lspid; // 最后发送消息的进程 ID
pid_t msg_lrpid; // 最后接收消息的进程 ID
time_t msg_stime; // 最后一次发送消息的时间
time_t msg_rtime; // 最后一次接收消息的时间
time_t msg_ctime; // 消息队列的最后修改时间
};
msgsnd 函数分析:
struct
,并且必须包含一个 long
类型的 mtype
字段,该字段用于表示消息的类型(消息队列通常会按消息类型排序)。mtype
字段。实际消息的大小应小于或等于消息队列的最大字节数限制。errno
为 EAGAIN
)。msgrcv 函数分析:
long
类型的 mtype
字段,接收到的消息会被复制到这个结构中。mtype
字段)系统会在该大小限制内复制消息。如果消息的内容超过该大小,msgrcv
会截断消息。0
,则接收队列中最先到达的消息msgrcv
会立即返回失败(并设置 errno
为 ENOMSG
),而不是阻塞。msgsz
的大小,系统会自动截断消息。#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
const std::string pathName = "/home/island/code";
const int pro_id = 0600;
struct mymsgbuf{
long mtype;
char mtext[108];
};
int main()
{
// 1. 创建消息队列
key_t key = ftok(pathName.c_str(), pro_id);
int msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0600);
if(msqid == -1){
std::cout << "msgget error" << std::endl;
return 1;
}
else std::cout << "msgget success, id: " << msqid << std::endl;
// 2. 发送消息
struct mymsgbuf buf; // 创建系统提供的 struct msgbuf 类型数据块
buf.mtype = 1;
strncpy(buf.mtext, "IsLand1314 Hello Everyone", sizeof(buf.mtext) - 1);
buf.mtext[sizeof(buf.mtext) - 1] = '\0'; // 确保空字符串结尾
int n = msgsnd(msqid, &buf, sizeof(buf.mtext), 0);
if(n == -1) {
std::cout <<"msgsnd error" << std::endl;
return 2;
}
else std::cout <<"msgsnd success, 消息类型为:" << buf.mtype << std::endl;
// 3. 接收消息
struct mymsgbuf info;
size_t sz = msgrcv(msqid, &info, sizeof(info), 1, 0);
if(sz == -1){
std::cout << "msgrcv error" <<std::endl;
}
else std::cout << "msgrcv success: " << info.mtext << ", 消息类型为: " << info.mtype << std::endl;
// 4. 销毁消息队列
int ret = msgctl(msqid, IPC_RMID, nullptr);
if(ret == -1) {
std::cout << "msgget error" << std::endl;
return 4;
}
else std::cout << "msgget success" << std::endl;
return 0;
}
前提知识:
💦 信号量(Semaphore) 是一种同步机制,用于控制多个进程或线程对共享资源的访问。它主要用于解决进程间的同步与互斥问题,防止资源冲突和数据竞争。信号量是一个整数,用来表示可用资源的数量,操作系统通过它来协调并发执行的进程
由于信号量本质是一个对资源进行预订的计数器,因此必须解决下面两个问题:
信号量必须能被多个进程看到 。
信号量的 - - 与 ++ 操作(PV操作)必须具有原子性
💢 原子性:原子性是指一个操作是不可中断的,即该操作要么全部执行成功,要么全部执行失败,不存在执行到中间某个状态的情况
访问临界资源的步骤:1.申请信号量 2.访问临界资源 3.释放信号量
信号量和共享内存、消息队列一样,需要实现被不同的进程访问,所以信号量本身也是一个共享资源
举个例子 💫
S
,初始值为 5,表示有5个资源可以被并发访问。若一个进程执行P操作,S
减1,变成4,表示该进程占用了一个资源。当该进程释放资源时,执行V操作,S
加1,变回5由于信号量也是遵循System V标准的,所以它的常用方法和前面的类似。信号量主要是用于同步和互斥的。
保护的常见方式:
因此,我们所写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区) 所谓的对共享资源的保护,本质是对访问共享资源的代码进行保护
(1)创建 / 获取信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg)
参数:
常见标志位:
返回值:成功返回非零的信号量标识符;失败返回 -1,并设置 errno 以指示错误原因
(2)删除信号量
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, ...)
参数:
semid
:是信号量集合的标识符,由 semget 函数返回semnum
:信号量在信号量集合中的索引(从0开始)(如果要删除整个信号量集,则填0)cmd
:指定要执行的控制命令常见命令:IPC_RMID:删除信号量集合 返回值:成功返回0;失败返回-1并设置 errno
(3)操作信号量
#include <sys/sem.h>
int semop(int semid, struct sembuf *sops, size_t nsops);
参数说明
semget
函数获得。信号量集是一个由多个信号量组成的集合:
指向 struct sembuf
数组的指针,这个数组包含了要对信号量集合进行的操作sops
数组的长度)semop 操作的核心是 struct sembuf
结构体,它定义了每个操作的细节。该结构体的定义如下:
struct sembuf {
unsigned short sem_num; // 信号量的索引(信号量集合中的第几个信号量)
short sem_op; // 操作数,表示对信号量的操作
short sem_flg; // 操作标志,控制操作的行为
};
(4)信号量指令
System V 是如何实现IPC的,和管道为什么不同呢?
根据上面我们可以发现,它们内部都有一个 ipc_perm
的东西。我们可以推测一下,在 OS 层面,IPC 是同类资源。
我们也可以获取IPC对应的属性,案例如下:
由于需要让 IPC 资源被所有进程看到,那么它一定是全局的。所以IPC资源在内核中一定是一个全局变量
此时,所有的IPC资源都可以直接被柔性数组直接指向
柔性数组(了解)
struct my_struct {
int size; // 普通成员
char data[]; // 柔性数组成员(没有指定大小)
};
言归正传,例如:
那么不就可以使用柔性数组 (类型强转) ,管理所有的IPC资源了吗?数组下标就是之前的 xxid,即 xxget 的返回值!这也就是为什么之前我们见到的各种 IPC资源的 id 是连续的了。
所以,所有的 IPC 资源之所以能够区分 IPC 的唯一性,都是通过 key来进行的
注意:各类型的 IPC 资源之间的 key 也可能会冲突
直接强转,(struct msg_queue*) p[1] ->其它属性
这时,我们所看到的 kern_ipc_perm 就是 基类,与之相关的三个就是子类,继承了基类,此时就可以使用基类来管理所有的子类了,这是 C语言实现多态的另一种方式。
那具体是怎么识别是哪一种子类的呢?
以上就是我对消息队列、信号量、IPC 的理解,那么我们的进程间通信(IPC) 就讲到这里啦,我们后面就开始进入进程信号的知识哩