Linux系统编程之进程间通信--管道pipe()函数详解

前言

本文主要介绍了Linux编程中进程间通信方式中的管道,内容包括管道的原理、作用,pipe()函数的使用以及注意事项等。通过图文和代码相结合的方式帮助大家更好地理解管道这一概念,以及如何更好更合理地去使用管道,希望能够对大家有所帮助。如文章有出现错误的地方,欢迎大家扶正错误,同时也欢迎大家多多提一些建议。

在了解管道之间,我们首先需要简单了解下进程之间的通信方式有哪些?这些进程间的通信方式各自的优势是什么?管道的作用是什么?管道的原理又是什么?等等问题。让我们带着这些问题往下学习,相信你会有更多的收获。下面我们就开始本文的正文部分。

进程间的通信

每个进程各自有不同的用户地址空间,任何一个进程的变量在另一个中都是无法看到的,数据相互独立。所以进程之间要交换数据必须通过内核,在内核中开辟一块缓冲区,进程A把数据从用户空间拷到内核缓冲区,进程B再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信,即IPC(interProcess communication)

进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的发展,有一些方法由于自身设计缺陷被淘汰或者弃用,如今常用的方式有:

  1. 管道(使用最简单)
  2. 信号(开销最小)
  3. 共享映射区(无血缘关系)
  4. 本地套接字(最稳定)

管道

管道的概念

管道是一种最基本的IPC机制,把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入,作用于有血缘关系的进程之间,完成数据传递,管道是内核空间中的一块缓冲区,默认大小为4096字节。调用pipe系统函数即可创建并打开一个管道。

举例说明

shell中执行命令,经常会将上一个命令的输出作为下一个命令的输入,由多个命令配合完成一件事情。而这就是通过管道来实现的。| 这个竖线就是管道符号

ls -l | grep string   //grep是抓取指令

ls命令(其实也是一个进程)会把当前目录中的文件都列出来,但它不会直接输出,而是把要输出到屏幕上的数据通过管道输出到grep这个进程中,作为grep这个进程的输入;然后这个进程对输入的信息进行筛选(grep的作用),把存在string的信息的字符串(以行为单位)打印在屏幕上。

如图所示:
画图解释管道原理

管道的特性

  • 其本质是一个伪文件(实为内核缓冲区)。
  • 由两个文件描述符引用,一个表示读端,一个表示写端。
  • 规定数据从管道的写端流入管道,从读端流出。
  • 管道不是普通的文件,不属于某个文件系统,其只存在于内存中,俗称伪文件,不需要占用磁盘空间。
  • 管道没有名字,只能在具有公共祖先的进程(父进程和子进程,或两个兄弟进程)之间使用
  • 管道随进程,进程在管道在,进程消失管道对应的端口也关闭,两个进程都消失管道也消失。

管道的原理

管道实为内核使用环形队列机制,借助内核缓冲区实现。

管道的局限性

  • 管道不允许进程自己写,自己读。
  • 管道中数据不可反复读取。一旦读走,管道中数据不再存在。
  • 采用半双工通信方式,数据在同一时刻只能在一个方向上流动(单向流动)。
  • 只能再有公共祖先的进程间使用管道。

这里简单举例子介绍一下通信方式,方便大家理解:

单工:遥控器,一端只负责发射信号,一端只负责接收信号。

双向半双工:对讲机,两端都可以发射和接受信号,但是不允许同时进行。

双向全双工:手机,运行同时发射和接收信号。

管道的读写行为

读管道:

  1. 管道有数据,read返回实际读取到的字节数。
  2. 管道无数据
    1. 管道写端被全部关闭,read返回0。(类似读到文件尾)
    2. 管道写端没有被全部关闭,read阻塞等待(不久可能有数据传达,此时会让出cpu)。

写管道:

  1. 管道读端全部被关闭,进程异常终止(也可捕捉SIGPIPE信号,使进程不终止)。
  2. 管道读端没有被全部关闭
    1. 管道已满,write阻塞。
    2. 管道未满,write将数据写入,并返回实际写入的字节数。

pipe()函数

用于创建并打开一个管道,实现进程间的通信。

SYNOPSIS
       #include <unistd.h>

       int pipe(int pipefd[2]);

参数

  • pipefd:是文件描述符数组,其中fd[0] 表示读端,fd[1]表示写端。

