侧边栏壁纸
  • 累计撰写 218 篇文章
  • 累计创建 59 个标签
  • 累计收到 5 条评论

进程间通信03:匿名管道

barwe
2022-07-14 / 0 评论 / 0 点赞 / 1,158 阅读 / 2,337 字
温馨提示:
本文最后更新于 2022-07-14,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

管道

一个没有名字的管道只能用于父进程和它 fork 出的子进程之间的通信。

popen函数用文件流来表示管道,pipe函数用文件描述符来表示管道。

popen可以实现通过|符号将多个无关程序的输入输出串起来。

pipe用一个int[2]类型的数组来保存管道的读端[0]和写端[1]

匿名管道是一种很古老的 IPC 形式,其特点是:

  • 半双工:虽然父子进程都能用管道收发数据,但是同一时间管道内的数据只能向一个方向流动。就好像一条不限方向但是又只能并行通过一辆车的马路
  • 父子进程通信

popen

stdio.h 中关于 popenpclose 的声明如下:

FILE *popen (const char *command, const char *modes);
int pclose (FILE *stream);

popen 函数通过指定一个 shell 命令及其参数列表,在当前进程中启动一个子进程来执行指定的程序。

stdio.hpopen 函数的描述是:

Create a new stream connected to a pipe running the given command.

popen 会创建一个文件流,这个文件流被关联到了一个管道,这个管道连接了主进程和执行目标程序的子进程。

因为是用管道实现的,所以主进程可以向子进程发送数据,或者接受子进程发送过来的数据。

发送数据或者接收数据由 popenmodes 参数决定,该参数只能是"r"或者"w"

  • "r": 主程序通过管道向子程序发送数据,主程序将数据写入文件流,子程序从文件流中读取数据
  • "w": 主程序通过管道读取子程序发送的数据,子程序将数据写入流,主程序从流中读取数据

popen 函数返回的是一个 FILE 类型的指针,因此我们可以通过操纵文件的方式来发送和读取数据。

pclose 函数用来关闭由 popen 函数打开的文件流对象,这是一个安全的关闭方法:它只在子程序执行完毕后才会关闭文件流。

popen 本质上还是调用的系统 shell,通过 shell 的参数解析功能,可以执行参数非常复杂的子程序。与此同时,调用 shell 会在已经启动一个子进程时另外启动一个 shell 进程,即一次 popen 调用会启动两个进程,故其效率和资源较为浪费。

popen 是 stdio 提供的一个相对来说比较高级的函数,高级函数的普遍特点就是:用起来方便,但是会损失掉部分效率和性能。

pipe

相对于 popen 来说,unistd.h 提供了一个更加底层的 pipe 函数来实现管道传递数据:

int pipe(int file_descriptor[2]);

popen 执行的子程序需要启动一个额外的 shell 进程,同时其创建的文件流只能是只读的或者只写的。

pipe 函数没有上面的两个问题:不会启动额外的 shell 进程,可以更加细致的控制数据的读写。

可以预料的是,虽然更加底层的方法效率更高,但是它用起来会更麻烦,需要使用者了解更多的东西。

pipe 函数接收一个长度为 2 的整型数组,这个数组在传入 pipe 函数后会储存代表管道管道两端的文件描述符,同时 pipe 函数会返回管道启动状态:0 表示成功,-1 表示失败。

在使用 pipe 函数启动管道时并没有指定要执行的子程序,这是因为 pipe 函数执行子程序不是通过 shell 方式实现的,而是直接在源代码中指定子程序需要执行的代码。

pipe 会填充二元的文件描述符数组,使数组中的两个文件描述符以一种特殊的方式关联起来:基于先进先出(FIFO)的原则,写入 file_descriptor[1] 的数据可以从 file_descriptor[0] 原样读出。

popen 基于文件流工作,而 pipe 基于文件描述符工作。

按照规范,只能往 file_descriptor[1] 中写入数据,然后从 file_descriptor[0] 中读取数据。

下面是一个简单使用管道的例子:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#define MAX_LEN 128 // 从管道中读取的最大字节数

int main() {
    // rwpipe[0] 是管道的读端, rwpipe[1] 是管道的写端
    int rwpipe[2] = {};
    
    // 创建管道,用 rwpipe 保存管道的两端
    if (pipe(rwpipe) < 0) {
        perror("create pipe failed\n");
        return -1;
    }
    
    // fork 子进程
    pid_t pid = fork();
    // 返回值小于0说明创建子进程失败
    if (pid < 0) {
        perror("fork failed\n");
        return -1;
    }
    // 返回值等于0说明下面的程序已经在子进程中执行了
    else if (pid == 0) {
        // 子进程关闭通信管道的读端,此时子进程只能往管道中写数据
        close(rwpipe[0]);
        // 子进程往通信管道中写一点东西
        char s[] = "hello world";
        write(rwpipe[1], s, sizeof(s));
    }
    // 返回值大于0说明下面的程序会在父进程中执行
    else {
        // 父进程关闭写端,然后父进程只能从管道中读数据
        close(rwpipe[1]);
        // 父进程从管道中读取数据
        char line[MAX_LEN] = {}; // 读取的内容
        int n = read(rwpipe[0], line, MAX_LEN); // 实际读取的字节数,小于MAX_LEN
        printf("read %d bytes from pipe: %s\n", n, line);
    }
    
    return 0;
}
0

评论区