自学内容网 自学内容网

【Linux】 IPC 进程间通信(三)(消息队列 & 信号量)

📃个人主页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_IRUSRS_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);

 参数

  1. msqid 

    • 这是消息队列的标识符,是由 msgget() 函数返回的消息队列 ID
    • 表示要操作的目标消息队列
    • 在消息队列操作中,msqid 必须有效,即指向一个存在的消息队列
  2. cmd 

    • cmd 参数指定了 msgctl 要执行的操作类型。它有以下几种常用的值:

      • IPC_STAT:获取指定消息队列的当前状态和属性。
      • IPC_SET:修改指定消息队列的属性(如权限、队列容量等)。
      • IPC_RNID:删除指定的消息队列。

    这些命令定义了对消息队列的不同操作。

  3. 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) 是一种同步机制,用于控制多个进程或线程对共享资源的访问。它主要用于解决进程间的同步与互斥问题,防止资源冲突和数据竞争。信号量是一个整数,用来表示可用资源的数量,操作系统通过它来协调并发执行的进程

由于信号量本质是一个对资源进行预订的计数器,因此必须解决下面两个问题:

  1. 信号量必须能被多个进程看到 。
  2. 信号量的 - - 与 ++ 操作(PV操作)必须具有原子性

💢 原子性:原子性是指一个操作是不可中断的,即该操作要么全部执行成功,要么全部执行失败,不存在执行到中间某个状态的情况

1. 信号量的种类

  1. 计数信号量(Counting Semaphore)计数信号量的值可以为任何非负整数,表示资源的数量。当资源可用时,信号量的值增加;当资源被占用时,信号量的值减少。

    • P操作(Proberen):进程申请资源,信号量值减1,如果值为负,进程会被阻塞。
    • V操作(Verhogen):进程释放资源,信号量值加1,若有阻塞进程,唤醒其中一个。
  2. 二值信号量(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) 

参数:

  1. key:是一个键值,用于唯一标识信号量集
  2. nsems:指定信号量集中信号量的数量,通常这个值至少为1
  3. 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. 信号量的应用

  1. 互斥锁:二值信号量通常用于互斥,确保只有一个进程可以访问临界区,避免数据竞争。例如,多个进程需要访问共享文件时,可以使用信号量来保证每次只有一个进程能进行读写操作。

  2. 生产者消费者问题:信号量可以用于协调生产者和消费者的关系,控制缓冲区的读写操作。生产者放入商品时减少空位信号量,消费者取出商品时减少产品信号量,从而保证生产与消费的同步。

  3. 资源分配:在有限资源(如打印机、数据库连接等)情况下,信号量用来管理资源的分配,确保资源的公平使用。

  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] ->其它属性

  1. 那么一个指针,指向结构体的第一个元素,其实就是指向了整个结构体
  2. 访问头部,直接访问
  3. 访问其它属性,做强转,这种结构不就是C++中的多态吗?

这时,我们所看到的 kern_ipc_perm 就是 基类,与之相关的三个就是子类,继承了基类,此时就可以使用基类来管理所有的子类了,这是 C语言实现多态的另一种方式

那具体是怎么识别是哪一种子类的呢?

  • 实际在内核中,会定义各种的 ipc_ids,但是它们的 entries 指针都指向同一个 kern_ipc_perm 数组


四、小结

以上就是我对消息队列、信号量、IPC 的理解,那么我们的进程间通信(IPC) 就讲到这里啦,我们后面就开始进入进程信号的知识哩

【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果我的这篇博客可以给你提供有益的参考和启示,可以三连支持一下 !!


原文地址:https://blog.csdn.net/island1314/article/details/143574975

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!