【Linux系统编程二十】:消息队列/信号量(system v标准的内核数据结构的设计模式)
- 一.消息队列
- 二.system v标准的内核数据结构的设计
- 三.四个概念(互斥/临界)
- 四.信号量
- 1.多线程并发访问
- 2.计数器
- 3.原子的
- 4.总结
一.消息队列
一个叫做a进程啊,一个叫做b进程。
首先让两个进程通信,那么前提条件是必须先让两个不同的进程先看到同一份资源。我们目前已经知道了,它可以是一个文件缓冲区,可以是一个内存块。所以。这个公共资源种类的不同,决定了通信方式的不同。
你以文件方式给我的一个内存缓冲区,行,这就叫做管道啊,那么允许命名还是匿名,那么我们就叫命名和匿名管道。你给我一个内存块映射到地址空间,这就叫做共享内存。当然我也可以给你一个叫做队列。这个队列呢,第一它得首先让a进程看到,二它得首先再让b进程看到。
进程向我们的内核叫做内核中。发送数据块。说白了就是我的a进程,它可以有一个简单的数据块。那么这个数据块呢,将来呢我们在系统内我们把它处理好了。
处理好之后呢,可以通过操作系统,让操作系统把这个数据块链入到我们对应的队列当中。那么后面呢b进程呢,它说我也想发送数据,此时他也要把自己的数据块链入到队列当中。
双方都可以把自己那么对应的数据呢,那么链入到我们对应的这个数据块当中,列入到这个数据块当中呢,其中呢我们就可以让a进程从队列当中拿b进程给a进程发的数据块,b进程就可以拿a进程给他发的数据块。
这样我们可以完成一个叫做基于数据块的啊,叫做我们可以通信发送啊,叫做以数据块的形式发送我们对应的数据。你发你的,我们俩有个共享的队列,你把你的数据呢往这个队列里放,他把他的数据往队列里放,那么此时我们两个就可以直接进行通信了。
那最后你扔数据块,我也数据块,可是这个数据块我怎么区分呀?我怎么知道这个数据块是我曾经自己写的,还是人家b给我发的?
所以我们这里的数据块要加前提,前提就是,发送带类型的数据块啊。
说白了你呢未来呢可以给你的数据块带上类型。
操作系统要管理消息队列,那么怎么管理呢?
答案:是先描述再组织。
所谓的消息队列,在我们看来就是操作操作系统内提供的对应的结构体对象。
它除了有队列这种结构之外,那么还必须得有对应的各种数据结构来描述它的消息队列。比如说这个消息队列有多少个数据节点啊,谁创建的,什么时候创建的,最近一次发消息的是谁最近一次收消息的是谁?
消息队列的使用方式跟共享内存是类似的,接口也类似。
二.system v标准的内核数据结构的设计
信号量呢也是一种通信方案。
它在通信的时候,操作系统要管理信号量时,它也有自己对应的结构体对象叫做struct semid_ds。
而且它的第一个成员也是ipc_perm,也有它对应的key值,那么我们通过上面的观察呢,可以告诉大家最终一个概念叫做进程间通信。它是被精心设计过的,尤其是system v那么它内部的所有的通信,不管是共享内存、消息队列,还是我们下面介绍的信号量,它都会有在内核级别或者在用户级别都有struct XXX_ds这样的结构。
并且struct XXX_ds这样的结构体的第一个成员,不管是哪一种通信方式,第一个成员都叫做struct ipc_XXX_perm。这种结构里面的包含的字段全都是一样的,都是key还有他权限等等等等啊。
这些都是在内核层面上的数据结构的设计,好。所以共享内存了,消息队列了,包括信号量,它其实都要被操作系统叫做先描述再组织管理起来。
不同的通信方式有不同的结构对象呢啊这什么叫做它们的整个结构体的第一个字段类型都是一样的
。
好,那也就是说系统内的所有的i p c资源啊,不管是哪一种通信资源,那么他们之间是怎么被设计的呢?
首先我要告诉大家的是。在操作系统所有的i p c资源是全部被叫做整合在我们一个叫做i p c模块当中的。
那么在操作系统当中它专门为我们设计了一个叫做模块,就叫做进程间通信。
就是我们所有对应的一个ipc资源呢,那么在我们的操作系统层面上是被统一管理起来的,是如何被管理的呢?
不管是消息队列啊,不管是我们对应的消息队列信号量,还是我们对应的共享内存。 大家的第一个字段那么都是一样的,用的都是同一个结构体。所以现在的问题是,我们该如何把这些数结构管理起来呢?它也是用数组来管理的。
好,它也是用数组管理的。
数组它的类型叫做struct ipc_perm arry[],它大小其实是可变的啊,里面存放的是strcut ipc_perm 类型的地址。
然后如果你今天创建了一个共享内存,那么操作系统当中就要为我们创建对应的s struct shmid_ds的结构。那么这个结构当中第一个字段是struct ipc_perm shm_perm。所以呢我们就把它第一个字段地址填入到它对应的零号下标里啊,好,或者是其他下标里啊都可以。那么后来呢,你又创建了一个消息队列。
没关系,你的消息队列的,虽然这个数据类型不一样。但是啊你数据类这个类型数据类型不一样,但是你的第一个结构体的类型和上面的是一样的。把你第一个数结构的类型地址填入到数字的二号下标里。后呢,你对应创建一个信号量,我也不懂,反正第一个字段也是ipc_perm类型
所以我们把第一个我们这里叫做ipc_perm这样的它的地址呢也填充到我们对应的数组里。
所以从此往后,我要管理操作系统内的所有的ipc资源。一先描述,对于不同的资源你可以有不同的描述方式。二、我要对所有的资源进行增删查改,最后转化成了对该数组进行增删查改。
我说的是每一种ipc资源,它只是把它结构体对象当中第一个字段把指针传递过来。因为大家的第一个字段类型是一样的用的是同一个结构体。
我想访问对应的这个数组,那么那访问某个资源的时候呢,那么其中我是不是就得定位这某一个资源啊?
所以我将来呢我要确认下一个进程a它来了,它说我要申请个共享内存。
那么申请共享内存的时候,它会给我一个key值,key值就存储在ipc_perm结构体里。
因为在用户层它会给我给我个key之后,我会遍历这个数组,然后呢通过数组找到每一个对应的ipc资源,通过比较它们的ipc_ perm当中所对应的key。它的数组下标就是传说中的s h m i d啊,或者是某某某i d
,比如说我们之前讲的shmid,那么其实它的本质也是数组的下标。
我们只要把对应的地址。强转成指定的类型,然后你想访问其他的东西,那么想访问哪一个就可以访问哪一个。所以我将来访问某个资源的时候,拿着它的下标,然后呢根据下标直接索引到它对应的数据结构对象的地址。找到了地址之后,然后对它的地址再进行强转。那么那么进行强转之后呢,我们此时不就可以把它内部的东西不就拿到了吗?
所以我们脑海里立刻就形成了一个立马我们就形成了一个概念,就是操作系统呢有个单独的模块啊,也就是未来如果你想使用我这个资源的话,你只需要告诉我你对应的shid就可以了。操作系统它就会自动去通信模块里去帮我找。
三.四个概念(互斥/临界)
在介绍信号量之前我们先理解几个概念:
1.数据不一致问题
2.互斥
3.临界资源
4.临界区
当我们利用共享内存进行通信时,比如A正在写入,写入了一部分,还没有写完整,就被B拿走了,就会导致双方发送和接收的数据不完整,这就是数据不一致问题。
在任何时刻,只允许一个执行流访问共享资源的行为就叫做互斥。
在任何时刻只允许一个执行流访问的资源就叫做临界资源。
如说我们今天所对应的这个共享内存,严格意义上来讲它只能叫共享资源。因为你没有对它做保护。如果你对它进行了互斥保护,那么它就是临界资源。
1.atm机里面去取钱的时候,你你去取钱的时候,有没有可能旁边有个人过来,哎,你干啥呢?啊,哎我围观一下,看你怎么弄。
这合理吗?当然不合理,大部分的a t m机现在都已经有保护机制了。一个房间里就只能进一个人来取钱,你进了门之后呢,那么在里面一反锁,你自己一边忙活,忙活完你再出来。你就是一个执行流。而atm就是一个临界资源。
2.所以多执行多执行流访问时,那么我们只允许一个执行流访问这个资源。
如果你们两个都过来访问了,就有可能会造成数据混乱。
因为管道呢本身在通信时,它是有原子性保证和同步互斥的。管道是被我们都两个进程同时看到的。那么这个管道就其实本质它就是一个共享资源,但是因为它加了我们的保护机制,其实在任何时刻只允许一个直接的访问。所以它就是一种临界资源。
那这个临界资源到底指的一般是什么呀啊?
一般都是我们由操作系统或用户所维护的一段内存空间啊。 管你是什么管道,你的本质是不是内存内存空间。
最终你的一个管道文件让我找到我们俩共享时,那个管道的就是缓冲区,那不就是一段内存空间吗?
访问临界资源的那部分代码就叫做临界区。
我们自己有时候打印的时候,发现他的消息啊本来应该和我命令行分开的,可是我发现他跟命令行也混在一起了。那么这个问题我们该怎么去理解呢?
显示器是文件吗?
显示器是文件。
那么显示器它既然是文件,它的本质其实当我们在向显示器写入,是先要向显示器文件的缓冲区进行写入数据。
然后操作系统才把我的数据刷新到选区上,最后你才看到的。
那么当我们有多个进程尝试着对同一个显示器文件进行打印时,那么都向同一个显示器上打印。
前提条件是每个进程都得先看到同一个显示器资源。
所以我们的显示器资源在多进程场景当中,本质就是一一种叫做共享资源。
而当我们在打印时,你打你的,我打我的,我们并彼此之间并没有添加任何的互斥或者同步保护机制。
所以我们对应的显示器资源在打印时,在写入时导致了数据不一致问题。
好,所以如果你想让他打印不出现错乱,你就必须想办法要打印的时候,要把你的显示器资变成这个临界资源。
四.信号量
说信号量,然后再谈信号量的周边问题。信号量的本质其实就是一个计数器。它用来叫做描述啊临界资源中资源数量的多少。
举一个生活中的例子:
你去看电影的时候。
你得先买票,这是第一个。
第二个呢,电影院有一百个座位,他最多这个这个放映厅一百个座位,他最多卖一一百张票。
如果我没有去电影院。那么还没有看一场电影。但是我把票买了。
那么最后这个电影院的那个座位在特定的时间段内是不是就属于我了?整个电影院的放映厅它就是一份公共资源。我们买到了票,我就一定有座位。不用说我真正的坐在那个座位上,这个座位才是我的。买了票就是我的了。
第一,买电影票的本质。
买票的本质是对资源的预定机制。
其二,票数计数器
每卖张票,那么剩余的票数就要减减。所以呢对于电影院来讲,它内部需要维护一个叫做票数的技术性。是我每卖一张票,计数器就要减一。那么是不是就相当于放映厅里面的资源?
就少一个,对应的票数减到我们的我们的票数计数器减到零。
好,那么它到我们对应的零之后,那么表示的是资源已经被申请完毕了。
一.买票的本质是对资源的预定机制。
二.在电影院当中,他自己内部需要维护一个计数器。卖一张票他计数器就要减一,代表的是我们放映厅里的资源就少了一个。
1.多线程并发访问
那么临界资源呢它可能可以被划分成很多很多小块的资源。你今天有一个执行流,他如果想访问对应的这个临界资源的话。他可能只使用其中的一部分。
我们对应的这个系统,在整个的就临界资源的使用上呢,你可以让整整个临界资源只被你一个人访问。可是对不起,你只有一个人在访问呀。当你只有一个人访问,你只访问其中一块呀,我可以把这一块分配给你。没有必要将整个资源都给你。
那么另外一个执行流他也来了,他也来的时候,那么我你也只访一块资源啊,我没必要把整个资源全部给我们锁住,或者是把这个资整个资源全部保护住,我只要把这一部分资源给你就行了。所以我们把临界资源拆成一小份,那么前提是能拆一小份儿,并且我们对每每个执行流它只使用一小份资源。
在这种情况下,我们就可以同时允许这两个执行流都进来进行访问。好,那么这种情况为什么呢?
因为我们可以提高多线程,而多执行流访问临界资源的并发度,可以在一定程度上提高效率。只要你们能访问不同资源,那么这个临界资源呢那么此时就可以提前的让我们多个线程的并发的去访问这个临界资源了。
2.计数器
那么如果我们的临界资源允许被多执行流并发的同时去访问时,我们最怕的是什么啊?
那么如果是你说的这种情况,我们最怕的是什么?我们是不是最怕的是叫做多个执行流访问同一个资源。它就可能会引起我们之前讲的数据不一致问题。所以我们不想让多个执行流访问同一个资源啊,我们最好的做法是什么?
是不是我们就要进行引入一个计数器啊,进程进行申请资源的时候,就对该计数器做减减。计数器就是就是减到零的时候啊资源被我们的申请完了。那么再有如果再有现成再有执行流申请资源,那么我不给你了啊。所以通过这样的一个计数器,我们就能够恒定的去限制进到这个临界资源里。
1.第一
所以我只要申请计数器成功了。就表示我具有访问资源的权限了就表示我已经得到了这个资源。
2.第二
当我要访问其中某一个区域的时候,我申请了计数器。那么申请成功的时候呢,我当前有没有访问这个资源呢?没有,但是那块资源是属于我的,申请计数器资源的本质是对资源的一种预定。
3.第三
计数器减到零,我们就不让你申请了。计数器呢它可以有效控制进入共享资源的执行流的数量
4.第四
每一个执行流你想访问这个共享资源的手,那么不是直接访问。
而是先申请计数器。所以如果要让多个信息流并发的去访问临界资源里的不同区域,所以我们就需要有一个叫做计数器的东西。
当计数器只能是1或者0时,我们叫做二元信号量,二元信号量其实本质就是一个叫做锁。
所以信号量本质就是一把计数器。
那么这把计数器呢就可以用来进行让我们进行对于某种资源进行保护。
因为所有人在访问我们对应的临界资源的时候,他都得先申请说好,或者先申请我们的资源。
信号量计数器它也是共享资源。
【计数器减减是不安全的】
我们的进程在运行时啊,叫做进程在运行的时候,那么可以随时被切换啊。
好,那么同学们进程它在运行的时候,或者执行流在运行的时候在c p u上跑。
那么它什么时候被调度、被切换、被阻塞、被挂起,完完全全就是要由我们调度器就说了算的。
所以一个进程在运行的时候可以随时随地被切换。
有可能当它在进行对这个变量内容做读取的时候,在三条语句当中的某一个临界点的时候,它就被切走了。
切走的时候就有可能多执行流都访问这个变量时,它就有可能会把这个抗的值减减出问题啊,要么多减,要么少减,反正它会出问题啊,那么怎么出问题呢?
申请信号量本质是对叫做计数器进行减减。这个操作在信号量当中,它专门封装了一个操作,这个操作我们称之为p操作。面不用这个资源,你把资源用完了,我们叫做释放资源。那么就必须叫做释放信号量。这个操作我们称之为v操作。那么其中我们把信号量的申请和释放我们申请和释放我们称为叫做p v操作。它就是对计数器做减减和加加。
3.原子的
那么叫做一件事情要么不做啊,那么要做就做完。也就是两派的。我们对应的p v操作在所有的操作系统内部,以及我们后面用的这个信号量当中呢,它在设计的时候,它的p v操作减减和加加操作就必须得被设计成原子的。因为它只有是原子的,它才能保证自己的安全,它能保证自己的安全,它才能保护别人。
对于计算机来讲,对于我的进程来讲,这一条语句你要么执行了,要么不执行。
所以在操作系统层面上调度的时候,你你不可能把我这一条语句再切割了。切割不了要么不执行,要么执行完了。
4.总结
1.信号量本质是一把计数器。
2.对计数器匹配的操作叫做p v操作。
3.p v操作啊是原子的。
4.那么所有的执行流申执行流申请资源必须啊必须先进行申请信号量资源,那么得到信号量之后啊才能访问。
5.二元信号量。那么其实本质上啊那么它二元信号量就是互斥功能。
通信不仅仅是我们进行所谓的叫做传送数据,那么通信也在于双方互相协同啊。两个进程之间要协同,通过我们对应的信信号量来那么申请信号量实现什么互斥。那么本质也是通信。