一文让你明白,什么是管道(pipe)?进程之间利用管道进行通信的具体流程?以及C++简单利用管道API函数的使用案例。

管道(pipe)

什么是进程间通信?

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

1、定义(水管(pipe))

  • 管道是一种最基本的进程间通信机制。 把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入
  • 管道本质上是内核的一块缓存,内核维护了一块缓冲区与管道文件相关联,对管道文件的操作,被内核转换成对这块缓冲区内存的操作。

举例:

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

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

2、分类

2.1、匿名管道

2.1.1、特征

  • 1、管道的作用是在具有亲缘关系的进程之间传递消息,所谓有亲缘关系,是指有同一个祖先
    • 管道并不是只可以用于父子进程通信,也可以在兄弟进程之间还可以用在祖孙之间等,反正只要共同的祖先调用了pipe函数,打开的管道文件就会在fork之后,被各个后代所共享
  • 2、管道是字节流通信没有消息边界,多个进程同时发送的字节流混在一起,则无法分辨消息,所有管道一般用于2个进程之间通信;
    • 流:相当于水流,写入数据时,写多少字节和你自己有关系,读的时候没有 格式的要求,完全取决你自己;
    • 字节流:以字节来读取和写入,字节数的大小完全取决于自己
  • 3、管道的内容读完后不会保存;
  • 4、管道是单向的,一边要么读,一边要么写,不可以又读又写,想要一边读一边写,那就创建2个管道
    在这里插入图片描述
  • 5、管道是一种文件,可以调用read、write和close等操作文件的接口来操作管道。另一方面管道又不是一种普通的文件,它属于一种独特的文件系统:pipefs。
  • 6、管道内部自带同步机制:子进程写一条,父进程读一条
  • 7、当进程退出之时,管道也随之释放,与文件保持一致

2.1.2、父子进程通信过程解析

(1)父进程创建管道,得到两个文件描述符指向管道的两端;

  • 该进程既可以往写入端描述符写入信息,也可以从读取端描述符读出信息。 可是一个进程管道,起不到任何通信的作用。这不是通信,而是自言自语。

在这里插入图片描述

(2)父进程fork出子进程,子进程也有两个文件描述符指向同一个管道。

  • 此时,可以是父进程往管道里写,子进程从管道里面读;也可以是子进程往管道里写,父进程从管道里面读。这两条通路都是可选的,但是不能都选,原因如下。
    • 管道里面是字节流,父子进程都写、都读,就会导致内容混在一起,对于读管道的一方,解析起来就比较困难

在这里插入图片描述

(3)父进程关闭fd[0](读端),子进程关闭fd[1](写端),因为管道只支持单向通信。

  • 父进程可以往管道写,子进程可以从管道读,管道是由环形队列实现的,
    在这里插入图片描述
    父进程的两个子进程之间如何利用管道通信?

父进程再次创建一个子进程B,子进程B就持有管道写入端,这时候两个子进程之间就可以通过管道通信了。

  • 父进程为了不干扰两个子进程通信,很自觉地关闭了自己的写入端。从此管道成为了两个子进程之间的单向的通信通道。
    在这里插入图片描述

2.1.3、API函数的使用以及注意点

 #include <unistd.h>
 int pipe(int pipefd[2]);

参数说明:

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

返回值:

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

注意点:

  • 1、管道内没有数据时,读端(read)发生阻塞,等待有效数据进行读取

  • 在这里插入图片描述

  • 2、管道容量被数据填满时,写端(write)发生阻塞,等待进程将数据读走再进行写入
    在这里插入图片描述

  • 3、如果所有管道写端对应的文件描述符被关闭read返回0,但会将之前管道里的数据读完
    在这里插入图片描述

  • 4、如果所有管道的读端对应的文件描述符被关闭write操作会产生信号,SIGPIPE,进而导致write进程退出
    在这里插入图片描述

  • 5、当要写入的数据量不大于管道的容量(PIPE_BUF)时,linux将保证写入的原子性

  • 6、当要写入的数据量大于管道容量(PIPE_BUF)时,linux将不再保证写入的原子性

2.1.4、应用实例

