System消息队列

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:操作系统统一管理

内核必须维护管理结构:

  1. struct msqid_ds:队列总控结构体(权限、消息总数、字节上限、创建者 PID 等);
  2. struct node:单条消息链表节点,保存 type、数据、下一条消息指针;所有消息、权限、统计信息全部由 OS 内核管理,进程只通过系统调用收发数据。

结论3:多进程共享同一队列的关键 —— key

和共享内存逻辑完全一致:

  1. 双方用相同路径 + 项目 ID 调用ftok()生成同一个key
  2. 通过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 【删除消息队列(最常用)
  • bufstruct 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 非临界区

进程代码分成两部分:

  1. 临界区访问临界资源的那一段代码

    重点结论:保护资源的本质,不是锁住数据本身,而是锁住访问数据的这段代码。

  2. 非临界区:不碰共享资源的代码,多个进程可以随便并行执行,不需要加锁

互斥

互斥定义:任何时刻,只允许一个执行流访问临界资源,同一临界区同一时间只能有一个进程进去。就比如区ATM取钱每次只能一个人

互斥的底层硬性要求:操作必须具备原子性,加锁 / 申请锁操作必须具备原子性(要么完整执行、要么完全不执行),否则可能加锁操作过程到一半就会被打断,进而竞争失效。

进入区(加锁)→ 临界区(操作共享资源)→ 退出区(解锁)→ 非临界区

同步

在互斥基础上,规定多个执行流访问资源的先后顺序。

同步只管控时序,但无法阻止多执行流同时进入临界区;没有互斥,同步逻辑再完善也会数据错乱

2者结合:

  • 进入区① 先执行互斥加锁(原子操作,保证独占临界区);② 判断同步条件:不满足访问条件就阻塞等待(比如缓冲区满,生产者等待);
  • 临界区:独占读写共享缓冲区;
  • 退出区① 同步唤醒等待的另一个执行流(生产者写完唤醒消费者);② 互斥解锁,释放资源访问权限;
  • 非临界区:无关逻辑自由并发。

即先用互斥先保证同一时间只有一个进程进入临界区,;如果有先后执行要求,再叠加同步,控制访问顺序;