匿名管道及其应用

目录

一、什么是匿名管道?

三、创建与使用匿名管道

三、匿名管道的特点

匿名管道的四种情况

匿名管道的五种特性

四、匿名管道的实践应用---进程池


在编程的世界中,匿名管道是一种非常重要的通信机制。今天,让我们一起来深入探讨一下匿名管道的奥秘。

一、什么是匿名管道?

匿名管道是一种在具有亲缘关系的进程间进行单向通信的方式。它主要用于父子进程之间的数据传递。 Linux指令中的 | 就是在使用匿名管道:

用于查找当前系统中所有包含字符串 vim 的进程

  • ps ajx:使用 ps 命令获取系统中的进程信息。
  • |:竖线 | 是管道符号,将前一个命令的输出作为输入传递给下一个命令
  • grep vim:使用 grep 命令在前面获取的进程信息中搜索包含字符串 vim 的行。

可以发现,管道是操作系统提供的资源,让ps ajx这个进程的输出重定向到这个管道资源,然后由另一个进程grep vim 来读取这个管道的内容作为输入,以上就是一个简单的进程间使用匿名管道通信的过程。


三、创建与使用匿名管道

在代码中,可以通过特定的系统调用来创建匿名管道。一旦创建成功,父进程和子进程就可以通过相应的读写操作来进行通信。

1、匿名管道的创建,需要通过下面这个系统调用:

//返回值:成功返回0,失败返回-1
int pipe(int fd[2]) //参数fd是输出型参数,返回两个fd

这里表示创建一个匿名管道,并返回了两个文件描述符,一个是管道的读取端描述符 fd[0],另一个是管道的写入端描述符 fd[1]

注意,这个匿名管道是特殊的文件,只存在于内存,不存于文件系统中。

到此为止,也只是一个进程通过系统调用pipe创建了管道,如何实现通信呢?

我们可以使用 fork 创建子进程,创建的子进程会复制父进程的文件描述符,这样就做到了两个进程各有两个「 fd[0] 与 fd[1]」,两个进程就可以通过各自的 fd 写入和读取同一个管道文件实现跨进程通信了。

管道只能一端写入,另一端读出,上面这种模式容易造成混乱,所以创建子进程后,我们需要让管道只能单向通信,父子进程根据实际情况各自切断一个读写fd。

  • 父进程关闭读取的 fd[0],只保留写入的 fd[1];
  • 子进程关闭写入的 fd[1],只保留读取的 fd[0];

最终实现父进程持有写入fd,子进程持有读取fd:

单向信道建立完成后,两个进程分别通过write、read的系统调用来向管道读写,从而实现了进程的通信;

匿名管道的通信的单向的(半双工),所以如果需要父子进程互相通信,我们就要再创建一个管道

以上是父子进程的通信的例子,那如果是向上面指令 ps ajx | grep vim ,通过匿名管道实现通信的原理细节是怎样的呢?

在 shell 里面执行 A | B命令的时候,A 进程和 B 进程都是 shell 创建出来的子进程,A 和 B 之间不存在父子关系,它俩的父进程都是 shell;继承了shell的文件描述符,子进程A、B再通过各自关闭自己的一个文件fd,就可以实现A、B进程的单向通信了!

所以,匿名管道可以实现的具有亲缘关系的进程之间的通信(父子、兄弟、爷孙…)

三、匿名管道的特点

对于匿名管道,我们可以总结出四种情况、五种特性

  • 匿名管道的四种情况

1、正常情况下,如果管道没有数据了,读端会阻塞等待,直到写端写入数据

2、正常情况下,如果管道被写满,写端会阻塞等待,直到读端读取数据

管道是一种临界资源,同一时刻只允许一个进程读取或写入;

管道的数据被读取后,就会标记为失效,允许数据写入时覆盖;

3、写端关闭,读端会一直读取,直到读完管道内的数据,读端read会返回0,表示读到文件结尾

4、读端关闭,此时写端再向这个管道写入已经没有意义且浪费系统资源,OS会向写端进程发送SIGPIPE(13)信号,终止写端进程

  • 匿名管道的五种特性

1、匿名管道仅限于具有血缘关系的进程间通信,常用于父子、兄弟

2、匿名管道默认给读写端提供同步机制,确保读写操作的正确性和顺序性

