【Linux系统编程】第三十三弹---深入探索进程间通信:原理、方式、及管道技术详解

news/2024/10/20 16:26:35/

个人主页: 熬夜学编程的小林

💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】

目录

1、进程为什么要通信

2、进程如何通信

3、进程间常见的通信方式

4、管道

4.1、什么是管道

4.2、匿名管道 

4.2.1、定义

4.2.2、特点

4.2.3、创建与使用

4.2.4、测试管道接口


1、进程为什么要通信

进程也是需要某种协同的,然而协同的前提条件是通信(信息的传递与交换过程),因此进程需要通信。

事实:进程具有独立性,进程 = 内核数据结构 + 数据和代码,独立即内核数据结构独立 和 数据和代码独立。

进程间通信目的

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

2、进程如何通信

进程间通信,成本可能会稍微高一些,因此进程的独立性。

进程间通信的前提:先让不同的进程看到同一份(操作系统)资源("一段内存")。

1、一定是某一个进程先需要通信,然后让OS创建一块共享资源

2、OS必须提供很多的系统调用。

  • OS创建的共享资源不同,系统调用接口也不同 ---- 进程间通信的种类也会有不同的种类。

3、进程间常见的通信方式

管道(直接复用内核代码通信)

  • 匿名管道pipe
  • 命名管道

System V IPC

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

POSIX IPC

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

4、管道

4.1、什么是管道

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

4.2、匿名管道 

4.2.1、定义

匿名管道本质上是一个内存级的文件,用于在进程之间(特别是父子进程之间)传输数据。与命名管道(Named Pipe,也称为FIFO)不同,匿名管道没有文件名,且在文件系统中没有对应的节点。它通常仅存在于内存中,随进程的结束而消失。

4.2.2、特点

  1. 单向通信:匿名管道是单向的,数据只能从一个方向流动。如果需要双向通信,需要创建两个匿名管道。
  2. 半双工模式:在任何给定时间内,一个管道只能用于读或写操作,但不能同时进行。
  3. 自动创建与销毁:当调用pipe()系统调用创建管道时,系统会自动为其分配两个文件描述符,分别用于读和写。管道的生命周期与进程相关,随着进程的结束而销毁。
  4. 有限容量:匿名管道通常有一个有限的缓冲区容量,如果缓冲区满,写操作将会阻塞,直到有数据被读取并腾出空间。
  5. 无名性:与命名管道不同,匿名管道没有文件名,无法在文件系统中直接访问。

4.2.3、创建与使用

匿名管道通过调用pipe()系统调用来创建。该函数会返回一个包含两个文件描述符的数组,其中数组的第一个元素是读端文件描述符第二个元素是写端文件描述符。然后,可以通过fork()系统调用创建子进程,并通过这两个文件描述符在父子进程间进行数据传输。

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

测试创建管道

会用到的头文件和常量

#include <iostream>
#include <cerrno>  // <==> errno.h
#include <cstring> // <==> string.h
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string>const int size = 1024;

主函数

// 测试创建管道
int main()
{// close(0);// 关闭文件描述符// 1、创建管道int pipefd[2];int n = pipe(pipefd); // 输出型参数 rfd wfd// 返回0表示成功,不等于0则失败且更新错误码if(n != 0){std::cerr << "errno: "<< errno << "errstring: " << strerror(errno) << std::endl;return 1;}// pipefd[0] 读(0 -> 嘴巴 -> r) pipefd[1] 写(1 -> 笔 -> w)// 打印文件描述符std::cout << "pipefd[0]: " << pipefd[0] << ",pipefd[1]: " << pipefd[1] << std::endl;return 0;
}

执行结果

 正确打印出文件描述符,表示成功创建管道。

关闭0号文件描述符,继续测试是否满足文件描述符规则!

 同样符合规则,先占用0号文件描述符,且成功创建管道。

进行通信测试

获取信息函数

std::string GetOtherMessage()
{static int cnt = 0;// 静态变量,全局的,在函数能使用std::string messageid = std::to_string(cnt); // stoi string -> intcnt++;pid_t id = getpid();std::string stringid = std::to_string(id);std::string message = "messageid: ";message += messageid;message += "stringid: ";message += stringid;return message;
}

