进程间通信——《匿名管道》

news2024/12/26 21:34:23

文章目录

  • 前言:
  • 进程间通信介绍
    • 进程间通信目的
    • 进程之间如何通信?
    • 进程间通信分类
  • 管道
    • 什么是管道?
    • 匿名管道
      • 🧨尝试使用:
      • 🍗处理细节问题:
    • 🚀管道的4种情况和5种特征:
      • 4种情况:
      • 5种特征:

前言:

前面的学习中,我们已经可以初步的了解操作系统对文件I/O操作的具体实现,通过学习重定向认识到了文件fd以及操作系统是如何管理文件的,不管是管理打开的文件的还是存在磁盘的文件,我们都已经有过了解了。

接下来的内容,我们想要深入一点,之前我们总是研究单一的进程,而我们之前又学习过进程具有独立性,那么对一个计算机来说,同一时间段内存在特别多的进程在运行,有一些数据肯定会从一部分进程中读取,那么进程与进程之间又是如何构建起来通信的呢?我们下面就来聊聊,进程间的通信问题!!!

进程间通信介绍

进程间通信目的

  • 数据传输:一个进程需要将它的数据发送给另一个进程
  • 资源共享:多个进程之间共享同样的资源。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变

进程之间如何通信?

由于进程具有独立性,因此两个进程之间并的通信是不容易的!**如果 A 进程想要直接将数据交给 B 进程,是 A 进程去访问 B 进程的一段内存区域将数据拷进去呢?还是 B 进程去直接读取 A 进程的内存区域将数据拷出来呢?**所以这是行不通的!

回顾一下我们以前有没有过上课写小纸条的经历呢?你和你的同学在课上由于各种原因不能直接进行交流,即你们都是一个独立的个体,那么你们实现交流就是在小纸条上面写写画画,要么是我写然后你读,也可是是你写然后给我读!进程之间的通信也是如此。

image-20240923135502878

  • 所以不管进程是如何进行通信的,本质就是让不同的进程看到同一份资源 (即同一个媒介),从操作系统的角度来说,就是在内存中找到同一份资源
  • 这个资源通常是由 OS 提供的,不能由 A / B 进程的任何一个提供, 但 A / B 进程可以去向 OS 申请。

以上也是进程间通信的前提!
image-20240923140621603

进程间通信分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

管道

什么是管道?

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

image-20240923140841690

匿名管道

我们先来回顾一下曾经学习的文件fd他们的结构是怎么样的:
image-20240923151920311

以上就是操作系统管理文件的一个结构,针对于打开的文件的管理。

现在我想创建一个子进程,然后让子进程以写方式打开文件。

首先,在struct files_struct内部,我们都知道会默认打开0 && 1 && 2号文件,子进程会继承父进程打开0 && 1 && 2号文件。最开始是父进程bash默认打开了0 && 1 && 2号文件,此后所以的所有进程都会默认打开0 && 1 && 2号文件。
image-20240923153809381

那么对于这种情况来说,针对于同一个文件,我们其实没有必要再给“写方式”的struct file再创建inode 和 内核级缓冲区了,直接共用就好了!
image-20240923154033980

至此我们就可以创建子进程了,所以我们会fork出一个子进程,这个子进程会创建一个新的PCB,然后继承父进程的struct files_struct,与其说是继承,倒不如说是用着父进程的struct files_struct的一份拷贝,也可以说是共享
image-20240923154325989

由于子进程是与父进程"共享"一个struct files_struct,那么对于父进程以两种方式打开的两个struct file,子进程通过struct files_struct的文件描述符fd,也可以轻松找到。
image-20240923154839671

(由于inode在这里没有作用,所以我把inode区域给擦掉了)
好了,那我们通过上图就可以发现一个神奇的地方,我们写数据会往内核级的缓冲区去写,而读数据也是在内核级缓冲区里读。那这个“缓冲区”不就将我们的父子进程连接起来了吗?父子进程都看到了内存中的同一片区域了呀!满足了前提,当然构成进程间的通信。

