✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人



🌇前言

System V 通信标准中,还有一种通信方式:消息队列,以及一种实现互斥的工具:信号量;随着时代的发展,这些陈旧的标准都已经较少使用了,但作为 IPC 中的经典知识,我们可以对其做一个简单了解,扩展 IPC 的知识栈,尤其是 信号量,可以通过它,为以后多线程学习中 POSIX 信号量的学习做铺垫

图示


🏙️正文

1、消息队列

1.1、什么是消息队列?

消息队列(Message Queuing)是一种比较特殊的通信方式,它不同于管道与共享内存那样借助一块空间进行数据读写,而是 在系统中创建了一个队列,这个队列的节点就是数据块,包含类型和信息

  • 假设现在进程 AB 想要通过消息队列进行通信,首先创建一个消息队列
  • 然后进程 A 将自己想要发送给进程 B 的信息打包成数据块(其中包括发送方的信息),将数据块添加至消息队列队尾处
  • 进程 B 同样也可以向消息队列中添加数据块,同时也会从消息队列中捕获其他进程的数据块,解析后进行读取,这样就完成了通信

图示
遍历消息队列时,存数据块 还是 取数据块 取决于 数据块中的类型 type

注意: 消息队列跟共享内存一样,是由操作系统创建的,其生命周期不随进程,因此在使用结束后需要删除

因为消息队列比陈旧且较少使用了,所以这里就不详细讲解原理,关于消息队列更详细的介绍可以看看这两篇文章:

1.2、消息队列的数据结构

同属于 System V 标准,消息队列也有属于自己的数据结构

注:msg 表示 消息队列

struct msqid_ds
{
	struct ipc_perm msg_perm;	/* Ownership and permissions */
	time_t msg_stime;			/* Time of last msgsnd(2) */
	time_t msg_rtime;			/* Time of last msgrcv(2) */
	time_t msg_ctime;			/* Time of last change */
	unsigned long __msg_cbytes; /* Current number of bytes in queue (nonstandard) */
	msgqnum_t msg_qnum;			/* Current number of messages in queue */
	msglen_t msg_qbytes;		/* Maximum number of bytes allowed in queue */
	pid_t msg_lspid;			/* PID of last msgsnd(2) */
	pid_t msg_lrpid;			/* PID of last msgrcv(2) */
};

共享内存 一样,其中 struct ipc_perm 中存储了 消息队列的基本信息,具体包含内容如下:

struct ipc_perm
{
	key_t __key;		  /* Key supplied to msgget(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 */
};

可以通过 man msgctl 查看函数使用手册,其中就包含了 消息队列 的数据结构信息

1.3、消息队列的相关接口

论标准的重要性,消息队列的大小接口风格与共享内存一致,都是出自 System V 标准

1.3.1、创建

使用 msgget 函数创建 消息队列

图解

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

int msgget(key_t key, int msgflg);

关于 msgget 函数

组成部分含义
返回值 int创建成功返回消息队列的 msqid,失败返回 -1
参数1 key_t key创建共享内存时的唯一 key 值,通过函数计算获取
参数2 int msgflg位图,可以设置消息队列的创建方式及创建权限

共享内存shmget 可以说是十分相似了,关于 ftok 函数计算 key 值,这里就不再阐述,可以在这篇文章中学习 《Linux进程间通信【共享内存】

简单使用函数 msgget 创建 消息队列,并使用 ipcs -q 指令查看资源情况

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

using namespace std;

int main()
{
    //创建消息队列
    int n = msgget(ftok("./", 668), IPC_CREAT | IPC_EXCL | 0666);
    if(n == -1)
    {
        cerr << "msgget fail!" << endl;
        exit(1);
    }
    return 0;
}

图示

程序运行后,创建出了一个 msqid0 的消息队列

因为此时并 没有使用消息队列进行通信,所以已使用字节 used-bytes 和 消息数 messages 都是 0

注意:

  • 消息队列在创建时,也需要指定创建方式:IPC_CREATIPC_EXCL权限 等信息
  • 消息队列创建后,msqid也是随机生成的,大概率每次都不一样
  • 消息队列生命周期也是随操作系统的,并不会因进程的结束而释放
1.3.2、释放

消息队列也有两种释放方式:通过指令释放、通过函数释放

释放指令:ipcrm -q msqid 释放消息队列,其他 System V 通信资源也可以这样释放

  • ipcrm -m shmid 释放共享内存
  • ipcrm -s semid 释放信号量集

结果

释放函数:msgctl(msqid, IPC_RMID, NULL) 释放指定的消息队列,跟 shmctl 删除共享内存一样

图示

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

int msgctl(int msqid, int cmd, struct msqid_ds *buf);

关于 msgctl 函数

组成部分含义
返回值 int成功返回 0,失败返回 -1
参数1 int msqid待控制的消息队列 id
参数2 int cmd控制消息队列的具体动作,同样是位图
参数3 struct msqid_ds *buf用于获取或设置所控制消息队列的数据结构

简单回顾下参数2部分可传递参数:

  • IPC_RMID 表示删除共享内存
  • IPC_STAT 用于获取或设置所控制共享内存的数据结构
  • IPC_SET 在进程有足够权限的前提下,将共享内存的当前关联值设置为 buf 数据结构中的值

同样的,消息队列 = 消息队列的内核数据结构(struct msqid_ds) + 真正开辟的空间

1.3.3、发送

利用消息队列发送信息,即 将信息打包成数据块,入队尾,所使用函数为 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);

关于 msgsnd 函数

组成部分含义
返回值 int成功返回 0,失败返回 -1
参数1 int msqid待发送数据块的消息队列 id
参数2 const void *msgp待发送的数据块
参数3 size_t msgsz待发送数据块大小
参数4 int msgflg表示发送数据块的方式,一般默认为 0

参数2 表示待发送的数据块,这显然是一个结构体类型,需要自己定义,结构如下:

struct msgbuf
{
    long mtype;    /* message type, must be > 0 */
    char mtext[1]; /* message data */
};

mtype 就是传说中数据块类型,据发送方而设定;mtex 是一个比较特殊的东西:柔性数组,其中存储待发送的 信息,因为是 柔性数组,所以可以根据 信息 的大小灵活调整数组的大小

关于 柔性数组 的详细介绍可以看看这篇文章 《C语言进阶——动态内存管理

1.3.4、接收

消息发送后,总得接收吧,既然发送是往队尾中添加数据块,那么接收就是 从队头中取数据块,假设所取数据块为自己发送的,那么就不进行操作,其他情况则取出数据块,使用 msgrcv 函数接收信息

图示

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

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

关于 msgrcv 函数

组成部分含义
返回值 int成功返回实际从 mtext 数组中读取的字节数,失败返回 -1
参数1 int msqid待接收数据块的消息队列 id
参数2 void *msgp接收到的数据块,是一个输出型参数
参数3 size_t msgsz要接收数据块的大小
参数4 long msgtyp要接收数据块的类型
参数5 int msgflg表示接收数据块的方式,一般默认为 0

同样的,接收的数据结构如下所示,也包含了 类型柔性数组

struct msgbuf
{
    long mtype;    /* message type, must be > 0 */
    char mtext[1]; /* message data */
};

1.4、消息队列小结

消息队列 的大部分接口都与 共享内存 近似,所以掌握 共享内存 后,即可快速上手 消息队列

但是如你所见,System V 版的 消息队列 使用起来比较麻烦,并且过于陈旧,现在已经较少使用了,所以我们不必对其进行深究,知道个大概就行了,如果实际中真遇到了,再查文档也不迟


2、信号量

2.1、什么是信号量?

信号量(semaphore)一种特殊的工具,主要用于实现 同步和互斥

