进程同步与通信:System V 消息队列 + 信号量一站式解析

system V消息队列

一、是什么?

System V 消息队列 = 内核里的一个消息链表

  • 进程把带类型的消息放进去
  • 另一个进程按类型取
  • 自带同步、排队、阻塞

二、核心原理

  1. 内核维护一个消息队列链表
  2. 消息 = 类型 + 数据
  3. 发送:把消息挂到队列尾部
  4. 接收:可按类型取(只取我想要的)
  5. 生命周期:内核常驻,进程退出不删

三、相关接口

1. ftok

c 复制代码
#include <sys/types.h>
#include <sys/ipc.h>

与共享内存创建差不多,都需要提前约定好一个key键值。

c 复制代码
// 用户指明
#define PATHNAME "."
#define PROJ_ID 66

key_t Getkey() {
    return ftok(PATHNAME, PROJ_ID);
}

// 1. 构建键值
key_t k = Getkey();
if (k < 0) {
    std::cerr << "获取key键值失败:" << strerror(errno) << std::endl;
    exit(1);
}

消息队列系列接口的头文件为

c 复制代码
#include <sys/types.h>   // 基本类型定义
#include <sys/ipc.h>    // IPC通用(ftok也用这个)
#include <sys/msg.h>    // 消息队列专用

2. msgget

c 复制代码
int msgget(key_t key, int msgflg);

msgflg

  • IPC_CREAT | 0666:不存在则创建,已存在就用之------获取

  • IPC_CREAT | IPC_EXCL | 0666:不存在创建,已存在则报错------创建

  • 0664:权限(和文件权限一样)

返回:成功返回 msgid(共享内存 ID),失败 -1

3. msgctl

c 复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

常用:

c 复制代码
// 直接删,第三个参数给 NULL
if (msgctl(msqid, IPC_RMID, NULL) == -1) {
    perror("msgctl delete failed");
    exit(1);
}

4. msgsnd

c 复制代码
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

**作用:**往消息队列里发送一条消息

返回值:成功 → 返回 0;失败 → 返回 -1。

参数2 const void* msgp

指向自己定义的消息结构体指针 ,结构体必须以 long mtype 开头

c 复制代码
// 固定格式!!!
struct msgbuf {
    long mtype;   // 消息类型,必须 > 0
    char mtext[4096]; // 消息内容
};
参数3 size_t msgsz

消息数据部分的大小,只算内容,不算 mtype! ,例如 sizeof(msg.mtext)

参数4 int msgflg
  • 通常填 0 :队列满了就阻塞等待
  • IPC_NOWAIT:队列满了不等待,直接报错

5. msgrcv

c 复制代码
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

作用:从消息队列里取出一条消息;最大特点:可以按类型接收,只收我想要的

返回值:成功,返回接收到的数据字节数;失败,**返回 **-1。

参数2 void *msgp

存放消息的结构体指针(和发送时用同一个结构体)

参数3 size_t msgsz
  • 要接收的最大数据长度
  • 写法:sizeof(msg.mtext)
  • 只算内容,不算 mtype!
参数4 long msgtyp最关键:消息类型

决定你收哪条消息

  • 0 → 拿走队列里最早的一条(不挑类型)
  • > 0 → 只拿走类型等于这个数的第一条
  • < 0 → 取小于等于绝对值的最小类型(极少用)
参数5 int msgflg
  • 一般填 0 :没消息就阻塞等待
  • IPC_NOWAIT:没消息直接返回,不等待
示例
c 复制代码
// 1. 定义消息结构体
struct msgbuf {
    long mtype;
    char mtext[4096];
};

struct msgbuf msg;

// 2. 接收:只收类型为 1 的消息
msgrcv(
    msqid,        // 队列ID
    &msg,         // 存到哪里
    sizeof(msg.mtext), // 最大接收长度
    1,            // 只收类型=1
    0             // 阻塞等待
);

// 3. 打印收到的内容
printf("收到:%s\n", msg.mtext);

system V信号量

一、补充概念

  1. 多个执行流,能同时看到并访问的公共资源------共享资源
  2. 任何时候,只能有一个执行流访问公共资源------互斥;互斥的存在就是保护共享资源
  3. 被保护的共享资源------临界资源
  4. 临界资源就像要使用的东西,访问临界资源的代码------临界区;那么一段代码就能分为:临界区VS非临界区
  5. 计算机中要么不做,要不做完(最小操作元)------原子性
  6. 按顺序、有先后、你干完我再干,不是同时干,而是排队干------同步

二、什么是信号量

信号量(semaphore) = 用来控制多个进程 / 线程 "同时能有多少人" 访问共享资源的计数器。

最形象的例子:信号量 = 厕所坑位计数器

  • 厕所一共 2 个坑位 → 信号量初始值sem = 2
  • 有人进去 → P 操作(计数器 -1)
  • 有人出来 → V 操作(计数器 +1)
  • 计数器变成 0 → 其他人必须等待

信号量就是管 "能进几个人" 的!


