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

文章目录

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

前言:

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

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

进程间通信介绍

进程间通信目的

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

进程之间如何通信?

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

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

image-20240923135502878

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

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

进程间通信分类

管道

System V IPC

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

POSIX IPC

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

管道

什么是管道

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.niftyadmin.cn/n/5693086.html

相关文章

家庭用超声波清洗机好用吗?推荐四款性能绝佳的超声波清洗机!

在现代社会快节奏的日常中&#xff0c;高效清洁辅助工具成为了众多家庭的追求热点。超声波清洗机&#xff0c;作为集高效与便捷于一体的新兴清洗神器&#xff0c;正逐渐成为大众宠儿。但面对琳琅满目的市场选择&#xff0c;不同的型号搭载多样化的功能设定及波动的价格区间&…

【ECMAScript 从入门到进阶教程】第四部分:项目实践(项目结构与管理,单元测试,最佳实践与开发规范,附录)

第四部分&#xff1a;项目实践 第十四章 项目结构与管理 在构建现代 Web 应用程序时&#xff0c;良好的项目结构和管理是确保代码可维护性、高效开发和部署成功的关键因素。这一章将深入讨论项目初始化与配置&#xff0c;以及如何使用构建工具来简化和优化项目建设过程。 14…

前缀和算法详解

对于查询区间和的问题&#xff0c;可以预处理出来一个前缀和数组 dp&#xff0c;数组中存储的是从下标 0 的位置到当前位置的区间和&#xff0c;这样只需要通过前缀和数组就可以快速的求出指定区间的和了&#xff0c;例如求 l ~ r 区间的和&#xff0c;就可以之间使用 dp[l - 1…

【深度学习】交叉熵

交叉熵&#xff08;Cross-Entropy&#xff09;是信息论中的一个重要概念&#xff0c;也是在机器学习和深度学习中用于分类任务的常见损失函数。它衡量的是两个概率分布之间的差异&#xff0c;特别是模型的预测概率分布与真实分布的差异。 交叉熵最初是从信息论引入的&#xff0…

Python小示例——质地不均匀的硬币概率统计

在概率论和统计学中&#xff0c;随机事件的行为可以通过大量实验来研究。在日常生活中&#xff0c;我们经常用硬币进行抽样&#xff0c;比如抛硬币来决定某个结果。然而&#xff0c;当我们处理的是“质地不均匀”的硬币时&#xff0c;事情就变得复杂了。质地不均匀的硬币意味着…

掌握 WPF 开发:基础、数据绑定与自定义控件

WPF&#xff08;Windows Presentation Foundation&#xff09;是用于构建现代桌面应用程序的强大框架。它通过 XAML&#xff08;Extensible Application Markup Language&#xff09;与丰富的控件体系&#xff0c;提供了灵活的 UI 开发方式。本文将介绍 WPF 的基础知识、XAML 语…

探索Python的魔法:装饰器模式的奥秘

引言 装饰器模式是一种结构型设计模式&#xff0c;它通过创建一个包装对象来包含真实的对象&#xff0c;从而在不修改原有对象的基础上扩展其功能。在Python中&#xff0c;装饰器模式尤为流行&#xff0c;因为它提供了一种非常Pythonic的方式来增强函数或类的功能。 基础语法…

ARM Assembly 6: Shift 和 Rotate

基础概念 LSL&#xff08;Logical Shift Left&#xff09; 功能: 将寄存器中的位向左移动&#xff0c;右边用零填充。左移相当于对二进制数进行乘以2的幂的操作。 语法: LSL{S} Rd, Rn, #shamt Rd: 结果存储的目标寄存器。 Rn: 要进行位移的源寄存器。 #shamt: 位移的位数&…