信号量 又称 信号灯,是各大高校《操作系统》课程中老师提及的高频知识点,往往伴随着 P、V 操作出现,但大多数老师都只是提及了基本概念,并未对 信号量 的本质及使用场景作出详细讲解

在正式学习 信号量 相关知识前,需要先简单了解下 互斥相关四个概念,为后续 多线程中信号量的学习作铺垫(重点)

2.2、互斥相关概念

1、并发 是指系统中同时存在多个独立的活动单元

  • 比如在多线程中,多个执行流可以同时执行代码,可能访问同一份共享资源

2、互斥 是指同一时刻只允许一个活动单元使用共享资源

  • 即在任何一个时刻,都只允许一个执行流进行共享资源的访问(可以通过加锁实现)

3、临界资源临界区,多执行流环境中的共享资源就是 临界资源,涉及 临界资源 操作的代码区间即 临界区

  • 在多线程环境中,全局变量就是 临界资源,对全局变量的修改、访问代码属于 临界区

4、原子性:只允许存在 成功 和 失败 两种状态

  • 比如对变量的修改,要么修改成功,要么修改失败,不会存在修改一半被切走的状态

所以 互斥 是为了解决 临界资源 在多执行流环境中的并发访问问题,需要借助 互斥锁 或 信号量 等工具实现 原子操作,实现 互斥

图示

关于互斥锁(mutex) 的相关知识在 多线程 中介绍,现在先来学习 信号量,搞清楚它是如何实现 互斥

2.3、信号量的感性理解

将整个程序看作现实世界,形色各异的人看作 执行流,电影院 等公共资源看作 临界区,而单场电影的电影票看作 临界资源,主角 信号量 就是电影院中单场电影余票的 计数器,即余票越多,计数器值越大,当有人买票时,计数器 -1,当有人看完电影时,计数器 +1

当电影票卖完时,计数器归零,其他想看电影的人也无法购票观看本场电影

下面这些情况应运而生:

  1. 当你购票成功后,计数器 -1,你必然可以去看这场电影,其他人也无法与你争夺,因为那个位置当电影放映之时就是属于你一个人的
  2. 如果你买票晚了,票已告罄,计数器为 0,你就无法购票观看这场电影,即使自己偷偷溜进去也不行,会被保安叉出去,这是规定
  3. 得益于计数器的控制,电影院在放映电影时,有效划分了电影票这个 临界资源 的所属权限,从而保证了在电影放映时,绝对不会发生位置冲突、位置爆满、非法闯入等各种情况

看电影

信号量 的设计初衷也是如此,就是为了避免 因多执行流对临界资源的并发访问,而导致程序运行出现问题

因为电影院一次能容纳几十个人,所以可能不太好理解 互斥 这个概念,将场景特殊化,现在有一个 顶级VIP放映室,每天饮料零食随便吃,但 一次只允许一个人看电影,与普通电影院一样,这个 顶级VIP放映室 也有自己的售票系统,其本质同样是 计数器,但此时 计数器初始值为 1

所以:当一群人都想进这个顶级VIP放映室看电影时,必须等到 计数器 为 1 时,才能进行抢票,才有资格进去看电影,当然一次只能放一个人进去,同时计数器是否恢复 1,取决于上一个看电影的人是否出了放映室 -> 看电影结束 -> 计数器 +1

规定:只允许一个人看电影

图示

透过现象看本质,在 顶级VIP看电影 不就是代码中 多个执行流对同一个临界资源的互斥访问吗? 此时的 信号量 可以设为 1,确保 只允许一个执行流进行访问,这种 信号量 被称为 二元信号量,常用来实现 互斥

综上所述,信号量本质上就是 计数器 count,所谓的 P 操作(申请)就是在对 count--V 操作(归还)则是在对 count++

2.4、信号量的数据结构

下面来看看 信号量 的数据结构,通过 man semctl 进行查看

