当前位置: 首页 > web >正文

进程间通信——管道

概念

进程间通信(Inter-Process Communication,简称 IPC)是指在不同进程之间进行数据交换和信息传递的机制。它的目的主要有4种:

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
    在Linux中,主要的进程间通信有,管道、信号、System V进程间通信、POSIX进程间通信。这篇博客讲的是管道。而管道又分为匿名管道和有名管道。它们的区别是:
  • 匿名管道:只能在具有亲缘关系(父子进程或兄弟进程等)的进程间使用。它是一种半双工的通信方式,数据只能单向流动。匿名管道通常用于父子进程之间传递少量数据。
  • 有名管道:可以在不相关的进程间使用。有名管道有一个特定的文件名,进程可以通过这个文件名来打开管道进行通信。有名管道也是半双工的。

管道通信原理

管道是Unix中最古老的进程间通信的形式。所谓管道,是指用于连接一个读进程和一个写进程,以实现进程之间通信的一种共享文件,又称为Pipe文件。向管道提供输入的是发送进程,或称为写进程,它负责向管道送入数据,数据的格式是字符流;而接收管道数据的接收进程称为读进程。由于发送进程和接收进程是利用管道来实现通信的,所以称为管道通信。
为了协调双方的通信,管道通信机制必须提供以下几个方面的协调能力。

  1. 互斥。当一个进程正在对管道进行读或写操作时,另一个进程必须等待。
  2. 同步。管道的大小是有限的。所以当管道满时,写进程必须等待,直到读进程把它召唤醒为止。同理,当管道没有数据时,读进程也必须等待,直到写进程将数据写入管道后,读进程才被唤醒。
  3. 对方是否存在。只有确认对方存在时,方能进行通信。
    请添加图片描述

以上是一个进程,它打开了一些文件,于是在进程PCB中的struct file_struct成员中,就会指向当前进程已经打开的文件的结构体struct file
然后该进程创建了一个子进程:
请添加图片描述

该子进程会继承父进程的一些数据,同时也继承了file_struct结构体,相当于2个进程同时打开了一份文件。
所以管道通信的原理就是:让进程之间发生PCB的继承,从而通过PCB打开同一份文件。

站在文件描述符的角度,理解管道

  1. 父进程创建管道
    请添加图片描述

  2. 父进程通过fork创建子进程
    请添加图片描述

  3. 父子进程关闭各自的读端或者写端(图片中父进程关闭读端,子进程关闭写端)
    请添加图片描述

管道的通信是单向的:必须一端读,另一端写。

  • 当父进程关闭写端时,那么子进程要关闭读端。这时父进程向管道中读取数据,子进程向管道中写入数据。
  • 当父进程关闭读端时,那么子进程要关闭写端。这时父进程向管道中写入数据,子进程向管道中读取数据。

匿名管道

如果想要在系统中创建一个管道,那么就需要调用一个系统接口函数pipe,它的头文件是<unistd.h>。功能是创建一个匿名管道。它的函数原型是:

int pipe(int pipefd[2]);

其中,它的参数pipefd[2]是一个文件描述符数组,也是一个输出型参数,pipedf[0]表示读端,pipefd[1]表示写端。
它的返回值有2个,当管道创建成功时返回0,当创建失败时,返回-1,并生成一个错误码。
我们先来看一下这个数组是代表什么意思。

int main()
{int pipefd[2] = {0};int n = pipe(pipefd);if (n < 0)return 1;cout<< "pipefd[0]:"<<pipefd[0]<<",pipefd[1]:"<<pipefd[1]<<endl;return 0;
}

请添加图片描述

可以发现3和4其实就是之前我们学习过的文件描述符的下标,只不过0,1,2已经被使用了。


主函数

int main()
{int pipefd[N] = {0};int n = pipe(pipefd);if (n < 0)return 1;// child用写的方式,father用读的方式pid_t id = fork();if (id < 0)return 2;if (id == 0){// 子进程close(pipefd[0]);// IPC codeWriter(pipefd[1]);}// 父进程close(pipefd[1]);Reader(pipefd[0]);waitpid(id, NULL, 0);return 0;
}