子进程写入信息

// 子进程进行写入
void SubProcessWrite(int wfd)
{int pipesize = 0;std::string message = "father,I am your son process!";char c = 'A';while(true){std::string info = message + GetOtherMessage(); // 这条消息,就是我们子进程发给父进程的消息write(wfd,info.c_str(),info.size()); // 写入管道的时候,没有写入\0, 有没有必要?没有必要sleep(1); // 子进程写慢一点}
}

父进程读取信息

// 父进程进行读取
void FatherProcessRead(int rfd)
{char buffer[size];while(true){ssize_t n = read(rfd,buffer,sizeof(buffer) - 1); // strlen()// 返回值大于0表示成功读取if(n > 0){buffer[n - 1] = 0;// \0std::cout << buffer << std::endl;}// 返回值是0,表示写端直接关闭了,我们读到了文件的结尾else if(n == 0){std::cout << "client quit, father get return val: " << n << " father quit too!" << std::endl;break;}// 返回值小于0表示读取错误else if(n < 0){std::cerr << "read error" << std::endl;break;}}
}

主函数

int main()
{// close(0); // 关闭文件描述符// 1、创建管道int pipefd[2];int n = pipe(pipefd); // 输出型参数 rfd wfd// 返回0表示成功,不等于0则失败且更新错误码if(n != 0){std::cerr << "errno: "<< errno << "errstring: " << strerror(errno) << std::endl;return 1;}// pipefd[0] 读(0 -> 嘴巴 -> r) pipefd[1] 写(1 -> 笔 -> w)std::cout << "pipefd[0]: " << pipefd[0] << ",pipefd[1]: " << pipefd[1] << std::endl;// 2、创建子进程pid_t id = fork();if(id == 0){std::cout << "子进程关闭不需要的fd了,准备发消息了" << std::endl;// 子进程  --write// 3、关闭不需要的文件描述符 readclose(pipefd[0]);SubProcessWrite(pipefd[1]);close(pipefd[1]);exit(0);}sleep(1);// 父进程// 3、关闭不需要的文件描述符 writestd::cout << "父进程关闭不需要的fd了,准备收消息了" << std::endl;close(pipefd[1]);FatherProcessRead(pipefd[0]);std::cout << "5s,father close rfd" << std::endl;sleep(5);close(pipefd[0]);// 回收子进程int status = 0;pid_t rid = waitpid(id,&status,0);// 返回值大于0回收成功if(rid > 0){std::cout << "wait child process done, exit sig: " << (status & 0x7f) << std::endl;std::cout << "wait child process done, exit code(ign): " << ((status >> 8) & 0xFF) << std::endl;}return 0;
}

用fork来共享管道原理 

运行结果

原理

 

父进程竟然要关闭不需要的fd,为什么开始需要打开呢?可以不关闭?

必须打开,因为需要让子进程继承下去。可以不关闭,但是建议关闭,防止误写。

为什么父子进程会向同一个终端显示器打印数据呢?

由于父子进程共享了相同的输出环境(通常是终端或显示器)。

进程默认会打开0,1,2号文件描述符,怎么做到的呢?

该进程时bash的子进程,bash打开了,所有子进程也默认打开了,我们只需要做好约定即可。

close();为什么我们子进程主动close(0,1,2),不影响父进程继续使用显示器文件呢? 

因为子进程close(0,1,2)不是直接就关闭文件描述符,而是存在一个引用计数,当有进程指向该文件描述符时就++,关闭文件描述符先对引用计数--,如果引用计数等于0则关闭文件描述符。

4.2.4、测试管道接口

代码验证:

管道的四种情况:

1、如果管道内部是空的 && write fd 没有关闭,读取条件不具备,读进程会被阻塞 -- 即 wait,等待读取条件具备,即写入数据条件具备。

2、管道被写满 && read fd 不读且没有关闭, 写进程会被阻塞,即写条件不具备,需要等待读取数据。

3、管道一直在读 && 写端关闭了wfd,读端返回值会读到0,表示读到文件结尾。 

演示一

演示二

 

4、rfd直接关闭,写段wfd一直写入?写段操作会被操作系统直接使用13号信号关掉。相当于进程出现了异常。

管道的五种特征:

1、匿名管道:只用来进行具有血缘关系的进程之间进行通信,通常用于父子之间进行通信。因为子进程能看到父进程的数据。

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

可能会出现管道被多个进程读取的情况,那么数据就可能出现不一致问题,因此管道内部自带同步机制。

3、管道文件的生命周期是随进程

4、管道文件在通信的时候,是面向字节流的。写的次数和读取的次数不是一一匹配。

代码

运行结果

 

5、管道的通信模式,是一种特殊的半双工模式(在任何给定时间内,一个管道只能用于读或写操作,但不能同时进行)。 

全双工模式:在任何给定时间内,一个管道能用于读和写操作,能同时进行。

补充:


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

相关文章

两个数列问题

# 问题描述 给定长度分别为 n 和 m 的两个数列a[n]、b[m]&#xff0c;和一个整数k。求|(a[i] - b[j])^2 - k^2|的最小值。 ## 输入格式 第一行有 2 个整数 n、m、k&#xff0c;分别表示数列 a、b 的长度&#xff0c;以及公式中的整数 k。 第二行有 n 个整数&#xff0c;表示…

【Flutter】Dart:库

在 Dart 中&#xff0c;库&#xff08;Library&#xff09;是组织和重用代码的基本方式。通过库&#xff0c;我们可以将代码分割成模块化的部分&#xff0c;方便管理和共享&#xff0c;同时避免命名冲突。Dart 提供了大量内置库&#xff0c;用于支持常见的功能&#xff0c;比如…

pandas-使用技巧

pandas-使用技巧 简单技巧 仅个人笔记使用&#xff0c;感谢点赞关注 简单技巧 pd.to_dict()&#xff1a;Dataframe格式数据转字典数据pd.dropna()&#xff1a;去nan值& | ~&#xff1a;pd逻辑运算符pd.isnan()&#xff1a;判断是否为nan值pd.concat&#xff1a;多个pd拼接…

CR6609 刷机

如果要刷openwrt&#xff0c;必须用pb-boot&#xff0c;如果用breed&#xff0c;会进不了系统。 刷了breed的话&#xff0c;可以在breed改刷pb-boot&#xff0c;在bootloader那里选择pb-boot的固件上传更新即可 而用pb-boot的话&#xff0c;也可以很方便的刷回breed&#xff…

深入解析JavaScript中的箭头函数及其在React中的应用(箭头函数与传统函数的区别、如何在不同上下文中使用箭头函数)

文章目录 1. 引言2. 箭头函数的定义与用法3. 箭头函数与传统函数的区别3.1 this绑定3.2 不能作为构造函数3.3 arguments对象 4. 如何在不同上下文中使用箭头函数4.1 在类方法中使用箭头函数4.2 在回调函数中使用箭头函数 5. 深入探讨箭头函数在React中的应用5.1 在事件处理器中…

网站cms系统 开源cms建站系统

在数字化时代&#xff0c;企业对于快速、灵活且成本效益高的网站构建方案的需求日益增长。开源CMS&#xff08;内容管理系统&#xff09;建站系统因其灵活性、可定制性和强大的社区支持而成为众多企业和开发者的首选。本文将探讨开源CMS系统的优势、功能、应用案例以及如何选择…

3D 数字人与 2D 数字人的区别

3D 数字人是通过计算机图形技术、建模技术等手段创建出的具有三维立体形态的虚拟人物形象。其创建过程较为复杂&#xff0c;通常包括以下步骤&#xff1a; **1、建模&#xff1a;**利用专业的三维建模软件&#xff0c;根据设定的人物形象、特征等进行精细的建模&#xff0c;构建…

数据结构编程实践20讲(Python版)—15完全图

本文目录 15 完全图(Complete Graph)S1 说明特点可以解决的问题S2 示例S3 问题1:旅行商问题(TSP)S4 问题2:任务分配问题往期链接 01 数组02 链表03 栈04 队列05 二叉树06 二叉搜索树07 AVL树08 红黑树09 B树10 B+树11 线段树12 树状数组13 图形数据结构14 邻接矩阵15 完全…