你了解system V的ipc底层如何设计的吗?消息队列互相通信的原理是什么呢?是否经常将信号量和信号混淆呢?——问题详解

**前言:**本节主要讲解消息队列, 信号量的相关知识。 ------博主主要是以能够理解为目的进行讲解, 所以对于接口的使用或者底层原理很少涉及。 主要的讲解思路就是先讨论消息队列的原理, 提一下接口。 然后讲解ipc的设计------这个设计一些底层原理。 最后就是会让友友们理解一下信号量的相关概念。

ps:本届内容设计共享内存, 友友们务必学完共享内存后再来观看哦

目录

消息队列的原理

消息队列的接口

msgget

msgctl

ipc在内核里面的设计

信号量

储备知识

理解信号量

信号量接口

信号量与进程间通信


消息队列的原理

本节讲述的消息队列是system V标准的。 我们知道的是, 我们的进程, 因为是用户写的代码, 有操作系统的task_struct以及地址空间, 所以它是在用户和操作系统之间的。 而消息队列,是属于内核的。那么这里就是下图的样子:


想要让上面两个进程a 和 b进行通信, 前提是,必须先让两个进程看到同一份资源!!!(这一句话是通信领域的结论, 地位就相当于管理层面的先描述再组织)

现在我们要知道的是这个同一份资源, 有可能是文件缓冲区, 有可能是内存块 。 所以,公共资源种类的不同, 决定了通信方式的不同。 其中,以文件的形式给我们一个文件缓冲区, 这就叫做管道。是否允许匿名,这个就叫做匿名管道给我们一个内存块, 映射到地址空间, 这个就叫做共享内存;也可以给我一个队列, 这个队列就是我们在数据结构里面学到的那个队列, 也就是消息队列。 现在来谈这个队列。

这个队列, 如果我们想要让两个进程a,b能够进行通信。 就必须让两个进程看到同一个队列。 当两个不同进程看到同一个队列之后 ,我们知道, 两个进程想要通信, 就要能够将数据块入队列当中。 但是问题来了, 两个进程怎么保证, 能够拿到对方的数据块呢 ?这个是因为允许不同的进程能够向内核中发送不同类型的数据块。 这些数据块假如a进程的是a类型的数据块, b进程的假如是b类型的数据块。 那么a进程要获取b进程的通信, 就可以拿到消息队列里面的b类型的数据块。 所以, 综上, 消息队列是什么?------消息队列的原理就是:a进程, b进程以数据块的形式发送数据进行通信!!!

消息队列的接口

注:消息队列的接口不作为博主讲解的重点, 如果想要学习相关接口的友友自行去查阅相关资料哦。

msgget

里面的返回值是int类型。 成功返回,会返回一个消息队列标识符。 失败返回-1。------无需指明大小, 只需直接创建即可。 第二个参数是就是那个key(使用路径和项目id确定的唯一key), 第三个参数就是创建消息队列的使用方式。

msgctl

这个函数是用来控制消息队列,这里不讲解这个接口的用法, 我们用这个接口和信号量的控制接口, 以及共享内存的控制接口来进行对比, 下面是信号量和共享内存的控制接口:

由上面三个函数我们就可以观察到, 我们的进程间通信, 是被精心设计过的, 无论是我们的共享内存 , 还是我们的消息队列 , 亦或者我们的信号量 。 他在内核层面上都有我们的semid_ds这样的结构, XXXid_ds结构体。 并且, 他们三个的XXXid_ds结构体里面第一个成员一定是struct_ipc_perm XXX_perm。并且, 这种结构体里面包含的字段全部都是一样的, 都是key还有各种权限。

ipc在内核里面的设计

在操作系统中, 所有的IPC资源, 都是整合进操作系统的IPC模块中的在以后的过程中, 我们管理共享内存, 管理消息队列, 管理信号量, 其实本质上就是管理下面这三种结构体:

那么我们如何把这些数据结构管理起来呢?在操作系统当中, 其实也是用数组管理的:

如果我们今天创建一个共享内存, 那么操作系统当中, 就要为我们创建shmid_fd这样的结构。 那么我们就要把shmid_ds的第一个字段填到数组的零号下标里面

那么, 如果后来我们又创建了一个消息队列. 没关系, 虽然我们的struct_ds的类型不一样, 但是我们的结构体的第一个字段和上面的字段是一样的。 所以, 我们就把这个第一个字段的地址, 填到数组的二号下标里面。 同样的信号量. 所以, 从此往后, 我们要管理操作系统内部的不同的IPC资源要如何如何进行管理呢?------答案是先描述:对于不同的资源, 我们可以使用不同的描述的方式。 再组织, 我们可以对不同的资源进行增删查改, 最后转化为对该数组进行增删查改。

未来呢, 我们一个进程如果想要申请一个共享内存, 那么申请共享内存, 就会给我们一个key, 然后操作系统就会遍历这个数组, 找到每一个ipc资源, 比较key, 确认是否已经创建过共享内存。 ------其中的, 这里的每一个ipc_perm结构, 他对应的数组的下标, 就是我们所说的shmid或者XXXid!!!

那么, 我们如何利用这个数组元素访问XXXid_ds里面的其他字段 呢?------只需要强转一下我们的类型------((shmid_ds*)array[])->某个字段------但是问题来了, 它是怎么知道要强转成为哪一个类型呢?ipc_perm是操作系统在用户层做的让我们用户看到的属性, 但是内核层面上叫做kern_ipc_perm, 这个东西里面有mod, delete等等。 其中mod里面就有一个选项, 这个选项是一种类型标志位, 让代码去区分自己是哪一种ipc资源。------也就是说, 操作系统能够区分指针指向的对象的类型(事实上, 我们也可以, 就是在对象里面添加一个标志位即可)。

其中, 这个数组其实就是实现了多态!!!而其中的ipc_perm就是基类, 其他的字段就是子类。 指针数组指向了这些ipc_perm, 就是实现了多态。

这个数组是操作系统层面创建的数组, 和进程没有关系, 所以这个下标和文件描述符并不类似。 shmid这个下标是线性递增的。 就比如我们今天申请了这个资源, 释放掉, 下次再申请, 这个资源的数字会变大。 ------不会因为释放而减小。 但是不需要担心越界, 因为他是会发生回绕的, 但是我们的这个数组的下标永远是从零开始的, 只是在使用的时候会有一个起始计数器, 我们最后的shmid就会使用这个起始计数器加上我们的小标进行计算。

信号量

储备知识

理解信号量, 我们需要先有一些储备知识, 先回忆一下这一张图

这张图, 当a进程想要读取, b进程想要写入。 他们会不会发生错乱呢? ------比如我们今天a进程想要给b进程发送100字节, 并且这100个字节是应该整体被读取的 。 但是如果这个时候a刚写了50个字节, b进程就过来读了, 那么b进程就可能把这50个字节先读走。 ------这就造成了一种情况,b进程只读取了a进程想要发给他的信息的一部分, 这就是数据不一致问题

  • 那么a和b看到的同一份资源, 我们叫做共享资源, 因为是共享的, 如果不加保护, 可能会导致数据不一致问题。
  • 那么我们要如何保护呢?就要用到一种加锁的方式。通过加锁保证一种工作状态。------这就是互斥访问。即:任何时刻只允许一个执行流, 访问共享资源。 ------这种概念称之为互斥, 就比如我们去ATM机取钱, 我们一个人去取钱了, 其他人就不能够进去了。
  • 共享资源是任何时刻只允许一个执行流访问(访问就是指的执行访问代码)的资源, 我们就叫做临界资源管道就是临界资源, 但是临界资源一般是内存空间。
  • 假设我们写了100行代码, 其中的5 ~ 10行在访问临界资源。 所以, 这些访问临界资源的代码, 就叫做临界区!!!

通过上面的讲解, 我们就能解释一个现象: 假如有五个进程在死循环的打印hello world。 为什么显示器上面的消息是:错乱的, 混乱的并且和命令行混在一起的。 ------这是因为我们在向显示器打印数据的时候, 是先向显示器文件的缓冲区中打印数据, 然后再将显示器文件的缓冲区的内容刷到显示器上面打印出来 。 这其中, 我们的进程,所有的进程都在向我们的显示器文件的缓冲区中打印数据, 很显然他们都能够看到我们的显示器文件, 那么我们的显示器文件就是一种共享资源。又因为我们的显示器文件没有保护机制, 那么多进程一起打印的时候, 就会发生数据不一致问题。 想要进行保护, 就要通过加锁互斥访问的形式变成临界资源!!!

理解信号量

那么如何理解信号量?信号量的本质, 其实就是一把计数器!类似于一个整数的的计数器:比如 int cn t------信号量是用来秒数临界资源中,资源数量的多少!!!

临界资源中的资源数量是什么意思?------就比如我们去看电影, 电影院有100个座位, 那么对应着就一定有100张电影票。当我们去看电影的时候, 如果我们买了票了, 但是没有去。那么此时电影院其实就是这么一个大的临界资源, 我们每一人对应每一个进程, 每一张票就代表着我们能够访问电影院里面的一个座位。 那么请问我们去看电影, 是我们做到这个座位上票是我的? 还是我们将票买到了, 这个座位是我的?是不是我们将票买到了, 这个座位就是我的了呢?------所以,**买票的本质, 是对资源的预定机制!!!**对于电影院, 需要维护一个票数的计数器, 每卖出一张票, 我们的计数器就要减一, 放映厅里面的资源就会少一个!!!票数计数器到零之后, 资源已经被申请完毕了!!! ------就如同下图:

这一块临界资源按照某个单位划分为一块一块的。 假如今天有一个执行流访问临界资源, 那么这个执行流可能只访问临界资源的一部分。

对于我们的临界资源, 如果今天有一个执行流想要访问临界资源, 那么他可能只访问其中的一部分。 我们对应的系统在临界资源的使用上, 我们可以让整个临界资源只被这一个执行流访问。但是这个执行流只访问临界资源其中的一块, 所以系统就可以只把这一块分配给这个执行流。 当又来了一个执行流的时候, 这个执行流也只是访问一块资源, 那么这个时候就没必要将整块资源全部锁住, 只需要将刚刚第一个执行流的资源锁住, 将那一块资源给第二个执行流就行了。 所以我们把临界资源拆成一小份, 前提是能够被拆成一小份, 并且每个执行流只使用一小份资源, 这种情况下, 我们就可以使得这两个执行流同时都进来访问。 这种情况可以提高多执行流访问临界资源的并发度, 一定程度上提高效率。

在这种情况下, 我们最怕的是, 多个执行流访问同一个资源, n个资源但是有n+个执行流。 排除我们分配资源的bug问题, 为了防止出现n个资源有n+个执行流的问题, 我们就会引入一个计数器:

int cnt = 15;(假如有15个资源)

int number = cnt--; (每申请一个资源就渐渐)

cnt <= 0; (小于等于0的时候资源没了, 不再分配资源, 直到有资源空出来了!)

那么, 经过上面的解释, 我们可以得出结论------

  • 1、我只要申请计数器成功了, 就表示我具有访问资源的权限了!
  • 2、申请了计数器资源, 就代表我当前要访问我的资源了吗?------并没有, 但是我们只要申请成功了, 并且没有释放, 那么这里面的资源就一定要有一个供我使用。------所以申请计数器资源的本质就是对资源的一种, 预定机制。
  • 3、如果多个执行流全部都申请, 那么计数器减为零, 就不能申请了。------这个就说明计数器可以有效保证进入共享资源的执行流的数量。
  • 4、所以每一个执行流想访问共享资源的时候, 不是直接访问, 而是先申请计数器资源, 就如同我们看电影需要先买票。
    那么, 上面提这么多还是没有提到信号量, 但是其实我们一直都在说信号量------因为这个"计数器"就被成为信号量!!!

现在再来看这个问题, 如果电影放映厅里面只有一个座位呢?------是不是也就意味着, 我们只需要一个值为一的计数器。------那么如果有人想买票, 就需要先抢这个座位, 并且所有人中, 只有一个人能够抢到, 也就只有一个人能看电影。 在放电影期间, 只有一个执行流在访问临界资源。 ------这个概念叫做互斥。 我们把只能为一为零两态的计数器叫做二元信号量, 本质就是一个锁!!!

那么, 我们要访问临界资源, 先要申请计数器资源, 所以信号量计数器资源的本质, 不也就是共享资源吗?------所以, 刚刚我们举得例子当中的int本身就是一个临界资源, 而这个临界资源是用来保护我们的要访问的临界资源的, 但是我们的信号量在保证别人的前提下, 是不是也要保护自己是安全的?(因为数据不一致可能发生在计数器上面)------那么请问这个计数器临界资源减减的时候是不是安全的呢?------答案是不是的:cnt--在c语言上, 是一条语句, 但是变成汇编语言, 那么至少会变成多条汇编语句,我们知道cnt是保存在内存中的, 而cnt在--的时候要转移到cdu中进行计算, 那么减减操作就是:

1、首先cnt变量的内存从内存转移到cpu中的寄存器中。 2、在cpu内进行计算。 3、将计算结果写回cnt变量的内存位置。

但是这里有一个问题:就是进程在运行的时候, 可以随意的切换, 有可能在我们的三步执行过程之间的某一个邻接点的时候,这个进程就被切走了!!切走的时候, 多执行流都访问这个变量时, 就有可能让cnt减出问题, 比如多减了一下, 或者少减了一下。

所以, 我们的cnt不能随意地减减, 所以这里就有了一个概念------申请计数器, 本质是对计数器减减, 这个操作在信号量当中专门封装了一个方法, 就好比这个信号量类里面有一个计数器成员,申请的时候就有一个成员方法------申请信号量, 对信号量做减减, 这个操作就可以成为p操作。 ------释放资源, 就好比释放信号量, 本质是对计数器进行++操作。 并且这个操作表示归还资源, 因为加加资源变多了, 就表示归还资源。 归还资源, 其他进程就可以申请了, 这个操作, 就叫做v操作。 ------信号量的申请和释放, 就被我们叫做pv操作。 ------其中, 这些操作, 在汇编层面上就会将我们的语句c语句变成多条汇编语句, 所以多执行流调度的时候, 一定会已出现多执行流混合交叉的问题, 就可能这个变量出现问题。 ------所以, 这里就需要我们我们申请和释放的这个pv操作必须是原子的。 ------什么是原子的? 在技术层面上来说, 如果一条语句转化为汇编语句, 只有一条汇编语句, 那么就是原子的。

所以, 通过以上的论述, 我们就能总结出一个结论。------就是对于信号量来说, 信号量本质就是一把计数器, 对于计数器匹配的操作, 就叫pv操作, 这个pv操作是原子的。 所有的执行流申请资源, 就必须先申请信号量资源, 得到信号量之后, 才能访问临界资源。 如果信号量的值为1/0两态的, 我们就称为二元信号量, 就是互斥。 其中, 我们申请信号量的本质, 就是对临界资源的预定机制!!!

信号量接口

信号量接口博主没有具体学习过, 这里不讲解具体, 只是提一下

其中第二个参数是申请多个信号量, 多个信号量和信号量是几, 是不一样的概念。

上面的参数中有一个结构体叫做sembuf, 这个结构体如下:

信号量与进程间通信

信号量为什么是进程间通信的一种?

  • 1、我们的通信, 不仅仅是传送数据, 通信也在于双方之间的互相协同。
  • 2、协同虽然不是以传送数据为目的。 但是他以时间通知为目的。 它的本质就是在传递信息, 只不过不是传统的hello world、hello linux这样的数据了。 ------就比如我们上课, 以及我们吃饭, 假如小王和校长, 小张吃饭的时候喜欢和小王吃, 所以小王吃饭的时候就会给小张打电话。 小王喜欢逃课, 小张在上课的时候, 如果有签到, 就给小王打电话。 这两个人互相通知,这个互相就叫做协同;这个通知, 就叫做信号。 这个通知是不是通信?是的!!!------所以,信号量的本质, 其实也是通信。

------------------以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!同时对于本节内容, 博主整理了笔记图片, 友友们可以保存方便查阅哦:

相关推荐
maosheng11465 小时前
RHCSA的第一次作业
linux·运维·服务器
wifi chicken6 小时前
Linux 端口扫描及拓展
linux·端口扫描·网络攻击
旺仔.2916 小时前
Linux 信号详解
linux·运维·网络
放飞梦想C6 小时前
CPU Cache
linux·cache
颜酱6 小时前
DFS 岛屿系列题全解析
javascript·后端·算法
Hoshino.417 小时前
基于Linux中的数据库操作——下载与安装(1)
linux·运维·数据库
小码哥_常7 小时前
Java后端定时任务抉择:@Scheduled、Quartz、XXL - Job终极对决
后端
uzong7 小时前
Skill 被广泛应用,到底什么是 Skill,今天详细介绍一下
人工智能·后端·面试
恒创科技HK7 小时前
通用型云服务器与计算型云服务器:您真正需要哪些配置?
运维·服务器
小码哥_常8 小时前
Kafka平替!SpringBoot+Redis Stream+消费组打造极致消息队列
后端