文章目录
- 1. 进程间通信介绍
- 1.1 进程间通信目的
- 1.2 进程间通信分类
- 2. 管道
- 2.1 什么是管道
- 2.2 站在文件描述符角度-深度理解管道
- 2.2.1 具体通信的过程
- 2.3 匿名管道
- 2.4 代码实现
- 3. 进程控制
- 4. 管道读写规则
- 5. 管道特点
- 6. 命名管道
- 6.1 创建一个命名管道
- 6.2 代码实现
1. 进程间通信介绍
1.1 进程间通信目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
1.2 进程间通信分类
我们知道:进程是具有独立性的。如果我们想让进程之间交互数据,我们需要进程通信。而通信之前,我们最关键的是让不同的进程看到同一份资源。其实这里我们要学习的不是如何通信。而是如何看到同一份资源。由于资源的不同,决定了不同种类的通信方式。
2. 管道
2.1 什么是管道
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
2.2 站在文件描述符角度-深度理解管道
这是我们之前说的:一个进程打开文件的过程。现在我们想fork一下,那么它的子进程会拷贝哪些呢?答案是:子进程会拷贝父进程部分的PCB和struct file_struct,而不会拷贝struct file。而拷贝下来的子进程和父进程指向同一文件。
管道就是让写入的数据不在刷新到磁盘中,而是写入缓冲区中。所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
那么管道具有什么特点呢?
在进程间通信中,管道是用来传输数据的,并且是单向的。
2.2.1 具体通信的过程
这里首先父进程是用两个文件描述符来打开管道的读和写。然后fork出子进程后,子进程也会指向同一个资源。如果父进程传数据给子进程,那么父进程会关闭读接口,子进程关闭写接口。反之,父进程会关闭写接口,子进程关闭读接口
现在有如下几个问题:
为什么父进程要分别打开读和写?
为了让子进程继承,那么子进程就不需要打开了。
为什么父子要关闭对应的读或写?
因为管道是单向通信的。
2.3 匿名管道
那么我们如何一次创建两个文件描述符来进行读和写呢?
系统提供了我们一个接口:
2.4 代码实现
1. 创建管道:
2. 创建子进程:
3. 实现单向的:
现在我们需要子进程来进行只读,父进程只需要写。那么我们需要关闭子进程的写和父进程的读。
现在有一个问题是:数组里那个下标放的是读,那个放的是写?
规定:下标0是读端,下标1是写端。
验证一下是否通信:
我们在父进程里写入一些变化的信息,然后通过管道让子进程去读。
这里有一个问题:子进程怎么知道父进程结束了?
原因是:管道里有一个引用计数,可以知道有多少文件指向自己。当父进程写完时就会关闭文件描述符,管道里的引用计数就会减1。
大家有没有想过这样的一个问题:父进程还没写完,子进程读完了?
答案是:这种情况在管道里不会发生。我们看下面的例子:
运行情况如下:
我们可以发现,子进程并没有去执行第二种情况。而是一直等待父进程。
结论:当父进程没有写入数据的时候,子进程在等。当父进程写入数据后,子进程才能read到数据。子进程打印读取数据要以父进程为主。父进程和子进程读写的时候,是有一定顺序性的。
在父子进程向显示器写入时,就没有这样顺序。因为它们缺乏访问控制。管道内部,是自带访问控制机制。
管道内部,没有数据,read就必须阻塞等待(等管道有数据)。
管道内部,如果数据被写满,writer就必须阻塞等待(等待管道中有空间)。
那么在命令行中的 | 这个管道是什么呢?
其实就是匿名管道。
我们可以看出管道两边的命令的ppid是一样的,也就是说它们的父亲是一样的。它们的关系是兄弟。
它是过程如下:
这是一个创建子进程的管道,当我们再创建一个进程时。
再创建一个子进程后,父进程的读写端都关闭,然后让这两个兄弟进程通信。
3. 进程控制
现在,我们想控制进程做事情,我们这样写:
这个和上面是一样的,创建管道和创建子进程。
这里我们写一个函数集合,为了让子进程去执行这些方法。
这里写了一个子进程去执行任务的代码。
那么这个(void)s是什么意思呢?
原因:assert断言,是编译有效,在debug模式下存在,release 模式,断言就没有了,一旦断言没有了,s变量就是只被定义了,没有被使用。release模式中,可能会有warning。
这是父进程指派任务的过程,父进程给子进程派送10次。
运行结果如下:
那么我们需要控制一批子进程呢?
现在父进程要给三个子进程安排不同的任务,我们有什么解决办法呢?
有几个进程,我们创建几个管道。如果我们想让进程1做某些任务,我们就往1管道里面写,如果我们想让进程2做某些任务,我们就往2管道里面写,依次类推。
代码实现:
我们这里添加了一个pair类型的结构和进程的个数。
那么第一步:我们需要创建processNum个进程。
第二步:父进程给哪个进程指派任务。
这里父进程做的事,让子进程和对应管道写端的结构体放进这个数组里。这样就能知道哪个进程对应哪个管道。
当所有的子进程创建成功后,我们就开始让父进程来派发任务。
派发任务的代码我们如何写呢?
第三步:子进程去执行任务。
那么我们就要完成这个work函数:
子进程在对应的blockFd去读。
第四步:回收资源。
运行结果如下:
4. 管道读写规则
当没有数据可读时:
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候:
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN。
如果所有管道写端对应的文件描述符被关闭,则read返回0。
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
5. 管道特点
- 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信。通常,一个管道由一个进程创建,然后该进程调用fork,此后父,子进程之间就可应用该管道。
- 管道只能单向通信(内核实现决定的)。管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。
- 管道自带同步机制(pipe满,write等,pipe空,read等),也就是自带访问控制。
- 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
- 管道是面向字节流的,先写的字符,一定是先被读取,没有格式边界,需要用户来定义区分内容的边界(比如:上面写的sizeof(uint32_t))。
6. 命名管道
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用命名管道。
6.1 创建一个命名管道
命名管道可以从命令行上创建,命令行方法是使用下面这个命令:
命名管道也可以从程序里创建,相关函数有:
我们知道:进程间通信的本质是不同的进程看到同一份资源。匿名管道是通过子进程继承父进程的文件描述符表。命名管道是通过一个fifo文件,文件有路径,所有具有唯一性。所以通过路径我们可以找到同一份资源。
6.2 代码实现
我们在这里创建两个文件,一个是客户端文件,一个是服务端文件。
首先,我们要让这两个进程看到同一份资源:
我们在这里创建了一个隐藏文件。在这个头文件里。然后我们让clientFifo文件和serverFifo文件都包含这个头文件,这样这两个进程都可以看到这个路径了。
这是makefile里面的代码,因为我们要形成两个可执行程序。
现在我们就需要创建管道文件,只需要一个进程创建管道文件,另外一个来使用这个就可以了。那么我们就在服务端创建这个管道文件:
我们先看一下运行结果:
从运行结果我们可以看到:创建了一个.fifo的文件。
现在我们想让clientFifo进程去写入,让serverFifo进程去读取:
首先,我们以写的方式打开文件,让管道的引用计数+1。
下面我们就要让它完成写入功能:
写完后进行关闭:
serverFifo也是类似的道理,我们以读的方式打开文件:
那么下面就是serverFifo读取的过程:
读取完之后,我们需要关闭:
运行结果:
从结果我们可以看到:在clientFifo里面写入,在serverFifo就能读取。
当clientFifo进程按Ctrl+d退出时,serverFifo也退出了,并且.fifo也自动删除。