#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main()
{
        int fds[2];
        if(pipe(fds) < 0){//创建一个管道,用于父子间进行通信
                 perror("pipe");
                 return 1;
        }
        char buf[1024];//临时数组,用于存放通信的消息

        printf("Please enter:");
        fflush(stdout);//对标准输出流的清理,但是它并不是把数据丢掉,而是及时地打印数据到屏幕上
        ssize_t s =  read(0,buf,sizeof(buf)-1);//0对应文件描述符
        if(s > 0){//判断读取的字节数
            buf[s] = 0;
        }

        pid_t pid = fork();//fork()子进程
        if(pid == 0){//子进程只写,关闭读端
                close(fds[0]);
                while(1){
                        sleep(1);
                        write(fds[1],buf,strlen(buf));//将buf的内容写入管道
                }
        }
        else{//父进程只读,关闭写端
              close(fds[1]);
              char buf1[1024];
              while(1){
                      ssize_t s = read(fds[0],buf1,sizeof(buf1)-1);//从管道里读数据,放入buf
                       if(s > 0){
                                buf1[s-1] = 0;
                                printf("client->farther:%s\n",buf1);
                       }
               }
      }
}

 ssize_t read(int fd, void *buf, size_t count);
  fd:文件描述符,用来指向要操作的文件的文件结构体
  buf:一块内存空间
  count:希望读取的字节数
  返回值表示实际读到的字节数(字符串结束符 '\0'不算)

在这里插入图片描述

2.2、命名管道

不同于匿名管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存储于文件系统中。

  • 命名管道是一个文件,因此,即使进程与创建FIFO的进程不存在亲缘关系,只要可以访问该路径,就能够通过FIFO相互通信。
  • 值得注意的是,FIFO(first input first output)总是按照先进先出的原则工作,第一个被写⼊的数据将首先从管道中读出。

2.2.1、特征

  • 1、 可以进行不相干进程间的通信
  • 2.、命名管道是一个文件,对于文件的相关操作对其同样适用
  • 3、 对于管道文件,当前进程操作为只读时,则进行阻塞,直至有进程对其写入数据
  • 4、 对于管道文件,当前进程操作为只写时,则进行阻塞,直至有进程从管道中读取数据

2.2.2、使用API函数

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *path,mode_t mode);

path为创建的命名管道的全路径名:
mod为指定了文件的读写权限; 

成功都返回0,失败都返回-1

命名管道和管道的使用方法法基本是相同的。

  • 只是使用命名管道时,必须先调用open()将其打开。因为命名管道是一个存在于硬盘上的文件,而管道是存在于内存中的特殊文件。

需要注意的是,调用open()打开命名管道的进程可能会被阻塞。

  • 但如果同时用读写方式( O_RDWR)打开,则一定不会导致阻塞;
  • 如果以只读方式( O_RDONLY)打开,则调用open()函数的进程将会被阻塞直到有写方打开管道;
  • 同样以写方式( O_WRONLY)打开也会阻塞直到有读方式打开管道。

2.2.3、应用实例

server.c

//https://blog.csdn.net/zhangye3017/article/details/80189861
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
//
int main()
{
    umask(0);//将权限清0
    if(mkfifo("./mypipe",0666|S_IFIFO) < 0){//创建管道
        perror("mkfifo");
        return 1;
    }

    int fd = open("./mypipe",O_RDONLY);//打开管道
    if(fd < 0){
        perror("open");
        return 2;
    }

    char buf[1024];
    while(1){
        buf[0] = 0;
        printf("请等待。。。\n");
        ssize_t s = read(fd,buf,sizeof(buf)-1);

        if(s > 0){
            buf[s-1] = 0;//过滤\n
            printf("服务器:%s\n",buf);
        }else if(s == 0){//当客户端退出时,read返回0
            printf("客户端退出,自己退出\n");
            break;
        }
    }
    close(fd);
    return 0;
}

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
int main()
{

    int fd = open("./mypipe",O_WRONLY);//打开管道
    if(fd < 0){
        perror("open");
        return 1;
    }

    char buf[1024];
    while(1){
        printf("客户端:");
        fflush(stdout);
        ssize_t s = read(0,buf,sizeof(buf)-1);//向管道文件中写数据
        if(s > 0){
            buf[s] = 0;//以字符串的形式写
            write(fd,buf,strlen(buf));
        }
    }
    close(fd);
    return 0;
}

开两个终端:

gcc -o client  client.c
./client 

gcc -o server  server.c
./server 

在这里插入图片描述

参考

1、https://blog.csdn.net/zhangye3017/article/details/80189861
2、https://www.cnblogs.com/zengyiwen/p/5755170.html
3、https://blog.csdn.net/zhangye3017/article/details/80019282

猜你喜欢

转载自blog.csdn.net/JMW1407/article/details/107700451