【Linux】进程间通信之管道

news/2025/2/12 22:15:28/

进程间通信之管道

  • 一、管道
    • 1、管道的基本使用
    • 2、管道的原理
    • 3、实例代码
    • 4、管道的特点
  • 二、有名管道
    • 1、创建一个命名管道
    • 2、匿名管道与命名管道的区别
    • 3、命名管道的原理
    • 4、用命名管道实现server&client通信

一、管道

1、管道的基本使用

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

例如我们通过who | wc -l命令可以看到who进程将数据传递给了wc -l进程,两个进程通过管道完成了简单的通信。

在这里插入图片描述

在这里插入图片描述

有一点需要注意的是我们使用管道时,管道两边的的进程都会运行起来,而不是先运行管道左边的进程然后运行管道右边的进程。而且在命令行中用管道链接的进程属于兄弟进程关系

在这里插入图片描述

2、管道的原理

Linux中一切皆文件,管道也是文件,而且管道是一个彻彻底底的内存文件,也就是说,管道不能够向磁盘输出数据,因为这个特性管道里面就可以存储两个进程通信的数据了。

当我们的一个父进程分别以读和写的方式打开一个同一管道文件,这时我们的进程中就有了两个文件描述符指向这个管道文件。

在这里插入图片描述

然后我们再让父进程创建子进程,这时我们父子进程的内核数据结构是几乎相同的,此时我们子进程的也有两个文件描述符指向管道文件而且这个管道文件与父进程指向的管道文件是同一个一个管道文件。

(注意:创建子进程的时候,fork子进程,只会复制进程相关的数据结构对象不会复制父进程曾经打开的文件对象!
这就是为什么fork之后,父子进程printfcout,都会向同一个显示器终端打印数据的原因!)

这个时候我们的父子进程就可以一个向管道文件写入数据一个从管道中读取数据,这样两个进程就可以完成通信了。

最后一个步骤就是关闭不需要的文件描述符了,如果父进程进行写入,就关闭父进程的读端,关闭子进程的写端。反之则同理。

在这里插入图片描述

关闭不需要的文件描述符的原因

因为文件的缓冲区只有一个,一个缓冲区只有一个读和写位置,管道也是。例如父进程向管道中写入数据,然后子进程也写入数据,由于缓冲区的读写位置只有一个,那么我们在读取数据时,父进程与子进程写入管道的数据根本没有办法区分,就有可能造成通信错误。管道这种进程间的通信方式只能进行单向通信。

如果我们想要父子进程都能够进行读写,我们可以创建两个管道,这样它们的读写的数据就不会相互影响了。

补充:我们介绍的这种管道被称为匿名管道,因为所有的文件都要有路径以及文件名的,而这里我们并不知道所以叫匿名管道。

3、实例代码

纸上得来终觉浅,绝知此事要躬行。下面我们尝试在代码中来使用管道来进行进程间的通信。

我们先来了解一个Linux的系统调用pipe,这个系统调用可以帮我们打开一个匿名管道文件。

在这里插入图片描述

  • 参数:输出型参数,外部传入一个数组(此数组至少要有两个int的空间),数组的0号下标的位置放的是读端的文件描述符fd,数组的1号下标的位置放的是写端的文件描述符fd
  • 返回值:返回值为0,代表打开管道文件成功,返回值为-1,代表打开管道文件失败了。

在这里插入图片描述
我们让多个进程通信大致分为以下几个步骤:

  1. 父进程打开一个匿名管道文件。
  2. 父进程创建子进程,关闭不需要的文件描述符。
  3. 进行进程间的通信。
  4. 关闭所有管道文件,进程退出。

代码示例:

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define NUM 1024int main()
{// 1.打开管道int pipefd[2] = {0};int err = pipe(pipefd);if (err == -1){std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;exit(-1);}// 2.1创建子进程int id = fork();if (id < 0){std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;exit(-1);}else if (id == 0){// child processchar buffer[NUM] = {0};// 2.2.关闭子进程不需要的文件描述符close(pipefd[1]);// 3.子进程进行进程间的通信ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1);if (n < 0){std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;}buffer[n] = '\0';std::cout << "父进程给我的消息是:" << buffer << std::endl;// 4.子进程关闭管道文件,子进程退出。close(pipefd[0]);exit(0);}else{// parent processchar buffer[NUM] = {0};const char *s = "Hello, I am parent process!";snprintf(buffer, NUM, "%s : %d", s, getpid());// 2.2.父进程关闭不需要的文件描述符close(pipefd[0]);// 3.父进程进行进程间的通信ssize_t n = write(pipefd[1], buffer, strlen(buffer));if (n < 0){std::cout << "错误码" << errno << ",错误信息:" << strerror(errno) << std::endl;}}// 4.父进程关闭管道文件,父进程退出。close(pipefd[1]);int status = 0;waitpid(id, &status, 0);if (WIFEXITED(status)){std::cout << "子进程的退出码" << WEXITSTATUS(status) << std::endl;}else{std::cout << "子进程异常退出!" << std::endl;}return 0;
}