先通过pipe创建管道。如果n小于0说明管道创建失败,然后子进程关闭读端,打开写端进行写入;父进程关闭写端,打开读端读取数据。
Writer函数

void Writer(int wfd)
{string s = "hello,I am child";pid_t self = getpid();int number = 0;char buffer[NUM];while (true){// 构建发送字符串buffer[0] = 0; // 字符串清空,为了提醒阅读代码的人,我把这个数组当做字符串了snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);// 发送/写入给父进程write(wfd, buffer, strlen(buffer));sleep(1);}
}

参数wfd是写端,其中NUM是1024。snprintf函数是 C 语言标准库中的一个函数,用于格式化输出字符串,并将结果写入到指定的缓冲区5。其原型为:

int snprintf(char *str, size_t size, const char *format,...);

最后通过write函数将buffer数组中的内容写给wfd
Reader函数

void Reader(int rfd)
{char buffer[NUM];while (true){buffer[0] = 0;ssize_t n = read(rfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;cout << "father get a message[" << getpid() << "]#" << buffer << endl;}}
}

reader函数会从管道中读取内容到buffer中,随后输出pidbuffer中的内容。
运行程序,看看结果:
请添加图片描述

父进程成功读取到管道中的内容。

管道读写规则

当管道没有数据时,读进程也必须等待,直到写进程将数据写入管道后,读进程才被唤醒。

这是刚开始我们学习过的。我们发现Reader函数中没有sleep(1),只有Writer中有,输出结果的时候,每隔1秒输出一次。所以能够证明上述规则:管道中没有数据的时候,就进行等待,直到写端写入数据。写端每隔1秒才写入一条数据,自然读端每隔1秒才能读到数据。


写端一直写,读端不读取数据,直到管道写满时,写端将被阻塞,进入等待状态,暂停执行。这意味着写端进程不能继续进行其他与写入管道相关的操作,直到管道中有足够的空间可供写入新的数据。

现在我们修改Writer函数

void Writer(int wfd)
{string s = "hello,I am child";pid_t self = getpid();int number = 0;char buffer[NUM];while (true){char c = 'c';write(wfd,&c,1);number++;cout<<number<<endl;}
}

写端一直向管道中写入字符c,利用number计数,并且不调用Reader函数。
请添加图片描述

可以看到最后输出的number是65536,就不再写往管道中写入数据了,说明管道已经被写满了。我是在Centos 7.8版本测试的,由此说明管道大小约为64kb。


当写端关闭时,此时读端依然可以读取到管道中的数据,不过read的返回值是0。说明读取到内容的结尾处。

继续修改Writer函数。

void Writer(int wfd)
{string s = "hello,I am child";pid_t self = getpid();int number = 0;char buffer[NUM];while (true){write(wfd, "x", 1);number++;if (number == 1024)  //写入1024个字符后,终止循环,关闭写端break;}close(wfd);
}

Reader函数

void Reader(int rfd)
{char buffer[NUM];sleep(5);while (true){buffer[0] = 0;ssize_t n = read(rfd, buffer, sizeof(buffer));if (n > 0){buffer[n] = 0;cout << "father get a message[" << getpid() << "]#" << buffer << endl;}else if (n == 0)   //等于0说明读取到文件末尾了{printf("father read file done!\n");break;}else{cout << "n:" << n << endl;}sleep(1);}
}

先让Reader休眠5秒,等到Writer写完后,通过read读取到管道中的内容。
请添加图片描述

当写端关闭后,读端把所有的数据都读取到了,随后输出father read file done!说明返回值是0,由于数据已经被读完了,所以下次读取的时候没有数据可以读取,于是read的返回值是-1。


管道的读端被关闭后,写端进程直接终止。

对于写端进程来说,它通常会期望能够将数据写入管道,并由读端进程读取。如果读端关闭,写端进程继续写入数据将变得没有意义,因为没有接收者。
当写端进程尝试向一个已关闭读端的管道写入数据时,操作系统会检测到这种情况,并向写端进程发送一个特定的信号。这个信号通常是13号信号SIGPIPE,表示管道破裂。默认情况下,SIGPIPE 信号会导致写端进程终止。这是为了防止写端进程在无法成功写入数据的情况下无限期地阻塞或继续执行,从而浪费系统资源。

管道特点

  1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  2. 一般而言,进程退出,管道释放,所以管道的生命周期随进程。
  3. 管道提供流式服务
  4. 一般而言,内核会对管道操作进行同步与互斥
  5. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
    请添加图片描述

有名管道

学习完匿名管道之后,接着学习一下有名管道。有名管道,顾名思义就是有名字的管道。之前我们学习的管道它的一个限制就是只能在具有亲缘关系之间才能够进行通信,通常使用匿名管道来实现。而如果我们想在==不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为有名管道。==有名管道是一种特殊类型的文件。

2个管道之间的区别

一、命名方式

  1. 匿名管道:没有特定的名称标识,通常在具有亲缘关系的进程(如父子进程)间使用。
  2. 有名管道:有一个特定的文件名,可以在不相关的进程间使用。有名管道的文件名存在于文件系统中,进程可以通过这个文件名来打开管道进行通信。

二、创建方式

  1. 匿名管道:通常在程序中通过调用特定的系统函数(如pipe函数)来创建。创建匿名管道后,会返回两个文件描述符,一个用于读,一个用于写。
  2. 有名管道:可以通过命令行工具(如mkfifo命令)或者在程序中使用特定的系统函数(如mkfifo函数)来创建。创建有名管道后,它就像一个普通的文件,可以使用文件操作的方式(如openreadwrite等函数)来进行操作。

三、生命周期

  1. 匿名管道:通常随着创建它的进程的结束而被销毁。当父进程创建了匿名管道并创建了子进程后,如果父进程或子进程结束,匿名管道也会被关闭。
  2. 有名管道:有名管道的生命周期独立于创建它的进程。即使创建有名管道的进程结束了,只要还有其他进程在使用这个管道,它就会一直存在。只有当所有使用有名管道的进程都关闭了它,或者通过特定的命令(如rm命令)删除了有名管道的文件名,有名管道才会被销毁。

四、使用范围

  1. 匿名管道:只能在具有亲缘关系的进程间使用,因为它的创建和使用通常是在一个进程创建子进程的过程中进行的。例如,父进程创建匿名管道后,将其文件描述符传递给子进程,从而实现父子进程间的通信。
  2. 有名管道:可以在不相关的进程间使用,只要这些进程知道有名管道的文件名,就可以通过打开这个文件来进行通信。这使得有名管道在不同的应用场景中更加灵活,可以用于多个不相关的进程之间的协作。

创建方式

我们可以通过mkfifo指令来创建一个有名管道;当然也可以通过函数接口进行创建。我们先通过指令来进行演示。语法形式是:mkfifo filename
请添加图片描述

请添加图片描述

可以看到我们通过mkfifo创建了一个有名管道,它的文件表示是p,并且文件大小是0,因为与磁盘上的普通文件不同,管道不用于存储数据以供将来读取。管道中的数据是临时的,仅当至少有一个进程正在写入且至少有一个进程正在读取时才存在。
请添加图片描述

当我们向管道中写入信息时,命令行就会进入阻塞状态,只有当别人读取管道中的内容时,阻塞状态才会被终止。


下面,我们使用函数接口的形式来演示。它的头文件是<sys/types.h>和<sys/stat.h>,函数原型是:int mkfifo(const char *pathname, mode_t mode);

  • pathname:表示的是有名管道创建的路径。
  • mode:文件的初始权限。
    管道创建成功时返回0,创建失败时返回-1,并设置一个错误码。

我们执行下面代码:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>int main()
{umask(2);int n = mkfifo("./test", 0666);if(n == 0)printf("create sucess\n");elseprintf("create fail\n");return 0;
}

请添加图片描述

后续只需要通过在不同进程中通过open接口打开这个文件,就可以进行进程间通信了
如果想要销毁这个管道文件,只需要在程序中执行接口unlink("./test")即可。它的函数原型是:
int unlink(const char *pathname);,当被成功销毁时返回0,销毁失败时返回-1,并产生一个错误码。当然也可以直接使用指令unlink filename来销毁管道文件。
在刚才代码中加入这个接口,并运行代码。

int x = unlink("./test");
if(x == 0){printf("delete sucess\n");
}
else{printf("delete fail\n");
}

请添加图片描述

有名管道打开的规则

  1. 如果当前打开操作是为读而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
    • O_NONBLOCK enable:立刻返回成功
  2. 如果当前打开操作是为写而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

有名管道的通信

接着我们来写一份代码来实现有名管道之间的通信。进程A向管道中写入信息,而进程B则向管道中读取信息。
进程A(test1.cpp)

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;int main()
{// 1.创建管道文件mkfifoumask(0); // 这个设置并不影响系统的默认配置,只会影响当前进程int n = mkfifo("./test", 0666);if (n == -1) // 失败返回-1,打印错误码并且退出1{cout << errno << " : " << strerror(errno) << endl;return 1;}// 2.让进程A开启管道文件int wfd = open("test", O_WRONLY);if (wfd == -1) // 失败返回-1,打印错误码并且退出2{cout << errno << " : " << strerror(errno) << endl;return 2;}// 3.向管道中写入"i am process A"while (true){char buffer[1024] = "i am A";ssize_t m = write(wfd, buffer, strlen(buffer));assert(m > 0);(void)m;sleep(1);}close(wfd);unlink("test"); // 销毁管道文件的链接,也就是删除test文件return 0;
}

进程B(test2.cpp)

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;int main()
{// 1.不需创建管道文件,只需要打开对应的文件即可int rfd = open("test", O_RDONLY);if (rfd == -1) // 失败返回-1,打印错误码并且退出1{cout << errno << " : " << strerror(errno) << endl;return 1;}// 2.可以进行常规通信了while (true){char buffer[1024];int n = read(rfd, buffer, sizeof(buffer));assert(n >= 0);cout << "我是B进程,读取到的数据是:" << buffer << endl;sleep(1);}close(rfd);return 0;
}

运行结果
请添加图片描述
如果出现以下错误,需要手动删除管道文件,再执行。
请添加图片描述

http://www.xdnf.cn/news/3930.html

相关文章:

  • Paramiko 核心类关系图解析
  • Android Compose 中 CompositionLocal 的全面解析与最佳实践
  • ARM介绍及其体系结构
  • 【Linux我做主】进度条小程序深度解析
  • 浅析AI大模型为何需要向量数据库?【入门基础】
  • 2021年第十二届蓝桥杯省赛B组Java题解
  • KaiwuDB X 遨博智能 | 构建智能产线监测管理新系统
  • Python推导式:简洁高效的数据处理利器
  • PCB实战篇
  • Java 基础语法篇
  • 编程学习思考
  • 基于多策略混合改进哈里斯鹰算法的混合神经网络多输入单输出回归预测模型HPHHO-CNN-LSTM-Attention
  • BUCK电路制作负电源原理
  • Linux网络:bond简介与配置
  • AVL树(2):
  • 0.1 数学错题---基础
  • 嵌入式按键原理、中断过程与中断程序设计(键盘扫描程序)
  • chrome 浏览器怎么不自动提示是否翻译网站
  • C++ STL简介:构建高效程序的基石
  • SwinTransformer 改进:与PSConv结合的创新设计
  • 管理配置信息和敏感信息
  • 前端开发,文件在镜像服务器上不存在问题:Downloading binary from...Cannot download...
  • 在JSP写入Text文件方法指南
  • 【IP101】边缘检测技术全解析:从Sobel到Canny的进阶之路
  • 2023年第十四届蓝桥杯省赛B组Java题解【 简洁易懂】
  • Spark,Idea中编写Spark程序 2
  • 题解:AT_abc245_e [ABC245E] Wrapping Chocolate
  • Go语言中的无锁数据结构与并发效率优化
  • Circular Plot系列(三):【视频教程】复现NCS图表之高大上的单细胞UMAP环形图
  • process terminated with status -1073741515