1.什么是管道 ?

所谓管道,是指用于连接一个读进程和一个写进程,以完成它们之间通讯的同享文件,又称 pipe 文件。

向管道(同享文件)供给输入的发送进程(即写进程),以字符流方式将很多的数据送入管道;而接纳管道输出的接纳进程(即读进程),可从管道中接纳数据。由于发送进程和接纳进程是利用管道进行通讯的,故又称管道通讯。

为了和谐两边的通讯,管道通讯机制必须供给以下3 方面的和谐能力。

  • 互斥。当一个进程正在对 pipe 进行读/写操作时,另一个进程必须等候。
  • 同步。当写(输入)进程把一定数量(如4KB)数据写入 pipe 后,便去睡觉等候,直到读(输出)进程取走数据后,再把它唤醒。当读进程读到一空 pipe 时,也应睡觉等候,直至写进程将数据写入管道后,才将它唤醒。
  • 对方是否存在。只要确认对方已存在时,才干进行通讯。

2.pipe()函数创立管道

2.1 函数原型

包含头文件

#include <unistd.h>
  • 函数原型
int pipe(int pipefd[2]);
​
#define _GNU_SOURCE
#include <unistd.h>int pipe2(int pipefd[2], int flags);
  • 函数功用
pipe() creates a pipe, a unidirectional data channel that can be used for interprocess communication.
  • 函数参数

pipefd[2]:读端和写端的文件描述符 函数回来值

On success, zero is returned.
On error, -1 is returned, and errno is set appropriately.

2.2 作业原理

一般来说,要在子进程创立之前运用pipe()来创立管道,这姿态进程才干同享这两个文件描述符fd[1]和fd[2]。pipe()函数创立一个管道就相当于翻开了一个伪文件(这个伪文件实践上是内核缓冲区,像管道文件读写数据其实是在读写内核缓冲区,由于这个缓冲区只能单向流转数据,所以形象的称为管道),所以调用成功会回来两个文件描述符给参数pipefd[2],其中fd[0]代表读端,fd[1]代表写端,就像0代表规范输入1代表规范输出一样作为一种规则。并且这两个文件描述符在运用的时分不需要open()翻开,可是需要咱们手动的close()封闭。

管道创立成功后,父进程一起拥有读写两头,由于子进程是对父进程的仿制,所以子进程也会拥有读写两头。下面经过图示来阐明进程间是如何经过管道通讯的。

① 父进程调用pipe()函数创立管道,并得到指向管道读端和写端的文件描述符fd[0]和fd[1]。创立出来的管道实践上是内核的一块缓冲区,咱们能够像读写文件一样来操作这个缓冲区,所以也能够把他理解为一个伪文件。

Framework—进程间通信(IPC) 系列之管道(pipe)

② 父进程调用fork()创立子进程,子进程将同享这两个指向管道读写端的文件描述符。

Framework—进程间通信(IPC) 系列之管道(pipe)

③ 假如父进程封闭管道读端,子进程封闭管道写端,此时父进程能够向管道中写入数据,子进程将管道中的数据读出,反之同理。由于管道是利用环形队列完成的,数据从写端流入管道,从读端流出,这样就完成了进程间通讯。

Framework—进程间通信(IPC) 系列之管道(pipe)

2.3 经过实战分析管道的特性

示例1:父子进程读写管道

/************************************************************
  >File Name  : pipe_test.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月21日 星期六 17时53分56秒
  >************************************************************/
  >#include <stdio.h>
  >#include <stdlib.h>
  >#include <unistd.h>
​
int main(int argc, char* argv[])
{
    int fd[2];
    pipe(fd);
    pid_t pid = fork();
    
    if(pid == 0)
    {
        /*子进程向管道写*/
        /*sleep(3); read读设备的时分,默许是会堵塞等候的,写进程睡觉的时分,读进程会堵塞等候,直到读取到数据*/
        char str[] = "hello pipe...\n";
        write(fd[1], str, sizeof(str));
    }
    if(pid > 0)
    {
        char buf[15] = {0}; /*创立一个缓冲区来缓存读出的数据*/
        /*read读设备的时分,默许是会堵塞等候的*/
        int ret = read(fd[0], buf, sizeof(buf));
        if(ret > 0)
        {
            write(STDOUT_FILENO, buf, ret);
        }
    }
    return 0;
}
​

由于resd()函数读设备时默许堵塞等候的特性,即便写进程没有当即写,读进程也能读到数据,由于它会堵塞等候。

Framework—进程间通信(IPC) 系列之管道(pipe)

❀示例2:运用管道完成 ps | grep 指令

