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

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

目录

1、system V 消息队列 

1.1、什么是System V消息队列

1.2、基本原理

1.2.1、消息队列的结构

1.2.2、消息队列的操作

1.2.3、补充知识

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

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