【Linux系统编程】第三十七弹---深入理解System V IPC机制:消息队列、信号量与共享内存的实战解析

✨个人主页:熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

[1、system V 消息队列](#1、system V 消息队列)

[1.1、什么是System V消息队列](#1.1、什么是System V消息队列)

1.2、基本原理

1.2.1、消息队列的结构

1.2.2、消息队列的操作

1.2.3、补充知识

[2、system V信号量](#2、system V信号量)

2.1、基本概念

2.2、主要操作

2.3、相关函数

2.4、工作原理

3、共享内存,消息队列,信号量


1、system V 消息队列

在Linux系统中,进程间通信(IPC)是一个关键机制,它允许不同的进程间交换数据和信号。System V消息队列是这些IPC机制中的一种,它提供了一个通过消息进行同步和通信的方式。

1.1、什么是System V消息队列

System V消息队列是UNIX和类UNIX系统(如Linux)中一种经典的IPC机制,它允许多个进程通过发送和接收消息来进行通信。这些消息队列是由内核管理的,它们为进程提供了一个可靠的数据交换方式。

1.2、基本原理

一个进程向另一个进程发送有类型数据块的方式。

1.2.1、消息队列的结构

在System V消息队列中,每个消息队列都由一个消息队列描述符(通常是一个整数值)来标识。消息本身则是一个固定大小的数据块,由两部分组成:消息类型(message type)和消息数据(message data)。消息类型是一个正整数,用于对消息进行分类,使得消费者可以根据消息类型来选择性地接收消息。

1.2.2、消息队列的操作

System V消息队列提供了以下基本操作:

  • 创建或打开消息队列 :使用msgget()系统调用。如果指定的消息队列不存在,且调用者有足够的权限,msgget()将创建一个新的消息队列。

    // 创建一个消息队列

    #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/msg.h>

    int msgget(key_t key, int msgflg);

    参数:
    key:使用 "key_t ftok(const char *pathname, int proj_id);"函数获取key值
    msgflg: IPC_CREAT IPC_EXCL ,原理同共享内存

    返回值:
    成功返回0,失败返回-1并更新错误码

    // 获取key值
    #include <sys/types.h>
    #include <sys/ipc.h>

    key_t ftok(const char *pathname, int proj_id);

    返回值:
    成功返回key值,失败返回-1并更新错误码

  • 发送消息 :使用msgsnd()系统调用。它将一个消息发送到指定的消息队列中。如果队列已满(达到了系统允许的最大消息数),则发送操作可能会被阻塞或立即返回错误。

发送接收消息相关头文件

msgrcv, msgsnd - 消息队列操作
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

发送消息函数讲解

// 向指定消息队列发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:
    msgp:
    msqid:消息队列的标识符,是一个由 msgget() 函数返回的非负整数。
    msgp:指向消息缓冲区的指针,该缓冲区包含了要发送的消息。
消息缓冲区必须以 struct msgbuf 的形式组织,但通常使用更具体的结构体来匹配消息的实际内容。
struct msgbuf 通常至少包含 long mtype;(消息类型)和 char mtext[1];
(消息数据,实际大小可变)两个字段。
struct msgbuf {
               long mtype;       /* message type, must be > 0 */
               char mtext[1];    /* message data */
           };
    msgsz:消息数据部分的大小(不包括消息类型字段)。
    msgflg:控制 msgsnd() 行为的标志。如果设置了 IPC_NOWAIT 标志,
则 msgsnd() 在队列满时将不会阻塞;相反,它会立即返回一个错误。
如果没有设置 IPC_NOWAIT,则 msgsnd() 可能会阻塞。

返回值:
    成功时,msgsnd() 返回 0。
    出错时,返回 -1,并设置 errno 以指示错误的原因。可能的错误包括 
EAGAIN(在非阻塞模式下队列已满)、EIDRM(消息队列已被删除)、
EINTR(调用被信号中断)、EINVAL(消息大小无效或消息类型小于 0)、
EMSGSIZE(消息太大,无法放入队列)以及 EPERM(调用进程没有写权限)。
  • 接收消息 :使用msgrcv()系统调用。它从指定的消息队列中接收一个消息。消费者可以根据消息类型来过滤接收到的消息。

接收消息函数讲解

// 接收消息

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);

参数
    msqid:消息队列的标识符,是一个由 msgget() 函数返回的非负整数。
    msgp:指向消息缓冲区的指针,该缓冲区用于存储接收到的消息。
消息缓冲区必须以 struct msgbuf 的形式组织,但通常使用更具体的结构体来匹配消息的实际内容。
struct msgbuf 通常至少包含 long mtype;(消息类型)和 char mtext[1];
(消息数据,实际大小可变)两个字段。

    msgsz:缓冲区的大小(以字节为单位),它指定了 msgp 指向的缓冲区能够接收的最大消息数据量
(不包括消息类型字段)。
    msgtyp:用于指定接收消息的类型。其值可以有以下几种情况:
        0:接收队列中的第一个消息(不考虑消息类型)。
        大于0:接收队列中类型等于 msgtyp 的第一个消息。
        小于0:接收队列中类型值不大于 msgtyp 绝对值的最低优先级消息
(即类型值最小且不大于 msgtyp 绝对值的消息)。

    msgflg:控制 msgrcv() 行为的标志。如果设置了 IPC_NOWAIT 标志,
则 msgrcv() 在没有符合条件的消息时将不会阻塞;相反,它会立即返回一个错误。
如果没有设置 IPC_NOWAIT,则 msgrcv() 可能会阻塞。

返回值
    成功时,msgrcv() 返回实际接收到的消息数据的长度(不包括消息类型字段)。
    出错时,返回 -1,并设置 errno 以指示错误的原因。可能的错误包括 
EAGAIN(在非阻塞模式下没有符合条件的消息)、EIDRM(消息队列已被删除)、
EINTR(调用被信号中断)、ENOMSG(在非阻塞模式下且队列为空时尝试接收消息)、
EBADF(无效的消息队列标识符)等。
  • 控制消息队列 :使用msgctl()系统调用。它允许进程对消息队列执行各种控制操作,如删除队列、获取队列状态等。

    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() 函数返回的非负整数。
    cmd:指定要执行的操作。对于删除消息队列,应使用 IPC_RMID 命令。
    buf:指向 struct msqid_ds 结构体的指针。对于 IPC_RMID 命令,这个参数可以是 NULL,
    因为删除操作不需要修改消息队列的属性。然而,在一些系统或上下文中,
    为了保持代码的一致性和健壮性,建议总是传递一个有效的 struct msqid_ds 指针,
    并将其内容初始化为零(尽管在这种情况下,内核可能会忽略它)。

    返回值:
    成功时,msgctl() 返回 0。
    出错时,返回 -1,并设置 errno 以指示错误的原因。可能的错误包括
    EINVAL(无效的消息队列标识符或命令)、EIDRM(消息队列已被删除)、
    EPERM(调用进程没有相应的权限)等。

1.2.3、补充知识

消息队列声明周期也是随内核的,进程结束前不删除消息队列需要手动删除(命令!!!

ipcs -q # 查看消息队列属性信息
ipcrm -q msgid # 删除消息队列

[root@ubuntu:Msg]# ipcs -q

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages

    关键字(key):用于在创建消息队列时指定一个唯一的标识符。
这个关键字可以是任何整数值,但通常使用ftok()函数生成,以确保其唯一性。
    消息队列ID(msqid):每个消息队列都有一个唯一的标识符(ID),
用于区分系统中的其他消息队列。
    所有者(owner):显示创建消息队列的用户ID(UID)和组ID(GID),
表示该消息队列的拥有者。
    权限(perms):表示消息队列的访问权限,类似于文件系统的权限设置。
这些权限决定了哪些用户或组可以访问(读、写或控制)该消息队列。
    已用字节数(used-bytes):表示当前消息队列中已经占用的字节总数。
这有助于了解消息队列的使用情况。
    消息数量(messages):表示消息队列中当前存储的消息总数。
这是衡量消息队列负载的重要指标。

2、system V信号量

System V信号量是一种用于进程间通信(IPC)和同步的机制,它主要用于控制对共享资源的访问,解决竞争条件和死锁等并发编程中的问题。以下是对System V信号量的详细讲解:

2.1、基本概念

  1. 信号量集:System V信号量以信号量集的形式存在,一个信号量集可以包含多个信号量,每个信号量都可以独立地进行操作。
  2. 信号量类型
    • 二元信号量:其值只能为0或1,类似于互斥锁,用于控制单个资源的访问。
    • 计数信号量:其值在0和某个限制值之间,表示可用资源的数量。

补充5个概念:

1、多个执行流(进程)能看的一份资源:共享资源

2、被保护起来的资源:临界资源;临界资源的两个特性:同步和互斥;用互斥的方式保护共享资源:临界资源

3、互斥:任何时候只能有一个进程在访问共享资源

4、资源 --- 要被程序员访问 --- 资源被访问,朴素的认识就是通过代码访问,代码 = 访问共享资源的代码(临界区) + 不访问共享资源的代码(非临界区)

5、所谓对共享资源进行保护,本质是对访问共享资源的代码进行保护

对信号量的理论理解

信号量(信号灯) --- 保护临界资源(code)

信号量本质是一个计数器

使用生活中看电影来理解信号量:

普通用户去电影院看电影

  • 1、先买票
  • 2、让买票人(执行流)和座位(资源)一一对应 (程序员编码实现),不关心

看电影买票的本质:是对资源的 预定 机制!

最担心超过资源个数的买票!

int count = 25; // 总共25张票
if(count > 0) count--;
else wait;
// 购票
  • 电影院:共享资源(临界资源)
  • 买票:申请信号量
  • 票数:信号量初始值

申请信号量的本质是对公共资源的一种预定机制!!!

超级VIP去电影院看电影

使用信号量的通信过程:

  • 1、申请信号量
  • 2、访问共享内存
  • 3、释放信号量

信号量是一个计数器,能不能使用全局变量 gcount 标记信号量?

不能!!!

1、因为全局变量不能让所有进程看到。因此有了信号量,和共享内存,消息队列一样,必须先让不同的进程看到同一块资源(计数器)。意味着信号量也是一个公共资源,作用是保护临界资源的安全,前提自己得是安全的!!!

2、gcount ++/--不是原子(要么执行要么不执行)的。

2.2、主要操作

System V信号量支持两种基本操作:P(也称为wait或down)操作和V(也称为signal或up)操作。

  1. P操作
    • 用于获取(或等待)一个信号量。
    • 如果信号量的值大于0,则将其减一,并允许进程继续执行。
    • 如果信号量的值已经是0,则阻塞当前进程,直到信号量的值变为非0(即有其他进程释放了信号量),然后再将其减一。
  2. V操作
    • 用于释放(或增加)一个信号量。
    • 将信号量的值加一。
    • 如果有其他进程因为等待该信号量而被阻塞,则唤醒其中一个被阻塞的进程。

2.3、相关函数

  • 1、semget

    • 用于创建或获取一个System V信号量集。

    • 参数包括唯一标识信号量集的键值、信号量集中的信号量数量以及控制函数行为的标志位(如IPC_CREAT和IPC_EXCL)。

      #include <sys/types.h>
      #include <sys/ipc.h>
      #include <sys/sem.h>

      int semget(key_t key, int nsems, int semflg);

      参数:
      key:这是一个关键参数,用于标识信号量集。
      nsems:指定需要创建或检查的信号量的数量。当创建一个新的信号量集时,
      这个参数指定了集合中信号量的数量。如果是获取一个已存在的信号量集,
      这个参数通常被设置为0,因为不需要改变已有信号量集的大小。
      semflg:这个参数控制信号量集的访问权限和状态。

      返回值
      成功时:semget函数返回信号量集的标识符(一个非负整数),
      该标识符用于后续的信号量操作(如semop和semctl)。
      失败时:semget函数返回-1,并设置errno以指示错误类型。

  • 2、semctl

    • 对一个信号量执行各种控制操作,如获取或设置信号量的值、获取信号量集的状态信息等。

    • 常见的操作包括GETVAL(获取信号量的当前值)、SETVAL(设置信号量的值)、IPC_RMID(删除信号量集)等。

      #include <sys/types.h>
      #include <sys/ipc.h>
      #include <sys/sem.h>

      int semctl(int semid, int semnum, int cmd, ...);

      四个参数:
      semid:信号量集的标识符(ID),由semget函数返回。
      semnum:操作信号在信号集中的编号,从0开始。
      cmd:指定要执行的控制命令。
      ...:第四个参数根据cmd的不同而变化,可能是指向特定数据结构的指针,用于传递额外的信息或数据。

      参数cmd:
      PC_STAT:读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
      这允许调用者获取信号量集的当前状态信息。
      struct semid_ds {
      struct ipc_perm sem_perm; /* Ownership and permissions /
      time_t sem_otime; /
      Last semop time /
      time_t sem_ctime; /
      Creation time/time of last
      modification via semctl() /
      unsigned long sem_nsems; /
      No. of semaphores in set /
      };
      IPC_SET:设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
      这允许调用者更改信号量集的权限等属性。
      struct ipc_perm {
      key_t __key; /
      Key supplied to semget(2) /
      uid_t uid; /
      Effective UID of owner /
      gid_t gid; /
      Effective GID of owner /
      uid_t cuid; /
      Effective UID of creator /
      gid_t cgid; /
      Effective GID of creator /
      unsigned short mode; /
      Permissions /
      unsigned short __seq; /
      Sequence number */
      };
      IPC_RMID:将信号量集从内存中删除。这个操作会唤醒所有因调用semop()
      而阻塞在该信号量集合里的进程(这些调用会返回错误,并且errno被设置为EIDRM)。

  • 3、semop

    • 用于执行P和V操作。

    • 通过一个指向sembuf结构体的指针数组来指定要操作的信号量、操作类型(P或V)以及SEM_UNDO标志(用于在系统崩溃时自动恢复信号量的值)。

      #include <sys/types.h>
      #include <sys/ipc.h>
      #include <sys/sem.h>

      int semop(int semid, struct sembuf *sops, size_t nsops);

      参数:
      semid:信号量集的标识符(ID),由semget函数返回。
      sops:指向sembuf结构体数组的指针,该数组包含了要执行的操作序列。
      sembuf结构体通常包含三个成员:sem_num(信号量在信号集中的编号)、
      sem_op(要执行的操作,正数表示V操作,负数表示P操作,0表示等待信号量变为0)、
      sem_flg(操作标志,如IPC_NOWAIT表示非阻塞操作,
      SEM_UNDO表示在进程结束时自动撤销对该信号量的修改)。
      nsops:sops数组中sembuf结构体的数量,即要执行的操作个数。

      函数功能:
      semop函数按照sops数组中sembuf结构体的顺序,原子性地执行指定的操作序列。
      这意味着,要么所有操作都成功执行,要么都不执行,从而保证了操作的原子性。

      当sem_op为正数时,表示V操作,即释放资源。如果信号量的值加上sem_op后
      

      不会超出其允许的最大值,则操作成功,信号量的值相应增加。
      当sem_op为负数时,表示P操作,即请求资源。如果信号量的绝对值
      大于或等于sem_op的绝对值,则操作成功,信号量的值相应减少。如果信号量的值
      小于sem_op的绝对值,且sem_flg中未设置IPC_NOWAIT标志,则调用进程将被阻塞,
      直到信号量的值变为足够大或信号量集被删除。
      当sem_op为0时,semop函数将检查信号量的值是否为0。如果是,
      并且sem_flg中未设置IPC_NOWAIT标志,则调用进程将被阻塞,
      直到信号量的值变为非零。如果设置了IPC_NOWAIT标志,
      则函数将立即返回,并设置errno为EAGAIN。

      返回值
      成功执行时,semop函数返回0。
      失败时,semop函数返回-1,并设置errno以指示错误类型。
      可能的错误值包括EACCES(权限不足)、
      EAGAIN(资源暂时不可用且设置了IPC_NOWAIT标志)、EINVAL(无效参数)、
      EIDRM(信号量集已被删除)等。

信号量指令:

[root@ubuntu:Msg]# ipcs -s # 查看信号量属性信息

------ Semaphore Arrays --------
key        semid      owner      perms      nsems 

键(key):用于创建信号量集时指定的键,它是信号量集在系统中的一个引用或名称。
信号量集ID(semid):这是信号量集的唯一标识符,用于在系统中唯一标识一个信号量集。
不同的进程可以通过相同的键来访问同一个信号量集。
拥有者(owner):信号量集的创建者或当前所有者,通常表示为UID(用户ID)和GID(组ID)。
权限(perms):信号量集的权限设置,类似于文件的权限设置,用于控制哪些进程可以访问信号量集。
信号量数量(nsems):信号量集中包含的信号量个数,这取决于信号量集在创建时的设置。

ipcrm -s semid # 删除指定信号量集

[root@ubuntu:Msg]# ipcs # 查看进程间通信属性信息

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages    

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status      

------ Semaphore Arrays --------
key        semid      owner      perms      nsems 

2.4、工作原理

System V信号量通过内核中的数据结构来管理,每个信号量集都有一个对应的semid_ds结构体,用于存储信号量的权限、状态等信息。当进程通过semget、semctl和semop等函数对信号量进行操作时,内核会根据这些操作来更新信号量的值,并处理进程的阻塞和唤醒等操作。

3、共享内存,消息队列,信号量

OS是如何把共享内存,消息队列,信号量统一管理起来的?

先描述在组织。

1、System V标准

2、XXXget,XXXctl

3、xxxid_ds,struct ipc_perm

相关推荐
yaoxin5211238 分钟前
第二十七章 TCP 客户端 服务器通信 - 连接管理
服务器·网络·tcp/ip
内核程序员kevin10 分钟前
TCP Listen 队列详解与优化指南
linux·网络·tcp/ip
Theodore_10221 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
网易独家音乐人Mike Zhou2 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
‘’林花谢了春红‘’3 小时前
C++ list (链表)容器
c++·链表·list
----云烟----3 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024063 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic4 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it4 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康4 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud