前言:本节主要讲解消息队列, 信号量的相关知识。 ——博主主要是以能够理解为目的进行讲解, 所以对于接口的使用或者底层原理很少涉及。 主要的讲解思路就是先讨论消息队列的原理, 提一下接口。 然后讲解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 cnt——信号量是用来秒数临界资源中,资源数量的多少!!!
临界资源中的资源数量是什么意思?——就比如我们去看电影, 电影院有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这样的数据了。 ——就比如我们上课, 以及我们吃饭, 假如小王和校长, 小张吃饭的时候喜欢和小王吃, 所以小王吃饭的时候就会给小张打电话。 小王喜欢逃课, 小张在上课的时候, 如果有签到, 就给小王打电话。 这两个人互相通知, 这个互相就叫做协同;这个通知, 就叫做信号。 这个通知是不是通信?是的!!!——所以,信号量的本质, 其实也是通信。
——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!同时对于本节内容, 博主整理了笔记图片, 友友们可以保存方便查阅哦: