【Linux进程】进程间的通信

目录

1. 进程间通信

1.1 进程间通信的目的

2. 管道

 2.1 什么是管道

 2.2. 匿名管道

匿名管道的特性

 管道的4种情况

 联系shell中的管道

2.3. 命名管道

 代码级建立命名管道

 2.4. 小结

总结


在这里插入图片描述

1. 进程间通信

        进程间通信(Inter-Process Communication,IPC)是指在操作系统中,多个进程之间进行数据交换和信息传递的一种机制。由于进程在内存中有各自的地址空间,它们不能直接访问对方的内存,因此需要通过一些特定的方法来实现通信;

1.1 进程间通信的目的

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

进程间通信的本质:让不同的进程先看到同一份资源,资源通常是由操作系统提供

进程间通信的方式也有很多:匿名管道、命名管道、共享内存、消息队列...;

本文主要介绍匿名管道、命名管道这两种进程通信;

2. 管道

 2.1 什么是管道

        管道是Unix中最古老的进程间通信的形式;

        联系一下日常生活中的管道,水在管道中流到一般都是单向的,这里的管道也是如此,进程之间通过管道通信也是单向的;

        管道(Pipes)是一种进程间通信(IPC)机制,用于在一个或多个进程之间传递数据。它通过创建一个临时的通道,允许一个进程的输出直接作为另一个进程的输入。管道主要分为两种类型:匿名管道和命名管道;

管道的通信特点就是单向的,单个管道只能进行单向通信,想要双向通信怎么办?--创建两个管道;

 2.2. 匿名管道

        匿名管道主要用于有亲缘关系的进程(如父子进程)之间进行通信;

最常见的就是父子进程体系:

         父进程创建子进程,子进程会拷贝父进程的 struct file_struct(这里可以认为是浅拷贝);files_struct 中的内容完全和父进程相同,这样父进程和子进程就同时指向了同一块资源;

files_struct:是进程用于管理打开的文件,所描述出来的数据结构;

管道可以视为一种特殊的文件,只存在于内存中的文件;

怎么理解?一个被打开的文件通常包含三个结构:inode、方法集(虚拟文件系统)、文件缓冲区;管道也是如此;只不过他不会把缓冲区的数据刷新到磁盘中;

如上图的结构,父子进程执行的是相同的文件描述对象,如果同时对文件进行写入,是无法辨识数据是谁写入的;为了避免这样的情况,管道在设计之初,它的写入就是单向的;

比如:父进程向子进程发送数据,父进程只能往管道写入数据,子进程只能从管道读取数据;数据传输的过程和管道类似,又因为它内部所使用的文件并不会在磁盘中存在,只会在内存中使用,所以也被称为匿名管道;

#include <unistd.h>
// 功能:创建一无名管道
// 原型
int pipe(int fd[2]);
// 参数
// fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
// 返回值:成功返回0,失败返回错误代码

 示例:

#include <iostream>  
#include <cassert>  
#include <unistd.h>  
#include <cstring>  
#include <sys/wait.h>  
#include <sys/types.h>  using namespace std;  int main()  
{  // 第一步:建立管道  int pipefd[2] = { 0 };  int n = pipe(pipefd);  assert(n == 0);  cout << "pipefd[0]:" << pipefd[0] << ", pipefd[1]:" << pipefd[1] << endl;  // 第二步:创建子进程  pid_t id = fork();  if (id < 0)  {  perror("fork");  return 1;  }  // 父进程写入,子进程读取  // 第三步:关闭不需要的fd,形成单向通信的管道  else if (id != 0) // 父进程  {  close(pipefd[0]); // 关闭读端  int cnt = 0;  while (cnt < 10)  // 发送10次  {  char msg[1024];  snprintf(msg, sizeof(msg), "hello child! I am father, pid: %d, cnt: %d", getpid(), cnt);  cnt++;  if (write(pipefd[1], msg, strlen(msg)) < 0) {  perror("write");  break; // 处理写入错误  }  sleep(1);  }  cout << "father close write point" << endl;  close(pipefd[1]); // 关闭管道写端  pid_t rid = waitpid(id, nullptr, 0);  if (rid == id)  {  cout << "wait success !" << endl;  }  }  else // 子进程  {  close(pipefd[1]); // 关闭写端  char buffer[1024];  while (true)  {  ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 预留一个位置给字符串结尾添加'\0'  if (n > 0)  {  buffer[n] = '\0'; // 添加字符串结束符  cout << getpid() << ", father says: " << buffer << " to me!" << endl;  }  else if (n == 0)  {  cout << "write quit, me too !" << endl;  break; // 读到EOF  }  else  {  perror("read");  break; // 处理读取错误  }  sleep(1);  }  cout << "read point close" << endl;  close(pipefd[0]);  sleep(5); exit(0);  }  return 0;  
}

 在文件描述符的视角:

1. 父进程创建管道

 开始父进程创建管道,以读方式打开一次以写方式打开一次,打开两次;

 2. 父进程创建子进程

父进程创建子进程,子进程继承父进程的属性,也可以对管道也可以进行读和写; 

 3. 父进程关闭读端、子进程关闭写端

 实现单向时,比如父进程向子进程传输数据;父进程关闭读方式打开的文件,子进程关闭写方式打开的文件这样就形成了单向信道;

实际情况:

         事实上,父进程以写方式打开管道文件(写端),然后再以读方式打开同一个管道文件(读端),实际上会创建两个不同的文件描述符对象,分别指向管道的写端和读端;

        在Linux环境下,两个文件描述符最终会指向同一个inode,共享相同的方法集和缓冲区;

问题来了,父进程以写的方式打开文件,又以读的方式打开文件,单向通信时,父进程需要关闭读方式打开的文件,子进程关闭以写方式打开的文件,父进程关一个,子进程关一个,这样这两个文件描述对象不就没了吗?这还怎么通信?

        这样的类似的问题我们之前也遇到过,如何解决呢?--引用计数的方法所以在struct file中有一个类似于引用计数的概念;一个进程关闭一个文件时,引用计数就减一;当一个文件描述对象它的引用计数为0时,它就会自动把文件关掉;

        fork创建子进程时,子进程可以读取到父进程的数据,那为什么还要大费周章的这样传输数据?

        子进程确实可以通过fork进程父进程的数据,但那也只是在创建时读取到,它无法读取到变化的数据如果任意一方写入修改数据,就会发生写时拷贝;

匿名管道的特性

管道的五种特性:

  1. 匿名管道,可以允许具有血缘关系的的进程之间进行进程间通信,常用于父与子;
  2. 匿名管道默认给读写端提供同步机制;
  3. 面向字节流;
  4. 管道的生命周期是随进程的;
  5. 管道是单向通信的,半双工通信的一种特殊情况;

 如何去理解?

比如:在以前,在shell中执行父子进程,父子进程各自执行自己的,互不干涉;而匿名管道这里,默认存在读写同步机制;

情况一:子进程写,父进程读,但子进程写的慢;会出现管道中没有数据,此时读端必须等待,直到有数据为止;

情况二:子进程写的快,父进程读的慢;子进程也会等待父进程,子进程会写一部分,然后等父进程读取,读取之后再写;
怎么写和怎么读之间没有什么关系,并不是写一条,读一条;子进程写的快,父进程读的慢,父进程可能一次就读子进程写的几十次或者上百次的数据都读出来,“可以一次只读一个字节,也可以定义一个缓冲区一次就把缓冲区全打满;

 管道的4种情况
  1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止
  2. 正常情况,如果管道被写满了(管道大小约为64kB),写端必须等待,直到有空间为止
  3. 写端关闭,读端一直读取,当read返回值为0,表示读到文件结尾
  4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程;
 联系shell中的管道

shell中经常使用的管道 ” | “;

比如:

sleep 1000 | sleep 2000 | sleep 3000

通过上述的测试命令,可以看到sleep之间是兄弟进程,那么它们的原理是什么? 

上述测试命令:3个兄弟进程,使用两个管道;父进程先创建管道,因此两个管道对于这个三个进程都是可见的,然后再使用fork创建子进程;

 对于进程1,关闭其他的文件,只保留pipefda[1](写);

进程2,只保留对pipefda[0](读)以及对pipefdb[1](写);

进程3,只保留对pipefdb[0](读);

其次就是重定向,在使用管道时,原本要输出在显示器的数据没有显示,而是作为输入,传输给了第二个进程,把进程1的 stdout 重定向为 pipefda[1];(dup2(pipefda[1],1]))

让进程2的输入从管道中读取,所以我们也需要对进程2进行重定向,后续的读写也是需要进行重定向,以此类推;

2.3. 命名管道

上述介绍的匿名管道通信,是用于亲缘关系之间的进程进行通信,如果没有任何关联的进程如何通信呢? --命名管道,都是在内存中作用;

命名管道简单示例:

两个终端实现通信:

终端一:

 终端二:

命名管道可以从命令行上创建

命令:mkfifo filename 

命名管道是一种特殊类型的文件,它在文件系统中有对应的文件节点,但实际上并不占用磁盘空间存储数据;

两进程之间要想实现通信,那么就需要看到同一份资源;

命名管道(Named Pipe)在文件系统中有对应的文件节点,因此它是存在磁盘中的;

路径是唯一的,所以可以使用路径+文件名访问,让不同的进程来访问相同的资源;

        当进程访问命名管道时,操作系统会为该进程创建文件描述符,并为管道在内存中创建缓冲区。数据通过管道在进程间传输时,会经过内存缓冲区进行交换,而不会把数据加载到磁盘;

 通信原理:

 由此我们也可以发现命名管道的文件节点存放在磁盘中的意义:为了让没有血缘关系的进程能够找到命名管道,进而使得两进程可以访问同一个管道;

 代码级建立命名管道

 接口:

int mkfifo(const char *filename,mode_t mode);

 

 创建成功返回0,失败返回-1;

有了这些可以模拟的写一个使用命名管道通信的服务端和客户端:

服务端:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include "comm.h"
// 服务端
//using namespace std;#define FILENAME "fifo"int Makefifo()
{int n = mkfifo(FILENAME, 0666);if (n < 0){std::cerr << "error: " << errno << " errorstring: " << strerror(errno) << std::endl;return false;}std::cout << "mkfifo success... " << std::endl;return true;
}
int main()
{
Start:// 打开管道文件int rfd = open(FILENAME, O_RDONLY);if (rfd < 0){std::cerr << "error: " << errno << " errorstring: " << strerror(errno) << std::endl;if (Makefifo()) goto Start;else return 1;//return 2;}std::cout << "open fifo success...read " << std::endl;char buffer[1024];while (true){//读取管道数据ssize_t s = read(rfd, buffer, sizeof(buffer) - 1); // 预留一个空间给\0if (s > 0){buffer[s] = 0; //把最后设置为\0表示字符串的结尾std::cout << "Client say: " << buffer << std::endl;}else if (s == 0){std::cout << "Client quit,server quit too " << std::endl;break;}}// 关闭文件close(rfd);std::cout << "close fifo success... " << std::endl;return 0;
}

 这里需要特别注意,创建命名管道时,最好先以读方式打开;

先让读端创建好,此时读端会阻塞等待;直到写端打开管道为止;

这种行为保证了在读端尝试读取数据之前,有写端可以向管道写入数据。读端会等待写端打开,以确保有效的通信;

如果是写端先创建好,就可能出现这种情况:

写端先创建好,向管道写数据,但此时读端还未创建好,就会导致程序被OS杀死;

客户端:

#include <iostream>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstring>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>
#include "comm.h"
// 客户端int main()
{int wfd = open(FILENAME, O_WRONLY);if (wfd < 0){std::cerr << "error: " << errno << " errorstring: " << strerror(errno) << std::endl;return 1;}std::cout << "open fifo success...write " << std::endl;std::string msg;while (true){std::cout << "Please Enter#";std::getline(std::cin, msg);//读取客户端一行的输入信息ssize_t s = write(wfd, msg.c_str(), msg.size());//把数据写入到管道中if (s < 0){std::cerr << "error: " << errno << " errorstring: " << strerror(errno) << std::endl;break;}}close(wfd);std::cout << "close fifo success... " << std::endl;return 0;
}

 2.4. 小结

命名管道与匿名管道的区别:

  •  匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完 成之后,它们具有相同的语义; 
  • 命名管道用于没有任何关系的进程之间进行通信;
  • 匿名管道用于有亲缘关系的进程之间进行通信
  • 命名管道的文件节点会存储在磁盘中,但管道数据并不会存在磁盘中;

管道的四种情况:

  1. 正常情况,如果管道没有数据了,读端必须等待,直到有数据为止
  2. 正常情况,如果管道被写满了(管道大小约为64kB),写端必须等待,直到有空间为止
  3. 写端关闭,读端一直读取,当read返回值为0,表示读到文件结尾
  4. 读端关闭,写端一直写入,OS会直接杀掉写端进程,通过想目标进程发送SIGPIPE(13)信号,终止目标进程;

总结

        以上便是本文的全部内容,希望对你有所帮助或启发,后续也将会继续介绍其他进程间通信的方式,感谢阅读!

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

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

相关文章

leecode494.目标和

这道题目第一眼感觉就不像是动态规划&#xff0c;可以看出来是回溯问题&#xff0c;但是暴力回溯超时&#xff0c;想要用动态规划得进行一点数学转换 class Solution { public:int findTargetSumWays(vector<int>& nums, int target) {int nnums.size(),bagWeight0,s…

会话守护进程

会话&&守护进程 文章目录 会话&&守护进程1.会话1.概念和特性2.创建会话3.getsid和setsid函数getsid函数setsid 函数 4.代码 2.守护进程3.创建守护进程模型守护进程创建步骤&#xff1a;两个函数 完整代码&#xff1a; 1.会话 1.概念和特性 进程组&#xff0c…

学习反射(反射的使用,反射的应用场景)

目录 反射的使用 总的测试代码如下 反射的应用场景 反射的使用 大家先看一个案例 有一个person 类 属性有 String 类型的 name ,int age &#xff0c;还有一个 方法 a。 package fs;public class Person {private String name;private int age;public void a(){System.out.p…

在ESP32使用AT指令集与服务器进行TCP/IP通信时,<link ID> 解释

在ESP32使用AT指令集与服务器进行TCP/IP通信时&#xff0c;<link ID> 是一个非常重要的参数。它用于标识不同的连接实例&#xff0c;特别是在多连接场景下&#xff08;如同时建立多个TCP或UDP连接&#xff09;。每个连接都有唯一的<link ID>&#xff0c;通过这个ID…

Ansible 批量管理华为 CE 交换机

注&#xff1a;本文为 “Ansible 管理华为 CE 交换机” 相关文章合辑。 使用 CloudEngine - Ansible 批量管理华为 CE 交换机 wsf535 IP 属地&#xff1a;贵州 2018.02.05 15:26:05 总体介绍 Ansible 是一个开源的自动化运维工具&#xff0c;AnsibleWorks 成立于 2012 年&a…

【python虚拟环境安装】linux centos 下的python虚拟环境配置

linux centos 下的python虚拟环境配置 在 CentOS 环境中处理 pip 安装警告的方法1. 创建并使用虚拟环境2. 忽略警告并继续使用 root 用户安装&#xff08;不推荐&#xff09;报错问题处理 在 CentOS 环境中处理 pip 安装警告的方法 当在 CentOS 环境中遇到 pip 安装警告时&…

【Datawhale AI 冬令营】如何动手微调出自己的大模型

目录 总体思路实操案例数据集构造收集数据数据构造 模型微调选择模型选择数据集参数配置开始训练 模型使用 总体思路 微调大模型主要以开源的通用大模型为基础&#xff0c;喂给模型自己准备的数据&#xff0c;将通用的大模型往自己想要的方向引导&#xff0c;变成更偏向某一领…

Python编程常用的19个经典案例

Python 的简洁和强大使其成为许多开发者的首选语言。本文将介绍36个常用的Python经典代码案例。这些示例覆盖了基础语法、常见任务、以及一些高级功能。 1. 列表推导式 fizz_buzz_list ["FizzBuzz" if i % 15 0 else "Fizz" if i % 3 0 else "Buzz…

关于数据流图绘制和使用上的一些个人经验

假设我们需要开发一个项目进度管理系统&#xff0c;在这个项目进度管理系统之中&#xff0c;我们需要开发一个功能&#xff1a;项目成员的列表。我们具有这样的业务需求&#xff1a; 在项目进度管理系统中&#xff0c;我们需要知道参与项目的人员到底有哪些&#xff0c;并且项目…

手眼标定工具操作文档

1.手眼标定原理介绍 术语介绍 手眼标定&#xff1a;为了获取相机与机器人坐标系之间得位姿转换关系&#xff0c;需要对相机和机器人坐标系进行标定&#xff0c;该标定过程成为手眼标定&#xff0c;用于存储这一组转换关系的文件称为手眼标定文件。 ETH&#xff1a;即Eye To …

AlipayHK支付宝HK接入-商户收款(PHP)

一打开支付宝国际版 二、点开商户服务 三、下载源码

基于Arduino的平衡车机械臂

两轮驱动机器人车与机械臂的DIY指南 视频&#xff1a; 基于Arduino的平衡车机械臂 资料下载链接 引言 在这篇文章中&#xff0c;我们将一起探索如何构建一个两轮驱动的机器人车&#xff0c;并配备有一个机器人臂&#xff0c;这个项目适合初学者&#xff0c;并且可以在动态环…

【练习Day20】字符串变形

链接&#xff1a;字符串变形_牛客题霸_牛客网 方法一&#xff1a;双逆转&#xff08;推荐使用&#xff09; 思路&#xff1a; 将单词位置的反转&#xff0c;那肯定前后都是逆序&#xff0c;不如我们先将整个字符串反转&#xff0c;这样是不是单词的位置也就随之反转了。但是单…

ip地址和网络号关系是什么

在浩瀚的网络世界中&#xff0c;每一个连接互联网的设备都需要一个独特的标识来确保数据的准确传输。这个标识就是IP地址。然而&#xff0c;在深入探索IP地址的同时&#xff0c;我们不得不提及一个与之紧密相关的概念——网络号。网络号与IP地址之间存在着怎样的联系与区别&…

android 登录界面编写

1、登录页面实现内容 1.实现使用两个EditText输入框输入用户名和密码。 2.使用CheckBox控件记住密码功能。 3.登录时候&#xff0c;验证用户名和密码是否为空。 4.当前CheckBox控件记住密码勾上时&#xff0c;使用SharedPreferences存储用户名和密码。 5.登录时候使用Prog…

run postinstall error, please remove node_modules before retry!

下载 node_modules 报错&#xff1a;run postinstall error, please remove node_modules before retry! 原因&#xff1a;node 版本出现错误&#xff0c;我的项目之前是在 12 下运行的。解决方法&#xff1a; 先卸载node_modules清除缓存将node版本切换到12重新下载即可

Docker 安装 禅道-21.2版本-外部数据库模式

Docker 安装系列 1、拉取最新版本&#xff08;zentao 21.2&#xff09; [rootTseng ~]# docker pull hub.zentao.net/app/zentao Using default tag: latest latest: Pulling from app/zentao 55ab1b300d4b: Pull complete 6b5749e5ef1d: Pull complete bdccb03403c1: Pul…

visual studio 2022 c++使用教程

介绍 c开发windows一般都是visual studio&#xff0c;linux一般是vscode&#xff0c;但vscode调试c不方便&#xff0c;所以很多情况都是2套代码&#xff0c;在windows上用vs开发方便&#xff0c;在转到linux。 安装 1、官网下载vs2022企业版–选择桌面开发–安装位置–安装–…

python学opencv|读取图像(十七)认识alpha通道

【1】引言 前序学习进程中&#xff0c;我们已经掌握了RGB和HSV图像的通道拆分和合并&#xff0c;获得了很多意想不到的效果&#xff0c;相关链接包括且不限于&#xff1a; python学opencv|读取图像&#xff08;十二&#xff09;BGR图像转HSV图像-CSDN博客 python学opencv|读…

设计模式--单例模式【创建型模式】

设计模式的分类 我们都知道有 23 种设计模式&#xff0c;这 23 种设计模式可分为如下三类&#xff1a; 创建型模式&#xff08;5 种&#xff09;&#xff1a;单例模式、工厂方法模式、抽象工厂模式、建造者模式、原型模式。结构型模式&#xff08;7 种&#xff09;&#xff1…