Linux:详解进程间通信——管道(匿名管道和命名管道)(图文并茂)(一)

本文详细解析了匿名管道的工作原理,包括从命令行应用、内核机制到编程实践,以及非阻塞特性和命名管道的区别。通过实例展示了如何利用管道进行父子进程通信,并介绍了它们在信息技术中的应用场景和特性。


前言

进程间通信的作用

由于进程独立性的存在,两个过程想要直接交换数据是非常困难的,所以引入进程间通信来解决进程与进程之间交换数据的问题。
目前,最大的进程间通信的方式是网络。

进程间通信的方式:

实现进程进程间通信的方式主要有三种:

  • 管道(匿名管道和命名管道)
  • 共享内存
  • 消息队列和信号量

本节,我们就深入探讨一下管道是如何实现进程间通信的。

1. 匿名管道

在说匿名管道之前,我们有必要来提一下,什么是管道

  • 管道是Unix中最古老的进程间通信形式。
  • 我们把从一个进程连接到另外一个进程的一个数据流称为一个“管道”。

1.1 从命令去感受管道

举个例子:使用ps aux | grep [关键字]命令去查看其对应的进程信息

在这里插入图片描述

我们可以看到执行ps aux命令所产生的内容通过管符|将其传递给了grep bash命令,然后grep命令对其进行了相应的搜索,从而得出bash的一些进程信息。这个过程其实就是一个进程间通信的过程。两个进程之间通过管道实现了数据之间的交换。

1.2 从内核角度去解释管道

ps aux是一个可执行程序(进程),而grep是另一个可执行程序(进程),那么它们是如何实现数据的交互呢?

如图所示:
在这里插入图片描述

那么我们可以初步的认为,管道是内核中的一块缓冲区(内存),进程A和进程B可以通过这个缓冲区来交换数据

1.3 使用代码创建管道

int pipe(int pipefd[2])

参数:

pipefd:是一个整型的数组,有两个元素,pipefd[0]和pipefd[1],它们当中保存的是一个文件描述符

  • pipefd[0]:对应的文件描述符可以从管道当中进行读,不能写
  • pipefd[0]:对应的文件描述符可以从管道当中进程写,不能读

对参数的探究:

pipefd[0]和pipefd[1]当中的值是pipe函数进行赋值的,直白的说,就是当我们调用pipe函数的时候,只需要给pipe函数传递一个拥有两个元素的整型数组,pipe函数在创建完管道之后,就会给pipefd[0]和pipefd[1]进行赋值。是一个输出型的参数。

返回值:

  • -1:创建失败
  • 0:创建成功

用图来解释它实现进程间通信的过程就是:

在这里插入图片描述

那么问题来了:现在我们知道了pipe函数可以实现进程间通信,但是该如何去用呢?我们在testA.c文件中使用pipe函数创建了管道,那么在另一个testB.c的文件中可以使用testA.c创建的管道吗?答案肯定是不能的,由于进程间的独立性,testA和testB之间是互不影响的;那么该如何使用pipe函数实现进程间通信呢?

答案是使用frok()创建出一个子进程,由于父子进程的进程虚拟空间是相同的,父进程创建出来的管道,子进程也是可以获取到的。那么,由此就可以实现父子进程之间的进程间通信。

举个例子:我们现在在父进程中创建一个管道,并在其写入"i am father,my PID is xxxxx",然后在子进程中对该管道进行读,并将其读出来的打印在屏幕上。

代码如下:

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

