匿名管道及其应用

目录

一、什么是匿名管道?

三、创建与使用匿名管道

三、匿名管道的特点

匿名管道的四种情况

匿名管道的五种特性

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


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

一、什么是匿名管道?

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

「 安全设计 」68家国内外科技巨头和安全巨头参与了CISA发起的安全设计承诺,包含MFA、默认密码、CVE、VDP等七大承诺目标

美国网络安全和基础设施安全局&#xff08;CISA&#xff0c;CyberSecurity & Infrastructure Security Agency&#xff09;于2024年5月开始呼吁企业是时候将网络安全融入到技术产品的设计和制造中了&#xff0c;并发起了安全设计承诺行动&#xff0c;该承诺旨在补充和建立现…

数据挖掘原理与应用------分类预测

在数据挖掘和机器学习领域&#xff0c;TPR&#xff08;True Positive Rate&#xff09;是指在实际为阳性的情况下&#xff0c;模型正确预测为阳性的比例。TPR也被称为灵敏度&#xff08;Sensitivity&#xff09;或召回率&#xff08;Recall&#xff09;。它是评估分类模型性能的…

【LeetCode算法】1768. 交替合并字符串

提示&#xff1a;此文章仅作为本人记录日常学习使用&#xff0c;若有存在错误或者不严谨得地方欢迎指正。 文章目录 一、题目二、思路三、解决方案 一、题目 给你两个字符串 word1 和 word2 。请你从 word1 开始&#xff0c;通过交替添加字母来合并字符串。如果一个字符串比另…

