目录
- 1. 概况
- 2. 管道通信的原理
- 2.1 初步理解
- 2.2 深入理解
1. 概况
-
是什么:两个及以上的进程实现数据层面的交互,称为进程间的通信。
因为进程独立性的存在,所以一个进程无法直接访问另一个进程的数据,即便是父子进程,子进程也只能看到父进程所共享的代码和数据,一旦一方有数据写入行为,就会发生写时拷贝,所以进程通信的成本是比较高的。
-
为什么:因为有通信的需求,所以多进程直接需要进行通信,场景可以有如下几种:
- 进程间发送基本数据
- 发送命令(让一个进程通过对另一个进程发送一些命令达到控制另一个进程的目的)
- 协同工作
- 通知进程(让 A 进程告知 B 进程某些事件发生,也可以是一个父进程只负责监听其所有子进程的,当条件满足时,通知唤醒子进程去协同工作等等)
-
怎么办:
- 进程间是互相独立的,又为了保证不打破进程之间的独立性,但又必须让不同的进程看到同一份 “资源”,这样才能够让进程间进行通信。
- 这一份让不同进程都能访问的资源是一段特定形式的内存空间,一般由操作系统提供。而为什么这段资源不能由一方的进程提供呢?还是因为进程之间具有独立性,如果有一方提供,那么其它进程要访问该资源,就等价于访问这个进程的数据,这样就破坏了进程的独立性。因此由第三方提供最合适不过了。
- 所以进程访问这段空间进行通信,本质就是访问操作系统。而进程代表着用户,操作系统又不允许用户直接访问它,因此该 “资源” 从创建 ---- 使用 ---- 释放,都只能通过系统调用接口来实现。
- 操作系统为了进程间能够通信,并且不能破坏进程的独立性,必然要从底层设计、接口设计,都要由操作系统自己独立设计。而一般的操作系统,会有一个独立的通信模块,隶属于文件系统 ------ IPC通信模块。
- 关于操作系统的通信模块,很多企业都在做,因此就会存在各种各样的通信方案,所以需要定制一套通信标准。因此进程间通信是有标准的,System V && POSIX。(因为通信有标准,所以不同品牌设备,不同的底层硬件也好,不同的操作系统之间,才能够进行通信,比如我的 windows 依旧可以给 mac 电脑的你发信息等等)
- 除此之外,还有一种文件级别的通信,即管道通信。
2. 管道通信的原理
who 命令:查询当前连接主机的用户量
wc -l :统计行数量
这两个命令在将来运行起来都是一个进程,而管道的整体逻辑就是将 who 这个进程的输出信息写入管道文件,再由 wc 这个进程读取数据,然后做标准输出给用户层。
一个进程被创建,有 task_struct 内核数据结构,里面有一个 files_struct 结构体,结构体内存储了 struct file* fd_array[ ] 这样的文件描述符表,存储指向打开文件对象的指针,并且默认打开三个标准输入输出流。当我们打开一个文件,操作系统创建 struct file 对象,然后为该文件分配一个没有被使用的最小的文件描述符,将 struct file 的地址填到文件描述符表对应的下标处。
不仅如此,每个文件都有自己独立的 inode,提供一个用于各种底层设备读写的 file_operators 方法集,里面存储的是指向各个底层外设读写方法的函数指针,以此来实现一切皆文件的理念。接着,每个文件还要提供属于自己的文件页缓冲区。而不管是该文件是被创建于磁盘中,还是从磁盘中打开的,对文件做读写时,都需要先将文件的数据加载到内存中,然后再内存中做修改,修改完后再刷新回磁盘(只读取文件内容也需要先加载到内存中)。
-
我们创建一个文件,但是该文件在磁盘中并不存在(按照以前对于文件系统的理解,打开文件如果不存在即在磁盘中创建,然后分配 inode 和数据块,修改位图结构等等),但是此刻我想要创建的这种文件,一样有 inode,有 file_operators 方法集,也有文件页缓冲区,但是就是不存在于磁盘中,能实现吗??
能实现!这就是内存级别的文件,不与外设交互的文件,inode 里面的各种文件属性照常,只需要把方法集内原本指向外设读写的函数指针改为直接指向页缓冲区做读写,这样就跟外设没有任何关系了,也不需要往磁盘刷新数据之类的操作了。在操作系统内核中是存在很多诸如这样的内存级文件的。
2.1 初步理解
接着,当一个进程打开一个文件时,我在这个进程中创建出一个子进程,操作系统就需要以父进程为模板,为子进程创建一个 task_strcut、拷贝父进程的进程地址空间 和 页表等内核结构,包括父进程的 files_struct 结构体也同样要拷贝一份!说白了,这个 files_struct 代表的就是一个进程打开的文件的列表,也算是进程的一个数据结构,同时子进程也需要能够看到父进程的一切数据(在不发生写时拷贝的前提),父进程打开的所有文件也是一种数据,因此子进程需要看到父进程打开的各个文件,所以这个 files_struct 子进程同样会拷贝父进程的。
但是,父进程打开文件时,创建的各种文件对象 struct file,子进程则不拷贝这些文件对象。因为文件对象是文件管理模块的东西,进程是进程,文件是文件,不代表该文件是这个进程打开的,该文件就属于这个进程的了。文件与进程是操作系统两个模块来着的,文件不属于任何一个进程,往深一点说,文件是操作系统帮助进程打开的,因此操作系统才是文件的管理者,而非进程,文件与进程只是关联关系。所以既然该文件不属于进程,为什么子进程要拷贝呢?子进程想拷贝也无法拷贝,因为这不是父进程的东西。
所以既然子进程也拷贝了父进程的 files_struct,那么子进程的 files_struct 结构体也同样指向了父进程所打开的文件对象(三个标准输入输出流 以及 父进程单独打开的一个文件),而我们在概况进程通信时就说过,进程通信的本质前提就是要让不同的进程看到同一份 “资源”!那子进程的 files_struct 中的某一个文件描述符也指向父进程所打开的那个文件啊,这不就是父子进程看到同一份 “资源” 了吗?!
加上我们说的,创建一个文件但可以让它不存在于磁盘中,所以父子进程看到的这一同个文件,父进程往文件写入数据,子进程读取数据,这样不就可以使父子进程通信起来了吗?!
而我们上述说的这一切,就是管道的本质,管道就是一个内存级别的文件。
-
现在父子进程的文件描述符表都指向了同一个文件,当一方正在对文件做读写操作时,另一方突然把文件关闭了,会影响到正在写入的进程吗,又或者影响对文件写入的这个操作吗??
不影响,我们要知道,struct file 结构体里面是维护了一个引用计数 count 的,用于记录当前指向该文件的进程个数,所以当创建了子进程,多了一个进程指向该文件,那么 count + 1,当一方在关闭该文件的文件描述符,count - 1,但是只要 count ≠ \neq = 0,该文件的 struct file 对象以及文件的属性,内容等数据就不会被释放,因此不影响。
-
哎哎哎,你上面说子进程是拷贝父进程的 file_struct 结构体,那我父进程要是打开文件时是以 r 权限打开的呢,那子进程拷贝下来的 file_struct,对该文件的权限不也是 r,大家都是 r,通信个屁啊。还怎么做到父进程写、子进程读,或者子进程写、父进程读这样的通信
但其实父进程在打开一个管道文件时,并不是只以 w / r 的方式打开的,而是同时打开以读和写的方式打开文件,之后父进程创建出来的子进程也同样的以读和写的方式指向该文件,再结合用户的实际需求,关闭一方的读或写端口,这样只有一方保持的一种状态,即可完成进程间的通信。
2.2 深入理解
因为要建立通信,所以父进程在打开一个文件时,不仅仅是以读或写的方式打开,父进程需要以两种方式打开文件,而对于该行为,操作系统会为同一个文件创建两个struct file,一个读方式,一个写方式,并且让进程中的文件描述符表分别指向读文件和写文件。而至于为什么操作系统要创建两个struct file,不直接将读写混为一个文件,可以理解为当读写都在一个文件进行时,父进程写完,子进程要读取时,文件内部的指针是指向父进程写入之后的位置,那么文件在做读取操作时,也是从文件指针往后开始读取,这样的话,子进程就读不出来父进程写的数据。当然,在技术实现上,这个问题并不能,可以将文件指针置为文件起始处,然后再进行读取,但这样就显得有些复杂麻烦了,有点简单问题复杂化的感觉,明明可以创建两个 struct file 搞定的事情,一定要频繁的移动文件指针。所以将读写文件分开,父进程写入时,由写文件的文件指针记录着位置,下次写入直接继续往后写入即可,子进程同理,读取到什么位置,都有一个文件指针维护着,这样既不互相干扰,何乐而不为呢?这也是操作系统的设计理念,可以用简单的方式解决问题,绝对不用复杂的方案。
不过虽然读写文件分开了,但因为这是属于同一个文件,文件属性,数据块都是相同的,因此这两个 struct file 都指向同一个文件缓冲区。接着,父进程创建子进程,子进程也以读写的方式各自指向这两个struct file 文件。
但是操作系统为了不让父子进程混淆各自的数据,即只让父子进程进行单向通信!单向通信即关闭父子进程其中一端文件操作,只让父子进程有读或写的一段。如果父子进程同时对文件的保留读写权限,那么父进程就既可以向文件写入,也可以读取,这样的话,父子进程就需要去确认哪些数据是自己写入的,哪些数据是从另一方进程读取过来的,这样又把简单问题复杂化了,因此操作系统在这件事上,只支持单向通信!如果确实有双向通信的需求,那么再重新建立一个管道文件,实现另一个方向的通信即可。
因为两个struct file 文件都指向的同一个页缓冲区,所以父子进程同时对这个缓冲区是可视状态的,而为了双方进程都互不干涉,互不影响,一般都会关闭该进程不使用的一端文件操作的权限,即如果父进程不使用读取端,指向struct file r 的文件描述符就会被关闭,子进程同理。
而上述这种基于文件级别的通信,就是管道通信的原理!但是,对于管道这个名字的由来,不要把因果关系搞反了,是因为这种通信是基于文件级别的 + 单向通信,因此才叫做管道,而不是它叫管道,所以它才是单向通信的。
-
如果两个进程不具备父子关系,或者没有任何关系,能不能用管道文件的方式进行通信呢??
如果两个进程不具备任何关系,那么答案是 不能! 如果两个进程具有血缘关系,那么是可以进行管道通信的。
血缘关系就是父进程( A ) 的子进程( B ) 再创建了一个子进程( C ),那么这个 C 进程就是 A 进程的孙子进程,它是可以跟 A 和 B 进程进行通信,因为 C 进程中的 file_struct 是拷贝 B 进程的,B 进程中的 file_struct 是拷贝 A 进程的,因此它们都指向同一个 struct file,所以它们能进行通信;不仅如此,假如 A 再创建了一个子进程 B2,B2 也可以跟 A、B、C的任何一个进程通信,换言之,只要进程间具有血缘关系,那么就可以使用管道通信。
-
匿名管道: 诸如上述这种内存级别的文件,它没有文件名,也没有路径,没有 inode 等文件属性信息,因为它不需要通过路径来定位该文件,它存在于内存中,它也不需要根据文件名 和 inode 来区分与其它文件的关系,因为这种文件只有父子进程这样具有血缘关系的进程才能够看到,诸如父子进程这样的进程也不用担心找不到该文件资源,这一份父子进程共同看到的 “资源”,是在进程创建时就被继承下来了,因此不需要任何文件信息,父子进程依旧能够看到这个 “资源”,而这种管道文件就称为 匿名管道。
至此,两个进程还没有进行通信!你没听错,上述的一切,只是建立了通信信道,进程间并没有开始通信起来。这就是我们在概况中提到的,进程间具有独立性,因此进程间的通信是有成本的!
由于篇幅问题,关于管道是如何建立通信的,以及管道的应用场景,管道通信(下)
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!