代码运行结果:

在这里插入图片描述

可以看到通过管道我们能够让两个进程完成进程之间的通信。

4、管道的特点

根据前面我们讲的管道的原理以及实验现象。我们可以总结出一些管道的特点。

  1. 管道只能进行单向通信,管道的一种特殊半双工通信(管道两侧只能一个进行写入,一个读取,一旦分工确定就不能够再进行更改了)
  2. 管道的本质是文件,文件描述符fd的生命周期是随进程的,进程退出时,文件描述符fd也会消失,所以管道的生命周期是随进程的。
  3. 用匿名管道进行通信,这种方式通常只能够让有血缘关系的进程进行通信,因为没有血缘关系的进程并不知道应该打开哪一个管道文件,有血缘关系的进程可以通过继承来打开同一个管道文件。

接下来我们来看一些特殊场景,来帮助我们更好的理解管道的特点:

  1. 当父进程写的比较慢,子进程读的比较快时。
    在这里插入图片描述
    运行结果:
    在这里插入图片描述
    运行结果是正常的,说明这种通信是合理的。

  2. 当父进程写的比较快,子进程读的比较慢时。

在这里插入图片描述

运行结果:

在这里插入图片描述

可以看出,父进程写了多次的内容,子进程一次就全部读取出来了,这就显现出了管道的第四个特点:读和写的次数并没有强相关


接下来我们继续通过一些特殊情况,来研究管道:

  1. 假设我们管道的写端不发数据,那读端会怎么办?

在这里插入图片描述
运行结果:
在这里插入图片描述

可以看到:如果读端读取完毕了所有的管道数据,写端对方不发,读端就只能等待

  1. 如果我们写端讲管道写满了还能进行写入吗?
    在这里插入图片描述
    运行结果:

在这里插入图片描述
当你运行时,你可以以看到写入65535个后,不再进行写入,过4-5秒以后,可以看到,子进程读取了数据。

运行结果说明了,管道写满了就不能够再进行写入了,而且也说明了管道的大小是64KB

通过1、2这两种特殊情况,我们能够总结出管道的第五个特点:管道有一定的协同能力,让读端和写端能够按照一定的步骤进行通信(自带同步机制)

  1. 如果我们在读端读取数据时,突然关闭了写端,会发生什么?
    在这里插入图片描述

运行结果:
在这里插入图片描述

  1. 写端一直写,读端关闭,会发生什么呢?答案是:这种行为没有意义!OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程! OS会通过信号来终止进程,13)SIGPIPE

在这里插入图片描述

运行结果:

在这里插入图片描述

管道的第六个特点:

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
    在这里插入图片描述
    LinuxPIPE_BUF通常代表的是4096字节。

二、有名管道

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道,命名管道是一种特殊类型的文件。

1、创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo 文件名

在这里插入图片描述

Linux中以p开头的文件类型是管道文件,管道文件不会讲内容刷新到磁盘,所以不管我们怎么操作管道文件,管道文件的大小始终都为0

可以看到的是,本来应该在左边终端里面打印的消息,却被打印到了右边!

在这里插入图片描述

另一种方式是在代码中创建有名管道:

在这里插入图片描述

  • 参数:第一个是参数是路径加文件名,第二个参数是创建的文件的权限。
  • 返回值 :如果成功创建就返回 0,如果创建失败就返回 -1

实例代码:

#include <iostream>
#include <sys/stat.h>
#include <sys/types.h>int main()
{umask(0);mkfifo("./fifo", 0666);return 0;
}

运行结果:

在这里插入图片描述

2、匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open,FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
  • 由于管道不需要刷盘,所以管道文件只有inode没有Data block

3、命名管道的原理

因为命名管道是有名称的,我们可以让两个进程分别以读和写的方式打开文件,然后就可以让两个进程进行通信了,命名管道的使用要比匿名管道的使用简单的多!

在这里插入图片描述

4、用命名管道实现server&client通信

实例代码:

通过下面的代码我们能够让两个进程显示相同的消息

command.hpp

#include <iostream>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>#define NUM 1024// 创建一个有名管道
int CreatFifo()
{umask(0);int err = mkfifo("./fifo", 0664);if (err < 0){std::cerr << "mkfifo fail: " << errno << strerror(errno) << std::endl;return -1;}return 0;
}