注:sem 表示 信号量

struct semid_ds
{
    struct ipc_perm sem_perm; /* Ownership and permissions */
    time_t sem_otime;         /* Last semop time */
    time_t sem_ctime;         /* Last change time */
    unsigned long sem_nsems;  /* No. of semaphores in set */
};

System V 家族基本规矩,struct ipc_perm 中存储了 信号量的基本信息,具体包含内容如下:

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_perm 结构体中的内容都是一模一样的,结构上的统一可以带来管理上的便利,具体原因可以接着往下看

2.5、信号量的相关接口

2.5.1、创建

信号量的申请比较特殊,一次可以申请多个信息量,官方称此为 信号量集,所使用函数为 semget

图示

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

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

关于 semget 函数

组成部分含义
返回值 int创建成功返回信号量集的 semid,失败返回 -1
参数1 key_t key创建信号量集时的唯一 key 值,通过函数 ftok 计算获取
参数2 int nsems待创建的信号量个数,这也正是 集 的来源
参数3 int semflg位图,可以设置消息队列的创建方式及创建权限

除了参数2,其他基本与另外俩兄弟一模一样,实际传递时,一般传 1,表示只创建一个 信号量

使用函数创建 信号量集,并通过指令 ipcs -s 查看创建的 信号量集 信息

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

using namespace std;

int main()
{
    //创建一个信号量
    int n = semget(ftok("./", 668), 1, IPC_CREAT | IPC_EXCL | 0666);
    if(n == -1)
    {
        cerr << "semget fail!" << endl;
        exit(1);
    }
    return 0;
}

结果

程序运行后,创建了一个 信号量集nsems1,表示在当前 信号量集 中只有一个 信号量

注意:

  • 信号量集在创建时,也需要指定创建方式:IPC_CREATIPC_EXCL权限 等信息
  • 信号量集创建后,semid也是随机生成的,大概率每次都不一样
  • 信号量集生命周期也是随操作系统的,并不会因进程的结束而释放
2.5.2、释放

老生常谈的两种释放方式:指令释放、函数释放

指令释放:直接通过指令 ipcrm -s semid 释放信号量集

释放

通过函数释放:semctl(semid, semnum, IPC_RMID),信号量中的控制函数有一点不一样

图示

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

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

关于 semctl 函数

组成部分含义
返回值 int成功返回 0,失败返回 -1
参数1 int semid待控制的信号量集 id
参数2 int semnum表示对信号量集中的第 semnum 个信号量作操作
参数3 int cmd控制信号量的具体动作,同样是位图
参数4 ...可变参数列表,不止可以获取信号量的数据结构,还可以获取其他信息

注意:

  • 参数2 表示信号量集中的某个信号量编号,从 1 开始编号
  • 参数3 中可传递的动作与共享内存、消息队列一致
  • 参数4 就像 printfscanf 中最后一个参数一样,可以灵活使用
2.5.3、操作

信号量的操纵比较ex,也比较麻烦,所以仅作了解即可

使用 semop 函数对 信号量 进行诸如 +1-1 的基本操作

图示

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

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

关于 semop 函数

组成部分含义
返回值 int成功返回 0,失败返回 -1
参数1 int semid待操作的信号量集 id
参数2 struct sembuf *sops一个比较特殊的参数,需要自己设计结构体
参数3 unsigned nsops可以简单理解为信号量编号

重点在于参数2,这是一个结构体,具体成员如下:

unsigned short sem_num;  /* semaphore number */
short          sem_op;   /* semaphore operation */
short          sem_flg;  /* operation flags */

其中包含信号量编号、操作等信息,需要我们自己设计出一个结构体,然后传给 semop 函数使用

可以简单理解为:sem_op 就是要进行的操作,如果将 sem_op 设为 -1,表示信号量 -1(申请),同理 +1 表示信号量 +1(归还)

sem_flg 是设置动作,一般设为默认即可

