Linux:进程间通信之system V
一、共享内存
进程间通信的本质是让不同的进程看到同一份代码。
1.1 原理
第一步:申请公共内存
为了让不同的进程看到同一份资源,首先我们需要由操作系统为我们提供一个公共的内存块。
第二步:挂接到要通信进程的地址空间中 (共享区)
然后我们要将该内存挂接到需要进行通信的进程的地址空间中(共享区),这样才能让通信双方都能看到这份资源。
第三步:通过地址空间的映射实现通信
然后我们就可以通过代码来让某些进程写,某些进程读,都通过他们的地址空间映射到该共享内存,从而实现了通信
第四步:通信结束后相关进程去关联
通信结束释放内存之前一定要去关联,相当于是告诉相关进程不能再使用这些内存了。
第五步:由用户手动释放内存
因为我们申请的共享内存关联的进程可能有很多,所以如果我们不手动去释放的话,操作系统并不能知道到底哪个进程结束后需要去释放这段空间,因为担心其他的进程没有结束或者没有去关联。所以共享内存必须由用户通过系统调用接口或者命令去手动释放
因为操作系统中可能会存在多个共享内存,所以我们的操作系统必须想办法把共享内存管理起来,所以必然有相关的内核数据结构采用先描述再组织的方式管理起来!!
问题:上述的操作都是由进程做的吗??
——>肯定不能由进程做,因为如果由进程做,那么因为进程的独立性所以这个空间并不能被共享 所以我们的共享内存并不能单独属于某个进程,所以必须由操作系统来完成申请和释放,所以我们必须要使用系统调用接口!!!
1.2 创建共享内存——shmget
系统调用接口shmget
1.2.1 初识key
1、key是一个数字,这个数字是几并不重要,关键在于他在内核中具有唯一性,能够让不同的进程进行唯一标识!(如果我们申请的共享内存是一扇门,那么key就像是可以开启这扇门的钥匙)
2、第一个进程通过key创建共享内存,往后的进程只需要拿着同一个key就可以和该进程看到同一块共享内存(相当于你想让别人进你的门,你就给他配钥匙)
3、对于一块已经创建好的共享内存,他的key被保存在描述该共享内存的内核数据结构里!(因为其他进程都要配钥匙,所以钥匙的模具肯定不能放在某个进程中,而是放到专属于这个内存块的内核数据结构中)
4、 第一次创建共享内存的时候就需要一个key了,那么key从哪来??——>操作系统专门提供了一个ftok函数!
问题1:为什么操作系统要提供ftok这样的函数来获取key呢??
——>这个key就相当于一把钥匙(必须具备唯一性),本质上也是也就是一串数字,如果我们随意设置一个key就可能会跟别的共享内存块的key起冲突,所以ftok就是操作系统为我们提供的一个获取key的算法,他可以通过参数(唯一路径和指定的工作id)来尽可能生成一个不会冲突的key(当然也有可能失败,只是因为路径具有唯一性,所以冲突的概率极小),这样后期想跟他使用同一个内存块的进程也就可以用同样的方法来获取key
问题2 : 为什么key要由我们自己通过ftok指定,而不是由操作系统直接帮我们生成??
——> 假设key由操作系统在我们申请共享内存的时候自动生成,那么由于进程具有独立性,你怎么能够把key交给另一个和你通信的进程呢??(你可能会想到用管道将key传过去,但如果我们使用了管道,那么共享内存就不是一个独立的方案了!)所以哪个进程和哪个进程会通信操作系统并不清楚,只有用户清楚。——>所以约定了由用户通过ftok来下达给操作系统生成key,这样后期只要你想让这个进程和之前的进程通信,你只需要和他传递一样的参数(保证不同进程看到同一份共享内存),那么ftok生成的必然是同一个key,就可以进行通信了!!
5、因此key和路径一样,具有唯一性
1.2.2 共享内存标示符
shmget中返回值是int类型,其含义就是共享内存标示符
问题1:shmget返回值 vs key
——>key是操作系统内标定唯一性,而shmget返回值是在进程内标定唯一性!
问题2:我怎么保证这个共享内存是否存在?
——>key是在内核数据结构中保存的,所以必然会将所有生成的key通过某种数据结构管理起来,这样我们在创建这个key的时候就可以去进行搜索,如果搜索到了就说明这个共享内存存在了!搜索不到就说明生成的是新的共享内存
我们还会发现无论是key还是shmid的返回值,都是一些很奇怪的数字,跟文件系统的fd规则不一样,这说明共享内存是一个独立的体系。
1.3 挂接共享内存——shmat
参数shmaddr:想让共享内存挂接到地址空间的什么位置,一般来说我们默认传null,这样就是由操作系统帮我们决定
参数shmflg:调整权限,比如说我们可以让该进程以只读的方式挂接,但是一般来讲都是默认和该共享内存的权限一样(传0)
其实shmdt和malloc有点像,都是在进程地址空间开辟一段连续的空间 ,返回值都是void* 需要进行一下强转
1.4 解除共享内存——shmdt
进程退出的时候会自动解关联,但是我们也可以手动通过shmdt去解关联
shmaddr:映射共享内存的那一块在进程地址空间的起始位置
shmdt和free有点像,参数都只要传起始地址即可——>这更说明了该共享内存在操作系统层面有相关的结构体,里面记录了共享内存的大小,又因为申请的内存是连续的,所以我们只需要知道起始地址就可以知道共享内存的范围。
1.5 ipcs和ipcrm
ipcs -m :可以查看当前的共享资源
nattch表示关联数
ipcrm -m (shmid) :释放指定的共享内存
共享内存的生命周期是随内核的!!用户不主动关闭,共享内存会一直存在,除非内核重启!(因为操作系统并不知道到底有几个进程要使用这块共享内存,所以不能盲目地释放)
1.6 控制共享内存——shmctl
cmd:
设置和标记删除的时候,第三个参数传null
1.7 开始通信
a文件:要创建和释放共享内存
b文件:不需要创建和释放共享内存
读端:
写端:
但是其实我们shmaddr就和相当于是我们自己可以使用的空间,所以我们其实不需要单独再搞个缓冲区!!
我们会发现:(1)对读方来说,一旦有人把数据写到共享内存里,里面就能看到了!!(不需经过系统调用)(2)对于写方来说,一旦将共享内存挂接到自己的地址空间上,就直接把他当做自己的空间来用就可以了!!(也不需要系统调用)
——>这说明 共享内存比管道快很多 ,因为采用的是地址空间映射到同一块内存的方法,所以拷贝的次数少了速度就快了!!
1.8 共享内存的特点
1、共享内存是所有进程间通信中速度最快的!!(少拷贝)
2、共享内存没有同步和互斥的保护机制!(所以容易出现数据错乱的问题)
3、共享内存内部的数据由用户去维护!!(创建、使用、释放)
1.9 共享内存的属性
1.10 利用管道进行同步
由于共享内存没有同步互斥的保护机制,所以很容易出现数据错乱(比如你这个命令写到一半就被读走了,然后由于没有收到完整指令执行不了 就会错乱),因此我们可以尝试用管道来帮助我们进行同步!
在写入完毕的时候再通过像管道写入来通知对方 你已经写完了 可以进行读取
在读取之前,先向管道读取,看看是否收到了写方传达的通知,如果没读到就阻塞着等,读到了才会开始后面的读取命令!
问题:那为啥不直接用管道呢??
——>因为共享内存也是有自己的优点的,那就是拷贝次数少所以更快,因为如果在处理一些大数据的时候,通过管道这种较小的消耗来配合共享内存其实是更好的一种方案。
二、消息队列原理
管道是通过让不同进程看到同一个文件缓冲区,共享内促是看到同一个内存块,而消息队列就是看到同一个队列!
特点:允许不同的进程,向内核中发送带类型(因为消息队列是将信息以数据块的形式链接到消息队列中,可以双向通信,但其中的内容有别人的数据,也会有自己的数据,所以我们需要通过不同的类型来判断队列中的数据块是别人传的还是自己传的)的数据块!
大部分接口的设置和共享内存相同
但有msgsnd和msgrcv,一个是发送一个是接受
其参数msgp需要我们自定义一个“块”传过去,这个块包括类型(区分是自己的数据还是别人的数据),以及数据块信息
三、IPC在内核中的数据结构设计
其实共享内存、消息队列、信号量都隶属于System V接口,所以他们的接口在设计的时候非常相似,并且也都遵循使用key值。
我们会发现无论是 共享内存、消息队列还是信号量,他们都有一个ipc_perm 的结构体,里面存储着key值,并且他们key值的生成方法都是用ftok
——>所以操作系统内部将存储ipc_perm结构体的地址数据管理起来。
(1)可以让共享内存、消息队列、信号量都通过一种方式管理起来,ipc_perm结构体中有key值,方便我们在创建key的时候查看是否有冲突的key。
(2)ipc_perm就相当于是 基类 而包含他们的结构体对象就是 子类,而管理ipc_prem的数组就相当于是 虚函数表 所以其实这个Cpp中的继承和多态是一样的。只不过C语言没有这套规则,所以我们只能用这种方案(用不同的数据结构对象,里面包含着相同的数据结构对象,然后把他们相同的数据结构对象用数组管理起来,然后我们在访问的时候只需要对相关的指针做强转,就可以访问到对应的属性——>多态的本质:相同的指针,但是指针指向不同的对象访问的就是不同的信息)
(3)数组下标按道理是从0开始的,只不过有个起始计数器的概念,实际上是通过线性递增且会回绕的数组下标来定位的(因为我们用的越久可能数组下标就会越大,所以使用的时候会一直递增,直到满的时候再回绕 跟fd中的始终是最小的下标不一样)
四、信号量
4.1 概念铺垫
1、共享内存虽然很快,但是由于没有像管道一样的同步互斥的保护机制,所以当A写入的时候,刚写入一部分就被B拿走了,导致双方收发命令不一致——>共享资源如果不加以保护,就会存在数据混乱问题
2、 数据错乱本质上就是因为多个执行流同时访问公共数据——>所以我们希望任何时候都只有一个执行流在访问这个公共数据(互斥)
3、 任何时刻只允许一个执行流访问的资源,我们叫做临界资源(一般是内存空间)
4、一份代码中可能只有一部分代码是属于访问临界资源,所以我们把这一部分代码叫做临界区
4.2 理解信号量
信号量本质性就是一把计数器(描述临界资源中资源数量的多少)
其实就是某些临界资源可能很大,但是实际进程在访问的时候可能只访问其中的一小部分,所以如果我们能够做到以下三点:(1)讲临界资源进行合理拆分 (2)在执行流访问的时候对资源进行合理分配,不让多个执行流访问同一个资源(3) 引入计数器来了解当前是否还存在临界资源 ——>就可以促进多进程的并发运行,提高效率
可能出现我们最害怕的两种情况:
(1)执行流不超过资源数目时却出现了多个执行流访问同一块资源的情况——>调整执行流访问时资源分配不合理的bug
(2)执行流超过资源数目必然会导致多个执行流访问同一块资源的情况——>在拆分资源的时候顺便搞个计数器,表明当前有多少资源还没被申请,当计数器为0的时候,其他任何执行流都进不来。
所以程序员把这个计数器叫做信号量
4.3 计数器
因为执行流如果超过资源的数目必然会引发多个执行流访问同一块内存的情况,因此我们引入了计数器,并且规定每个执行流在访问共享内存之前,必须要先访问一下计数器 !(相当于看电影要先买票)!
1、申请计数器如果成功,则表明我具有了访问特定资源的权限
2、申请了计数器资源,不代表就访问了,这是对资源的一种预定机制
3、计数器有效保证了访问共享内存的执行流数量
4、如果计数器为1,意味着只能有一个执行流访问该资源(这就是互斥)!,而我们把只有0、1两态的计数器叫做二院信号量(这就是锁!)
问题1:凭什么计数器为1?
——> 本质上就是希望该资源不要被分成很多块,而是当成一个整体去申请和释放!
问题2:访问临界资源必须先访问计数器资源,可是计数器不也是共享资源么??
——>没错!!所以计数器虽然承担着保护临界资源的任务,但是要想保护好别人也得先保护好自己!! (因为看不到其他进程在做什么,所以我们必须把它设置成原子的才能保证自身安全!)
在技术的角度,一条汇编语句默认就是原子的,因为已经没有办法做区分的,要么是没执行完要么是已经执行完了。
从生活的角度,比如你的父亲问你以前学费才一千多,现在怎么要五六千了??你妈就会骂他说:儿子都上大学了能一样么,你平时关心过他的学习过程吗??要么就是不关心他,要么就是问他考了多少分——>所以此时你的学习经历对于你的父亲来说就是原子的
4.4 信号量操作
4.5 信号量总结
1、信号量本质上就是一把计数器,PV操作,原子的
2、执行流申请临界资源时,规定了必须先访问计数器申请信号量资源,申请成功了才能访问临界资源
3、信号值如果是0,1两态的,二元信号量就是互斥功能
4、申请信号量的本质就是对资源的预定机制
5、多个信号量和信号量是几是不同的意思 前者表示有多个共享资源 后者表示一个共享资源的其中某个部分
终极问题:信号量凭什么是进程间通信的一种呢??
——>(1)通信不仅仅是通信数据,协同也是(协助进程间通信) (2)要协同 本质也是通信,且信号量首先要被所有的通信进程都看到!!(3)信号量的存在不是传输数据,而是让两个执行流更好地进行协同读取数据的(维持秩序——>不直接参与通信却影响着通信)。
4.6 mmap函数
mmap也是一种共享内存技术 (System V的共享内存技术接口是最难的!)
一文读懂 mmap 原理 - 知乎 (zhihu.com)