/************************************************************
  >File Name  : mpsgrep.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月21日 星期六 18时08分56秒
  >************************************************************/
  >#include <stdio.h>
  >#include <stdlib.h>
  >#include <unistd.h>
​
int main(int argc, char* argv[])
{
    int fd[2];
    pipe(fd);
    pid_t pid = fork(); /*一个进程履行ps一个进程履行grep来完成 ps | grep*/
    if(pid == 0) /*子进程履行ps*/
    {/*把ps的履行成果传给grep,所以子进程写,父进程读*/
        /*首先把ps指令的履行成果重定向到管道的写端(默许将履行成果输出到stdout)*/
        dup2(fd[1], STDOUT_FILENO);
        /*拉起ps进程*/
        execlp("ps", "ps", "aux", NULL);
    }
    if(pid > 0) /*父进程履行grep*/
    {
        /*把grep读取重定向到fd[0],由于默许grep是在stdin获取输入的*/
        /*假如在shell指令行运用grep,形式是在规范输入中匹配*/
        dup2(fd[0], STDIN_FILENO);
        /*拉起grep进程*/
        execlp("grep", "grep", argv[1], NULL);
    }
    return 0;
}
​

上面的程序履行后,能够看到输出成果,的确显示了bash相关的进程信息

Framework—进程间通信(IPC) 系列之管道(pipe)

咱们再起一个终端,运用 ps aux 指令检查进程会发现,子进程中拉起的ps进程变成了僵尸进程,并且父进程没有退出。(实践上,假如父进程退出了,子进程就会被init进程收养并收回)

Framework—进程间通信(IPC) 系列之管道(pipe)

ps进程变成僵尸进程是由于,咱们在父进程中并没有收回子进程,由于execlp()函数拉起一个进程后,假如履行成功,就不会再回来了,那么咱们也没办法去收回这个子进程ps。可是咱们知道,假如父进程停止了,子进程就会被init进程收养并收回,所以咱们只要让父进程(也便是程序中的grep进程)退出,就能够处理子进程收回问题了。

下面,咱们分析下父进程为什么没有退出,正常状况下,父进程履行完grep指令就应该正常退出的。实践上,这是管道的特性引起的,咱们知道,pipe()创立管道后会在内核分配一个缓冲区,并回来两个文件描述符,父进程和子进程都持有读写这两个文件描述符。咱们在进程间通讯的时分,由于管道是单向数据流转,所以只要一个进程写一个进程读,比方上面的程序,咱们让子进程写,让父进程读,但这并不代表父进程不持有写端文件描述符。问题就在这里,虽然子进程已经变成了僵尸进程,可是父进程依然持有写端文件描述符,所以父进程就会认为还存在其他进程来写入管道,于是父进程就会等候写入,而不退出。

处理方法便是,咱们在进程间通讯时,要确保数据单向流转,在读进程中封闭管道的写端文件描述符,在写进程中封闭管道的读端文件描述符。咱们依据这个准则来改造一下上面的程序即可。

/************************************************************
  >File Name  : mpsgrep_02.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月21日 星期六 18时08分56秒
  >************************************************************/
  >#include <stdio.h>
  >#include <stdlib.h>
  >#include <unistd.h>int main(int argc, char* argv[])
{
    int fd[2];
    pipe(fd);
    pid_t pid = fork(); /*一个进程履行ps一个进程履行grep来完成 ps | grep*/
    if(pid == 0) /*子进程履行ps*/
    {/*把ps的履行成果传给grep,所以子进程写,父进程读*/
        /*封闭读端文件描述符,确保数据单向流转*/
        close(fd[0]);
        /*首先把ps指令的履行成果重定向到管道的写端(默许将履行成果输出到stdout)*/
        dup2(fd[1], STDOUT_FILENO);
        /*拉起ps进程*/
        execlp("ps", "ps", "aux", NULL);
    }
    if(pid > 0) /*父进程履行grep*/
    {
        /*封闭写端文件描述符,确保数据单向流转,防止读进程堵塞*/
        close(fd[1]);
        /*把grep读取重定向到fd[0],由于默许grep是在stdin获取输入的*/
        /*假如在shell指令行运用grep,形式是在规范输入中匹配*/
        dup2(fd[0], STDIN_FILENO);
        /*拉起grep进程*/
        execlp("grep", "grep", argv[1], NULL);
    }
    return 0;
}
​

这样,父进程就不会堵塞等候,而是直接退出,而子进程也不会发生僵尸进程。

Framework—进程间通信(IPC) 系列之管道(pipe)

3.管道的读写行为

