进程间通信:管道教程

2019-05-21 16:52:28

在 Linux 系统中,有时候需要多个进程相互协作,共同完成某项任务。进程之间或线程之间有时候需要传递消息,有时候需要同步来协调彼此的工作。因此很有必要了解一下 Linux 中进程间通信方式。

线程在 Linux 中被实现为轻量级进程,线程之间的同步手段(互斥量和条件等待),本质上也是进程间通信。

进程间通信的手段,大体可以分为以下两类:

  • 第一类是通信类:这类手段的作用是在进程之间传递消息,交换数据。若细分下来,通信类也可以分为两种,一种是用来传递消息(比如消息队列),另一种是通过共享一块内存区域来完成信息交换的(比如共享内存)
  • 第二类是同步类:这类手段的目的是协调进程间的操作。某些操作,多个进程不能同时执行,否则可能会产生错误的结果,这就需要同步类的手段来协调

从历史角度来说,Linux 下进程间通信手段基本上是从 Unix 平台继承而来的。

AT&T 的贝尔实验室和加州大学伯克利分校的伯克利软件开发中心(BSD)分别开发出了风格迥异的进程间通信手段。前者通过对早期的进程间通信手段的改进和扩充,开发出 System V IPC,包括消息队列、信号量和共享内存。但这些方法,将进程间的通信始终局限在单个计算机这个范围之内。BSD 则走了一条完全不同的道路,开发出了套接字,跳出了单机的限制,可以实现不同计算机进程之间的通信。Linux 将这两者都继承了下来,丰富了进程间通信的方式。

System V IPC 出现比较早,几乎所有的 Unix 平台都支持 System V IPC,其可移植性较好,但是在使用过程中也暴露了一些弱点。POSIX IPC 提供了和 System V IPC 相对应的工具(消息队列、信号量和共享内存),它出现晚于 System V IPC。System V IPC 广泛应用了一段时间之后 ,才开始设计 POSIX IPC 的,因此设计者可以借鉴 System V IPC 的长处,避免其缺点。从设计的角度上讲, POSIX IPC 是优于 System V IPC 的,接口简单,易于使用。但是 POSIX IPC 的可移植性不如 System V IPC。

管道

管道概述

管道是最早出现的进程间通信的手段。在 shell 中执行命令时,经常将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。

管道的作用是在有亲缘关系的进程之间传递消息。所谓亲缘关系,是指有一个共同的祖先。所以管道并非只能用于父子进程之间,也可以用在兄弟进程之间,还可以用在祖孙进程之间甚至叔侄进程之间。总而言之,只要共同的祖先曾经调用了 pipe 函数,打开的管道文件就会在 fork 之后,被各个后代进程所共享。打开管道文件就像是创建了一个家族私密场所,由远祖进程来创建,家族所有成员都知晓。家族成员可以将消息存放进该秘密场所,等待另外一头的家族成员来取走消息,阅后即焚。

严格来说,家族里面的多个进程都可以往同一个秘密场所扔消息,也都可以从同一个秘密场所里面取消息,但是真的这么做的话又会存在风险。管道实质是一个字节流,并非前面提到的消息,没有消息边界。如果多个进程发送的字节流混在一起,则无法辨认出各自的内容。所以一般是两个有亲缘关系的进程用管道来通信。从程序设计的角度来讲,当进程调用 pipe 函数时,哪两个有亲缘关系的进程使用该管道来通信是事先约定好的,其他有亲缘关系的进程不应该进来搅局。当其他进程之间也需要通信时,可以创建他们之间通信的另外的管道。

前面提到过,管道中的内容是阅后即焚的,这个特性指的是读取管道内容是消耗性的行为,即一个进程读取了管道内的一些内容之后,这些内容就不会继续在管道之中了。一般来讲单向的。一个进程负责往管道里面写内容,另一个进程读取管道里面的内容。若两个有亲缘关系的进程进行双向通信,都要往管道里写,都要从管道里读,自然也是可以的,但是管道中的内容可能会变得混乱,从而无法完成通信的任务。如果两个进程之间想双向通信,可以创建两个管道:

管道是一种文件,可以调用 read、write 和 close 等操作文件的接口来操作管道。另一方面管道又不是一种普通文件,它属于一种独特的文件系统:pipefs。管道的本质是内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。

管道接口

在 Linux 下,使用如下接口来创建管道:

<pre class="has">

include <unistd.h>

int pipe(int pipefd[2]);


  
  
  
如果创建成功,则返回值是 0,如果失败,则返回值是 -1,并且设置 error,error 的可能值有:

  
  