当然这些函数我们不必深入去研究,知道个大概就行了

2.6、信号量小结

信号量 是实现 互斥 的其中一种方法,具体表现为:资源申请,计数器 -1,资源归还,计数器 +1,只有在计数器不为 0 的情况下,才能进行资源申请,可以设计 二元信号量 实现 互斥

System V 中的 信号量 操作比较麻烦,但 信号量 的思想还是值得一学的,等后面学习 多线程 时,也会使用 POSIX 中的 信号量 实现 互斥,相比之下,POSIX 版的信号量操作要简单得多,同时应用也更为广泛

因为 信号量 需要被多个独立进程看到,所以 信号量 本身也是 临界资源,不过它是 原子 的,所以可以用于 互斥

  • 多个独立进程看到同一份资源,这就是 IPC 的目标,所以 信号量 被划分至进程间通信中

3、深入理解 System V 通信方式

不难发现,共享内存、消息队列、信号量的数据结构基本一致,并且都有同一个成员 struct ipc_perm,所以实际对于 操作系统 来说,对 System V 中各种方式的描述管理只需要这样做:

  • 将 共享内存、消息队列、信号量对象描述后,统一存入数组中
  • 再进行指定对象创建时,只需要根据 ipc_id_arr[n]->__key 进行比对,即可当前对象是否被创建!
  • 因为 struct shmid_dsstruct ipc_perm shm_perm 的地址一致(其他对象也一样),所以可以对当前位置的指针进行强转:((struct shmid_ds)ipc_id_arr[0]) 即可访问 shmid_ds 中的成员,这不就是多态中的虚表吗?

这样一来,操作系统可以只根据一个地址,灵活访问 两个结构体中的内容,比如 struct ipc_perm shm_permstruct shmid_ds,并且操作系统还把多种不同的对象,描述融合入了一个 ipc_id_arr 指针数组中,真正做到了 高效管理

注:默认 ipc_id_arr[n] 访问的是 struct ipc_perm 中的成员

图示

注:上述图示只是一个草图,目的是为了辅助理解原理,并非操作系统中真实样貌

操作系统在进行比较判断时,如何判断类型呢?

  • 这就是操作系统设计的巧妙之处了,ipc_id_arr 没那么简单,它会存储对象的相应类型信息

通过下标(id) 访问对象,这与文件系统中的机制不谋而合,不过实现上略有差异,间接导致 System V 的管理系统被边缘化(历史选择了文件系统)

shmidmsqidsemid 都是 ipc_id_arr 的下标,为什么值很大呢?

  • 在进行查找时,会将这些 id % 数组大小 进行转换,确保不会发生越界,事实上,这个值与开机时间有关,开机越长,值越大,当然到了一定程度后,会重新轮回

将内核中的所有 ipc 资源统一以数组的方式进行管理

  • 假设想访问具体 ipc 中的资源,可以通过 ipc_id_arr[n] 强转为对应类型指针,再通过 -> 访问其中的其他资源

以上方法就是 多态,通过父类指针,访问成员


🌆总结

以上就是本次关于 Linux 进程间通信【消息队列、信号量】的全部内容了,消息队列和信号量相对来说不怎么重要,因此本文主要以理论为主,并未涉及很多实操代码;本文中最重要的内容莫过于理解 互斥 相关概念与 信号量 实现互斥的原理,最后关于操作系统对 System V 通信相关资源的封装也算得上是精彩绝伦


星辰大海

相关文章推荐

Linux进程间通信【共享内存】

Linux进程间通信【命名管道】

Linux进程间通信【匿名管道】

Linux基础IO【软硬链接与动静态库】

Linux基础IO【深入理解文件系统】

Linux【模拟实现C语言文件流】

Linux基础IO【重定向及缓冲区理解】

GitHub 加速计划 / li / linux-dash
6
1
下载
A beautiful web dashboard for Linux
最近提交(Master分支:3 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