那这个“缓冲区”就是我们的——匿名管道
image-20240923155244752

那既然这样,子进程完成写操作,父进程完成读操作,那两个进程各自都存在没有用的操作文件方式,因此我们可以将父进程的写操作关闭,子进程的读操作关闭
image-20240923155528508

这就是匿名管道的由来!理解我们曾经所讲解的——”Linux中一切皆文件“,我们也可以得知管道本身就是一个文件,而这个文件拥有两个struct file

通过上述例子我们可以解释一种现象:

由于子进程创建的时候会继承父进程的struct files_struct的 0 && 1 && 2 号文件,因此我们就算在子进程中关闭1号文件夹(显示器文件)也不会影响父进程,这也能侧方面反映出进程独立性!

int main()
{
    pid_t pid = fork(); // 创建子进程

    // 子进程
    if (pid == 0)
    {
        close(1); // 关闭显示器文件
        exit(1);
    }
    // 父进程
    cout << "hello linux!" << endl;

    return 0;
}

image-20240923160938044

🧨尝试使用:

既然我们现在理解了管道的底层,那我现在想利用管道来实现父子进程间的通信,如果单纯是对一个文件进行以读方式和写方式打开,再关闭读方式的fd和写方式的fd,最后写入文件缓冲区中,这样的操作过于复杂,因此我们操作系统提供了一个函数 —— int pipe(int pipefd[2]);用来创建管道。

作用:创建一个匿名管道,用来进程间通信;

参数:
int pipefd[2]这个数组是一个传出参数;
pipefd[0] 对应管道的读端;
pipefd[1] 对应管道的写端;

返回值:
成功 0;
失败-1;

代码如下:

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>

int main()
{
	// 创建管道
	int pipefd[2];
	int n = pipe(pipefd);
	if (n < 0)
	{
		std::cerr << "pipe failed" << std::endl;
		exit(1);
	}

	pid_t pid = fork(); // 创建子进程
	if (pid == 0)
	{
		// 子进程
		close(pipefd[0]);
		std::cout << "子:我是子进程,我关闭了读文件的操作!" << std::endl;
		static int count = 0;

		while (1)
		{
			sleep(4);
			std::cout << "写 -> 现在,我准备向管道里发送消息!" << std::endl;
			std::string message = "Hi I am child process!";
			pid_t id = getpid();
			message += " pid: ";
			message += std::to_string(id);
			message += " count: ";
			message += std::to_string(count);

			int n = write(pipefd[1], message.c_str(), strlen(message.c_str()));
			if (n < 0)
			{
				std::cerr << "failed to write" << std::endl;
				exit(1);
			}
			std::cout << "发送成功!" << std::endl;
			count++;
		}
	}

	// 父进程
	close(pipefd[1]);
	std::cout << "父:我是父进程,我关闭了写文件操作!" << std::endl;

	while (1)
	{
		std::cout << "读 -> 现在,我准备向管道里读取消息" << std::endl;

		char file_buffer[1024];
		ssize_t n = read(pipefd[0], file_buffer, sizeof(file_buffer) - 1);
		if (n < 0)
		{
			std::cerr << "failed to read" << std::endl;
			exit(1);
		}
		file_buffer[n] = 0;
		std::cout << "读取内容:";
		std::cout << file_buffer << std::endl;
		std::cout << "-----------" << std::endl;
	}
	return 0;
}

image-20240925212318965