信号量也可以想象成一个带计数功能的门卫

复制代码
P() :我要进去(申请资源)
      ↓
门卫检查还有没有名额

V() :我出来了(释放资源)
      ↓
门卫把名额还回去
      ↓
通知排队的人进来

二元信号量 :当sem = 1时,一次只能放进来一个人,实现了互斥

多元信号量:sem > 1。

三、相关接口

1. semget

c 复制代码
int semget(key_t key, int nsems, int semflg);

作用:拿到一个信号量集合(可以包含多个计数器)

  1. key:调用ftok
  2. nsems要几个信号量(通常填 1)
  3. semflg:IPC_CREAT |(IPC_EXCLL)| 0666
  4. 返回:成功 → semid(信号量 ID),失败 → -1

nsems参数:说明了信号量可以有多个,都被存放在了一个信号量集数组中。

2. semctl

c 复制代码
int semctl(int semid, int semnum, int cmd, ...);

作用:设置初始值、删除、获取信息

参数1int semid

  • 信号量集合的 ID
  • 来自 semget() 的返回值

参数2**semnum**:第几个信号量(在信号量集合数组中的下标)

参数3**cmd**:

  • SETVAL:设置初始值

  • IPC_RMID:删除信号量

  • 返回:成功 0,失败 -1

参数4...初始化信号量时要填的联合体

c 复制代码
// Linux中,必须用自己定义的联合体进行初始化
union semun
{
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

union semun arg;
arg.val = 1;

semctl(semid, 0, SETVAL, arg);
c 复制代码
// 删除0号信号量
semctl(semid, 0, IPC_RMID);

3. semop

c 复制代码
int semop(int semid, struct sembuf *sops, unsigned int nsops);

作用 :执行 P (-1) 申请V (+1) 释放

参数3unsigned int nsops

  • 要执行 多少个 sembuf 操作
  • 我们一次只做一个 P 或 V → 固定填 1

参数2struct sembuf *sops最重要的参数

c 复制代码
struct sembuf {
    short sem_num;   // 信号量下标(第几个信号量)
    short sem_op;    // 操作:-1=P,+1=V
    short sem_flg;   // 标志:填 0 → 阻塞(最常用);填 IPC_NOWAIT → 不阻塞,失败直接返回
};

struct sembuf 是头文件 <sys/sem.h> 中自带的一个结构体,在使用时不用再去定义这个结构体,只需要声明一个这种结构体变量就行。

一般写法

c 复制代码
// P 操作(申请资源 -1)
struct sembuf sem_buff = {
    .sem_num = 0,    // 第 0 个信号量
    .sem_op = -1,    // P 操作
    .sem_flg = 0     // 阻塞
};
semop(semid, &sem_buff, 1);  // 执行
c 复制代码
// V 操作(释放资源 +1)
struct sembuf sem_buff = {
    .sem_num = 0,    // 第 0 个
    .sem_op = 1,     // V 操作
    .sem_flg = 0
};
semop(semid, &sem_buff, 1);

四、P/V操作的原子性

假设信号量初始值:sem = 1

有两个进程同时执行 P 操作:P(sem);

如果 sem-- 不是原子的,可能发生:

复制代码
时间线:

A读取 sem=1

                B读取 sem=1

A计算 1-1=0

                B计算 1-1=0

A写回 sem=0

                B写回 sem=0

最终:sem = 0

看起来没问题,但实际上:两个进程都认为自己获得了资源

结果:两个进程同时进入临界区

同步机制彻底失效。

所以实际上内核做的是

c 复制代码
P()
{
    原子地 {
        sem--;

        if(sem < 0)
        {
            将当前进程加入等待队列;
            阻塞当前进程;
        }
    }
}
c 复制代码
V()
{
    原子地 {
        sem++;

        if(有等待进程)
        {
            唤醒一个等待进程;
        }
    }
}
相关推荐
RisunJan1 小时前
Linux命令-nohup(使进程忽略挂起(HUP)信号并在后台继续运行)
linux·运维·服务器
kebidaixu1 小时前
板级设备树驱动修改实战:从PWM到CAN,释放GPIO的完整指南
linux
一码当前1 小时前
【全志】 OKT153(sun8iw22) 启动链全流程详解
linux
键盘上的猫头鹰2 小时前
【Linux 基础教程(一)】概述、安装与网络配置:VMware + CentOS + NAT + XShell 远程连接
linux·网络·centos
枳实-叶2 小时前
【Linux驱动开发】第18天:I2C驱动深度解析
linux·运维·驱动开发
shandianchengzi2 小时前
【记录】Ubuntu|Ubuntu 26.04 笔记本耗电过快,排查 省电过程
linux·运维·ubuntu
陳10302 小时前
Linux:信号
linux·运维·服务器
小此方2 小时前
Re:Linux系统篇(二十五)进程篇·十:深度硬核!Linux 进程等待,从 task_struct 源码到位图状态解构
linux·运维·驱动开发
z202305082 小时前
RDMA之DCQCN (14)
linux·服务器·网络·人工智能·ai