Skip to content

匿名管道pipe

管道的概念

管道是一种最基本的IPC机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道。

有如下特质:

  • 管道的本质是一块内核缓冲区,内部的实现是环形队列;
  • 由两个文件描述符引用,一个表示读端,一个表示写端;
  • 规定数据从管道的写端流入管道,从读端流出管道(数据的流向是单向的);
  • 数据被读走之后,相应的数据在管道中也就消失了,当两个进程都终结的时候,管道也自动消失;
  • 管道的读端和写端默认都是阻塞的。

管道的原理

  • 管道的实质是内核缓冲区,内部使用环形队列实现。
  • 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。
  • 实际操作过程中缓冲区会根据数据压力做适当调整。

管道的局限性

  • 数据一旦被读走,便不在管道中存在,不可反复读取。
  • 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道
  • 只能在有血缘关系的进程间使用管道。

创建管道-pipe函数

函数作用

创建一个管道

函数原型

cpp
#include <unistd.h>

/* On Alpha, IA-64, MIPS, SuperH, and SPARC/SPARC64; see NOTES */
struct fd_pair {
    long fd[2];
};
struct fd_pair pipe();

/* On all other architectures */
int pipe(int pipefd[2]);


#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <fcntl.h>              /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);

函数参数

若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端

函数返回值

  • 成功返回0
  • 失败返回-1,并设置errno值。

函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端, fd[1]是写端,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?

先使用 pipe 创建管道然后再使用 fork 创建子进程,此时父子进程同时拥有管道的读端和写端。如果要进行单工通信那么父子进程各关闭一端即可,但是要关闭的不能是同端。如果要实现双工通信,可以一开始使用 pipe 创建两个管道。

父子进程使用管道通信

一个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。

第一步:父进程创建管道

第二步:父进程fork出子进程

第三步:父进程关闭fd[0],子进程关闭fd[1]

创建步骤总结:

  • 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]和fd[1],分别指向管道的读端和写端。
  • 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  • 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出,这样就实现了父子进程间通信。

父子进程间使用管道通信示例

c
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    int fd[2];
    int ret = pipe(fd);

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork error");
        return -1;
    }
    else if (pid > 0) {
        close(fd[0]);
        char content[] = "hello pipe";
        write(fd[1], content, sizeof(content));
        wait(NULL);
    }
    else {
        close(fd[1]);
        char buf[1024];
        read(fd[0], buf, sizeof(buf));
        printf("%s\n", buf);
    }
    return 0;
}

管道的读写行为

读操作

  • 有数据 read正常读,返回读出的字节数
  • 无数据
    • 写端全部关闭 read解除阻塞,返回0,相当于读文件读到了尾部
    • 没有全部关闭 read阻塞

写操作

  • 读端全部关闭 管道破裂,进程终止,内核给当前进程发SIGPIPE信号
  • 读端没全部关闭
    • 缓冲区写满了 write阻塞
    • 缓冲区没有满 继续write

如何设置管道为非阻塞

默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参考下列三个步骤进行:

cpp
//第1步: 
int flags = fcntl(fd[0], F_GETFL, 0);
//第2步: 
flags |= O_NONBLOCK;
//第3步: 
fcntl(fd[0], F_SETFL, flags);

若是读端设置为非阻塞:

  • 写端没有关闭,管道中没有数据可读,则read返回-1
  • 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
  • 写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
  • 写端已经关闭,管道中没有数据可读,则read返回0

如何查看管道缓冲区大小

命令

ulimit -a

函数

cpp
long fpathconf(int fd, int name);
printf("pipe size==[%ld]\n", fpathconf(fd[0], _PC_PIPE_BUF));
printf("pipe size==[%ld]\n", fpathconf(fd[1], _PC_PIPE_BUF));

练习

  1. 一个进程能否使用管道完成读写操作呢?

    可以,但是没有必要

  2. 父子进程间通信,实现ps aux | grep bash

    使用execlp函数和dup2函数

    cpp
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/wait.h>
    #include <unistd.h>
    
    int main(int argc, char *argv[]) {
        int fd[2];
        int ret = pipe(fd);
    
        pid_t pid = fork();
        if (pid < 0) {
            perror("fork error");
            return -1;
        } else if (pid > 0) {
            close(fd[1]);
            dup2(fd[0], STDIN_FILENO);
            wait(NULL);
            execlp("grep", "grep", "bash", NULL);
        } else {
            close(fd[0]);
            dup2(fd[1], STDOUT_FILENO);
            execlp("ps", "ps", "-ef", NULL);
        }
    
        return 0;
    }

    程序输出:

    shell
    $ gcc pipe01.c
    $ ./a.out
    ubuntu     52990       1  0 Mar07 pts/29   00:00:00 /bin/bash
    ubuntu     58180       1  0 Mar07 pts/35   00:00:00 /bin/bash
    ... ...
    ubuntu    839346  839340  0 11:47 ?        00:00:00 bash
    ubuntu    839564  839563  0 11:47 pts/173  00:00:00 -bash
    ubuntu    846505  795553  0 12:31 pts/175  00:00:00 /bin/bash
    ubuntu    848420  846505  0 12:40 pts/175  00:00:00 grep bash
    $
  3. 兄弟进程间通信,实现ps aux | grep bash

    使用execlp函数和dup2函数,父进程要调用waitpid函数完成对子进程的回收

    cpp
     #include <stdio.h>
     #include <stdlib.h>
     #include <string.h>
     #include <syss/linuxHeader.h>
     #include <time.h>
     
     int main(int argc, char *argv[])
     {
     int fd[2];
     int ret = pipe(fd);
     
         for (int i = 0; i < 2; ++i)
         {
             ret = fork();
             ERR_CHECK(ret, -1, "fork faild", -1);
     
             if (ret == 0)
             {
                 if (i == 0)
                 {
                     close(fd[0]);
                     dup2(fd[1], STDOUT_FILENO);
                     execlp("ps", "ps", "-elf", NULL);
                 }
                 else if (i == 1)
                 {
                     close(fd[1]);
                     dup2(fd[0], STDIN_FILENO);
                     execlp("grep", "grep", "bash", NULL);
                 }
             }
         }
     
         close(fd[1]);
         close(fd[0]);
     
         waitpid(0, NULL, 0);
     
         return 0;
     }

    程序输出:

bash
$ ./a.out 
0 S syss      122624  121718  0  80   0 -  5443 do_wai 4月24 pts/0   00:00:01 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
0 S syss      124463  121718  0  80   0 -  5376 do_sel 4月24 pts/1   00:00:00 /usr/bin/bash --init-file /usr/share/code/resources/app/out/vs/workbench/contrib/terminal/common/scripts/shellIntegration-bash.sh
0 S syss      308748  308746  0  80   0 -  4358 pipe_r 18:21 pts/0    00:00:00 grep bash