🍗处理细节问题:

  • 代码中,父子进程的状态?:

    image-20240926165532877
    我们先让父子进程都连接至管道,然后各自关闭自己不要的fd,然后做到子进程往管道里写信息,父进程往管道里读信息。

  • 为什么我的父子进程一定要关闭fd呢?可以不关闭吗?

    当然可以不关闭啦,但是我们在这里还是建议关闭,
    因为如果父子进程都保留读端和写端,则它们可以相互读写,这可能导致通信混乱。
    每个打开的文件描述符都会占用系统资源。关闭不必要的文件描述符可以释放这些资源,供其他进程或操作使用。
    如果不关闭不必要的文件描述符,在读写管道时可能会遇到未预期的阻塞或错误,尤其是在管道已满或为空的情况下。

  • 既然父子进程要关闭不需要的fd,那为什么当初还要创建和链接呢?

    操作系统这么做肯定是有他的考虑的,如果父进程不链接也不创建,那在创建子进程的时候又该如何让子进程继承父进程的fd和struct file呢?所以这么做其实是方便子进程继承罢了。

🚀管道的4种情况和5种特征:

4种情况:

  1. 如果管道内部是空的,且子进程的write的fd一直存在,那么父进程在读取的时候会被阻塞

    其实我们编写代码时,我们只是在子进程的写文件的部分加入了sleep(4),并没有在父进程读取的时候也加入sleep()这个函数,就比如这个时候父进程把管道内部的信息全部读完了,那么就会进入阻塞状态,等待子进程写入。
    我们可以输入指令:
    while :; do ps ajx | head -1 && ps ajx | grep test |grep -v grep; echo "---------------------"; sleep 1;来实现一个监视窗口,然后运行。
    image-20240926172500534

  2. 如果管道被写满,且父进程的读操作的fd不关闭,那么管道写满后子进程就会处于阻塞状态

    现在我们让子进程不断往管道里写字符’A’,然后关闭父进程的读操作,最后看看管道满了是什么样子的,子进程代码修改成这样:

    while (1)
    {
    	char a[] = "A";
    	int n = write(pipefd[1], a, 1);
    	if (n < 0)
    	{
    		std::cerr << "failed to write" << std::endl;
    		exit(1);
    	}
    	count++;
    	std::cout << count << std::endl;
    }
    

    image-20240926174222394

    最后停留在了65536这个数字,正好是64KB,因此在Ubuntu22.04下的匿名管道大小为64KB。

  3. 管道一直在读,并且此时子进程的关闭了写操作的fd,那么读端read就会返回0,代表读到了文件末尾。

    我们在子进程加入close(pipep[1])代表关闭了写操作。
    image-20240926174938403

    在这里我是把while循环关了,所以才没有不断的往下打印。打开cgdb调试也不难看出:
    image-20240926175241424

  4. 若读文件的fd关闭了 && 写文件的fd一直在写会怎么样?

    既然你一直在像一个无人知晓的空间写数据,那不就是无用功吗?这种方式显然是浪费时间的!因此OS不会允许这种时发生,所以会直接杀掉对应的进程,具体的操作是OS给进程发送异常信号(13号->SIGPIPE)。而对于这种情况的管道也叫 broken pipe。

    image-20241007133504142

    我们也可以通过代码验证:

    1. 父进程关闭pipe[0]
    2. 让子进程不断往管道里写数据
    3. 让父进程关闭pipe[0]后,等待子进程
    4. 最后打印出信号码即可
    int status = 0;
    pid_t rid = waitpid(pid, &status, 0);
    if(rid > 0)
    {
    	std::cout << "等待成功!" << std::endl;
    	std::cout << "退出信号:" << (status & 0x7F) << std::endl;
    	std::cout << "退出码:" << ((status>>8) & 0xFF) << std::endl;
    }
    

    image-20241007133839315

5种特征:

  1. 匿名管道只能用于具有“血缘关系”的进程之间进行通信,常用于父子进程

  2. 管道内部,自带进程之间的同步机制。在多执行流执行代码的时候,具有明显的顺序性。

  3. 文件(管道也是个文件)的声明周期是随进程的。

  4. 管道文件在通信的时候,是面向字节流的。

  5. 管道的通信模式,是一种特殊的**”半双工“**模式。

    image-20241007135227248

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

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

相关文章

C++引用(变量引用、数组引用与数组指针、引用本质-指针常量、常量引用)

C语言 ——对数组名进行解引用,取地址,还有sizeof和strlen进行操作解析_对数组名解引用得到什么-CSDN博客 C++引用(变量引用、数组引用与数组指针、引用本质-指针常量、常量引用)_c++11 数组引用-CSDN博客

【智能算法应用】指数分布优化算法求解二维路径规划问题

摘要 本项目采用指数分布优化算法来求解二维路径规划问题。通过构建合理的代价函数并结合智能算法进行优化&#xff0c;我们可以在复杂环境中找到最优路径。实验结果表明&#xff0c;该算法在多维空间中表现出高效性和稳定性。 理论 路径规划问题的核心在于从起点到终点选择…

中国喀斯特地貌分布shp格式数据

​ 中国几乎各省区都有不同面积的石灰岩的分布&#xff0c;出露地表的总面积约有130万平方公里&#xff0c;约占全国总面积的13.5%。被埋藏于地下的则更为广泛&#xff0c;有的地区累计厚度可达几千米。以至上万米。由此可见&#xff0c;喀斯特地形的研究对中国来说&#xff0c…

Nuxt.js 应用中的 link:prefetch 钩子详解

title: Nuxt.js 应用中的 link:prefetch 钩子详解 date: 2024/10/7 updated: 2024/10/7 author: cmdragon excerpt: link:prefetch 是一个强大的钩子,允许开发者在链接预取时执行附加逻辑。合理利用这个钩子,可以帮助优化页面的加载速度和用户体验,提升 Web 应用的整体性…

气膜馆的多元化盈利模式与市场前景—轻空间

随着市场经济的不断繁荣&#xff0c;气膜馆作为一种创新型场馆&#xff0c;凭借其独特的结构设计和灵活的运营模式&#xff0c;逐渐成为创业者关注的焦点。那么&#xff0c;气膜馆如何通过多元化经营实现盈利&#xff1f;本文将为您详细解析气膜馆的经营模式与发展机会。 气膜馆…

Hive3.x版本调优总结

文章目录 第 1 章 Explain 查看执行计划&#xff08;重点&#xff09;1.1 创建测试用表1&#xff09;建大表、小表和 JOIN 后表的语句2&#xff09;分别向大表和小表中导入数据 1.2 基本语法1.3 案例实操 第 2 章 Hive 建表优化2.1 分区表2.1.1 分区表基本操作2.1.2 二级分区2.…

Spring Boot医院管理系统:数据驱动的医疗

3系统分析 3.1可行性分析 通过对本医院管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本医院管理系统采用JAVA作为开发语言&#xff0c;Spring Boot框…

代码随想录算法训练营Day27 | 回溯算法理论基础、77.组合、216.组合总和Ⅲ、17.电话号码的字母组合

目录 回溯算法理论基础 77.组合 216.组合总和Ⅲ 17.电话号码的字母组合 回溯算法理论基础 视频讲解&#xff1a;带你学透回溯算法&#xff08;理论篇&#xff09;| 回溯法精讲&#xff01; 代码随想录&#xff1a;回溯算法理论基础 回溯函数与递归函数指的是同一个函数…

VSCode | 设置Jupyter Notebook显示行号

vscode中的jupyter notebook每个cell都是默认不显示行号的&#xff0c;如果出现了报错&#xff0c;比如在52行出现报错&#xff0c;如果代码多的话不显示行号就有点麻烦&#xff0c;本文介绍如何设置显示行号。 1、VScode点击文件-首选项-设置 2、搜索“python”&#xff0c;点…

Type-C那么多引脚是做什么用的?

一提到Type-C大家想到的肯定就是下面这个扁头接口。 如果大家仔细透过缝看里面的话&#xff0c;可以看到上下两排都有密密麻麻的引脚&#xff08;手机比较差拍不出来就不上图了&#xff09;。 虽然我们用Type-C口的时候我们不需要识别正反面&#xff08;这也是我喜欢Type-C的…

基于Java语言的充电桩平台+云快充协议+充电桩管理后台+充电桩小程序

软件架构 1、提供云快充底层桩直连协议&#xff0c;版本为云快充1.5&#xff0c;对于没有对接过充电桩系统的开发者尤为合适&#xff1b; 2、包含&#xff1a;启动充电、结束充电、充电中实时数据获取、报文解析、Netty通讯框架、包解析工具、调试器模拟器软件等&#xff1b;…

电脑提示d3dcompiler_47.dll缺失怎么修复,仔细介绍dll的解决方法

1. d3dcompiler_47.dll 概述 1.1 定义与作用 d3dcompiler_47.dll 是 Microsoft DirectX 的一个关键组件&#xff0c;作为一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;它在 Windows 操作系统中扮演着至关重要的角色。DirectX 是一套由微软开发的用于处理多媒体…

Flutter渲染过程

The rendering process is what transforms your widget tree into the actual pixels that are displayed on the screen. It’s like the magic behind the scenes that brings your app’s UI to life! 呈现过程将小部件树转换为显示在屏幕上的实际像素。它就像幕后的魔法&…

代码随想录算法训练营第二十六天|669. 修剪二叉搜索树 108.将有序数组转换为二叉搜索树 538.把二叉搜索树转换为累加树

669. 修剪二叉搜索树 给定一个二叉搜索树&#xff0c;同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树&#xff0c;使得所有节点的值在[L, R]中 (R>L) 。你可能需要改变树的根节点&#xff0c;所以结果应当返回修剪好的二叉搜索树的新的根节点。 思路&#xff1a; 首先…

JavaScript 获取浏览器本地数据的4种方式

JavaScript 获取浏览器本地数据的方式 我们在做Web开发中&#xff0c;客户端存储机制对于在浏览器中持久化数据至关重要。这些机制允许开发者存储用户偏好设置、应用状态以及其他关键信息&#xff0c;从而增强用户体验。本文将介绍几种常用的JavaScript获取浏览器本地数据的方…

【无人机设计与控制】基于蜣螂优化算法的无人机三维路径规划Matlab程序

摘要 使用蜣螂优化算法&#xff08;Dung Beetle Optimization, DBO&#xff09;&#xff0c;本文提出了一种无人机三维路径规划方法。该算法借鉴蜣螂导航行为&#xff0c;结合无人机避障需求&#xff0c;在复杂三维环境中生成最优路径。实验结果表明&#xff0c;基于DBO的路径…

Redis主从复制(replica)、哨兵

一、Redis主从复制介绍: 主从复制&#xff0c;master主机以写为主&#xff0c;slave从机以读为主&#xff0c;当主机数据变化的时候自动将新的数据异步同步到其他从机数据库&#xff1b;能够实现读写分离&#xff0c; 容灾恢复、 数据备份以及水平扩容支撑高并发 二、实现方法…

在ubuntu好部署jenkins发布vue项目时遇到的一些问题及解决方法以及使用jenkins发布vue项目-npm自动打包发布的实现

一、在ubuntu好部署jenkins发布vue项目时遇到的一些问题及解决方法 1. 问题&#xff1a;webpack-dev-server不是内部或外部命令&#xff0c;也不是可运行的程序 解决&#xff1a;使用webpack要安装webpack-cli这个包&#xff0c;才可以调用webpack和webpack-dev-server这些命…

C++——模拟实现stack和queue

1.传统模拟方式 namespace jxy {template<class T>class stack{//...private:T* _a;size_t _size;size_t _capacity;}; } 2.适配器方式模拟 namespace jxy {template<class T,class Container>class stack{public:void push(const T& x){_con.push_back(x);…

No.6 笔记 | Linux操作系统基础:全面概览与核心要点

1. 简介与历史 1.1 起源 创始人&#xff1a;Linus Torvalds&#xff08;芬兰赫尔辛基大学学生&#xff09;初衷&#xff1a;设计一个替代Minix的全功能Unix操作系统首次发布&#xff1a;1991年10月5日&#xff0c;Linux v0.01版本 2. Linux特点 多用户多任务&#xff1a;用…