前言: 本节内容将要讲解进程间通信。 之前我们说过进程之间是相互独立的, 但是,相互独立并不代表不能进行数据的输送。就好比我和你是相互独立的, 但是我们可以成为朋友, 可以互赠礼物。 而我们一般而言的, 两个或者多个进程之间实现数据层面的交互, 就叫做进程间的通信。现在, 开始我们的讲解吧。
ps:本节内容涉及到进程,系统文件接口相关的知识, 建议友友们了解相关知识之后再学习本节内容!
什么是进程间通信
什么是进程间通信, 其实前言已经提到过, 这里简单的说一下就是:两个或者多个进程实现数据层面的交互,就叫做进程间通信。 ——需要注意的是, 因为进程独立性的存在, 导致进程通信的成本比较高。
为什么要有进程间通信
为什么要有进程间通信, 为什么一个进程要给另一个进程发送数据? ——发送基本指令、发送命令、某种协同、通知。——是为了让多个进程之间相互协同起来。这就有了基本的需求, 所以最终就转化为了如何让进程之间通信起来。
如何让进程完成通信
然后上面知识基本的是什么为什么, 现在我们要谈论的细节就是, 通信是有成本的, 这个成本主要体现在要打破进程间的独立性。
而又因为我们进程间是独立的, 所以又不能过度的破坏进程间的独立性。
所以, 怎么办才能让进程间完成通信呢?
首先我们必须知道, 进程间通信的本质——必须让不同的进程看到同一份“资源"
其次, 有一个"资源”特定形式的内存空间——并且进程一个往里写, 一个往里读——什么意思呢, 怎么理解这个“资源”呢? 就类似于电视剧里面的绑匪, 电视剧里面的绑匪会与被绑架者的家属约定一个地点, 然后说让家属扔一百万到旁边的垃圾桶。 这个家属呢, 扔完钱就可以走了, 之后绑匪就去取这个钱。 这个故事里面的家属和绑匪, 就是两个进程, 而这个垃圾桶就是资源。 当家属将钱放到垃圾桶, 就类似于一个进程向资源中写入, 当绑匪拿到钱, 就类似于另一个进程从资源里获取资源。
那么, 这个资源是谁提供呢? ——一般是由操作系统提供, 为什么不是我们两个进程中的一个呢? 因为假如是一个进程提供资源, 那么这个资源就暴露了, 进程也就破坏了进程本身的独立性。 所以这个资源由操作系统提供, 操作系统就是第三方空间。
我们进程访问这个空间, 进程通信, 本质就是访问操作系统!!!——因为这个空间是由操作系统提供的, 因为进程是用户启动的, 所以进程代表的就是用户, 那么问题来了, 我们说过, 群众之间有坏人, 而操作系统并不相信用户。 所以呢, 我们的资源从创建, 到使用, 到终止(释放)这个过程, 并不允许我们的进程直接访问资源, 而必须使用对应的系统调用!!!
而我们所谓的系统调用, 从底层设计, 到接口设计,都是由操作系统设计。 一般的操作系统, 会有一个独立的通信模块——隶属于文件系统。 ——这个模块就叫做IPC通信模块。通信模块一开始是没有的, 是后来人们发现进程间通信越来越重要, 才设计出来的模块, 而且通信模块的设计有许多大佬的参与, 并且由大佬们挂到了文件系统上面, 那么我们要知道, 在设计操作系统的时候, 从底层原理设计上, 到通信模块上, 有许多家都在进程设计不同的操作系统。 那么这个时候就要定制标准。——也就是说, 从使用到原理上, 我们要定制一个大家都要遵守的标准, 定好之后, 我们大家都是用同一套规则, 这样linux的标准就是比较完善的了。 ——其实说这么多, 这里要谈的就是, 进程是有标准的。我们可以观察一个现象, 就是最近许多的操作系统出世, 比如说华为的鸿蒙, 小米的澎湃, 这些新的操作系统, 和我们传统的使用的安卓, windows并不是同一款操作系统。 那么, 就是这不同的操作系统之间却能够相互之间进行数据间的通信!!!换句话说, 我们的这些设备连底层的硬件——网卡都不一样, 但是却能够通信。 ——这其实就已经很能够说明问题了!
那么, 这个标准是什么呢? 这个标准有两个——system V(本机内部) && posix(网络通信)
ps:本篇内容不谈论这两个的任何一个, 而是要谈论另一个通信——管道。——光管道的内容就够友友们喝一壶的, 内容很多, 耐心观看。
管道
认识管道
当我们的进程没有上面谈论的两种标准的时候, 是如何进行通信的呢? ——其实就是使用的一种基于文件的通信方式——管道。
首先我们知道, 一个文件是能够被一个进程打开的, 但是问题是:一个文件能否被多个进程打开呢?——答案是可以的, 那么既然我们的文件能够被多个进程所打开, 那么只要我们的一个进程向文件里面写, 一个进程向文件里面读。 ——其实就是一个进程向文件里面刷新数据, 刷新到磁盘, 然后另一个进程向文件里面读, 就完成了进程的通信。
但是这种方式因为是将我们的数据写到外设里面, 所以这个就一定会伴随着很多很多的效率问题。
现在来学习一下管道, 如下图, 就是管道的一个应用例子。 其中wc就是统计行记录, 而我们的who是打印所有用户的行。
这里面的who, 我们知道的是它是一个指令, 它其实在运行的时候就已经变成了一个进程。 有了who这个进程之后, 还有一个叫做wc, 这两个进程中间就是用管道连接
文件原理
为了方便讲解原理, 这里我们先将图放出来, 以便讲解:
友友们可能不懂这张图, 但是我会慢慢讲解, 不急。
- 我们知道, 一个进程, 打开的时候, 就会默认打开三个文件输入输出流, 分别是stdin, stdout, stderror。并且, 这里的stdout, stderror其实是只打开了一个显示器文件, 这里为了更加直观, 博主将他们画成了三个。 其实是两个
- file_struct 和 文件描述符数组, 指向我们的文件struct file, 然后每一个数组元素天然就具备了下标, 这个下标就是我们返回上层时的fd(文件描述符)。
- 然后创建一个新的文件, 我们就要先将磁盘中的数据加载到内存中
- 那么,就要创建一个新的struct file
- 同时, 我们说过, 我们每一个文件都要提供是哪个核心的东西, 第一个是文件的详细属性——inode。第二个是能够访问底层不同的对应的操作集, 方法集——file operators(这个东西是文件里面的那个函数指针、指向了不同的底层硬件的各种使用方法。——类似于多态, 实现了一切皆文件)。第三个就是文件页缓冲区。
- 我们要打开的文件的属性会保存在inode里面。文件的数据会保存在文件的页缓冲区。然后呢, 如果我们数据为脏,就会将数据刷新回磁盘里面。这个过程就是落盘的过程。——什么意思, 就是说, 我们的磁盘的文件打开后, 一定要先将文件的数据保存到文件页缓冲区, 然后才能够对文件进行写入或者修改。 然后呢, 只要我们对内存中的数据进行修改或者写入, 那么就说这个数据为脏, 那么就要将修改或者写入后的数据重新刷会磁盘。——这个过程, 就叫做落盘的过程!!! 但是, 如果今天我们想要读取一个文件, 我们不写, 只读, 读取文件, 也照样要把磁盘当中数据加载到内存中。 也就是说, 在文件操作上, 无论读写, 都要先将数据加载到内存当中。
知道了上面的知识之后, 我们考虑一个问题, 就是我们如果磁盘中, 根本没有文件, 但是我们今天要打开它。也就是说, 我们今天打开一个文件, 这个文件在文件中有他的inode, file_operators, 文件页缓冲区。 但是呢, 在我们的磁盘中并没有这个文件时, 这个文件真正的变成了一个内存级文件, 请问, 这个可不可行?——现在我们就来谈一谈这种文件的可行性。
管道原理
单进程内存级文件
首先先说结论, 其实是有可行性的, 操作系统内部有非常非常多的内存级文件。 他们只需要在内存中用起来即可, 不需要在磁盘中存在。
原本的我们的文件的file_operators保存的是硬件层面的接口, 并且操作系统要让内存中的数据向磁盘中进行刷新。 如今呢, 我们的file_operators直接保存软件层面的接口, 也就是内存级层面的接口了, 操作系统也不需要让内存中的数据向磁盘中进行刷新了。
其实我们的管道本质上就是上面的内存级文件。
内存级文件的原理,或者说是管道的管理其实和磁盘文件级文件的管理是有相同的地方的。
上图的左边部分,我们知道, 是属于进程的部分。 而内存级文件和磁盘级文件的不同是属于文件系统的部分。 所以显然左边部分和上面学习的文件原理是一样的。 那么我们区别就是右边部分。 右边部分的区别是我们没有磁盘文件向文件缓冲区写入数据了。 而改成了我们的进程拿到file_operators,使用file_operators向文件缓冲区中写入数据
带有父子进程的内存级文件
当我们的一个进程创建出一个子进程, 那么就要创建PCB, 地址空间, 文件描述符等等。
但是, 我们的文件struct file需不需要拷贝给子进程呢?——但是不需要, 因为要知道, 我们的文件, 是属于系统的, 是有系统打开的, 并不是属于这个进程的。 我们进程与文件(这里的文件指的是内存里面的文件struct file)之类的关系, 并不是从属的关系。而是关联的关系, 我们的进程所有的file_struct 可以找到struct file, 可以使用struct file的file_operators接口, 可以修改数据到内存, 但也仅此而已!!!而且, 我们的操作系统也说过——操作系统的管理一般分为:进程管理, 驱动管理, 内存管理, 文件管理。 可见, 进程和文件之间是并列的关系, 并不是从属的关系。
那么, 再思考一个问题, 为什么我们的文件描述符表也要有呢?——因为文件描述符表是我们当前进程打开文件的列表——而父进程和子进程并不是同一个进程, 而且又有相同的内容,所以子进程就要拷贝父进程一份file_struct。——这也就是为什么我们的父进程打印, 我们的子进程也打印, 但是两个会同时向我们的同一个终端打印的原因。
管道的形成
进程间通信, 因为进程间具有独立性, 所以两个进程正常情况下不能够进行数据层面的交互的。要是让进程间进行通信, 本质, 也可以说是前提就是务必让不同的进程看到同一份操作系统的资源。 ——如果看不到的话, 就无法通信。
如果今天, 我们的进程打开了一个内存级文件, 那么如果这个进程又创建了一个子进程, 这个时候父进程会指向同一个struct_file, 那么如果父进程想要向子进程进行通信, 那么只要父进程将数据写到我们的文件页缓冲区, 然后子进程从文件页缓冲区进行读取, 那么是不是就能够将数据读取到了?就完成进程间的通信了。
管道的本质也叫做文件!!!管道就是文件, 只不过管道文件不是我们理解的磁盘文件。
但是这里有一个问题, 就是当我们的父子进程进行通信的时候, 如果一方不下心将文件关掉了, 那么会不会影响另一个呢?——答案是不会的, 因为我们的struct_file里面包含了一个int cnt的引用计数, 只要当我们有一个文件描述符指向struct_file的时候, 对应的struct_file就会加加。 也就是说, 父子进程都打开这个文件的时候, 这个文件的应用技术就一定会大于等于2!所以一个进程关掉文件后不会影响另一个进程。
另一个问题就是, 我们说,我们打开一个文件的时候, 设置的权限是只读"r", 这就造成了一个问题。 我们说我们要一个读一个写, 现在我们两个文件都只能读。 那么我们能不能将文件设置成可读可写呢?——答案是不能, 因为, 我们的管道在设计的时候, 并没有设计成可以支持读写。——所以, 这就导致, 我们的父进程再打开子进程的时候, 不能这么简单的打开了。
我们的父进程在系统中打开管道文件的时候, 并不只是把文件单方面的打开或者读或写, 而是打开这个管道文件的时候, 对于这个管道文件,既以读的方式打开, 又以写的方式打开。
然后父进程再创建子进程, 子进程拷贝文件描述符表。 ——所以, 父进程和子进程都有对应的读写段指向同一个文件。
然后我们就要结合具体场景, 确定我们的两个进程到底是读, 还是到底是写。 ——这就要求我们的父子进程分别关闭一个读写段, 形成一个单向的通信信道。 ——比如父进程关闭读, 子进程关闭写。 父进程用来写入, 子进程用来读取。 ——又比如父进程关闭写, 子进程关闭度。 父进程用来读取, 子进程用来写入。
那么我们站在内核的角度, 管道的本质是什么呢。
- 首先就是两个文件描述符
- 这个然后两个进程的文件描述符的两个fd都指向同一个文件。但是对于我们的操作系统来说, 我们如果想要再打开一次这个文件, 还是需要再创建一个struct file——因为这两个文件的读写方式不一样, 而又因为我们的每个文件都有我们的读写位置, 如果我们两个fd同时指向一个struct file, 那么很容易就出现问题。 所以, 操作系统要创建两个struct file。 但是!——这两个文件的inode, file_operators, 文件缓冲区是一样的。 这其实也可以读,也可以写。 就比如我们的r + w, 但是这就有一个问题, 因为我们的一个struct file里面有自己的读写位置。 并且这个位置, 也可以说是偏移量是欸一的, 那么当我们向文件中写入数据后, 如果想读这些数据, 我们是读不出来的。 因为我们的读写位置在写的时候已经发生了变化了。 我们读数据是在刚刚写入的地方读的。 除非我们写入数据后, 又用rewind吧读写的位置回归到最开始了!
现在的情况是, 我们的父进程和子进程都有读有写。 那么这个缓冲区里面的数据就不知道是谁的了。 所以我们的设计者就规定——我们只想让父子进行单向通信。 然后由用户规定谁给谁通信。
我们这里假如规定成子进程写入, 父进程读取。
所以, 那么我们上面的父子进程就要各自关闭一个文件描述符。
以上, 就是管道通信的本质。 而之所以叫做管道, 是因为它只能进行单向通信!——而为什么是单向通信的, 第一是因为它是基于文件通信的, 第二是因为设计者为了图简单。 ——所以就有了管道的名字。 如果我们要进行双向通信呢?——就用多个管道。
上面讲的是父子进程层面的, 但是我们如果两个进程没有任何关系, 可以使用我们刚刚讲的原理进行通信吗?不能!——这里的管道的形成, 必须是有血缘关系的!!!——比如爷孙, 比如父子。
——以上就是本篇的全部内容, 本篇内容到此就结束啦, 感谢友友的阅读, 下面是本节的笔记, 和正文几乎一样的, 觉得本节内容有用的话可以保存方便查阅哦。