serve.cpp

#include "common.hpp"int main()
{// 1.创建一个有名管道int err = CreatFifo();if (err < 0){return -1;}std::cout << "create fifo file success" << std::endl;// 2.打开管道的读端int fd = open("./fifo", O_RDONLY);if (fd < 0){std::cerr << "open fail: " << errno << strerror(errno) << std::endl;return -1;}std::cout << "open fifo success, begin ipc" << std::endl;// 3.等待客户端的消息char buffer[NUM] = {0};while (true){ssize_t n = read(fd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';std::cout << "server// " << buffer << std::endl;}else if (n == 0){std::cout << "客户端退出了,我也退出了" << std::endl;break;}else{std::cerr << "read fail: " << errno << strerror(errno) << std::endl;}}// 关闭文件描述符close(fd);// 删除管道文件unlink("./fifo");return 0;
}

client.cpp

#include "common.hpp"
#include <string>int main()
{// 1.打开文件int fd = open("./fifo", O_WRONLY);if (fd < 0){std::cerr << "open fail: " << errno << strerror(errno) << std::endl;return -1;}// 2.开始通信std::string s;while (true){std::cout << "client// ";std::getline(std::cin, s);if (s == "quit"){break;}ssize_t n = write(fd, s.c_str(), s.size());if (n < 0){std::cerr << "write fail: " << errno << strerror(errno) << std::endl;continue;}}// 关闭文件描述符,进程退出close(fd);return 0;
}

运行结果:

在这里插入图片描述


http://www.ppmy.cn/news/106053.html

相关文章

Day8 接口测试详解–接口概述、什么是接口测试、常用的接口测试工具、RESTFUL架构

Day8 接口测试详解–接口概述、什么是接口测试、常用的接口测试工具、RESTFUL架构 文章目录 Day8 接口测试详解--接口概述、什么是接口测试、常用的接口测试工具、RESTFUL架构1. 接口概述**1.1 什么是接口呢?****1.2 常见接口****1.3 使用接口的优点**2. 什么是接口测试?**2.…

MSP432学习笔记8:定时器A_PWM驱动舵机

开发板型号&#xff1a;MSP432P401r 今日得以继续我的MSP432电赛速通之路&#xff0c;文首提供本次学习实践项目文件。 注&#xff1a;我笔记实践都是从原始空项目工程文件开始配置的。 有道是 —_—_—_—_— “山无重数周遭碧&#xff0c;花不知名分外娇” “曲…

数据采集技术的实现原理有哪些?

数据采集技术是指通过各种手段和技术手段&#xff0c;从互联网、移动设备、传感器等各种数据源中获取数据&#xff0c;并将其存储、处理和分析&#xff0c;以便为业务决策和应用提供支持。本文将介绍数据采集技术的实现原理&#xff0c;包括数据采集的基本流程、数据采集技术的…

【图像水印 2022 ACM】PIMoG

【图像水印 2022 ACM】PIMoG 论文题目&#xff1a;PIMoG: An Effective Screen-shooting Noise-Layer Simulation for Deep-Learning-Based Watermarking Network 中文题目&#xff1a;PIMoG:深度学习水印网络中一种有效的截屏噪声层仿真 论文链接&#xff1a;https://dl.acm.o…

LeetCode 1110. 删点成林

【LetMeFly】1110.删点成林 力扣题目链接&#xff1a;https://leetcode.cn/problems/delete-nodes-and-return-forest/ 给出二叉树的根节点 root&#xff0c;树上每个节点都有一个不同的值。 如果节点值在 to_delete 中出现&#xff0c;我们就把该节点从树上删去&#xff0c…

NLP语料库学习

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言中文语料库 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 语料库有的是标记过的&#xff08;annotated&#xff09;&#xff0c;意味…

企业数字化转型,关于数据应用的三点分析

企业数字化转型已经成为当今商业领域的热门话题。在这个信息爆炸的时代&#xff0c;企业意识到数据的价值&#xff0c;开始将其作为一种战略资源来应用。数据应用是企业数字化转型中至关重要的一环&#xff0c;以下是关于数据应用的三点分析。 首先&#xff0c;数据应用为企业提…

家政服务预约APP的系统设计与实现

摘 要&#xff1a;针对家政行业蓬勃发展&#xff0c;老套的家政服务方式已经跟不上互联网时代的步伐这个问题。基于Android移动平台的分析和设计过程、C/S模式、Eclipse平台&#xff0c;采用Java语言进行开发设计&#xff0c;设计了基于MVC架构的实现方案。安卓客户端与服务器…