返回值

  • 若函数顺利执行,则返回0。
  • 若发生错误,则返回-1,并设置errno

pipe()函数的基本用法

父子进程之间通过管道进行通信。

#include <stdio.h>
#include <sys/types.h>
#include <errno.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    int ret;
    int fd[2];//数组
    pid_t pid;

    char *str = "hello pipe\n";
    char buf[1024];

    //创建管道
    ret = pipe(fd);
    if(ret == -1)//当ret为-1时,说明pipe函数发生错误执行失败
    {
        perror("pipe error\n");
        exit(1);
    }
    
    //创建子进程
    pid = fork();
    if(pid > 0)//父进程
    {
        close(fd[0]);//父进程关闭读端
        write(fd[1],str,strlen(str));
        close(fd[1]);
        sleep(1);//休眠一秒,目的是为了使子进程先于父进程终止,避免出现孤儿进程。
    }
    else if(pid == 0)//子进程
    {
        close(fd[1]);//子进程关闭写端
        ret  = read(fd[0],buf,sizeof(buf));//子进程读取管道中的数据
        write(STDOUT_FILENO,buf,ret);//写进标准输出中去
        close(fd[0]);
    }
    else//进程创建错误
    {
        perror("fork error\n");
        exit(1);
    }

    return 0;
}

运行结果如下:

[root@model function]# ./pipe
hello pipe
[root@model function]#

注意

创建管道应该在创建进程之前进行。这是因为管道的文件描述符需要在父进程中创建,然后通过 fork 传递到子进程(以确保子进程能够继承管道的文件描述符)。管道的本质是父子进程之间共享的通信通道,而这种共享是通过 fork 复制文件描述符表实现的。

如果在 fork 之后创建管道,子进程不会继承管道的文件描述符,因此无法使用它进行通信。

使用pipe函数实现父子进程间管道原理

使用pipe、dup2、fork、execlp函数实现ps aux | grep pipe命令。

思路

  1. 创建管道
  2. 创建子进程
  3. 子进程中,关闭读端,重定向标准输入到管道写端。
  4. 父进程中,关闭写端,重定向标准输出到管道读端。
  5. 子进程关闭写端,再通过execlp函数实现ps aux命令,同时设置错误提示信息。
  6. 父进程关闭读端,再通过execlp函数实现grep pipe命令,同时设置错误提示信息。

代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <string.h>

int main()
{
    pid_t pid;
    int fd[2];
    int ret;

    //创建管道,创建管道要在创建进程之前完成
    ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe error\n");
        exit(1);
    }

    //创建子进程
    pid = fork();
    if(pid == 0)//子进程,子进程中执行ps aux命令
    {

        close(fd[0]);//关闭读端
        dup2(fd[1],STDOUT_FILENO);//重定向标准输出到写端
        close(fd[1]);
        sleep(2);
        
        //execlp函数实现ps aux命令,父进程中同理
        execlp("ps","ps","aux",NULL);
        //设置错误提示信息
        perror("execlp error\n");
        exit(1);

    }
    else if(pid > 0)//父进程,父进程中执行grep pipe命令
    {
        close(fd[1]);//关闭写端
        dup2(fd[0],STDIN_FILENO);//重定向标准输入到读端
        close(fd[0]);

        //execlp函数实现grep pipe命令
        execlp("grep","grep","pipe",NULL);
        //设置错误提示信息
        perror("execlp error\n");
        exit(1);

        //回收子进程
        /*
        int status;
        waitpid(pid,&status,0);
        if(WIFEXITED(status))
        {
            printf("child exited with status %d\n",WIFEXITED(status));
        }
        */
    }
    else
    {
         perror("fork error\n");
         exit(1);
    }
    return 0;
}

运行结果如下:

[root@model function]# ./temp
root       4784  0.0  0.0 112828   968 pts/0    R+   12:54   0:00 grep pipe
[root@model function]#

注意

  • 管道要在创建子进程之间创建,否则子进程无法继承父进程的管道,导致进程间通信不成功。
  • 建议先在子进程中执行命令,然后通过管道输出,作为输入给父进程,再执行父进程中的命令。因为我在之前实验中,采用的是父进程先执行ps aux命令,通过管道,给子进程作为输入,但是无法达到预期的效果。个人以为出现这样的情况是,父进程先执行完之后,进程就终止了,同时因为传输的数据内容较少,子进程还没有来得及读取,父进程已经结束,父进程先于子进程结束,导致出现了孤儿进程,继而被init进程接替,所以无法实现预期的效果。这仅是我个人的猜想,目前还没有找到确切的理论验证。
  • 通过写端在管道中写完数据之后,有一个关闭写端的操作,之前我疑惑关闭之后,会不会导致execlp函数的执行的结果无法写入到管道中去。但其实并不会阻止数据写入管道,因为是通过dup2重定向了标准输出(STDOUT_FILENO)到管道的写端文件描述符,此时,标准输出和管道写端指向的是同一个内核管道对象,也就是说我们关闭的仅仅是原始的管道写端,而因为 dup2 已经创建了一个新的指向同一资源的文件描述符,所以不会影响标准输出到管道写端中。读端也是同理。

pipe函数练习

使用pipe、dup2、fork、execlp函数实现父子进程间ls | wc -l命令。

代码

#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>

int main()
{
    pid_t pid;
    int ret;
    int fd[2];

    //创建管道
    ret = pipe(fd);
    if(ret == -1)
    {
        perror("pipe error\n");
        exit(1);
    }

    //创建子进程
    pid = fork();
    if(pid > 0) //父进程
    {
        close(fd[0]);
        //重定向
        dup2(fd[1],STDOUT_FILENO);
        close(fd[1]);
        //父进程实现执行ls命令
        execlp("ls","ls",NULL);
        perror("parent execlp error\n");
        exit(1);
    }
    else if(pid == 0)//子进程
    {
        close(fd[1]);
        //重定向
        dup2(fd[0],STDIN_FILENO);
        close(fd[0]);
        //子进程实现执行wc -l命令
        execlp("wc","wc","-l",NULL);
        perror("child execlp error\n");
        exit(1);
    }
    else
    {
        perror("fork error\n");
        exit(1);
    }
    return 0;
}

运行结果如下:

[root@model function]# ./ls
[root@model function]# 55
    

问题及改进

以上代码执行后,会出现一个问题,不管在父子进程中如何操作,父进程都是先执行,先于子进程结束,这样也就会导致出现孤儿进程。所以我们为了避免这个情况出现,可以创建两个子进程来分别执行对应的功能命令,父进程只用负责回收两个子进程,也就是兄弟进程间通信

代码

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

int main()
{
    pid_t pid,wpid;
    int ret;
    int fd[2];
    int i = 0;

    //创建管道
    ret = pipe(fd);
    //设置管道创建错误信息提示
    if(ret == -1)
    {
        perror("pipe error\n");
        exit(1);
    }

    //循环创建两个子进程
    for(i = 0; i<2; i++)
    {
        pid = fork();
        //子进程中直接跳出次循环,目的是为了不让其继续创建子进程
        if(pid == 0)
            break;
        //若发生错误,设置错误信息提示
        else if(pid == -1)
        {
            perror("fork error\n");
            exit(1);
        }
    }
    if(i == 0) //第一个子进程
    {
        close(fd[0]);
        //重定向
        dup2(fd[1],STDOUT_FILENO);
        close(fd[1]);
        //父进程实现执行ls命令
        execlp("ls","ls",NULL);
        perror("parent execlp error\n");
        exit(1);
    }
    else if(i == 1)//第二个子进程
    {
        close(fd[1]);
        //重定向
        dup2(fd[0],STDIN_FILENO);
        close(fd[0]);
        //子进程实现执行wc -l命令
        execlp("wc","wc","-l",NULL);
        perror("child execlp error\n");
        exit(1);
    }
    else if(i == 2)//父进程
    {
        //父进程要关闭管道读写端,否则代码执行不成功,因为数据在管道中要求是单向流动。
        //要关闭多余的读写段,保证数据单向流动
        close(fd[0]);
        close(fd[1]);
        //通过循环回收两个子进程
        while((wpid = waitpid(-1,NULL,0)) != -1)
        {
            printf("wait child ID is %d\n",wpid);
        }
    }
    return 0;
}

运行结果如下:

[root@model function]# ./pipe_brother
wait child ID is 5529
55
wait child ID is 5530

注意

  • 父进程一定要关闭管道读写端,否则代码执行不成功。这是因为数据在管道中要求是单向流动,我们要关闭多余的读写段,保证数据单向流动。
  • 调用一次waitpid函数只能回收一个子进程,使用循环多次调用。
  • 管道要先于子进程创建。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值