运用管道进行进程间通讯的时分,假定没有设置O_NONBLOCK标志(也便是说都是堵塞I/O操作),有以下几种特殊状况

  • 假如一切指向管道写端的文件描述符都封闭了(管道写端引证计数为0),而仍然有进程从管道的读端读数据,那么管道中剩下的数据都被读取后,再次read会回来0,就像读到文件结尾一样。
  • 假如有指向管道写端的文件描述符没封闭(管道写端引证计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩下的数据都被读取后,再次read会堵塞,直到管道中有数据可读了才读取数据并回来。
  • 假如一切指向管道读端的文件描述符都封闭了(管道读端引证计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程反常停止。当然也能够对SIGPIPE信号施行捕捉,不停止进程。(在讲信号的时分会细说)
  • 假如有指向管道读端的文件描述符没封闭(管道读端引证计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会堵塞,直到管道中有空位置了才写入数据并回来。

其实,总的来说能够分为读管道和写管道两种的状况

读管道

  1. 假如管道中有数据,read回来实践读到的字节数。
  2. 假如管道中无数据:
  • 假如管道写端被悉数封闭,read回来0,相当于读到文件结尾。
  • 假如写端没有悉数被封闭,read堵塞等候(不久的将来可能有数据递达,此时会让出cpu),假如不想让read堵塞,能够运用fcntl设置非堵塞。

写管道

  1. 假如管道读端悉数被封闭,会发生一个信号SIGPIPE,进程反常停止(也可运用捕捉SIGPIPE信号,使进程不停止)。
  2. 假如管道读端没有悉数封闭
  • 假如管道已满,write堵塞,(管道实践上是内核中的一个缓冲区,它是有巨细的)。
  • 假如管道未满,write将数据写入,并回来实践写入的字节数。
/************************************************************
  >File Name  : pipe_test2.c
  >Author     : Mindtechnist
  >Company    : Mindtechnist
  >Create Time: 2022年05月21日 星期六 17时53分56秒
  >************************************************************/
  >#include <stdio.h>
  >#include <stdlib.h>
  >#include <unistd.h>
  >#include <sys/types.h>
  >#include <sys/wait.h>
​
int main(int argc, char* argv[])
{
    int fd[2];
    pipe(fd);
    pid_t pid = fork();
    if(pid == 0)
    {
        sleep(3); 
        close(fd[0]); /*封闭读端*/
        char str[] = "hello pipe...\n";
        write(fd[1], str, sizeof(str));
        close(fd[1]); /*封闭写端*/
        while(1)
        {
            sleep(1);
        }
    }
    if(pid > 0)
    {
        close(fd[1]); /*封闭写端*/
        close(fd[0]); /*封闭读端*/
        char buf[15] = {0}; 
        int status;
        wait(&status);
        if(WIFSIGNALED(status))
        {
            printf("kill: %d\n", WTERMSIG(status));
        }
        while(1)
        {
            int ret = read(fd[0], buf, sizeof(buf));
            if(ret > 0)
            {
                write(STDOUT_FILENO, buf, ret);
            }
        }
    }
    return 0;
}
​

4.管道(缓冲区)巨细

运用指令检查

ulimit -a

Framework—进程间通信(IPC) 系列之管道(pipe)

管道巨细是8个512byte的巨细。

也能够运用函数fpathconf()检查

#include <unistd.h>long fpathconf(int fd, int name); 
/*fd能够是fd[0]或fd[1],name是一个选项*/

实践上运用 ulimit -a 看到的是内核给管道的巨细,可是管道的容量实践上可能要比这个值大。

5.管道的优缺陷

优点:

  • 简略,比较信号,套接字完成进程间通讯,简略很多。(其实要想完成父进程和子进程双向通讯,能够创立两个管道)

缺陷:

  • 只能单向通讯,双向通讯需建立两个管道。
  • 只能用于有血缘联系的进程间通讯(父子、兄弟等有共同祖先的进程),有名管道可处理该问题。

以上便是Android Framework中的IPC通讯协议中的管道(pipe)解析;关于framework的通讯机制或更多framework的进阶,能够> 以下链接:

传送直达↓↓↓ :www.6hu.cc/go//?target=htt…

总结

  • 管道也称无名管道,是 UNIX 系统中进程间通讯(IPC)中的一种。
  • 管道由于是无名管道,因而只能在有亲缘联系的进程间运用。
  • 管道不是一般的文件,它是基于内存的。
  • 管道归于半双工,数据只能从一方流向另一方,也即数据只能从一端写,从另一端读。
  • 管道中读数据是一次性的操作,数据读取后就会开释空间,让出空间供更多的数据写。
  • 管道写数据遵循先入先出的准则。