同步: 两个或两个以上的进程在运行过程中协同步调,按预定的先后次序运行。比如,A任务的运行依赖于B任务产生的数据。

3、匿名管道是面向字节流的

在匿名管道中,数据的传输是连续的,没有明确的边界或结构。发送方可以逐个字节地向管道中写入数据,接收方可以逐个字节地从管道中读取数据,它不需要对数据进行额外的格式化或解析,发送方和接收方只需要关注字节的顺序和数量。

4、匿名管道的生命周期是随进程的

管道本质上是通过文件进行通信的,也就是说管道依赖于文件系统,那么当所有打开该文件的进程都退出后,该文件也就会被释放掉,所以说管道的生命周期随进程。

5、管道是单向通信的,半双工通信的一种特殊情况

半双工通信指数据可以双向交替传输,但不能同时双向传输。而管道这种严格的单向通信可以看作是半双工通信的一种更为特殊和受限的情况。


匿名管道的优势

  • 简单易用:提供了一种直接的通信方式。
  • 高效:对于少量、频繁的数据交换非常有效。

应用场景

  • 进程间简单的命令传递和结果反馈。
  • 一些需要快速交互的小型任务协调。

四、匿名管道的实践应用---进程池

接下来通过一个进程池demo,对匿名管道实践应用

首先,什么是进程池呢?

进程池是一种用于管理多个进程资源的机制。

具体来说,进程池预先创建一定数量的进程并保持它们处于待命状态。当有任务需要执行时,直接从进程池中选取一个空闲的进程来处理该任务,而不是每次需要执行任务时都临时创建新的进程。

进程池具有以下一些优点:

  • 提高效率:避免了频繁创建和销毁进程的开销,从而提升系统整体性能。
  • 资源管理:能够更好地控制和管理系统中的进程资源,确保资源的合理分配。
  • 并发处理能力:可以同时处理多个任务,提高系统的并发处理水平。

进程池常用于服务器等需要处理大量并发任务的场景,通过合理配置进程池的大小和管理策略,可以有效地应对高并发的业务需求。


父进程批量创建匿名管道和子进程,父进程设为写端,子进程设为读端,当父进程有任务需要交给子进程时,就选取一个管道写入控制指令,对应子进程读取数据后,根据指令执行特定的任务;我们要考虑子进程完成任务的负载均衡,可以较为平均的把任务交给子进程

以下是代码:

processpool.cc:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"const int num = 5;
static int number = 1;//表示通信信道,包含控制文件描述符、进程 ID 和名称
class channel
{
public:channel(int fd, pid_t id) : ctrlfd(fd), workerid(id){name = "channel-" + std::to_string(number++);}public:int ctrlfd;pid_t workerid;std::string name;
};void Work()
{while (true){int code = 0;ssize_t n = read(0, &code, sizeof(code));if (n == sizeof(code)){if (!init.CheckSafe(code))continue;init.RunTask(code);}else if (n == 0){break;}else{// do nothing}}std::cout << "child quit" << std::endl;
}void PrintFd(const std::vector<int> &fds)
{std::cout << getpid() << " close fds: ";for(auto fd : fds){std::cout << fd << " ";}std::cout << std::endl;
}// 传参形式:
// 1. 输入参数:const &
// 2. 输出参数:*
// 3. 输入输出参数:&
void CreateChannels(std::vector<channel> *c)
{// bugstd::vector<int> old; //记录上一轮创建的管道的写端文件描述符for (int i = 0; i < num; i++){// 1. 定义并创建管道int pipefd[2];int n = pipe(pipefd);assert(n == 0);(void)n;// 2. 创建进程pid_t id = fork();assert(id != -1);// 3. 构建单向通信信道if (id == 0) // child{if(!old.empty())//子进程需要关闭从父进程继承到的之前轮次的写端fd{for(auto fd : old){close(fd);}PrintFd(old);}close(pipefd[1]);dup2(pipefd[0], 0); //将子进程的读端重定向到0,Work就不用传参pipe[0]Work();exit(0); // 会自动关闭自己打开的所有的fd}// fatherclose(pipefd[0]);c->push_back(channel(pipefd[1], id));old.push_back(pipefd[1]);// childid, pipefd[1]}
}void PrintDebug(const std::vector<channel> &c)
{for (const auto &channel : c){std::cout << channel.name << ", " << channel.ctrlfd << ", " << channel.workerid << std::endl;}
}void SendCommand(const std::vector<channel> &c, bool flag, int num = -1)
{int pos = 0;while (true){// 1. 选择任务int command = init.SelectTask();// 2. 选择信道(进程)const auto &channel = c[pos++];pos %= c.size();// debugstd::cout << "send command " << init.ToDesc(command) << "[" << command << "]"<< " in "<< channel.name << " worker is : " << channel.workerid << std::endl;// 3. 发送任务write(channel.ctrlfd, &command, sizeof(command));// 4. 判断是否要退出if (!flag){num--;if (num <= 0)break;}sleep(1);}std::cout << "SendCommand done..." << std::endl;
}
void ReleaseChannels(std::vector<channel> c)
{// version 2// int num = c.size() - 1;// for (; num >= 0; num--)// {//     close(c[num].ctrlfd);//     waitpid(c[num].workerid, nullptr, 0);// }// version 1for (const auto &channel : c){close(channel.ctrlfd);waitpid(channel.workerid, nullptr, 0);}// for (const auto &channel : c)// {//     pid_t rid = waitpid(channel.workerid, nullptr, 0);//     if (rid == channel.workerid)//     {//         std::cout << "wait child: " << channel.workerid << " success" << std::endl;//     }// }
}
int main()
{std::vector<channel> channels;// 1. 创建信道,创建进程CreateChannels(&channels);// 2. 开始发送任务const bool g_always_loop = true;// SendCommand(channels, g_always_loop);SendCommand(channels, !g_always_loop, 10);// 3. 回收资源,想让子进程退出,并且释放管道,只要关闭写端ReleaseChannels(channels);return 0;
}
  • 定义了一个 channel 类来表示通信信道,包含控制文件描述符、进程 ID 和名称。
  • Work 函数用于子进程不断从标准输入读取指令并进行处理。
  • CreateChannels 函数创建一定数量的管道和相应的子进程,并构建单向通信信道,同时记录相关信息到 channel 对象并添加到vector<channel>中。
  • PrintDebug 函数用于打印信道的相关信息。
  • SendCommand 函数根据条件选择任务和信道,向信道发送任务命令。
  • ReleaseChannels 函数用于释放信道资源,包括关闭文件描述符和等待子进程结束。

需要注意的是,CreateChannels 中,创建了信道和子进程后,把子进程写端fd:pipefd[0]重定向到了0,将子进程的读端重定向到标准输入(文件描述符 0)之后,在 Work 函数中就可以直接从标准输入读取数据,而不需要再专门传递管道的读端文件描述符 pipe[0] 了;


是因为每个子进程的读端都被重定向到了0,当子进程执行Work时,就直接从它们各自的文件描述符表中读取0即可,因为进程池中的每个子进程原本的读端fd是不同的;子进程执行work,调用read时就需要不同的文件描述符。


还有一个需要注意的点: CreateChannels中old的作用

在创建新的进程和管道时,old用于记录上一轮创建的管道的写端文件描述符。当子进程创建后,在子进程中需要关闭之前轮次创建的这些管道写端,保证每个管道文件都只有一个写端指向和一个读端指向,以确保资源的正确管理和避免干扰。

Task.hpp:

#pragma once#include <iostream>
#include <functional>
#include <vector>
#include <ctime>
#include <unistd.h>// using task_t = std::function<void()>;
typedef std::function<void()> task_t;void Download()
{std::cout << "我是一个下载任务"<< " 处理者: " << getpid() << std::endl;
}void PrintLog()
{std::cout << "我是一个打印日志的任务"<< " 处理者: " << getpid() << std::endl;
}void PushVideoStream()
{std::cout << "这是一个推送视频流的任务"<< " 处理者: " << getpid() << std::endl;
}// void ProcessExit()
// {
//     exit(0);
// }class Init
{
public:// 任务码const static int g_download_code = 0;const static int g_printlog_code = 1;const static int g_push_videostream_code = 2;// 任务集合std::vector<task_t> tasks;public:Init(){tasks.push_back(Download);tasks.push_back(PrintLog);tasks.push_back(PushVideoStream);srand(time(nullptr) ^ getpid());}bool CheckSafe(int code){if (code >= 0 && code < tasks.size())return true;elsereturn false;}void RunTask(int code){return tasks[code]();}int SelectTask(){return rand() % tasks.size();}std::string ToDesc(int code){switch (code){case g_download_code:return "Download";case g_printlog_code:return "PrintLog";case g_push_videostream_code:return "PushVideoStream";default:return "Unknow";}}
};Init init; // 定义对象
  • 定义了任务类型 task_t 为 std::function<void()>,方便表示各种无参数无返回值的任务函数。
  • 定义了一些具体的任务函数,如 DownloadPrintLogPushVideoStream 等,它们输出一些描述信息和当前进程 ID。
  • Init 类负责管理任务集合:
    • 在构造函数中初始化任务集合,并设置随机数种子。
    • CheckSafe 方法用于检查任务码是否合法。
    • RunTask 方法根据任务码执行相应任务。
    • SelectTask 方法随机选择一个任务码。
    • ToDesc 方法根据任务码返回任务描述字符串。
  • 最后定义了一个全局的 Init 对象 init

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

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

相关文章

vivado Virtex-7 配置存储器器件

Virtex-7 配置存储器器件 下表所示闪存器件支持通过 Vivado 软件对 Virtex -7 器件执行擦除、空白检查、编程和验证等配置操作。 本附录中的表格所列赛灵思系列非易失性存储器将不断保持更新 &#xff0c; 并支持通过 Vivado 软件对其中所列非易失性存储器 进行擦除、…

单链表经典算法OJ题---力扣206,876(带图详解

1.链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;【点击即可跳转】 思路&#xff1a;创建三个指针&#xff0c;看下图 注意&#xff1a;n3如果为空&#xff0c;则不能继续指向下一节点&#xff0c;需要进行判断 代码实现&#xff1a; struct ListNode* reverseLi…

第二课,python基础语法(一),认识字面量和变量、注释

一&#xff0c;字面量 &#xff08;一&#xff09;什么是字面量 被写下来的的固定的值&#xff0c;称之为字面量 &#xff08;二&#xff09;常见的三种字面量类型 &#xff08;三&#xff09;练习一下&#xff0c;使用print去输出三种不同类型的字面量&#xff1a;10&#x…

树莓派安装opencv

安装opencv 上述步骤完成后&#xff0c;输入以下代码(基于python3) sudo apt-get install python3-opencv -y不行的话&#xff0c;试试换源&#xff0c;然后 sudo apt-get update成功&#xff01; 测试opencv是否安装成功 输入 python3 然后再输入 import cv2 没有报错就…

【Java】:向上转型、向下转型和ClassCastException异常

目录 先用一个生动形象的例子来解释向上转型和向下转型 向上转型&#xff08;Upcasting&#xff09; 向下转型&#xff08;Downcasting&#xff09; 向上转型 概念 例子 发生向上转型的情况 1.子类对象赋值给父类引用 2.方法参数传递 3.返回值 向下转型 概念 注意…

扩散模型(Diffusion Model)学习笔记

目录 Diffusion Model 基本原理 预测原理 ddpm 实例 ddmp数字图片生成 有的还没看完 Diffusion Model 基本原理 扩散模型1&#xff1a;基本原理 - 知乎 前向扩散过程可以理解为一个马尔可夫链&#xff0c;即通过逐步对一张真实图片添加高斯噪声直到最终变成纯高斯噪声图片…

labview技术交流-字符串数组连接成字符串

应用场景 我们可能需要将一维的字符串数组转换成一整条字符串&#xff0c;然后方便记录在数据库或表格中的一个单元格中。 代码展示 方案一 我们使用for循环完成这样的功能需求&#xff0c;见下图&#xff1a; 这种方案可能相对基础和普通&#xff0c;但是它更方便和易于扩展…

【科研绘图 基础版】01 使用Python绘制时间序列折线图

下面这段代码绘制了一个折线图&#xff0c;其中包含了实际平均温度数据和使用线性回归模型预测的平均温度数据&#xff08;用来近似地表示数据的整体趋势&#xff09;。 具体来说&#xff0c;图中的横轴表示年份&#xff0c;纵轴表示平均温度。蓝色的实心线代表了实际的平均温度…

《Python机器学习 》书籍分享

文章目录 前言内容介绍作者简介书籍目录 前言 随着计算能力的快速增长&#xff0c;大量任务都可在台式机上完成&#xff1b;在这样的背景下&#xff0c;机器学习应运而生&#xff0c;成为当今炙手可热的话题。但初出茅庐的新手常对机器学习感到十分畏惧&#xff1b;为给这些新…

面试集中营—Seata分布式事务

一、分布式事务 本地事务 在计算机系统中&#xff0c;更多的是通过关系型数据库来控制事务&#xff0c;这是利用数据库本身的事务特性来实现的&#xff0c; 因此叫数据库事务&#xff0c;由于应用主要靠关系数据库来控制事务&#xff0c;而数据库通常和应用在同一个服务器&am…

数据结构:包装类初始泛型

目录 1.包装类1.1 基本数据类型和对应的包装类1.2 装箱和拆箱 2.什么是泛型3.引出泛型3.1 语法3.2 泛型的使用 4.泛型是如何编译的4.1 擦除机制4.2 为什么不能实例化泛型类型数组 5.泛型的上界5.1 语法5.2 示例5.3 复杂示例 6.泛型方法6.1 定义语法6.2 示例6.3 使用示例-可以类…

【CMU 15-445】Proj4 Concurrency Control

Concurrency Control 通关记录Task1 TimestampsTask2 Storage Format and Sequential ScanTask3 MVCC ExecutorsTask3.1 Insert ExecutorTask3.2 CommitTask3.3 Update and Delete ExecutorTask3.4 Stop-the-world Garbage Collection Task4 Primary Key IndexTask4.0 Index Sc…

Springboot+logback 详细配置

一、添加依赖 这里使用springboot3.0.2 依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> </dependency><dependency><groupId>org.projectlombok</grou…

macos使用yarn创建vite时出现Usage Error: The nearest package directory问题

步骤是macos上使用了yarn create vite在window上是直接可以使用了yarn但是在macos上就出现报错 我们仔细看&#xff0c;它说的If /Users/chentianyu isnt intended to be a project, remove any yarn.lock and/or package.json file there.说是要我们清除yarn.lock和package.js…

yolo world 瑞芯微芯片rknn部署、地平线芯片Horizon部署、TensorRT部署

特别说明&#xff1a;参考官方开源的 yoloworld 代码、瑞芯微官方文档、地平线的官方文档&#xff0c;如有侵权告知删&#xff0c;谢谢。 模型和完整仿真测试代码&#xff0c;放在github上参考链接 模型和代码。 yoloworld出来的有一段时间了&#xff0c;还没有盘到板端上玩一玩…

芸众商城电商专业版400+插件源码+搭建教程

介绍&#xff1a; 芸众商城社交电商系统SAAS平台前端基于vue开发&#xff0c;后端基于研发积分商城系统源码 php&#xff0c;本文安装芸众商城全插件&#xff08;400多个&#xff09;商业版平台源码&#xff0c;可同时支持多端口部署运行&#xff1b;使用宝塔面板一键部署的形…

LibreNMS简介

目录 1 LibreNMS简单介绍1.1 LibreNMS介绍 2 安装2.1 Ubuntu安装1、安装依赖2、添加 librenms 用户3、下载 LibreNMS4、设置权限5、安装 PHP 依赖项6、设置时区7、配置 MariaDB8、配置 PHP-FPM9、配置 Web 服务器10、启用 lnms 命令11、配置 snmpd12、cron13、启用调度程序14、…

贪吃蛇(c实现)

目录 游戏说明&#xff1a; 第一个是又是封面&#xff0c;第二个为提示信息&#xff0c;第三个是游戏运行界面 游戏效果展示&#xff1a; 游戏代码展示&#xff1a; snack.c test.c snack.h 控制台程序的准备&#xff1a; 控制台程序名字修改&#xff1a; 参考&#xff1a…

练习队列的相关操作:循环队列

1. 思路解析 循环队列就是在只有有限的空间时使用队列实现循环存储数据&#xff0c;有双向链表和数组两种选择&#xff0c;这里我们使用数组实现循环队列&#xff08;因为链表我不会 >-<&#xff09; 2. 相关函数及其实现 2.1 判空与判满 判空&#xff1a;直接返回头尾…

Leetcode—946. 验证栈序列【中等】

2024每日刷题&#xff08;133&#xff09; Leetcode—946. 验证栈序列 实现代码 class Solution { public:bool validateStackSequences(vector<int>& pushed, vector<int>& popped) {int left 0;for(int i 0; i < popped.size(); i) {while(left &…