Linux :进程间通信之管道
一、进程间通信
1.1 是什么和为什么
1、进程间通信是什么??
——>两个或多个进程实现数据层面的交互,但是由于进程独立性的存在,导致通信的成本比较高。
2、既然通信成本高,那为什么还要通信呢??
——> 在某些场景下我们需要不同进程之间进行(1)基本数据的交互。(2)发送命令。(3)实现某种协同。(4)通知某些信息……
1.2 如何实现进程间通信
1、进程间通信的本质:要想办法让不同的进程看到同一份资源(以特定形式存在的内存空间)!
2、这个资源必须由操作系统提供!!
问题: 为什么必须由操作系统提供呢??难道不能由其中一个进程提供么??
——>假设由一个进程提供,而我们又让另一个进程看到这份资源,那么这份资源应该属于谁呢?? ——>所以这种做法会破坏进程的独立性!!所以我们必须需要第三方空间,因此只能由操作系统提供!
举个例子:保持进程独立性,可以理解为就是我俩不能见面,就好比说一个绑匪把你绑架了,但是他并不会直接去你家取赎金,也不会让你家人把赎金送到自己的住所,而是会要求你的家人把钱放到一个地方,然后你再去取。
3、我们进程访问这个空间,本质上就是在访问操作系统!!
——>因为进程代表的就是用户,而操作系统并不相信用户,所以“资源”从创建、使用再到释放,必须使用系统调用接口!!
4、一般操作系统,会有一个独立的通信模块——隶属于文件系统——IPC通信模块 其中有两套标准 system V && posix
5、基于文件级别的通信方式——管道
1.3 进程间通信的方案设计
进程是具有独立性的,但是早期有的人发现我们很多时候需要通信,比如果进程需要通信,网络也需要通信,所以大家发现通信很重要,另一方面由于通信模块的设计相对简单,且可实现方案多样化,因此大家你做你的,我做我的,导致现在很多Linux发型版本都不一样,即使一样,内部的标准也不一样!
因此当市面上出现了各种各样的通信方案后,我们需要做两件事 (1)选择一个最合适的方案。(2)需要去定制一个标准(这样强迫大家都遵守这个规则,不同的操作系统在设计的时候就不会有太大差异!!)
——>互联网行业标准十分重要,就好比为什么你的华为手机和苹果手机差异很大,但是却能实现通信! 就是因为各个领域都会有佼佼者在定制互联网标准!这样才能保证设备之间的通信!! ——>所以标准如果没制定好,往小了说就是功能交互实现不了,往大了说互联网和物联网都实现不了!! 所以技术无论再怎么自由,背后都必须遵循一套标准,否则就是不入流!!
但是这个标准如何制定呢??可能很多不同的机构和公司都提供了不同的通信方案,而定制标准是有版权的,就比如说有的标准用的是欧美的标准,就得付专利费,所以大家都在争这个,谁也不服谁(但其实无非就是代码有点差异)。但是标准肯定不是谁想定就能定的 必须满足(1)能力强且德高望重 (2)技术方案特别成熟,可以让其他人自愧不如!!
1.4 进程通信的分类
管道:
匿名管道pipe
命名管道
System V IPC:
System V 消息队列
System V 共享内存
System V 信号量
POSIX IPC:
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
1.5 进程通信的目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.6 一些现状
大部分互联网公司其实都想做一些低成本高收入的应用,想赚快钱,其实对研发一些新东西的使命感很低。
如果做社会导向的一些业务是值得尊重,但是很多公司并没有这种魄力,因为一旦产业路径形成后,后期就很那去转型。
在这方便比较有责任使命感的是华为,因为华为一方面在做一些商业化的东西,一方面也会去搞研发,他其实推动了国内很多东西的发展,比如鸿蒙、芯片…… 区别就是不会像别的公司一样,别人用什么就用什么。
二、管道文件
2.1 管道的原理
既然一个文件可以被多个进程打开,那么文件其实就算是一个公共资源,其实也可以做到一个进程读一个进程写,从而实现通信
——>但是这样会有一个问题,就是数据必须要写到外设上,所以就会有效率方面的问题
——>因此我们希望这个通信能在内存里面去进行而不牵扯到外设,所以我们需要有一个“内存级文件”概念(让用户能够通过扫描目录结构看到文件,但是只存在于内存中),然后我们需要让操作系统知道不要把内存级文件刷到磁盘上。而是只保留在缓冲区里。
——>管道就是一个内存级文件,其中又根据方案的不同,存在匿名管道和命名管道!
2.2 匿名管道
接下来我们要思考的就是,我们究竟如何让两个进程看到同一份资源并实现通信,所以第一个方案就是——>父子进程!!因为子进程可以做到和父进程看到同一份代码,所以可以尝试让父进程和子进程进行通信!!
创建子进程的时候,pcb和文件描述符表,肯定是要拷贝的,但是并不会创建新的文件!!因为进程管理和文件系统是两个模块(你能打开我是因为操作系统,你还敢要求这么多??我们是平级!!)
但是常规的文件虽然可以被看到,但是必然会被刷新到磁盘上,所以我们不想让他刷新的话,就必须引入“内存级文件”——匿名管道,这样操作系统能区分开来!
用fork来共享管道原理:
站在文件描述符角度-深度理解管道:
问题1:父进程和子进程一个读一个写实现了通信,但是如果其中一方不小心关掉了会不会导致另一方出错呢??
——>理论上是有可能的,但是操作系统已经考虑到了,就是通过引用计数。
问题2:管道文件是只读方式打开的。所以子进程继承的时候也只有只读权限,那么两个都是读就没办法通信了吗??
——>所以我们必然不能在父进程中用open打开这个文件,而是用pipe这个专门为管道文件设置的接口, 普通文件需要通过open打开,而管道文件在设计的时候得用pipe,一边以只读方式打开,一边以只写的方式打开(占据了两个fd的位置),然后关掉其中一个不用的,然后继承给子进程也关掉一个不用的,最后实现父子进程一个是写端一个是读端的单向通信(这就是管道这个名字的由来)。
——>管道在设计的时候就是不能支持同时读写的!!
——>另一方面 open是有路径的,因为他是一个真实存在的文件,而我们如果想打开一个内存级文件,就必须用接口pipe!
参数是一个 pipefd[2] 输出型参数 他会在该进程的文件描述符表中找到两个下标最小的位置,然后一个为读端打开的fd 放在pipefd[0]中 一个为写端打开的fd 放在pipefd[1]中 这样用户可以通过这个输出型参数拿到fd 关掉其中一个不用的 然后进行使用
成功了返回0 不成功返回-1
站在内核角度-管道本质:
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
2.3 匿名管道的设计
makefile:
1、建立通信
2、实现读写方法
fd的规则就是 无论这个文件是什么文件 我们都可以去进行操作
问题:我们为什么不直接定义出全局变量,这样fork之后至子进程不就能看到了吗??
——>这样是继承而不是通信,因为我们要拿到的数据可能是会变化的,这样通信才有意义
2.4 匿名管道的特征
1、具有血缘关系的进程进行进程间通信
2、管道只能单向通信(实现双向通信就必须有两个管道)
3、父子进程是会进程协同、同步与互斥的 ——>保护管道文件的数据安全
对于父进程来说,子进程写了多少次根本不重要,只要管道里有数据,有多少就会读多少,前提条件是我们缓冲区足够大。也就是说,当子进程向管道写满了,当父进程在读的时候,就会把多次写的信息一次读了出来,在父进程看来,它读到的就是一个一个的字符,对于我们用户用什么存取,如何区分,这是我们用户的事。所以我们得出一个管道的特点,管道是面向字节流的!
4、管道是面向字节流的!
写端写了自己,我不管,反正我读端默认会把整个缓存区都读出来(因为在我看来缓冲区就是一个个字节) 至于你要把读出来的这些怎么做分离,是你用户的事情,不是我这个管道应该操心的——>这种特点就是字节流
就好比石油管道,无论你放多少石油,我都会流向出口,但是你在出口是要用碗接,还是用桶接,我一点也不关心,那是你自己的事情
——>所以如果我们想改变这种字节流的话,比如说希望一次性只读取15个字节,那么就需要涉及到协议的知识!!
5、管道是有固定大小的(Linux中是64KB)且具有原子性,但是在不同的内核里可能有区别
验证管道大小:
最后写到65536说明管道大小是64kb
ulimit 命令用于限制 shell 进程及其所创进程的资源使用
ulimit -a查看到的pipe size一次原子写入为:512 bytes * 8 = 4096 bytes 。(4KB)
问题:可以我们验证的时候管道大小是64kb,那为什么pipesize是4kb呢??
——> 因为管道具有原子性!!pipesize的意思是管道一次原子写入的大小,意思就是只要你写入的内容不超过这个大小,那么在你写的期间,父进程不会来读!!
6、管道是基于文件的,而文件的生命周期是随进程的
2.5 管道中的四种情况
1、读写端正常,管道如果为空,那么读端就会阻塞 (防止读入一些垃圾数据)
2、读写端正常,管道如果被写满,写端就要阻塞 (防止覆盖之前的数据)
——>父子进程是会进程协同的,同步和互斥的 都是为了保护管道文件的数据安全!!
3、读端正常读,写端关闭,读端最后会读到0,表明读到了文件pipe结尾,不会被阻塞!
所以读端(父进程读到0的时候)发现读到文件结尾,没有读的意义了 就可以break。
4、写端正常写入,但是读端关闭了,操作系统就要杀掉正在写入的进程——>通过信号杀掉!
首先我们要知道,操作系统是不会做低效、浪费资源和时间等类似的工作的,如果做了,操作系统就是bug;所以我们想,写端正常,读端关闭后,还有实际意义吗?没有了!因为写满了又怎样呢,又没有进程去读,所以当写端正常,读端关闭了,操作系统就要 kill 掉正在写入的进程。如何 kill 呢?通过信号,其实操作系统会使用13号信号 SIGPIPE kill 掉正在写入的进程
所以为什么我们一般让父进程读子进程写呢??因为这样我们父进程可以在回收子进程的时候检测到子进程是被信号杀死的,还是正常退出的!!
2.6 管道的应用场景
那么我们上面学的管道,和我们以前学过的哪些有关系呢?
1、首先我们以前接触过 |
这个符号,其实这个就是管道,例如我们在多条指令中使用 |
:
我们会发现他们的PID不一样,但是PPID是一样的,说明他们的父进程都是bash,而他们是具有血缘关系的进程,所以“ | ”就相当于操作系统为他们创建了匿名管道来实现通信!!
2、可以实现进程池(通过系统调用的的次数来提高内存的申请速度)
所以,当父进程想布置任务的时候,无非就是做两件事,一就是选择任务,二是选择进程。
2.7 命名管道
上面我们学到的匿名管道是没有名字的,因为打开那个文件的时候并没有告诉我们文件名,也就是管道并没有命名。我们直接以读方式写方式打开父子进程,各自拿一个读写端就可以通行。正是因为它没有名字,那么所以匿名管道必须得让我们对应的父子进程看到通信资源,它采用的是让父子继承的方案看到的。
如果毫不相关的进程进行进程间通信呢??所以我们需要有下一个方案叫做命名管道。接下来我们先使用一下命名管道,先看现象再解释。其中建立命名管道的接口为 mkfifo
那么如何实现通信呢??我们创建两个终端,一个读一个写观察
该管道看起来是在磁盘中存在,但是它实际数据并不会刷新到磁盘上。
我们一直往管道里写,管道的大小都不会发生变化
问题1:父子进程可以通过继承看到同一个文件,那两个毫不相关的进程,我怎么知道这俩进程打开的是不是同一个文件呢??
——>同一路径下的文件名:路径+文件名 (唯一性)
命名管道有自己的名字,所以他的体系还是跟文件一模一样的体系,只不过区别是他不会刷盘!!
问题2:如果两个进程打开同一个文件,在内核中,操作系统会打开几个文件呢??
——> 只会打开一个文件,维护一个缓冲区
难道不怕两个进程写在缓冲区会混乱么??——>本身两个进程同时打开一个不受保护的文件,即使有两个缓冲区,写入也是会混乱的,所以你用户都不怕了,我操作系统怕什么???