📃个人主页:island1314
🔥个人专栏:Linux—登神长阶
⛺️ 欢迎关注:👍点赞 👂🏽留言 😍收藏 💞 💞 💞
一、消息队列 💌
1. 了解
🔥 消息队列(Message Queue) 是一种进程间通信(IPC)机制,它允许不同进程或线程之间通过发送和接收消息来交换数据。
🔥 消息队列提供了一个先入先出(FIFO)结构,消息被放入队列后,接收者按顺序取出。消息队列广泛用于分布式系统、并发程序设计以及需要可靠异步通信的场景。
- 消息队列的本质:一个进程向另外一个进程发送一块数据的方法
- 每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值
消息队列 VS 管道:消息队列基于消息,而管道则基于字节流
2. 消息队列函数
- msgget:获取消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflag);
参数:
- key_t key: 消息队列的标识符(键值),用于区分不同的消息队列。可以使用 ftok 函数生成一个唯一的键值。如果一个消息队列已经存在,msgget 会返回该队列的标识符;如果该队列不存在,则会创建一个新的消息队列。
- int msgflg: 控制标志,指示消息队列的操作方式。它可以是以下标志的组合:
- IPC_CREAT: 如果消息队列不存在,则创建一个新的消息队列。
- IPC_EXCL: 如果消息队列已经存在,则返回错误。
- 权限位: 类似于文件的权限控制,使用类似
S_IRUSR
、S_IWUSR
的权限位来设置对消息队列的访问权限。返回值
- 成功时,msgget 返回一个非负整数,该整数是消息队列的标识符。失败时,返回
-1
- msgctl:删除消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数
msqid
- 这是消息队列的标识符,是由 msgget() 函数返回的消息队列 ID
- 表示要操作的目标消息队列
- 在消息队列操作中,msqid 必须有效,即指向一个存在的消息队列
cmd
cmd
参数指定了 msgctl 要执行的操作类型。它有以下几种常用的值:
- IPC_STAT:获取指定消息队列的当前状态和属性。
- IPC_SET:修改指定消息队列的属性(如权限、队列容量等)。
- IPC_RNID:删除指定的消息队列。
这些命令定义了对消息队列的不同操作。
buf
- 这是一个指向
msqid_ds
结构体的指针,用于存储消息队列的状态或提供修改信息。- 对于 IPC_STAT 命令,用于接收当前消息队列的状态信息。
- 对于 IPC_SET 命令,包含要设置的消息队列新属性信息。
- 对于 IPC_RNID 命令,可以传递
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:发送消息,msgrcv: 接收消息
msgsnd 函数分析:
msqid
- 消息队列标识符,指定要发送消息的目标消息队列
msgp
- 一个指向消息结构的指针,包含了要发送的消息数据。
- 消息结构应该是一个
struct
,并且必须包含一个long
类型的mtype
字段,该字段用于表示消息的类型(消息队列通常会按消息类型排序)。msgsz
- 消息的大小(字节数),不包括
mtype
字段。实际消息的大小应小于或等于消息队列的最大字节数限制。msgflg
- 标志位,用于指定消息发送的特性。常用的标志有:
- IPC_NOWAIT:如果队列已满,消息不会阻塞调用,而是直接返回失败(设置
errno
为EAGAIN
)。- MSG_NOERROR:如果消息超出了队列的剩余空间,系统会自动截断消息,确保消息能够成功放入队列。
msgrcv 函数分析:
msqid
- 消息队列的标识符,指定要接收消息的目标队列
msgp
- 一个指向消息结构的指针,用于存储接收到的消息
- 该结构必须至少包含一个
long
类型的mtype
字段,接收到的消息会被复制到这个结构中。msgsz
- 接收的最大字节数(不包括
mtype
字段)系统会在该大小限制内复制消息。如果消息的内容超过该大小,msgrcv
会截断消息。msgtyp
- 消息类型,用于指定从队列中接收哪一类的消息
- 如果 msgtyp 是
0
,则接收队列中最先到达的消息- 如果 msgtyp 是大于 0 的整数,则接收与该类型匹配的第一个消息
- 如果 msgtyp 是负数,则接收小于或等于该类型的消息(按照优先级顺序)
msgflg
- 标志位,用于指定消息接收的特性。常见的标志有:
- IPC_NOWAIT:如果没有符合条件的消息,
msgrcv
会立即返回失败(并设置errno
为ENOMSG
),而不是阻塞。- MSG_NOERROR:如果消息超出了
msgsz
的大小,系统会自动截断消息。
-
ipcs -q:查看消息队列的指令
-
ipcrm -q + id:删除消息队列指令
3. 案例
#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. 信号量的种类
-
计数信号量(Counting Semaphore):计数信号量的值可以为任何非负整数,表示资源的数量。当资源可用时,信号量的值增加;当资源被占用时,信号量的值减少。
- P操作(Proberen):进程申请资源,信号量值减1,如果值为负,进程会被阻塞。
- V操作(Verhogen):进程释放资源,信号量值加1,若有阻塞进程,唤醒其中一个。
-
二值信号量(Binary Semaphore):二值信号量的值只能是0或1,通常用于互斥锁(mutex)。它可以确保在任意时刻只有一个进程能访问共享资源,防止多个进程同时执行关键区域代码。
访问临界资源的步骤:1.申请信号量 2.访问临界资源 3.释放信号量
- 申请信号量的本质就是对临界资源的预定
信号量和共享内存、消息队列一样,需要实现被不同的进程访问,所以信号量本身也是一个共享资源
2. 信号量的工作原理
- P操作(等待操作):信号量的值减1,如果信号量的值为负,表示没有足够的资源,调用该操作的进程会被阻塞,直到信号量的值大于等于0。
- V操作(释放操作):信号量的值加1,如果有进程因为信号量值为负而被阻塞,V操作会唤醒一个阻塞的进程。
举个例子 💫
- 假设有一个计数信号量
S
,初始值为 5,表示有5个资源可以被并发访问。若一个进程执行P操作,S
减1,变成4,表示该进程占用了一个资源。当该进程释放资源时,执行V操作,S
加1,变回5
3. 信号量操作
由于信号量也是遵循System V标准的,所以它的常用方法和前面的类似。信号量主要是用于同步和互斥的。
保护的常见方式:
-
互斥:任何时刻,只允许一个执行流(进程)访问资源
-
同步:多个执行流,访问临界资源的时候,具有一定的顺序性
因此,我们所写的代码 = 访问临界资源的代码(临界区) + 不访问临界资源的代码(非临界区)
所谓的对共享资源的保护,本质是对访问共享资源的代码进行保护
(1)创建 / 获取信号量
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg)
参数:
- key:是一个键值,用于唯一标识信号量集
- nsems:指定信号量集中信号量的数量,通常这个值至少为1
- semflg:是一组标志位,用于指定信号量集的属性
常见标志位:
- IPC_CREAT:如果信号量集存在则获取并返回;如果不存在则创建
- IPC_CREAT | IPC_EXCL:如果信号量集存在则报错;如果不存在则创建
返回值:成功返回非零的信号量标识符;失败返回 -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);
参数说明
- semid:信号量集的标识符,通常通过
semget
函数获得。信号量集是一个由多个信号量组成的集合- sops
:
指向struct sembuf
数组的指针,这个数组包含了要对信号量集合进行的操作- nsops:: 表示要执行的操作数目(即
sops
数组的长度)
semop 操作的核心是 struct sembuf
结构体,它定义了每个操作的细节。该结构体的定义如下:
struct sembuf {
unsigned short sem_num; // 信号量的索引(信号量集合中的第几个信号量)
short sem_op; // 操作数,表示对信号量的操作
short sem_flg; // 操作标志,控制操作的行为
};
(4)信号量指令
- 查看信号量 ipcs -s
- 删除信号量 ipcrm -s semid
4. 信号量的应用
-
互斥锁:二值信号量通常用于互斥,确保只有一个进程可以访问临界区,避免数据竞争。例如,多个进程需要访问共享文件时,可以使用信号量来保证每次只有一个进程能进行读写操作。
-
生产者消费者问题:信号量可以用于协调生产者和消费者的关系,控制缓冲区的读写操作。生产者放入商品时减少空位信号量,消费者取出商品时减少产品信号量,从而保证生产与消费的同步。
-
资源分配:在有限资源(如打印机、数据库连接等)情况下,信号量用来管理资源的分配,确保资源的公平使用。
-
进程同步:信号量也可用于进程同步,确保多个进程按照特定的顺序执行。例如,进程A完成某项任务后,信号量允许进程B开始执行。
5. 注意事项
- 死锁:不当使用信号量(例如多个进程循环等待)可能导致死锁,进程永远无法继续执行。
- 忙等待:如果信号量操作不当,可能导致进程处于等待状态,而没有有效地释放CPU资源,造成系统性能下降。
- 顺序问题:多个进程同时等待或释放信号量时,可能出现执行顺序不符合预期的情况,因此需要在使用信号量时小心设计
三、思考 -- IPC
System V 是如何实现IPC的,和管道为什么不同呢?
🐸 用户角度
- 首先我们要知道操作系统是如何管理 IPC 的:先描述,再组织。
- IPC有哪些属性呢?
根据上面我们可以发现,它们内部都有一个 ipc_perm
的东西。我们可以推测一下,在 OS 层面,IPC 是同类资源。
我们也可以获取IPC对应的属性,案例如下:
🐸 内核角度
由于需要让 IPC 资源被所有进程看到,那么它一定是全局的。所以IPC资源在内核中一定是一个全局变量
- 我们发现在消息队列、信号量与共享内存的源码中,结构体开头位置都是 kern_ipc_perm,这点和我们上面从用户层看到的是一样的。
此时,所有的IPC资源都可以直接被柔性数组直接指向
柔性数组(了解)
- 柔性数组的定义在 C 语言标准(特别是 C99 及以后版本)中引入
- 它允许结构体的最后一个成员声明为一个数组,但不指定数组的大小
- 结构体的大小由前面的成员决定,而柔性数组的大小则依赖于后续内存的分配
struct my_struct {
int size; // 普通成员
char data[]; // 柔性数组成员(没有指定大小)
};
言归正传,例如:
- p[0] = (struct kern_ipc_perm) &(shmid_kernel)
- p[1] = (struct kern_ipc_perm) &(msg_queue)
- p[2] = (struct kern_ipc_perm) &(sem_array)
- …
那么不就可以使用柔性数组 (类型强转) ,管理所有的IPC资源了吗?数组下标就是之前的 xxid,即 xxget 的返回值!这也就是为什么之前我们见到的各种 IPC资源的 id 是连续的了。
所以,所有的 IPC 资源之所以能够区分 IPC 的唯一性,都是通过 key来进行的
注意:各类型的 IPC 资源之间的 key 也可能会冲突
- 那么此时怎么访问IPC资源的其它属性呢?
直接强转,(struct msg_queue*) p[1] ->其它属性
- 那么一个指针,指向结构体的第一个元素,其实就是指向了整个结构体
- 访问头部,直接访问
- 访问其它属性,做强转,这种结构不就是C++中的多态吗?
这时,我们所看到的 kern_ipc_perm 就是 基类,与之相关的三个就是子类,继承了基类,此时就可以使用基类来管理所有的子类了,这是 C语言实现多态的另一种方式。
那具体是怎么识别是哪一种子类的呢?
- 实际在内核中,会定义各种的 ipc_ids,但是它们的 entries 指针都指向同一个 kern_ipc_perm 数组
四、小结
以上就是我对消息队列、信号量、IPC 的理解,那么我们的进程间通信(IPC) 就讲到这里啦,我们后面就开始进入进程信号的知识哩
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!