bash tab 补全报错 bash: syntax error near unexpected token `(‘

使用 vim 编辑文件时&#xff0c;敲下 vim xxx 后&#xff0c;再键入 tab 键报进行补全报错 bash: syntax error near unexpected token (. 打开 bash 的命令执行详情 set -v 定位到具体的代码&#xff1a; 显然&#xff0c;代码位于 bash 补全的逻辑当中。 定位代码具体的…

搭建属于自己的AI知识库

前言 最近在看一本书《在线》&#xff0c;将所有数据都需要在线&#xff0c;才有生命力&#xff0c;那么我们的知识库也是。我们现在就可以用先进的大预言模型搭建属于自己的在线 AI 知识库&#xff0c;他就是 ChatGLM 智谱清言智能体。 它可以将自己的知识库与 ChatGLM 结合&…

2024小红书电商实战营,养号打造IP/选爆品/开店铺/爆款笔记/等等(24节)

我们非常荣幸地为大家带来2024小红书电商实战营的第一期&#xff0c;在这里我们将带领大家一起深入学习如何利用小红书平台&#xff0c;实现个人品牌的发展和商业利益的增长。 首先&#xff0c;我们将讨论养号的重要性以及如何打造个人品牌。无论是建立自己的受众群体还是提高…

Python 中的 Lambda 函数:简单、快速、高效

大家好&#xff0c;今天再给大家介绍一个python的一个强大工具Lambda 函数&#xff0c;它允许你快速定义简单的匿名函数。这种函数是“匿名的”&#xff0c;因为它们不需要像常规函数那样被明确命名。 在本文中&#xff0c;我们将通过清晰的解释和实用的示例&#xff0c;深入了…

Golang — map的使用心得和底层原理

map作为一种基础的数据结构&#xff0c;在算法和项目中有着非常广泛的应用&#xff0c;以下是自己总结的map使用心得、实现原理、扩容机制和增删改查过程。 1.使用心得&#xff1a; 1.1 当map为nil和map为空时&#xff0c;增删改查操作时会出现的不同情况 我们可以发现&#…

基于C++和Python基础的Golang学习笔记

文章目录 一、基础1.DOS命令2.变量&#xff08;1&#xff09;局部变量&#xff08;2&#xff09;全局变量&#xff08;3&#xff09;数据类型&#xff08;4&#xff09;指针&#xff08;5&#xff09;运算符&#xff08;6&#xff09;自定义数据类型 3.语句&#xff08;1&#…

Navicat 干货 | 探索 PostgreSQL 中不同类型的约束

PostgreSQL 的一个重要特性之一是能够对数据实施各种约束&#xff0c;以确保数据完整性和可靠性。今天的文章中&#xff0c;我们将概述 PostgreSQL 的各种约束类型并结合免费的 "dvdrental" 示例数据库 中的例子探索他们的使用方法。 1. 检查约束&#xff1a; 检查…

Virtualbox7.0.10+Ubuntu20.04网络配置

虚拟机部署在服务器上时&#xff0c;需要进行网络配置&#xff0c;使虚拟机和服务器在同网段下&#xff0c;以保证内网的终端可以访问到虚拟机 1. 设置虚拟机 打开虚拟机设置&#xff0c;选择“网络”&#xff0c;将网卡设为桥接网卡 注&#xff1a;设置前&#xff0c;需要先…

[通用人工智能] 论文分享:ElasticViT:基于冲突感知超网的快速视觉Transformer

引言: 近年来&#xff0c;视觉Transformer&#xff08;Vision Transformer&#xff0c;简称ViT&#xff09;在计算机视觉任务中的应用日益广泛&#xff0c;从图像分类到对象识别等&#xff0c;均显示出优越的性能。然而&#xff0c;ViT模型也面临一些挑战&#xff0c;特别是在模…

抽丝剥茧:详述一次DevServer Proxy配置无效问题的细致排查过程

事情的起因是这样的&#xff0c;在一个已上线的项目中&#xff0c;其中一个包含登录和获取菜单的接口因响应时间较长&#xff0c;后端让我尝试未经服务转发的另一域名下的新接口&#xff0c;旧接口允许跨域请求&#xff0c;但新接口不允许本地访问&#xff08;只允许发布测试/生…

ARM架构安全特性概览

安全之安全(security)博客目录导读 目录 一、跨行业计算安全 二、Arm架构安全特性的益处 三、安全威胁与缓解 四、防御执行技术 五、隔离技术 六、通用平台安全服务 七、标准安全 API 八、PSA安全标准认证 一、跨行业计算安全 从一开始&#xff0c;Arm 生态系统一直是…

VS项目Debug下生成的EXE在生产机器上运行

使用Visual Studio开发应用程序时&#xff0c;为了临时在非开发机上看一下效果&#xff0c;就直接把Debug下的文件全部拷贝到该机器上&#xff0c;直接双击exe运行。双击之后&#xff0c;没有直接打开应用程序&#xff0c;而是弹出了一个Error弹框。  赶快在网上搜了一遍&…

Ardupilot开源代码之Rover上路 - 后续1

Ardupilot开源代码之Rover上路 - 后续1 1. 源由2. 问题汇总2.1 问题1&#xff1a;飞控选择2.2 问题2&#xff1a;飞控安装位置和固定2.3 问题3&#xff1a;各种插头、插座配套2.4 问题4&#xff1a;分电板缺陷2.5 问题5&#xff1a;电机编码器接线及正反向问题2.6 问题6&#x…

什么是等保2.0,相对等保1.0有哪些变化,支撑等保2.0的标准文档有哪些?

1. 等保1.0、等保2.0业界定义 等保1.0&#xff1a;以1994年2月18日年国务院颁布的 147 号令《中华人民共和国计算机信息系统安全保护条例》为指导标准&#xff0c;以2008年发布的《GB/T22239-2008 信息安全技术 信息系统安全等级保护基本要求 》为指导的网络安全等级保护办法。…

向量数据库:Chroma

目录 一、Chroma 介绍 二、安装 Chroma 2.1 创建虚拟 python 环境 2.2 安装 Chroma 2.3 运行 Chroma 三、Backend API 一、Chroma 介绍 Chroma是一个开源的嵌入式数据库。Chroma通过使知识(knowledge)、事实(facts)和技能(skills)可插拔&#xff0c;从而简化了大型语言模…

小猫咪邮件在线发送系统源码,支持添加附件

一款免登录发送邮件&#xff0c;支持发送附件&#xff0c;后台可添加邮箱,前台可选择发送邮箱 网站数据采取本地保存&#xff0c;所以使用前请给网站修改权限&#xff0c;否则很多功能将无法使用 安装教程&#xff1a; 1.上传服务器或者主机 2.登录后台&#xff0c;添加发送…

FCOS长文详解

1. 概述 FCOS是一种one-stage、全卷积&#xff08;Fully Convolutional&#xff09;结构的目标检测模型&#xff0c;发表于2019年ICCV。&#xff08;什么是one-stage&#xff1f;&#xff09; 论文原地址&#xff1a;https://arxiv.org/abs/1904.01355 作者源码&#xff1a;ht…