1. EFAULT :pipefd 参数不合适
2. EMFILE :进程使用了太多的文件描述符,已经多于 MAX\_OPEN-2
3. ENFILE :系统中已经打开的文件描述符已经超过了系统的限制

  
  
  
成功调用 pipe 函数之后,会返回两个打开的文件描述符,一个是管道的读取端描述符 pipefd\[0\],另一个是管道的写入段描述符 pipefd\[1\]。管道没有文件名与之关联,因此程序没有别的选择,只能通过文件描述符来访问管道,只有那些能看到这两个文件描述符的进程才能使用管道,即只有该进程及其子孙进程才能使用管道。

  
  
  
  
  
成功调用 pipe 函数之后,可以对写入段描述符 pipefd\[1\] 调用 write,向管道里面写入数据:

  
  
  
  
  
```
write(pipefd[1],buf,size);
```

  
  
  
向管道的写入段写入数据后,就可以对读取段描述符 pipefd\[0\] 调用 read,读出管道里面的数据:

  
  
  
  
  
```
```
read(pipefd[0],buf,size);
```

  
  
  
read 返回的字节数等于请求字节数和管道中当前存在字节数的最小值。如果管道为空,那么 read 调用会阻塞。

  
  
  
  
  
管道的一端是写入端,一端是读取段。不应该对读取端描述符调用写操作,也不应该对写入段描述符调用读操作。如果这样做了,会怎么样?

  
  
  
  
  
调用 pipe 函数返回的两个文件描述符中,读取端 pipefd\[0\] 支持的文件操作定义在 read\_pipefifo\_fops,写入段 pipefd\[1\] 支持的文件操作定义在 write\_pipefifo\_fops。对读取端执行 write 操作,内核就会调用 bad\_pipe\_w;对写入段执行 read 操作,内核就会调用 bad\_pipe\_r 函数。这两个函数比较简单,都是直接返回 -EBADF。因此对应的 read 和 write 调用都会失败,返回 -1,并置 error 为 EBADF。

  
  
  
  
  
调用 pipe 函数之后,系统给进程分配了两个文件描述符,即 pipe 函数返回的两个文件描述符。该进程既可以往写入端写入数据,也可以从读取端读出信息。这里只有一个进程,这不是通信,这是自言自语。

  
  
  
  
  
如果调用 pipe 函数的进程随后调用 fork 函数,创建子进程,情况就不一样了。fork 以后,子进程复制了父进程打开的文件描述符:

  
  
  
  
  
![](https://www.icode9.com/i/ll/?i=20190521160146364.png?,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pfeF9tX21fcQ==,size_16,color_FFFFFF,t_70)

  
  
  
  
  
此时,可以是父进程往管道里写,子进程从管道里读;也可以是子进程往管道里写,父进程从管道里读。这两条通路都是可选的,但不能都选。因为管道里面是字节流,父子进程都写、都读,就会导致内容混在一起,对于读管道的一方,解析起来是非常困难的。常规的使用方法是父子进程一方只能写入,另一方只能读出,管道变成一个单向的通道,以方便使用。如下,父进程放弃读(父进程把读端 pipefd\[0\] 这个文件描述符关闭掉),子进程放弃写(子进程把写段 pipefd\[1\] 这个文件描述符关闭掉),变成父进程写入,子进程读出,成为一个通信的通道。

  
  
  
  
  
![](https://www.icode9.com/i/ll/?i=20190521160410801.png?,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pfeF9tX21fcQ==,size_16,color_FFFFFF,t_70)

  
  
  
  
  
从内核角度看,调用 pipe 之后,系统给进程分配了两个文件描述符,调用 fork 之后,子进程也就有了与管道对应的两个文件描述符。和普通文件不同,这两个文件描述符对应的是一块内存缓冲区。

  
  
  
  
  
![进程间通信:管道教程](https://www.icode9.com/i/ll/?i=20190521161337210.png)

  
  
  
  
  
当父进程再次创建一个子进程 B,子进程 B 就持有管道写入端,这时候两个子进程之间就可以通过管道通信了。父进程为了不干扰两个子进程间的通信,很自觉地关闭了自己的写入端。从此管道成为了两个子进程之间单向的通信通道。在 shell 中执行管道命令就是这种情景,只是管道描述符占用了标准输入和标准输出两个文件描述符。

  
  
  
  
  
![](https://www.icode9.com/i/ll/?i=20190521162436481.png?,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3pfeF9tX21fcQ==,size_16,color_FFFFFF,t_70)

  
  
  
  
  
我们可以看出,任何两个有亲缘关系的进程,只要共同的祖先打开了一个管道,总能够通过关闭进程的某些管道文件描述符,来建立起两者之间单向通信的管道。

  
  
  
  
  
  
关闭未使用的管道文件描述符
-------------

  
  
  
  
  
  
  
  
  
  
命名管道 FIFO
---------

  
  
  
  
  
  
  
  
  
  
  
  
读写管道文件
------

  
  
  
  
  
  
  
  
  
  
  
  
使用管道通信实例
--------                
当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »