【Linux】进程间通信(1):进程通信概念与匿名管道

人与人之间是如何通信的?举个简单的例子,假如我是月老,我要为素不相识的但又渴望爱情的男女两方牵红线。我需要收集男方的信息告诉女方,收集女方的信息告诉男方,然后由男女双方来决定是否继续。对于他们而言,通信由一方发起,另一方判断是否进行接收,我是他们之间进行通信的媒介,是否继续进行通信的决定权在男女双方手中。男女两方通过月老进行传话、从月老获取信息的方式实现了男女双方间的间接通信。

在上述场景中,月老独立于男女方之间,但又能为男女方之间建立联系。而进程拥有独立的地址空间,进程之间的交流也需要依赖于类似于“月老”的“公共资源”。不同进程间通过对公共资源的读写操作,就可以间接实现进程间的通信。

一、进程间通信方式概述

        进程间通信就是在不同的进程间传播或交换信息,但由于进程的用户空间是相互独立的,他们之间是不能直接进行互相访问的。在这种情况下,我们想让不同进程间进行通信,就必须为这些进程之间提供一个“公共区域”,而这块公共区域不能单独属于某一个进程的地址空间,必须独立于各个进程间,且允许被进行通信的进程进行访问。显然,广义上系统(内核)空间、磁盘文件、数据库等都可以作为进程间交流的媒介。

        但通常,我们将使用内核空间进行进程间交流的方式称为“进程间通信”。

        在Linux系统中,进程间的通信方式有管道、消息队列、信号、信号量、共享内存、套接字等方式。而管道通信方式又分为“匿名管道”和“命名管道”两种方式。

常见的进程间通信机制

  • 管道(Pipes):允许数据在两个进程之间流动。包括匿名管道和命名管道(FIFO)。
  • 消息队列(Message Queues):允许进程以消息的形式交换信息。
  • 共享内存(Shared Memory):多个进程可以访问同一块内存区域,以实现高效的数据交换。
  • 信号量(Semaphores):用于实现进程间的同步和互斥控制。
  • 信号(Signals):用于通知进程某个事件的发生。
  • 套接字(Sockets):用于不同主机或同一主机上不同进程之间的通信,尤其适用于网络通信。

二、进程间通信的目的

  • 数据传输:在不同进程之间传递数据或状态信息。
  • 协调与同步:协调进程间的操作,避免冲突,确保一致性。
  • 资源共享:允许多个进程共享系统资源,如文件或内存。
  • 任务分配:将任务分配给多个进程,提高处理效率和负载均衡。
  • 消息传递:传递控制信息、指令或状态更新等。

三、管道的特点

        

1、匿名管道只能用于父子进程、兄弟进程等具有“亲缘关系”的进程之间。

2、管道是半双工的,即数据只能往一个方向流动;如果需要进行进程间的双方通信,则需要创建两个管道。

3、管道单独构成一种独立的文件系统。管道对于两端的进程而言,就是一个文件,但他不是普通的文件,并不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中

4、一个进程向管道中写入的内容被管道的另一端的进程读出。写入的内容每次被添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

5、管道的写入操作是非原子的,这意味着如果尝试一次性写入大量数据,当数据量超过管道的最大容量时(一般是4096字节,可以自行测试),操作系统可能不会将所有数据一次性放入管道。相反,它可能会将数据分成几个部分,并且接收端可能需要多次读取操作才能接收完整的消息。类似的,当管道中还有未被读取的数据时,当管道中的剩余空间不足以一次性将数据全部写入时,写端会先将剩余空间写满,然后写端进程会阻塞在写端。当读端进程对管道数据进行读入时,管道一出现剩余空间,写端进程就会继续写入。

管道中的数据就像流动的水,而管道就像一个水管。我们将写端视为自来水厂,读端视为水龙头。当水龙头打开时,水流会源源不断地从水厂经过管道流到水龙头处,工厂提供多少水,就用多少水。而当水龙头关闭时,水厂的水就会阻塞在管道前,等待下一次供水。而当水厂倒闭后,再次打开水龙头时,流出的水量只是当初残留在管道中的部分(也可能管道中已经没有残留的水了),当使用完管道中的水后,水龙头也没有存在的必要了,因为与水龙头连接的供应端已经无法供应水流,这时我们只能关闭水龙头了。

在上述例子中,写端就是自来水厂,管道就是水管,读端就是水龙头。当管道中有数据时,我们就可以对其进行读取操作。数据在管道中也可以看作是“流动的”,当读端读取数据后,所读取到的数据在管道中也就“不复存在了”,此时管道中会出现“剩余空间”,只要还有剩余空间,写端就可以往管道中进行写入。

假设你有一个管道,缓冲区大小是 4KB,如果你试图写入 10KB 的数据,操作系统会将数据分成多个块进行处理:

  1. 第一次写入 4KB 数据,管道的缓冲区会填满。
  2. 如果缓冲区满了,写操作会阻塞,直到有足够的空间。
  3. 等到读取端消费了一部分数据,管道中会有新的空间,写操作才会继续。

这也是为什么Linux不保证管道写入数据的原子性——数据是“流动的”。

(写入的原子性:只有写完数据和不写入数据两种结果,不存在只写一部份的情况。由此可见,管道的写入不符合原子性。)

四、 匿名管道的创建

在Linux系统中,pipe函数用于创建一个匿名管道,并且当这个函数成功执行后,管道的读端和写端文件描述符是默认被打开的。这些文件描述符可以用于在进程之间传递数据,实现进程间的通信。匿名管道只能作用于有亲缘关系的进程之间的通信,比如父进程和子进程。

                        

函数原型:

int pipe(int pipefd[2]);

参数:

  • pipefd[2] 是一个整型数组,输出型参数,用于返回管道的读端和写端的文件描述符
  • pipefd[0] 是管道的读端文件描述符,pipefd[1]  是管道的写端文件描述符。

返回值:

  • 函数执行成功返回0;
  • 若失败则返回 -1,并将错误原因存于errno中。

当父进程调用pipe函数创建匿名管道文件时,操作系统会在调用该函数的进程空间中为其分配两个新的文件描述符,一个用于管道的读端,另一个用于写端。如果之前没有打开其他文件或管道,文件描述符 0(标准输入)、1(标准输出)和 2(标准错误)通常会被保留,而管道的文件描述符会被分配为 3、4 或更高的数字。

        

五、父子进程使用匿名管道进行通信的简单示例

当使用fork()创建子进程后,子进程会继承父进程的资源,这其中就包括父进程中为已经打开的文件分配的文件描述符。所以子进程的文件描述符表中也会存在父进程所创建的匿名管道文件的读端和写端文件描述符。因此匿名管道只能作用于有亲缘关系的进程之间的通信的实质是具有亲缘关系 的进程中拥有相同的文件描述符表。

当我们确定好数据的流向后,应关闭不再需要的文件描述符,原因如下:1、长时间运行的程序如果不断打开新的文件描述符而不关闭旧的,最终可能会耗尽系统资源。2、管道是单向的,数据从写端流向读端。如果一个进程同时拥有读端和写端的文件描述符,那么它就可以既读取又写入数据,这可能会导致不可预测的行为和数据混乱。通过在一个进程中关闭读端,在另一个进程中关闭写端,可以明确数据流的方向,并确保每个进程只负责管道的一端。

例如,当确定父进程进行写入、子进程进行读取时,我们需要父进程的读端和子进程的写端,确保数据是从父进程单向流入子进程的。

自此,我们通过write()函数对父进程中的写端进行写入、使用read()函数对子进程中的读端进行读取,自此就实现了父子进程间的通信。

下面是一个父进程通过管道向子进程传递“hello world!\n"字符串并打印至屏幕上的例子:

#include <unistd.h>
#include <errno.h>
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>#define BUFFER_SIZE 1024 //定义缓冲区大小int main()
{int pipefd[2];if (pipe(pipefd) == -1){std::cerr << "Pipe create failed : " << strerror(errno) << std::endl;exit(-1);}pid_t id = fork();if (id == 0){close(pipefd[1]);//子进程先关闭写端char buffer[BUFFER_SIZE];int number = 0;if ((number = read(pipefd[0], buffer, sizeof(buffer) - 1)) == -1)//读入size-1个字节的数据,为'\0'字符留一个空间{std::cerr << "Read failed : " << strerror(errno) << std::endl;exit(-1);}buffer[number] = '\0';//字符数组末尾设置为'\0'字符std::cout << "子进程输出:" << buffer << std::endl;close(pipefd[0]);//子进程退出前关闭读端}else if (id > 0){close(pipefd[0]);//父进程关闭读端char str[] = "hello world!";if (write(pipefd[1], str, sizeof(str)) == -1)//向管道中写入字符串{std::cerr << "Write failed : " << strerror(errno) << std::endl;exit(-1);}close(pipefd[1]);//父进程关闭写端int status = 0;if (waitpid(id, &status, 0) == -1)//等待子进程退出{std::cerr << "Wait failed : " << strerror(errno) << std::endl;}else{if (WIFEXITED(status))//如果是正常退出{std::cout << "退出状态:" << WEXITSTATUS(status) << std::endl;} else if (WIFSIGNALED(status)) //如果是收到信号{std::cout << "终止信号:" << WTERMSIG(status) << std::endl;}}}else{std::cerr << "fork failed!" << strerror(errno) << std::endl;exit(-1);}return 0;
}

六、 匿名管道的规则与特征

1、如果管道的写端没有关闭,但进程A未往管道中写入数据,此时管道中没有数据。在进程B的读端未关闭的情况下,读端进程将阻塞在read()处,直到管道中写入数据,read()函数成功读取数据之后继续执行后续代码。

2、如果写端关闭了,如果管道中仍有未被读取的数据,则读端继续读,直到读完管道中的数据时,read()函数返回0,表示读到了文件的末尾。如果写端关闭且管道中没有数据,则read()函数直接返回0,意味着读到了文件的末尾。此时我们应在代码中进行判断,当read()函数返回值为0时主动关闭读端,退出程序。

3、如果管道的读端没有关闭,但读端不读入数据,写端会一直向管道中写入数据,直到管道被写满。此时写进程会阻塞在写端,直到读端进程对管道中的数据进行读入。

4、如果读端关闭了,写端还在写,那么操作系统将会给写端进程发送SIGPIPE信号,默认终止写端进程。

对于如上规则,对“模块五”中的代码稍作修改即可验证,此处不再进行代码验证。由于规则4需要对管道流向进行改变,所以此处仅验证规则4:

由于我们需要判断当读端关闭时写端进程是否会收到SIGPIPE信号,所以我们让父进程作为读端进程,子进程作为写端进程,使用waitpid()函数获取子进程的退出信号。

#include <unistd.h>
#include <errno.h>
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>#define BUFFER_SIZE 1024 // 定义缓冲区大小int main()
{int pipefd[2];if (pipe(pipefd) == -1){std::cerr << "Pipe create failed : " << strerror(errno) << std::endl;exit(-1);}pid_t id = fork();if (id == 0){close(pipefd[0]); // 父进程关闭读端char str[] = "hello world!";while (true){if (write(pipefd[1], str, sizeof(str)) == -1) // 向管道中写入字符串{std::cerr << "Write failed : " << strerror(errno) << std::endl;exit(-1);}}close(pipefd[1]); // 父进程关闭写端}else if (id > 0){close(pipefd[1]); // 子进程先关闭写端char buffer[BUFFER_SIZE];int r_number = 0;while (true){if ((r_number = read(pipefd[0], buffer, sizeof(buffer) - 1)) == -1) // 读入size-1个字节的数据,为'\0'字符留一个空间{std::cerr << "Read failed : " << strerror(errno) << std::endl;break;}if (r_number == 0){std::cout << "写端关闭,读到了文件末尾,读端进程退出" << std::endl;break;}else{buffer[r_number] = '\0'; // 字符数组末尾设置为'\0'字符std::cout << "子进程输出:" << buffer << std::endl;}close(pipefd[0]); // 子进程退出前关闭读端}int status = 0;if (waitpid(id, &status, 0) == -1) // 等待子进程退出{std::cerr << "Wait failed : " << strerror(errno) << std::endl;}else{if (WIFEXITED(status)) // 如果是正常退出{std::cout << "退出状态:" << WEXITSTATUS(status) << std::endl;}else if (WIFSIGNALED(status)) // 如果是收到信号{std::cout << "终止信号:" << WTERMSIG(status) << std::endl;}}}else{std::cerr << "fork failed!" << strerror(errno) << std::endl;exit(-1);}return 0;
}

可以观察到,当读端关闭后,read()函数读取失败,返回-1。此时写进程收到13号信号,由于写进程中未对信号进行捕捉处理,所以写端进程默认进行退出。

七、匿名管道的应用实例 

实例一:用于shell

管道可用于输入输出重定向,他将一个命令的输出直接定向到另一个命令的输入。例如,当在命令行中输入“who | wc -l”后,shell程序将创建who 和 wc两个进程,以及这两个进程间的管道:

实例二: 父进程向子进程派发任务

场景:程序中存在一系列任务,父进程通过对子进程发送任务编号/任务名称等方式,让子进程去实现指定的任务。

#include <unistd.h>
#include <errno.h>
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <functional>
#include <vector>#define BUFFER_SIZE 1024 // 定义缓冲区大小
#define TASK_NUM 2 // 定义任务数量using func_t = std::function<int(int, int)>;
std::vector<func_t> func(TASK_NUM);void SetTask()
{func[0] = [](int a, int b){return a + b;};func[1] = [](int a, int b){return a - b;};
}int HandleTask(int index)
{if(index < 0 || index > 1){std::cout << "任务列表中暂无此任务" << std::endl;return 0;}std::cout << "请输入两个操作数:";int a = 0, b = 0;std::cin >> a >> b;int ret = func[index](a, b);std::cout << "结果为:" << ret << std::endl;return 1;
}int main(int argc, char *argv[])
{SetTask();//设置任务int pipefd[2];if (pipe(pipefd) == -1){std::cerr << "Pipe create failed : " << strerror(errno) << std::endl;exit(-1);}pid_t id = fork();if (id == 0){close(pipefd[1]); // 子进程先关闭写端while(true){char buffer[2];int number = 0;if ((number = read(pipefd[0], buffer, sizeof(buffer) - 1)) == -1) // 读入size-1个字节的数据,为'\0'字符留一个空间{std::cerr << "Read failed : " << strerror(errno) << std::endl;break;}else if (number == 0){std::cout << "子进程退出" << std::endl;break;}else {buffer[number] = '\0'; // 字符数组末尾设置为'\0'字符int ans = atoi(buffer);if(!HandleTask(ans)){std::cout << "子进程调用任务失败" << std::endl;break;}}  }close(pipefd[0]); // 子进程退出前关闭读端}else if (id > 0){close(pipefd[0]); // 父进程关闭读端for(int i = 1; i < argc; i++){if (write(pipefd[1], argv[i], strlen(argv[i])) == -1) // 向管道中写入字符串{std::cerr << "Write failed : " << strerror(errno) << std::endl;exit(-1);}}close(pipefd[1]); // 父进程关闭写端int status = 0;if (waitpid(id, &status, 0) == -1) // 等待子进程退出{std::cerr << "Wait failed : " << strerror(errno) << std::endl;}else{if (WIFEXITED(status)) // 如果是正常退出{std::cout << "退出状态:" << WEXITSTATUS(status) << std::endl;}else if (WIFSIGNALED(status)) // 如果是收到信号{std::cout << "终止信号:" << WTERMSIG(status) << std::endl;}}}else{std::cerr << "fork failed!" << strerror(errno) << std::endl;exit(-1);}return 0;
}

【下一节:基于匿名管道通信实现的进程池】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/876314.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python | Leetcode Python题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; class Solution:def hIndex(self, citations: List[int]) -> int:n len(citations)left 0; right n - 1while left < right:mid left (right - left) // 2if citations[mid] > n - mid:right mid - 1else:left mid 1retur…

【Linux C | 网络编程】进程池大文件传输的实现详解(三)

上一篇实现了进程池的小文件传输&#xff0c;使用自定义的协议&#xff0c;数据长度数据本身&#xff0c;类似小火车的形式&#xff0c;可以很好的解决TCP“粘包”的问题。 【Linux C | 网络编程】进程池小文件传输的实现详解&#xff08;二&#xff09; 当文件的内容大小少于…

html+css 动态效果

1.波浪效果 <div class"sitesingle"></div> <style>.sitesingle{display:flex;justify-content:space-between;align-items:stretch;overflow:hidden;position:relative;height: 400px;}keyframes bgRotate{0%{transform:rotate(0deg)}to{transfor…

基于关联规则的分类算法(CBA) | 项集、频繁项集、关联规则 | arulesCBA库

基于关联规则的分类算法 目前使用较多且较为简洁的关联规则分类算法是基于关联规则的分类算法&#xff08;Classification Based on Association, CBA&#xff09;&#xff0c;下面将从该算法的相关概念开始介绍。 这部分笔记参考论文&#xff1a;孙菡悦.基于多因素交互效应的…

C++的STL简介(一)

目录 1.什么是STL 2.STL的版本 3.STL的六大组件 4.string类 4.1为什么学习string类&#xff1f; 4.2string常见接口 4.2.1默认构造 ​编辑 4.2.2析构函数 Element access: 4.2.3 [] 4.2.4迭代器 ​编辑 auto 4.2.4.1 begin和end 4.2.4.2.regin和rend Capacity: 4.2.5…

repo中的default.xml文件project name为什么一样?

文章目录 default.xml文件介绍为什么 name 是一样的&#xff0c;path 不一样&#xff1f;总结 default.xml文件介绍 在 repo 工具的 default.xml 文件中&#xff0c;定义了多个 project 元素&#xff0c;每个元素都代表一个 Git 仓库。 XML 定义了多个不同的 project 元素&…

树和二叉树(不用看课程)

1. 树 1.1 树的概念与结构 树是⼀种非线性的数据结构&#xff0c;它是由 n&#xff08;n>0&#xff09; 个有限结点组成⼀个具有层次关系的集合。把它叫做树是因为它看起来像⼀棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 • 有⼀个特殊的结点&am…

GD32相较于STM32的优劣势-完全总结

优势 1.更高的主频 GD32单片机的主频可以达到108MHz&#xff0c;‌而STM32的最大主频为72MHz&#xff0c;‌这意味着GD32在代码执行速度上具有优势&#xff0c;‌适合需要快速处理数据的场景 2.更低的内核电压 GD32的内核电压为1.2V&#xff0c;‌而STM32的内核电压为1.8V。…

《系统架构设计师教程(第2版)》第12章-信息系统架构设计理论与实践-05-信息系统架构案例分析

文章目录 1. 价值驱动的体系结构——连接产品策略与体系结构1.1 价值模型1&#xff09;概述2&#xff09;价值驱动因素3&#xff09;传统方法识别价值模型的缺陷&#xff08;了解即可&#xff09; 1.2 体系结构策略&#xff08;挑战&#xff09;1&#xff09; 优先级的确定2&am…

【C++】动态内存管理与模版

目录 1、关键字new&#xff1a; 1、用法&#xff1a; 2、理解&#xff1a; 3、与malloc的相同与不同&#xff1a; 1、相同&#xff1a; 2、不同&#xff1a; 2、模版初阶&#xff1a; 1、函数模版&#xff1a; 1、概念&#xff1a; 2、关键字&#xff1a;template&…

微信公众号获取用户openid(PHP版,snsapi_base模式)

微信公众号获取用户openid的接口有2个&#xff1a;snsapi_base、snsapi_userinfo 详情见微信公众号开发文档&#xff1a;https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 本文介绍用PHP方式调用snsapi_base接口获取微信用户…

苦学Opencv的第十一天:图像的形态学操作

Python OpenCV从入门到精通学习日记&#xff1a;图像的形态学操作 前言 图像形态学是图像处理中的一个重要分支&#xff0c;主要关注图像中物体的形状和结构。通过形态学操作&#xff0c;我们可以对图像进行有效的分析和处理&#xff0c;例如图像的腐蚀与膨胀、开运算与闭运算…

ansible基础讲解和加密文件讲解

ansible最重要的三个文件 /etc/ansible/ansible.cfg #####ansible的配置文件 /etc/ansible/host ##清单文件inventory ansible-navigator.yml ####以yml结尾的文件可以理解为conf结尾的文件&#xff0c;是配置文件&#xff0c;用于设置剧本playbook playbook讲解 以.yml结…

vue3中计算属性

假如需要修改,需要使用get,set let a ref(111) import {computed} from vue let changeimg computed({get(){return a},set(val){a.value val}}) 如果不需要修改 let a ref(111) import {computed} from vue let changeimg computed(() >{return a })

135.分发糖果,遍历方向+candy选取的详解

力扣135分发糖果 题目思路代码 题目 https://leetcode.cn/problems/candy/description/ 老师想给孩子们分发糖果&#xff0c;有 N 个孩子站成了一条直线&#xff0c;老师会根据每个孩子的表现&#xff0c;预先给他们评分。 你需要按照以下要求&#xff0c;帮助老师给这些孩子…

WordPress原创插件:自定义文章标题颜色

插件设置截图 文章编辑时&#xff0c;右边会出现一个标题颜色设置&#xff0c;可以设置为任何颜色 更新记录&#xff1a;从输入颜色css代码&#xff0c;改为颜色选择器&#xff0c;更方便&#xff01; 插件免费下载 https://download.csdn.net/download/huayula/89585192…

【一图流】Git下载与安装教程

下载Git Git官网&#xff1a;https://git-scm.com/?hlzh-cn 安装Git

UE5 C++跑酷练习(Part2)

一.首先GameMode里有Actor数组&#xff0c;组装直线路&#xff0c;和左右路 #include "CoreMinimal.h" #include "GameFramework/GameModeBase.h" #include "RunGANGameMode.generated.h"UCLASS(minimalapi) class ARunGANGameMode : public AG…

揭秘企业为何钟情定制红酒:品牌形象与不同的礼品的双重魅力

在商务世界的广阔天地里&#xff0c;红酒不仅仅是一种饮品&#xff0c;更是一种传递情感、展示品味的不同媒介。近年来&#xff0c;越来越多的企业开始钟情于定制红酒&#xff0c;其中洒派红酒&#xff08;Bold & Generous&#xff09;通过其品质和个性化的定制服务&#x…

网络访问(Socket/WebSocket/HTTP)

概述 HarmonyOS为用户提供了网络连接功能&#xff0c;具体由网络管理模块负责。通过该模块&#xff0c;用户可以进行Socket网络通滚、WebSocket连接、HTTP数据请求等网络通信服务。 Socket网络通信&#xff1a;通过Socket(嵌套字)进行数据通信&#xff0c;支持的协议包括UDP核…