引入

之前我们介绍了多进程以及创建进程的函数fork,下面我们将继续深入,讨论一下多进程间的通信问题;

pipe 管道

谈论多进程通信,就离不开pipe(管道),这是一个系统调用,用于在 UNIX 和类 UNIX 系统(如 Linux)上创建一个管道(pipe),实现进程间通信。它创建了一个双向的通信通道,允许一个进程向另一个进程发送数据。管道是单向的,即数据只能沿一个方向流动:从读端读取数据,从写端写入数据。 所以我们需要分别关闭管道两端的读取端和写入端,使得进程间信息可以单向传输;

函数定义

int pipe(int pipefd[2]);
  • 参数:
    • pipefd[2]:一个包含两个整数的数组,用来保存两个文件描述符(file descriptor):
      • pipefd[0] 是管道的读端(用于读取数据);
      • pipefd[1] 是管道的写端(用于写入数据);
  • 返回值:
    • 成功时返回0:表示管道创建成功;
    • 失败时返回-1:并设置errno表示

工作原理

pipe() 系统调用创建一个可以在进程之间传递数据的管道,它会生成两个文件描述符:

  • pipefd[0]:用于从管道的读端读取数据。
  • pipefd[1]:用于向管道的写端写入数据。

在典型的使用场景中:

  • 父进程可以创建一个管道,然后使用 fork() 创建子进程。
  • 子进程可以关闭管道的写端,通过读端读取父进程发送的数据。
  • 父进程可以关闭管道的读端,通过写端向子进程发送数据。

管道是一个单向的通信通道,因此数据只能从写端发送到读端。如果想要双向通信,需要创建两个管道,一个用于从父进程到子进程,另一个用于从子进程到父进程; 下面是示例代码:

#include <iostream>
#include <unistd.h>
#include <cassert>

int main() {
    int pipefd[2];  // 保存管道的读端和写端
    char buffer[100];

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe failed");
        return 1;
    }

    // 创建子进程
    pid_t pid = fork();
    assert(pid >= 0);

    if (pid == 0) {  // 子进程
        close(pipefd[1]);  // 关闭写端
        // 从管道读端读取数据
        read(pipefd[0], buffer, sizeof(buffer));
        std::cout << "子进程读取到的消息: " << buffer << std::endl;
        close(pipefd[0]);
    } else {  // 父进程
        close(pipefd[0]);  // 关闭读端
        const char* message = "Hello from parent!";
        // 向管道写端写入数据
        write(pipefd[1], message, strlen(message) + 1);
        close(pipefd[1]);
    }

    return 0;
}

解释

  • 父进程创建了一个管道,通过 pipefd[0] 和 pipefd[1] 分别代表管道的读端和写端。
  • 父进程使用 fork() 创建子进程,子进程会继承父进程的文件描述符。
  • 在子进程中,关闭写端,然后通过管道的读端 pipefd[0] 读取父进程写入的数据。
  • 在父进程中,关闭读端,然后通过管道的写端 pipefd[1] 写入一条消息。

常见问题

  1. 单向通信: 管道的一个限制是它只能进行单向通信。如果需要双向通信,可以使用两个管道,或者使用其他更复杂的 IPC(进程间通信)机制,如套接字、共享内存等。

  2. 管道阻塞:

  • 如果管道的写端没有被关闭,读操作可能会被阻塞,直到有数据可供读取。
  • 同样,如果管道的读端没有被关闭,写操作可能会阻塞,直到缓冲区有空间来写数据。
  1. 关闭文件描述符: 在父子进程中使用管道时,确保正确关闭不需要的文件描述符。例如,父进程应该关闭读端,而子进程应该关闭写端。否则,管道通信可能会出现错误。

管道的应用场景

  1. 进程间通信: 管道最常见的用途是实现父子进程之间的简单数据通信,尤其是在 fork() 之后。

  2. 子进程的输出重定向: 管道可以用于将子进程的标准输出重定向到父进程,以便父进程读取子进程的输出。

  3. 流水线处理: 在命令行中,使用管道 | 实现将一个程序的输出传递给另一个程序,正是基于 pipe() 的概念。例如:

ls | grep *.cpp
# 将ls的输出通过管道传递给 grep 进行过滤

注意,上述谈论的管道主要是匿名管道,与之对应的还有命名管道,这里不做深究,下一篇文章可以详细讲解一下二者之间的区别;

代码框架

有了上述的pipe管道函数的介绍,我们就可以编写一个用于测试父子进程间通信的程序,接下来我们将分析这个程序的主要功能及其实现方法,搭建出一个简易的框架:

  1. 首先我们需要一串父子进程,用于传输和接收信息,可以使用fork函数进行创建,同时还需要储存相应进程的信息,方便我们进行操作;
  2. 此外,我们还需要一个传输函数和接收函数,用于父进程发送信息和子进程接收信息;
  3. 同时,我们还需要一个待处理任务组,用于子进程处理相关的任务,这里为了简化操作,我们将采用简单的示例代码,只是为了检测相应的功能;
  4. 关于我们传递的信息内容,我们需要传递执行任务的子进程编号以及执行的任务编号,以便于子进程可以实现负载均衡image.png

代码实现

完成了上述的准备工作,我们就可以着手代码实现了,关于代码中的细节问题,注释中都有相应的解释,我后续也会再去完善相应的注释解释工作,代码具体见于我的GitHub仓库,实现代码在pipe分支