System消息队列
System V 消息队列
生命周期随内核,和共享内存一样,调用后删除命令后不会立刻销毁,仅做删除标记;所有当前挂载使用队列的进程全部退出后,内核才真正释放队列和里面所有消息
收发逻辑
- 进程 A 发送调用
msgsnd(),封装type=1+ 自定义数据,内核在队列链表尾部新增红色 node 节点。 - 进程 B 接收调用
msgrcv(),指定type=2,链表无 type=2 消息 → B 进程阻塞等待,等到内核从链表中寻找第一条 type=2 的绿色消息取出,唤醒B;队列中 type=1 的红色消息会继续留在链表,不会被读取。也可以B的type设置为1,则就能和A进行通信
结论 1:带类型的数据块传输
消息队列的核心特色:每条消息绑定一个整型type标记。
- 进程 A 发送
type=1的红色消息; - 进程 B 可以指定只接收
type=2的绿色消息,跳过队列里其他类型数据; - 对比管道:管道只能按先进先出顺序读,无法分类筛选。
结论2:操作系统统一管理
内核必须维护管理结构:
struct msqid_ds:队列总控结构体(权限、消息总数、字节上限、创建者 PID 等);struct node:单条消息链表节点,保存 type、数据、下一条消息指针;所有消息、权限、统计信息全部由 OS 内核管理,进程只通过系统调用收发数据。
结论3:多进程共享同一队列的关键 —— key
和共享内存逻辑完全一致:
- 双方用相同路径 + 项目 ID 调用
ftok()生成同一个key; - 通过
msgget(key, flag)打开 / 创建队列;内核依靠key匹配,保证不同进程映射到内核里同一个消息队列对象。
msgget 创建或打开
#include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget(key_t key, int msgflg);参数msgflg:
创建标志 +权限(和 shmget 规则完全一致)
IPC_CREAT:key 不存在则新建队列;存在则直接打开;IPC_CREAT | IPC_EXCL:不存在才创建,已存在直接报错,防止重复创建;
返回值
- 成功:返回非负整数
msqid(消息队列操作句柄,供 msgsnd/msgrcv/msgctl 使用) - 失败:返回
-1
eg:
// 创建队列,不存在则新建,权限0666,存在就直接打开 int msqid = msgget(key, IPC_CREAT | 0666); // 只打开已有队列,不创建 int msqid = msgget(key,0); // 强制创建 int msqid = msgget(key, IPC_CREAT | IPC_EXCL | 0666);msgctl —— 消息队列控制函数(删队列、改权限、查信息)
int msgctl(int msqid, int cmd, struct msqid_ds *buf);参数解析
msqid:msgget 返回的队列 id;cmd:控制命令(3 个最常用):IPC_STAT 【读取队列内核信息】,IPC_SET 【修改队列权限 / 所有者】,IPC_RMID 【删除消息队列(最常用)buf:struct msqid_ds结构体,存放队列内核信息,根据 cmd 决定是否使用。
//IPC_STAT 【读取队列内核信息】 struct msqid_ds info; msgctl(msqid, IPC_STAT, &info); printf("队列权限:%ho\n", info.msg_perm.mode); printf("当前消息条数:%ld\n", info.msg_qnum); //IPC_SET 【修改队列权限 / 所有者】 msgctl(msqid, IPC_STAT, &info); info.msg_perm.mode = 0644; // 修改权限 msgctl(msqid, IPC_SET, &info); // IPC_RMID 【删除消息队列(最常用)】 msgctl(msqid, IPC_RMID, NULL); 调用后不会立刻销毁,仅做删除标记;所有当前挂载使用队列的进程全部退出后,内核才真正释放队列和里面所有消息内核描述队列
msgsnd 发送 + msgrcv 接收
所有收发都必须自定义消息缓冲区,第一个成员必须是 long mtype(>0),后面自定义数据
struct MsgBuf { long mtype; // 消息类型,正数,核心过滤标识 char mtext[128];// 自定义承载数据 };msgsnd:发送消息到队列尾部
#include <sys/msg.h> int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);参数解析
msqid:msgget 返回的消息值msgp:自定义MsgBuf结构体指针msgsz:仅数据长度,不含 mtype;即sizeof(buf.mtext)msgflg阻塞控制标志:0:默认阻塞。队列满时,发送进程休眠,直到队列腾出空间IPC_NOWAIT:非阻塞。队列满直接返回 - 1,不等待
eg:
struct MsgBuf buf; buf.mtype = 1; // 设置消息类型 strcpy(buf.mtext, "hello"); // 阻塞发送,数据长度是字符串长度 msgsnd(msqid, &buf, strlen(buf.mtext), 0);返回值:
成功返回0,失败-1并设置errno
msgrcv:从队列读取匹配类型消息
#include <sys/msg.h> ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);参数解析:
msqid:msgget返回值msgp:接收消息的缓冲区结构体指针msgsz:缓冲区数据区最大可容纳字节(mtext 数组长度)msgtyp:消息过滤核心参数,分 3 种规则msgflg:阻塞控制0:无匹配消息时阻塞休眠IPC_NOWAIT:无匹配消息直接返回 - 1,不阻塞MSG_NOERROR:消息数据超过 msgsz 时截断,不报错;不加则返回 - 1
msgtyp 三种读取规则:
- msgtyp > 0(精准匹配)遍历链表,取出第一条 mtype == msgtyp的消息;其他类型消息保留在队列。例:msgtyp=2 → 只读 type=2 的消息,type=1 完全忽略。
- msgtyp = 0(先进先出,不筛选)永远取出链表最头部第一条消息,不管 type 是多少。
- msgtyp < 0(极少使用)取出队列中
mtype ≤ abs(msgtyp)里最小 type 的第一条消息。
返回值
成功返回实际拷贝到mtext缓冲区的字节数,失败返回-1,同步设置errno
eg:
struct MsgBuf buf; // 阻塞读取type=2的消息,缓冲区最大128字节 ssize_t n = msgrcv(msqid, &buf, sizeof(buf.mtext), 2, 0); if(n > 0) { printf("收到数据:%s\n", buf.mtext); }查看和删除消息队列
ipcs -q查看
ipcrm -q msqid删除
怎么通过同步和互斥来保护共享资源
临界资源
被保护起来的共享资源叫做临界资源,同一时刻只能一个进程使用
临界区 vs 非临界区
进程代码分成两部分:
- 临界区:访问临界资源的那一段代码
重点结论:保护资源的本质,不是锁住数据本身,而是锁住访问数据的这段代码。
- 非临界区:不碰共享资源的代码,多个进程可以随便并行执行,不需要加锁
互斥
互斥定义:任何时刻,只允许一个执行流访问临界资源,同一临界区同一时间只能有一个进程进去。就比如区ATM取钱每次只能一个人
互斥的底层硬性要求:操作必须具备原子性,加锁 / 申请锁操作必须具备原子性(要么完整执行、要么完全不执行),否则可能加锁操作过程到一半就会被打断,进而竞争失效。
进入区(加锁)→ 临界区(操作共享资源)→ 退出区(解锁)→ 非临界区
同步
在互斥基础上,规定多个执行流访问资源的先后顺序。
同步只管控时序,但无法阻止多执行流同时进入临界区;没有互斥,同步逻辑再完善也会数据错乱。
2者结合:
- 进入区① 先执行互斥加锁(原子操作,保证独占临界区);② 判断同步条件:不满足访问条件就阻塞等待(比如缓冲区满,生产者等待);
- 临界区:独占读写共享缓冲区;
- 退出区① 同步唤醒等待的另一个执行流(生产者写完唤醒消费者);② 互斥解锁,释放资源访问权限;
- 非临界区:无关逻辑自由并发。
即先用互斥先保证同一时间只有一个进程进入临界区,;如果有先后执行要求,再叠加同步,控制访问顺序;
