Linux C 进程间通信——管道

初识管道


  • 管道是Unix中最古老的进程间通信方式
  • 我们把从一个进程连接到另一个进程的一个数据流称为一个”管道”
  • 默认情况下,一个进程都默认打开3个设备文件:一个标准输入设备(键盘)、标准输出设备(显示器)、标准错误输出设备(显示器)。且默认从标准输入中读取数据,将正确的信息以及错误的信息写入到标准输出。使用管道” | “可以将两个命令连接起来,从而改变标准的输入输出方式。例如:


这里写图片描述

  • rpm -qa命令表示:列出所有被安装的rpm package;
  • grep telnet 命令表示:匹配telnet关键字
  • 所以上述命令是将rpm -pa的输出作为grep telnet命令的输入。连接输入/输出的中间设备即为一个管道文件。因此使用管道可以将一个命令的输出作为另一个命令的输入


这里写图片描述

匿名管道——PIPE


概念

  • 在前面一小节中,我们已经初步认识了管道的作用,即可以将一个命令的输出作为另一个命令的输入(运行时,一个命令创建一个进程),而这种管道是临时的,命令执行完成后将自动消失,通常我们称这类管道为匿名管道
  • 匿名管道的创建:
#include <unistd.h>

int pipe(int fd[2]);
  • 参数:pipefd[2]:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端
  • 返回值:成功返回0,失败返回错误码


这里写图片描述

  • 管道创建实例:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char buf[100];
    int fd[2];
    int len;

    if(pipe(fd) == -1)
    {
        perror("pipe");
        return -1;
    }

    //1.从标准输入中读取数据
    while(fgets(buf,100,stdin))
    {
        len = strlen(buf);
        //2.通过管道的写端将数据写入到管道中
        if(write(fd[1],buf,len)!=len)
        {
            perror("write to pipe!");
            return -1;
        }

        memset(buf,0x00,sizeof(buf));
        //3.从管道中读取数据
        if(read(fd[0],buf,sizeof(buf)!=len)
        {
            perror("read from pipe");
            return -1;
        }
        //4.将数据写到屏幕上
        if(write(1,buf,len)!=len)
        {
            perror("write to stdout");
            return -1;
        }
    }
    return 0;
}
  • 程序运行结果:


这里写图片描述

读写匿名管道

  • 匿名管道的读写和普通文件不一样。任何进程读、写匿名管道时必须确认还存在一个进程(这个进程可以是自己),该进程以写/读的方式(即读对应写,写对应读)访问管道(即可以操作相应的文件描述符)
  • 读写管道使用的系统调用是read和write,两者都默认以阻塞方式读写管道

当没有数据可读时:

  • O_NONBILOCK disable:read调用阻塞,即进程暂停执行,一直到有数据来为止
  • O_NONBILOCK enable:read调用返回-1,errno值为EAGAIN

当管道满的时候:

  • O_NONBILOCK disable:write调用阻塞,直到有进程读走数据
  • O_NONBILOCK enable:write调用返回-1,errno值为EAGAIN

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0

  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性,否则不能保证

进程共享管道原理


这里写图片描述

  • 父进程创建子进程后,关掉各自不用的文件描述符
  • 比如图中所示,若我们想要通过管道来实现:子进程从父进程处读取信息,则父进程需要关掉读端,子进程需要关掉写端,父进程往管道中写数据,子进程从管道中读数据,这样父子进程间就可以实现单向通信了
  • 通过程序实例来实现上述过程:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    int fd[2];
    char buf[200];
    int len; 

    if(pipe(fd) == -1)
    {
        perror("pipe");
        return -1;
    }

    fgets(buf,200,stdin);
    len=strlen(buf);
    pid_t pid;
    pid = fork();
    if(pid > 0) //父进程
    {
        //关闭读端,将标准输入写入写端,让子进程来读取
        close(fd[0]);
        printf("父进程写入管道:%s",buf);
        if(write(fd[1],buf,len) != len)
        {
            perror("write to pipe");
            return -1;
        }
        memset(buf,0x00,sizeof(buf));        
    }else if(pid ==  0) //子进程
    {
        //关闭写端,从管道读端读取父进程写入的数据,写到屏幕上
        close(fd[1]);
        if(read(fd[0],buf,sizeof(buf)) != len)
        {
            perror("read from pipe");
            return -1;
        }
        close(fd[0]);
        printf("子进程从管道读取:%s",buf);
        sleep(1);
    }else
    {
        perror("fork()");
        return -1;
    }
    printf("\n");
    return 0;
}
  • 程序运行结果:


这里写图片描述

管道特点

  • 只能用于具有亲缘关系的进程(例如父子进程)之间通信;通常,一个管道有一个进程创建,然后该进程调用fork,此后父、子进程之间就可以通行
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以说管道的生命周期随进程
  • 管道是半双工的,数据只能向一个方向流动;需要双方通行时,需要建立起两个管道


这里写图片描述

有名管道——FIFO


概念

  • 上面我们提到匿名管道只能用于具有亲缘关系的进程,那么没有亲缘关系的进程如何通过管道进行通信,命名管道有效的解决了这个问题,命名管道依赖于文件系统,是一个存在的特殊文件,实现不同进程对文件系统下的某个文件的访问是很方便实现的,因此FIFO可以在同主机任意进程之间实现通信
  • 命名管道和普通文件一样具有磁盘存放路径、文件权限和其他属性;但是,命名管道和普通管道又有区别,命名管道并没有在磁盘中存放真正的信息,它存储的通信信息在内存中,两个进程结束后自动丢失,拥有一个磁盘路径仅仅是一个接口,其目的是使进程间信息的编程更简单统一
  • 通行的两个进程结束后,命名管道的文件路径本身依然存在,这是和匿名管道不一样的地方
  • 命名管道的创建:
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • pathname:要创建的管道文件名
  • mode: 文件的权限
  • 返回值:执行成功返回0,失败返回-1
  • mkfifo()建立的FIFO文件,可以通过读写一般文件的方式。当时用open()打开FIFO文件时,O_BLOCKLOCK会有影响

读写命名管道

  • 在通过read和write系统调用来执行读写操作前,需要用open()函数打开该文件;另外,操作有名管道的阻塞位置为open位置,而不是匿名管道的读写位置

如果当前打开操作是为读而打开FIFO时:

  • O_BLOCKLOCK disable:阻塞直到有相应进程为写而打开该FIFO
  • O_BLOCKLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时:

  • O_BLOCKLOCK disable:阻塞直到有相应进程为读而打开该FIFO
  • O_BLOCKLOCK enable:立刻返回失败,错误码为ENXIO

两进程已经完成打开管道操作,中途其中一个进程退出

  • 未退出一端若为写操作,将返回SIGPIPE信号
  • 未退出一端如果是阻塞读操作,读操作将不再阻塞,直接返回0

命名管道应用实例

  • 文件拷贝


这里写图片描述

  • 程序代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <error.h>
#include <unistd.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
    mkfifo("fifo",0644);
    //读端
    int infd = open("file",O_RDONLY);
    if(infd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    //写端
    int outfd = open("fifo",O_WRONLY);
    if(outfd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[1024];
    int n = read(infd,buf,1024);
    if(n > 0)
    {
        write(outfd,buf,n);
    }
    close(infd);
    close(outfd);
    return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <error.h>
#include <fcntl.h>
int main(int argc,char* argv[])
{
    //打开copy文件的写端
    int outfd = open("file_copy",O_WRONLY | O_CREAT | O_TRUNC,0644);
    if(outfd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    //打开管道的读端
    int infd = open("fifo",O_RDONLY);
    if(infd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    char buf[1024];
    int n = read(infd,buf,1024);
    if(n > 0)
    {
        write(outfd,buf,n);
    }
    close(infd);
    close(outfd);
    //删除命名管道
    unlink("fifo");
    return 0;
}
  • 运行结果:


这里写图片描述

  • 文件拷贝结果:


这里写图片描述

  • server&&client通信
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <error.h>
#include <fcntl.h>

int main(void)
{
    mkfifo("svr_cli",0644);
    char buf[1024];

    int outfd = open("svr_cli",O_RDONLY);
    if(outfd == -1)
    {
        perror("open");
        exit(EXIT_FAILURE);
    }

    while(1)
    { 
        buf[0] = 0;
        ssize_t n = read(outfd,buf,sizeof(buf)-1);
        if(n > 0)
        {
            buf[n-1] = 0;
            printf("client say:%s\n",buf);
        }else if(n == 0)
        {
            printf("client quit...\n");
            exit(EXIT_SUCCESS);
        }else
        {
            perror("read");
            exit(EXIT_FAILURE);
        }
    }
    close(outfd);
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <error.h>

int main(void)
{
    char buf[1024];

    int infd = open("svr_cli",O_WRONLY);
    if(infd == -1)
    {
        perror("svr_cli");
        exit(EXIT_FAILURE);
    }

    while(1)
    {
        printf("please say#");
        fflush(stdout);
        ssize_t n = read(0,buf,sizeof(buf)-1);
        if(n > 0)
        {
            buf[n] = 0;
            ssize_t s = write(infd,buf,strlen(buf));
            if(s < strlen(buf))
            {
                perror("write");
                exit(EXIT_FAILURE);
            }
        }else 
        {
            perror("read");
            exit(EXIT_FAILURE);
        }
    }
    close(infd);
    return 0;
}
  • 程序运行结果:


这里写图片描述

管道基本特点总结

  • 管道是特殊类型的文件,在满足先入先出的原则条件下可能进行读写,但不能定位读写位置
  • 管道是单向的,要实现双向,需要两个管道。匿名管道一般只用于亲缘关系进程间的通信(非亲缘关系进程只能传递文件描述符),而命名管道已磁盘文件的方式存在,可以实现本机任意两进程间通信
  • 阻塞问题。匿名管道无须显式打开,创建时直接返回文件描述符,而在读写是需要确定对方的存在,即阻塞于读写位置;而命名管道在打开时需要确定对方的存在,否则阻塞

猜你喜欢

转载自blog.csdn.net/aurora_pole/article/details/80331681
今日推荐