匿名管道及其应用

news2024/11/26 4:35:33

目录

一、什么是匿名管道?

三、创建与使用匿名管道

三、匿名管道的特点

匿名管道的四种情况

匿名管道的五种特性

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


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

一、什么是匿名管道?

匿名管道是一种在具有亲缘关系的进程间进行单向通信的方式。它主要用于父子进程之间的数据传递。 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)
{
    // bug
    std::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
        }

        // father
        close(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();

        // debug
        std::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 1
    for (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;
        else
            return 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.coloradmin.cn/o/1669357.html

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

相关文章

Spring WebFlux-响应式编程-019

&#x1f917; ApiHug {Postman|Swagger|Api...} 快↑ 准√ 省↓ GitHub - apihug/apihug.com: All abou the Apihug apihug.com: 有爱&#xff0c;有温度&#xff0c;有质量&#xff0c;有信任ApiHug - API design Copilot - IntelliJ IDEs Plugin | Marketplace The Nex…

Redis-发布与订阅

发布与订阅 什么是发布与订阅 Redis 发布订阅 (pub/sub) 是一种消息通信模式&#xff1a;发送者 (pub) 发送消息&#xff0c;订阅者 (sub) 接收消息。 Redis 客户端可以订阅任意数量的频道。 Redis的发布与订阅 客户端订阅频道 当给这个频道发送消息后&#xff0c;消息就会…

Java | Leetcode Java题解之第86题分隔链表

题目&#xff1a; 题解&#xff1a; class Solution {public ListNode partition(ListNode head, int x) {ListNode small new ListNode(0);ListNode smallHead small;ListNode large new ListNode(0);ListNode largeHead large;while (head ! null) {if (head.val < x…

LwIP 之九 详解 UDP RAW 编程、示例、API 源码、数据流

我们最为熟知的网络通信程序接口应该是 Socket。LwIP 自然也提供了 Socket 编程接口,不过,LwIP 的 Socket 编程接口都是使用最底层的接口来实现的。我们这里要学习的 UDP RAW 编程则是指的直接使用 LwIP 的最底层 UDP 接口来直接实现应用层功能。这里先来一张图,对 LwIP 内部…

Java泛型,这一篇就够了

1. 为什么我们需要泛型 现实世界中我们经常遇到这样一种情况&#xff0c;同一个算法/数据结构适用于多种数据类型&#xff0c;我们不想为每一种类型单独写一个实现。举个例子来说&#xff0c;我们有一个Pair类型&#xff0c;存储key、value两个字段&#xff0c;代码如下。如果…

机器视觉技术精准测量点胶高度与宽度:提升生产质量的新利器

在现代化生产线中&#xff0c;点胶工艺是许多产品制造过程中的重要环节。点胶的高度和宽度直接影响到产品的质量和性能。传统的测量方法往往效率低下、精度不高&#xff0c;而机器视觉技术的引入&#xff0c;为点胶高度和宽度的测量带来了革命性的变革。本文将探讨机器视觉如何…

如何解决pycharm在HTML文件中注释快捷键出错的问题(HTML注释规则出错)

文章目录 💢 问题 💢🏡 演示环境 🏡💯 解决方案 💯⚓️ 相关链接 ⚓️💢 问题 💢 你是否在编程时遇到过这样的烦恼?当你正专注地编写HTML代码,想要快速注释掉某部分内容时,却发现PyCharm的注释快捷键失灵了(没有使用正确的注释格式)。这不仅打断了你的工作…

目标检测——DAGM2007纹理背景缺陷数据集

引言 亲爱的读者们&#xff0c;您是否在寻找某个特定的数据集&#xff0c;用于研究或项目实践&#xff1f;欢迎您在评论区留言&#xff0c;或者通过公众号私信告诉我&#xff0c;您想要的数据集的类型主题。小编会竭尽全力为您寻找&#xff0c;并在找到后第一时间与您分享。 …

Puppeteer的基本使用及多目标同时访问

文章目录 一、安装 puppeteer 并更改默认缓存路径1、更改 Puppeteer 用于安装浏览器的默认缓存目录2、安装 puppeteer3、项目结构目录 二、基本使用1、启动浏览器并访问目标网站2、生成截图3、生成 PDF 文件4、获取目标网站 html 结构并解析5、拦截请求6、执行 JavaScript7、同…

(Java)心得:LeetCode——18.四数之和

一、原题 给你一个由 n 个整数组成的数组 nums &#xff0c;和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] &#xff08;若两个四元组元素一一对应&#xff0c;则认为两个四元组重复&#xff09;&#xff1a; …

【CTF Web】QSNCTF 文章管理系统 Writeup(SQL注入+Linux命令+RCE)

文章管理系统 题目描述 这是我们的文章管理系统&#xff0c;快来看看有什么漏洞可以拿到FLAG吧&#xff1f;注意&#xff1a;可能有个假FLAG哦 解法 SQL 注入。 ?id1 or 11 --取得假 flag。 爆库名。 ?id1 union select 1,group_concat(schema_name) from information_sch…

反调试 - ptrace占坑

ptrace占坑 这是ptrace占坑的标志。 ptrace可以让一个进程监视和控制另一个进程的执行,并且修改被监视进程的内存、寄存器等,主要应用于调试器的断点调试、系统调用跟踪等。 在Android app保护中,ptrace被广泛用于反调试。一个进程只能被ptrace一次,如果先调用了ptrace方法,那…

AI办公自动化-用kimi把PDF文档按照章节自动拆分成多个docx文档

一个PDF文档很长&#xff0c;希望按照章节分拆成小文档。 可以在kimichat中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个编写拆分PDF文档的Python脚本的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&#xff1a;D:\chatgpt图书\图书1&…

爬虫工作量由小到大的思维转变---<第七十三章 > Scrapy爬虫详解一下HTTPERROE的问题

前言&#xff1a; 在我们的日常工作中&#xff0c;有时会忽略一些工具或组件的重要性&#xff0c;直到它们引起一连串的问题&#xff0c;我们才意识到它们的价值。正如在Scrapy框架中的HttpErrorMiddleware&#xff08;HTTP错误中间件&#xff09;一样&#xff0c;在开始时&…

JVM调优:JVM中的垃圾收集器详解

JVM&#xff08;Java Virtual Machine&#xff09;垃圾收集器是Java虚拟机中的一个重要组件&#xff0c;负责自动管理Java堆内存中的对象。垃圾收集器的主要任务是找出那些不再被程序使用的对象&#xff0c;并释放它们占用的内存&#xff0c;以便为新的对象分配空间。这个过程被…

ES6 笔记02

目录 01 对象的扩展 02 链判断运算符 03 属性名表达式 04 Symbol 类型 05 set集合的使用 06 Map集合的使用 07 Set集合和Map集合的遍历方式 08 iterator迭代器 01 对象的扩展 对象的属性和方法的简洁表示: es6允许在字面量对象里面直接写变量名 let 变量名变量值; let …

Hexo博客重新部署与Git配置

由于电脑重装了一次&#xff0c;发现之前Hexo与NexT主题版本过于落后&#xff0c;重新部署了下。 1 Node.js与git安装 这一块安装就不赘述了。去两个官网找安装文件安装即可。 node.js git 打开git以后配置的几个关键命令行。 git config --global user.name "你的gi…

langchain 自定义模型使用

目录 背景 参考 实现 调用 背景 在公司有大模型可以通过 api 方式调用&#xff0c;想使用 langchain 框架调用&#xff0c;langchina 已经封装好大部分模型了&#xff0c;但自己公司的模型不支持&#xff0c;想使用&#xff0c;相当于自定义模型 参考 Custom Chat Model …

基于Springboot的家教管理系统(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的家教管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&…

Idea插件Easy-Code模板文件

目录 需要引入的依赖application.yml.vmapplication-dev.yml.vmresult.java.vm (统一返回集)resultCodeEnum.java.vm &#xff08;统一返回集需要的枚举类&#xff09;globalCorsConfig.java.vm &#xff08;全局跨域处理&#xff09;entity.java.vm &#xff08;实体类&#x…