进程间通信——管道
文章目录
- 1. 进程间通信介绍
- 1.1 概念
- 1.2 分类
- 2. 匿名管道
- 2.1 匿名管道原理
- 2.2 匿名管道实现
- 2.3 匿名管道的特性与特殊情况
- 2.3.1 匿名管道五大特性
- 2.3.2 匿名管道四种情况
- 2.3.2.1 写端不写,写端不关,读端读
- 2.3.2.2 写端不写,写端关闭,读端读
- 2.3.2.3 写端写,读端不读,读端不关
- 2.3.2.4 读端关闭,写端写入
- 3. 命名管道
- 3.1 命名管道的创建
- 3.2 在进程中创建和使用命名管道
- 3.3 命名管道原理
- 4.命名管道与匿名管道
1. 进程间通信介绍
1.1 概念
进程间通信(IPC,Inter-Process Communication),本质上就是进程与进程之间进行数据信息的交流,从而达到信息交互或主从控制等功能。
为什么进程间通信会存在呢? 由fork
创建子进程,必然会引入多进程的问题,正如多人之间需要交流一样,多进程之间也是存在交流的需要的。
但是,进程本质具有独立性,一个进程想要拿到另一个进程的数据,是不可能的,就算是父子进程之间,即便共享数据,也要发生写时拷贝,这显然不是真正意义上的通信。
因此,进程间要实现通信,就应该让两个进程同时看到或者说拥有一块独立于两进程外的一块共享资源,这样两个进程之间就可以通信了——而要让两个独立的进程能共同看到另一块公共资源,必然会有操作系统的参与。
1.2 分类
进程间通信实现的方式共有三种:
- 管道。管道分为匿名管道和有名管道,这是最古老的进程间通信方式,管道实质上是基于文件读写操作的进程间通信方式,由于是基于文件实现的,因此管道通信是简单便捷的。
- System V。System V 通信方式是在管道之后发展出一种通信方式。不同于管道通信中对文件操作的大量复用,System V通信则是完整独立的进程间通信体系。但是无论是管道还是System V,这两种通信方式都只能在本地进行通信,即只能在一台机器的多进程间实现通信,无法使用互联网时代。
- POSIX。POSIX通信,是现在使用最多的通信方式,可以实现网络通信。
在本篇博客中,我们将会重点讲述管道这种古老而又简单的通信方式。
2. 匿名管道
2.1 匿名管道原理
我们前面说过,管道是基于文件实现的,那么匿名管道的原理是什么呢?
首先,先说一点结论——匿名管道是用在有血缘关系的进程间进行通信的。
我们以父子进程和普通文件为例,来进行切入。
在父进程中,打开一个文件后,再使用fork
创建子进程,那么子进程也会打开这个文件。并且相关的文件描述符,父子进程是相同的,同时,父子进程还会共用一个struct file
。
对于这个父子进程间共同打开的文件,父进程可以进行读写操作,子进程也可进行读写操作,而由于父子进程共享一个struct file,因此读写位置,即文件偏移量是会相互影响的。
另外,文件的读写最终本质是从文件的内核缓冲区中进行读写,而普通文件是位于磁盘上的,文件内核缓冲区中的内容会按照一定规则,向磁盘上做刷新。
管道是基于文件的,但是管道要实现进程间通信,肯定会与文件有所区别,接下来,我们来区分一下它们的不同。
- 首先,要实现进程间通信,父子进程之间的读写操作是不能互相影响的,即父子进程间不能共用一个文件偏移量,即不能共用一个struct file。所以,操作系统会对匿名管道做特殊处理,父子进程间通过同一个匿名管道通信,但各自有一个对应于该管道的
struct file
。 - 其次,匿名管道是用于进程间通信的,是不需要向磁盘做刷新的,因此实际上,操作系统在创建匿名管道时,将其创建为一个内存级文件,没有路径,也没有名称,因此被称为匿名。
- 最后,管道这种通信方式在设计之初,就被设计为是单向通信,因此被命名为
pipe
,因此父进程在打开管道时,会默认以读写方式打开,这样子进程也会以读写方式打开,然后父进程再关闭读写中的一端,子进程再关闭读写的一端,进而实现父读子写或子读父写的两种通信方式。
2.2 匿名管道实现
那么我们如何在进程中去实现一个匿名管道呢?
pipe:这个系统调用是用于建立匿名管道文件。对于形参,传一个包含两个整型变量的一维数组,在系统调用中会在其中存储文件描述符fd,其中pipefd[0]存储读端描述符,pipefd[1]存储写端描述符。对于返回值,如果成功建立匿名管道文件,则返回0;否则,返回-1,并设置错误码。
建立管道文件的具体流程如下:
- 父进程通过
pipe
建立管道文件,以读写两种方式 - 父进程创建子进程,子进程会继承父进程建立的管道文件,但有自己独立的struct file,同样以读写两种方式。
- 根据实际需要,父进程关闭读写中的某一端,子进程关闭读写中的某一端(本质上,即便不关闭,也可以进行正常通信,但管道被设计为单向通信,如果不关闭不需要的读写端,可能会出现误操作)
- 匿名管道初始化工作完成,可以开始通信。
以下是父子进程间通过匿名管道实现通信的代码示例:
实现的功能是:从键盘读入字符串到父进程中->父进程将字符串通过匿名管道写数据->子进程通过匿名管道读数据->子进程向显示器文件打印字符串
以下是父子进程间进行通信的结果:
2.3 匿名管道的特性与特殊情况
2.3.1 匿名管道五大特性
- 匿名管道用于是用于有血缘关系进程间的通信。因为匿名管道本质上是通过父进程创建子进程,子进程继承父进程所创建的管道,只不过子进程会独立拥有新的
struct file
,因此匿名管道是无法实现没有血缘关系进程间通信的。 - 匿名管道是单向通信的。 匿名管道要求通信进程双方,一方写,另一方读,实现单向通信。
- 管道的声明周期随进程。 因为匿名管道本质就是进程打开的一种特殊文件,只有当所有打开该文件的进程都结束时,操作系统才会将该文件的相关资源释放。
- 匿名管道是面向字节流的。 这点在此暂不做解释。
- 匿名管道自带同步机制。 关于这一点,会在匿名管道的四种情况中,做出说明。
2.3.2 匿名管道四种情况
接下来,我们来看匿名管道在四种特殊情况下会怎样。
2.3.2.1 写端不写,写端不关,读端读
在这种情况中,匿名管道的写端不写,但是写端不关闭,如果匿名管道中有内容,那么读端能正常读,但是一旦匿名管道中没有内容,读端就会阻塞——这就是同步机制的一种体现:匿名管道中没有内容,但写端还没有写入,那么读端就要阻塞,不能去执行别的任务,一定要等到写端写入,读端正常读取后,读端才能往下运行。
我们用代码来验证以下读端阻塞的情况。
上述代码执行后,父子进程都会阻塞住。子进程是因为父进程写端不关,但写端不写,因此阻塞在读端;父进程是因为子进程阻塞后,子进程不退出,而阻塞在waitpid
中。
将上述代码运行起来后,我们可以看到父子进程均处于阻塞状态。
2.3.2.2 写端不写,写端关闭,读端读
这种情况下,如果匿名管道中有内容,读端会正常读取,如果匿名管道中没有内容,读端进行读操作,此时不会阻塞,而是直接返回,read
会返回0,表示读到0个字节。
最终输出结果如下:
2.3.2.3 写端写,读端不读,读端不关
这种情况下,写端会一直写,直到将匿名管道写满为止后,就不再写入了,准确地说,当匿名管道写满后,再进行写入行为时,写端会发生阻塞。
从输出结果中,我们可以看到,当写入了65472个字节后,就已经将匿名管道写满了,之后父进程就在写端阻塞了。
2.3.2.4 读端关闭,写端写入
当读端关闭时,如果写端进行写入,此时的写入是没有意义的,因为通信的双方,读端缺失了,在这种情况下,操作系统会自动使用信号(编号为13的信号SIGPIPE)杀掉写进程。
最终程序运行的结果如下所示:
子进程确实被操作系统使用编号为13的信号杀死了。
3. 命名管道
匿名管道可以实现有血缘关系进程间的单向通信,但对于毫无关系的两个进程间,我们又该如何进行通信呢?
进程间通信的核心在于让两个进程看到同一块资源,所以,我们如何让两个毫无关联的进程间看到同一个资源呢?可以让两个进程打开同一个文件,但是这个文件既然用于进程间通信,肯定要做一定的特殊处理,以满足进程间通信的某些特殊要求——而这就是命名管道。
3.1 命名管道的创建
首先,我们来看命名管道如何被创建和使用。
我们可以直接在命令行中创建一个管道文件,因为命名管道本质也是一个磁盘级的文件。
我们可以使用mkfifo
这个命令,来创建一个管道文件。然后,我们可以通过输入输出重定向,来通过这个管道文件实现进程间通信。
此时,我们再打开另一个终端窗口,并进行输入重定向。
通过上述测试,我们成功用命名管道实现了两个进程间通信。
3.2 在进程中创建和使用命名管道
上述操作是在命令行中完成的(本质也是成为进程实现的),那么,我们是否可以在我们自己的进程中创建并使用命名管道呢?
这个系统调用,是用于在进程中创建命名管道。
pathname
:代表所创建的命名管道的路径名。通常这个参数我们要传入命名管道:路径+名称。但如果我们仅想在当前工作路径下创建管道,我们可以只传入相应名称。
mode
:这个代表创建命名管道时所赋予的权限,一般给0666
这个八进制即可(一般适用于非目录文件的权限).
接下来,我们在具体的代码中来看看mkfifo
的使用。
我们设计的程序用以实现client-server,即客户端和服务端之间的通信模式,由服务端创建命名管道,然后客户端向服务端发送消息,服务端接收消息并将其输出到显示器文件中。
关于上述代码,有几点需要额外说明:
mkfifo
仅用于创建命名管道文件,但并不会直接将其打开,这一点和创建并打开匿名管道的系统调用pipe
不同。- 对于命名管道,当只有一方,无论是读方式还是写方式打开这个命名管道文件,都会阻塞在open函数中,直到有另一方以另一种方式打开该命名管道,才会停止阻塞,正常从
open
中返回。 - 完成命名管道的创建和打开后,接下来的一切进程间通信的读写操作,均与普通文件的读写操作相同,所以命名管道也是基于文件的进程间通信方式。
上述程序运行结果:
启动server进程,在open处阻塞。
启动client进程
成功完成通信
3.3 命名管道原理
命名管道的原理其实和匿名管道是基本相同的,都是基于基本文件操作,只不过操作系统对于管道这种特殊文件又有额外的特殊处理。
命名管道所对应的文件,不是内存级的,而是磁盘级的,进程间通信过程中,本质上都是在对这个文件做写入和读出。和匿名管道一样,这个文件虽然是磁盘级文件,但实际通信过程中借助文件内核缓冲区进行通信,且该文件内核缓冲区的内容永远不会向磁盘上做刷新,因此命名管道文件永远是空文件。
在两个进程分别打开同一个命名管道文件时,操作系统会为这两个进程分别创建一个struct file
以对应该命名管道文件,每一个struct file
中都有一个独立的文件偏移量,这样就可以确保读写时不会发生冲突,但是这两个struct file
所对应的内核级的struct inode
和文件内核缓冲区是同一个。
4.命名管道与匿名管道
二者不同之处:
- 命名管道是磁盘级文件,匿名管道是内存级文件。
- 命名管道的创建和打开分别使用
mkfifo
和open(需两个进程分别以读写方式打开)
;匿名管道的创建和打开均使用pipe
,但之后还需一个进程关闭读端,一个进程关闭写端。
除了上述不同之处外,对于上述所介绍的匿名管道的特性和特殊情况,命名管道同样具备。
总结一下,匿名管道一般用于具有血缘关系进程间的单向通信,命名管道一般用于毫无关系的两进程间的单向通信。