51-无名管道

来源:互联网 发布:西安智高诚软件怎么样 编辑:程序博客网 时间:2024/06/06 11:48

这恐怕是最古老的 linux 进程间通信的方式了。这种方式简单而又强大,尤其适合有亲缘关系的进程(通常是父子进程)间通信了。

实际上,你或多或少的都用过管道这种通信方式,比如你在使用的 linux 命令中的管道连接符 |。所以,对于管道来说,你应该有一种亲切感。

1. 管道为何物?

如果你实践过前面我们讲过的使用本地文件进行进程间通信的方式,那么你就可以把管道理解成位于进程内核空间(如果你不记得这个概念,请翻阅前面的博文)的“文件”。


这里写图片描述
图1 假想的位于内核的管道文件

给文件加引号,是因为它和文件确实很像,因为它也有描述符。但是它确实又不是普通的本地文件,而是一种抽象的存在。

当进程使用 pipe 函数,就可以打开位于内核中的这个特殊“文件”。同时 pipe 函数会返回两个描述符,一个用于读,一个用于写。如果你使用 fstat函数来测试该描述符,可以发现此文件类型为 FIFO.

而无名管道的无名,指的就是这个虚幻的“文件”,它没有名字。

本质上,pipe 函数会在进程内核空间申请一块内存(比如一个内存页,一般是 4KB),然后把这块内存当成一个先进先出(FIFO)的循环队列来存取数据,这一切都由操作系统帮助我们实现了。

2. pipe 函数

  • 函数原型
int pipe(int pipefd[2]);

pipe 函数打开的文件描述符是通过参数(数组)传递出来的,而返回值表示打开成功(0)或失败(-1)。

它的参数是一个大小为 2 的数组。此数组的第 0 个元素用来接收以的方式打开的描述符,而第 1 个元素用来接收以的方式打开的描述符。也就是说,pipefd[0] 是用于读的,而 pipefd[1] 是用于写的。

打开了文件描述符后,就可以使用 read(pipefd[0]) 和 write(pipefd[1]) 来读写数据了。

  • 注意事项

这两个分别用于读写的描述符必须同时打开才行,否则会出问题。

  1. 如果关闭读(close(pipefd[0]))端保留写端,继续向写端(pipefd[1]) 端写数据(write 函数)的进程会收到 SIGPIPE 信号。
  2. 如果关闭写(close(pipefd[1]))端面保留读端,继续向读端(pipefd[0])读数据(read 函数),read 函数会返回 0.

3. 用 pipe 进行进程间通信

当在进程用 pipe 函数打开两个描述符后,我们可以 fork 出一个子进程。这样,子进程也会继承这两个描述符,而且这两个文件描述符的引用计数会变成 2。(如果忘记了这个特性,请复习《fork 函数与文件共享》 )

如果你需要父进程向子进程发送数据,那么得把父进程的 pipefd[0] (读端)关闭,而在子进程中把 pipefd[1] 写端关闭,反之亦然。为什么要这样做?实际上是避免出错。传统上 pipe 管道只能用于半双工通信(即一端只能发,不能收;而另一端只能收不参发),为了安全起见,各个进程需要把不用的那一端关闭(本质上是引用计数减 1)。

图2 和图 3 演示了上述过程。

  • 步骤一:fork 子进程


    这里写图片描述
    图2 fork 后的半双工管道

  • 步骤二:关闭父进程读端,关闭子进程写端


    这里写图片描述
    图3 从父进程到子进程的管道

要想实现全双工通信(既能发又能收)怎么办?你可以再使用 pipe 打开两个描述符,或者使用其它的 ipc 手段。

接下来,看实例。

4. 实例

下面的程序功能如下:父进程 fork 出一个子进程,通过无名管道向子进程发送字符,子进程收到数据后将字符串中的小写字符转换成大写并输出。

为了突显程序结构,删除了错误处理部分的代码。

  • 代码
// hellopipe.c#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <ctype.h>void child(int *fd) {  close(fd[1]); // 子进程关闭写端  char buf[64];  int n = 0,i;  while(1) {    n = read(fd[0], buf, 64); // 如果没有数据可读,read 会阻塞;如果父进程退出,read 返回 0.    for (i = 0; i < n; ++i) putchar(toupper(buf[i]));    if (*buf == 'q') {      exit(0);      close(fd[0]);    }       if (n == 0) {      puts("no data to read!");      sleep(1);    }     }  exit(0);  }int main() {  int fd[2];  int n = 0;  char buf[64] = { 0 };  if (pipe(fd) < 0) {    perror("pipe");    return -1;   }  pid_t pid = fork();  if (pid == 0) {    child(fd);  }  close(fd[0]);// 父进程关闭读端  while (1) {    n = read(STDIN_FILENO, buf, 64);     write(fd[1], buf, n);     if (*buf == 'q') {      exit(0);      close(fd[1]);    }     }  return 0;}
  • 编译和运行
$ gcc hellopipe.c -o hellopipe$ ./hellopipe
  • 运行结果
hello world // 输入HELLO WORLD // 输出24215lkjl // 输入24215LKJL // 输出

5. 总结

  • 掌握 pipe 函数
  • 理解 pipe 函数原理(fifo 队列)

练习:在运行上面的程序时,分别 kill 父进程和子进程,看看有什么结果,并解释现象。

0 0
原创粉丝点击