int main()
{
    int fd[2];
    int pipefd = pipe(fd);
    if(pipefd == -1)
    {
        perror("pipe");
        return 0;
    }

    int forkid = fork();
    if(forkid < 0)
    {
        perror("fork");
        return 0;
    }
    else if(forkid == 0)
    {
        //child
        puts("It's start to child read!");
        //子进程只进行读,不进行写,就关闭写端
        close(fd[1]);
        char buf2[1024] = {0};
        read(fd[0],buf2,sizeof(buf)-1;

        printf("%s\n",buf2);
        puts("It's end to child read!");

    }
    else
    {
        //father
        puts("It's start to father write!");
        //父进程只进行写,不进行读,就关闭读端
        close(fd[0]);
        char buf1[1024] = {0};
        sprintf(buf1,"i am father,my PID is %d\n",getpid());
        write(fd[1],buf1,strlen(buf1));
        puts("It's end to father write!");
    }
    //为了更好的进行验证,我们不让程序结束
    while(1)
    {
        sleep(1);
    }
    return 0;
}

结果验证:

在这里插入图片描述
查看该进程的文件描述符,发现多了两个文件描述符fd,并且它们指向的是一个pipe管道,且一直在闪烁:
在这里插入图片描述

1.4 从PCB角度去分析管道

①父进程首先创建一个管道
在这里插入图片描述
② 父进程fork出一个子进程
在这里插入图片描述
③ 父进程关闭读端,子进程关闭写段,实现进程间通信
在这里插入图片描述

1.5 匿名管道的特性

  • 匿名管道只适用于具有亲缘关系的进程,从而进行进程间通信。(原因是:匿名管道不具备标识符,不能被其他进程所访问到)
  • 要先创建管道,再创建子进程,父子进程才可以进行进程间通信。
  • 如果两个子进程想要通过使用匿名管道进行进程间通信,在理论上是可以的,但是必须要遵守规则2(即先创建管道,再创建子进程)。
  • 在子进程中关闭fd[0],fd[1],父进程是不会受到影响的。
  • 管道的数据只能从写端流向读端,这是一种半双工的通信方式。(全双工通信,指数据可以从A流向B,也可从B流向A)
  • 通过fd[0],从管道中读取数据时,是将数据读走了,而并不是拷贝一份。(就算遇到’\0’,也会一次性全部读走)
  • 在管道中写数据,是追加写。而不是覆盖写。
  • 从管道中读数据的时候,可以指定读取任意大小的数据,如果管道中没有数据,在默认的情况下,进行读,就会阻塞。
  • 多次写入数据间是没有明显的分界的,上一条数据的末尾连接下一条数据的开头。
  • 匿名管道的生命周期是跟随进程的

默认情况下的读写属性

(此处的默认情况是指并没有对管道创建出的文件描述符进行任何操作)

① 若读端不读,写端一直写。

写端将管道写满之后,就会进入阻塞状态。(管道的大小是:65536(216))

②若写端不写,读端一直读

读端将管道的数据读完之后,再次进行读的时候,就会进入阻塞状态

pipe_buf:程序员在操作管道读写时,保证读写原子性的数据最大的大小,这个大小可以由ulimit -a命令进行查看,在pipe size一行,最大的大小为8*512 = 4096

也就是说,当写入的字节数小于4096Byte的时候,则会保证一次性的写进去,而当写入的字节数量大于4096Bytes的时候,则不会保证一次全部写入,可能会分多次写入。

在这里插入图片描述

碎片知识:原子性是指非黑即白,即要么完成了,要么没有开始。

1.6 匿名管道的非阻塞特性

首先我们需要知道的是,匿名管道创建出来的文件描述符是阻塞特性。那么该如何将其修改为非阻塞特性呢?OS给我们提供了这样一个接口。fcntl函数:设置 / 获取文件描述符的属性

且该函数的头文件包含在#include<fcntl.h>

函数声明:

int fcntl(int fd,int cmd,...)

参数:

  • fd:就是给定的需要进行相应操作的文件描述符。
  • cmd:要给定对应的参数进行相应的设置 / 获取操作。(这些参数都是一个宏定义)
    ① F_GETFL:获取文件文件描述符的属性,其后面的可变参数列表不需要传递任何值。
    ② F_SETFL:设置文件描述符的属性,需要在后面的可变参数列表中设置对应的属性。

设置属性

  • O_RDONLY / O_WRONLY / O_RDWR ……等等,和open函数中设置文件描述符属性的可选项一样
  • O_NONBLOCK:非阻塞属性。

需要注意的是,在添加某个属性的时候,使用|运算符(按位或)即可

返回值:

若为获取,则返回文件描述符的属性,若失败则返回-1,若为设置,成功则返回0,失败返回-1。即当是获取的时候,需要接收获取到的文件描述符属性,若是设置,那就可以不用接收对应的值。

代码验证:

我们可以先创建一个管道,然后获取该管道读写两端的文件描述符的属性,然后再设置读写两端的文件描述符的属性为非阻塞属性。

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    int fd[2];
    int pipeid = pipe(fd);
    if(pipeid < 0)
    {
        perror("create pipe failed");
        return 0;
    }

    //获取读端的文件描述符属性
    puts("It's test to read_fd");
    int flag_read = fcntl(fd[0],F_GETFL); 
    printf("before set read fd ,flag = %d\n",flag_read);

    fcntl(fd[0],F_SETFL,flag_read | O_NONBLOCK);
    flag_read = fcntl(fd[0],F_GETFL); 
    printf("after set read fd ,flag = %d\n",flag_read);
    puts("read test over.\n");

    //获取写端的文件描述符属性
    puts("It's test to write_fd");
    int flag_write = fcntl(fd[1],F_GETFL); 
    printf("before set write fd ,flag = %d\n",flag_write);

    fcntl(fd[1],F_SETFL,flag_write | O_NONBLOCK);
    flag_write = fcntl(fd[1],F_GETFL); 
    printf("after set write fd ,flag = %d\n",flag_write);
    puts("write test over.");
    
    return 0;
}

结果如图:

在这里插入图片描述

小结:
① 读端进行读(非阻塞特性),写端不写(不操作)

  • 写端关闭:会调用read函数返回-1
  • 写端不关闭:读端会调用read函数,read函数就会返回-1,表示管道没有值。

那么该如何区分read返回的-1,到底是写端关闭的还是写端不关闭的呢?

我们只需判断它的错误码即可,read若是失败了,不仅仅返回一个-1,还会返回一个错误码errno,他包含在
#include<errno.h>中,若是 errno == EAGAIN,则表示管道为空,是正常的现象(非阻塞),则对应的就是写端不关闭的情况,反之,则为写端关闭的情况。

② 写端进行写 (非阻塞特性) ,读端不操作。

  • 读端关闭:当前再通过fd[1]往管道中写的时候,会导致管道破裂,调用写的进程,会被终止掉(被信号终止)。
  • 读端不关闭:再写的时候,返回 -1,若errno == EAGAIN,则表示写满。

2. 命令管道

2.1 概念

命名管道:命名管道也是在内核中开辟了一块缓冲区,并且这块缓冲区是有标识符的,可以被任何进程通过标识符来找到。

2.2 创建

我们可以使用mkfifo [管道名称]来创建一个管道文件。

例如:
在这里插入图片描述

这里需要注意的是虽然我们使用mkfifo命令创建出了一个管道,但是这里创建出的管道只是一个标识符,并没有实际的大小,所以对应的大小为0,而且在没有文件操作这个管道之前,OS是不会分配空间给它的,只有当文件对其进行操作的时候,比如说open,这个时候OS才会为其创建一个空间出来。

2.3 特性

  • 命名管道的生命周期也是跟随进程的。
  • 命名管道具有标识符,所以,命名管道是支持不同进程之间的进程间通信的。

其余的特性,均和匿名管道相同。

评论 2
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值