目录
1.进程间通信是什么
顾名思义,是两个或多个进程直接进行数据交互,例如发送命令、某种协同、通知......
由于进程之间天然具有独立性,它们的地址空间是相互隔离的,那么就导致进程间通信的成本比较高,显然进程间通信就需要"绕过"进程之间独立性
2.进程间通信的本质
由进程间通信定义可以得出进程间通信的本质: 让不同进程之间看到同一份"资源"
这里的"资源"可以理解为特定形式的内存空间,例如下图展示进程间的简单通信方式:

"资源"的提供方?
进程间通信的本质是让不同进程之间看到同一份"资源",那么这个"资源"由谁提供?
假设"资源"由进程提供,比如进程A和进程B要通信,"资源"由进程A提供,那么这个"资源"是属于进程A独有的,进程B想要访问这个"资源"会破坏进程A和B之间的独立性,假设不成立
因此需要操作系统(即第三方)提供"资源",可以得出: 进程访问这个"资源"进行通信,本质上就是访问操作系统
由于进程处于用户态,权限低,操作系统不允许进程直接访问操作系统的资源
进程间通信时就需要向操作系统申请使用这个"资源",那么进程需要使用系统调用来告诉操作系统进行"资源"的创建、使用、释放
操作系统需要管理进程间通信的资源
操作系统上有很多进程,它们或多或少都要通信,那么操作系统就要为这些进程提供提供很多资源,因此操作系统系统管理资源,即先描述再组织
IPC
一般的操作系统会有一个隶属于文件系统的独立的IPC通信模块,IPC全称是Inter-Process Communication,翻译过来是进程间通信
*注: 上述将的文件系统不仅仅管理磁盘上的文件,也管理内存级别的文件
简述进程间通信标准
早期的Linux内核是没有进程间通信模块的,这就导致开发者们需要各自指定通信标准
为了统一标准,需要权威人员来定制标准,方便不同设备之间 设备内的进程与进程之间通信等
最终落成两个标准:
System V 本机内部通信
POSIX 网络通信
理解基于文件级别形式的通信方式: 管道(以匿名管道为例)
实现原理
设想一下,如果进程间通信使用磁盘上的文件,那么由于访问外设,速度会很慢,而管道就能解决这个问题
之前在OS5.【Linux】基本指令入门(4)文章提到过管道,本文继续深入讲解管道
可以这样理解: 下图特定形式的内存空间可以是管道,管道是Unix中最古老的进程间通信的形式,而且是基于内存文件级别形式的通信方式

进程使用管道前,需要使用系统调用来创建管道文件并打开
进程管理文件是靠fd_array来进行的,之前在OS30.【Linux】文件IO (2) 文件描述符文章讲过,如下图

虽然管道在内存中,但根据Linux"的一切皆文件"思想,那么同样可以使用open、read和write来操作管道文件,比如:

对于普通文件,如果需要修改里面的内容,需要先将磁盘的文件加载到内存,在内存中修改数据,之后将其写回磁盘
→内存中修改过的数据和磁盘文件的数据不一样,就称”内存中修改数据”为”脏” 数据
得出一个事实: 访问磁盘中文件必须先将文件中的数据加载到内存中
虽然管道文件和普通文件一样有自己的inode,file_operations和缓冲区,但是管道文件的内容并不刷新到磁盘中,这是由管道的特性决定的: 内存级别的文件
举例父子进程通过管道通信
父子进程想要使用管道通信,那么它们的fd_array中就必须要有指针指向同一个管道文件的file_struct
可以这样做: 父进程打开管道文件,之后父进程创建子进程,那么子进程就能和父进程一样访问同一个管道文件
原因: 根据OS18.【Linux】进程基础知识(2)文章的结论: 用fork()创建子进程时,父子进程共用同一份代码,数据以写时拷贝的方式各自私有,那么子进程继承了父进程的file_struct,如果之后父子进程都没有打开或关闭文件(可以理解为父进程刚创建子进程那一瞬间),根据写时拷贝的原理,父子进程的file_struct指针都指向内存中的同一个file_struct
得出进程间通信的本质前提: 先让不同的进程看到同一份资源,这里是让父子进程都能看到同一个内存级别的文件——管道
如果父进程关掉管道文件,那么子进程读取管道文件会出错吗?
答: 不会,因为管道也是文件,那么必然有引用计数这个属性(有关引用计数的知识参见OS30.【Linux】文件IO (2) 文件描述符文章),父进程关掉管道文件,引用计数--,但是管道文件的引用计数不会为0,因为子进程仍然在打开管道文件
父子进程打开文件的方式
管道文件目标: 让一个进程读,另外一个进程写
设想一下,如果父进程以只读方式打开管道文件, 之后父进程创建子进程,由于父子进程之间具有继承关系,那么子进程也只能以只读方式打开管道文件无法做到一个进程读,一个进程写
初步解决方法:使用两个指针执行同一个管道文件,一个指针用来读,另外一个指针用来写,
假设父进程写管道,子进程读管道,最终的情况如下图:

补充: tty的含义
*注: 上图的tty是teletypewriter,即电传打字机,如下图:

但为什么标准输入(fd=0)、标准输出(fd=1)、错误输出(fd=2)在图中用tty表示呢?
在askubuntu.com What is a tty, and how do I access a tty?给出了回答:

回答中指出: tty是纯文本终端,而标准输入、标准输出、错误输出都是输出到终端显示器上的,那么就能理解tty的含义了
父进程写管道,子进程读管道,那么父进程以怎样的方式打开管道文件才能实现进程间通信呢?
方法1.父进程一开始只打开写端

子进程继承父进程的file_struct,那么:

但子进程没有打开读端,这就出问题了,那么需要改进方法1
方法2.父进程一开始打开读端和写端

注意到图中父进程分两次打开管道文件,一次打开读端(r),一次打开写端(w),非常不建议一次打开读写端(rw),因为读端和写端的指针指向文件中的位置不一定相同
(注: 图中的fd[0]=3,fd[1]=4之后再解释)
子进程继承父进程的file_struct,那么:

这样父进程就能写管道,子进程就能读管道了!
由于父进程写管道,子进程读管道,那么最好将父进程读端关闭,子进程写端关闭
原因: 目标是"让一个进程读,另外一个进程写",如果父子进程都参与读写,可能父进程读的太急,父进程会读到自己刚写的数据,这样子进程无法取得父进程写入的数据了; 同理, 如果子进程读的太急,子进程会读到自己刚写的数据,父进程无法取得子进程写入的数据,可以对管道中的数据要加标签来区分子进程和父进程写入的数据,但是操作系统维护成本比较高
强烈建议关闭父进程的写端和子进程的读端,否则父进程或子进程可能误操作,严格遵循单向通信
为了简单起见,Linux中的管道是单向通信的,如下图:

注意: 上图的管道是没有名字的,称为匿名管道,是管道文件的其中一种,因为是子进程继承父进程了管道,不需要名字
也可以这样表示,读写过程已在下图标注:

这样父进程的写端和子进程的读端都指向同一个管道文件的缓冲区
Linux的 /usr/include/unistd.h的注释也是这样说的:
/* Create a one-way communication channel (pipe).
If successful, two file descriptors are stored in PIPEDES;
bytes written on PIPEDES[1] can be read from PIPEDES[0].
Returns 0 if successful, -1 if not. */
extern int pipe (int __pipedes[2]) __THROW __wur;
#ifdef __USE_GNU
/* Same as pipe but apply flags passed in FLAGS to the new file
descriptors. */
extern int pipe2 (int __pipedes[2], int __flags) __THROW __wur;
#endif
管道是单向通讯通道(a one-way communication channel),如果父子进程需要双向通信可以使用两个管道
结论: 两个进程之间必须要有血缘关系才能使用管道进行通信
例如上述例子的进程A创建了进程B,A和B是父子关系,能进行管道通信,如果进程B创建了进程C,不仅B和C之间可以使用管道通信,A和C之间也可以,因为管道文件是一路继承下来的,所有有血缘关系的进程是共享同一个管道文件的
同理,如果进程A的父进程是进程D,此时血缘关系为血缘关系是D-->A-->B-->C(D-->A中D为父进程,A为子进程),那么A、B、C、D打开的是同一个管道文件,管道文件是共享的
→如果D创建出来A之后,D关闭读端,保留写端,而A保留读端,关闭写端,那么A创建的B和C都可以从管道中读取东西
进程E如果打开了管道文件,而且进程E有两个子进程D和F,那么这三个进程共享同一个管道文件
2544

被折叠的 条评论
为什么被折叠?



