进程间通信之匿名管道

news2024/11/18 7:24:26

进程间通信—管道

  • 一、进程间通信介绍
  • 二、管道
    • 1.匿名管道
      • 1.1父进程和一个子进程之间的通信
      • 1.2父进程和多个子进程之间的通信

一、进程间通信介绍

1.进程间为什么要进行通信?
进程间通信的是为了协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。

2.进程间通信的目的包括:
数据传输:一个进程需要将它的数据发送给另一个进程;
资源共享:多个进程间共享同样的资源;
通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事情,比如进程终止时需要通知其父进程;
进程间通信(IPC)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息;
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

3.进程间通信的分类:
(1)管道:1、匿名管道pipe;2、命名管道mkfifo
(2)System V IPC:1、System V 消息队列;2、System V 共享内存;3、System V 信号量。
(3) POSIX IPC:1、消息队列;2、共享内存;3、信号量;4、互斥量;5、条件变量;6、读写锁。
那么管道是如何进行通信的?

二、管道

什么是管道?

管道是一种通信机制,通常用于进程间的通信,它表现出来的形式将前面每一个进程的输出(stdout)直接作为下一个进程的输入(stdin)。

1.匿名管道

匿名管道是一种进程间通信机制,它仅限于本地父子进程之间通信,结构简单,类似于一根水管,一端进水另一端出水(单工)也就是只能够单向通信。匿名管道的作用之一是输出重定向。
在这里插入图片描述
所以用匿名管道pipe通信大概分为四部:1、创建匿名管道;2、创建子进程;3、关闭不需要的文件描述符;4、进行通信。

pipe函数,linux中输入 man 2 pipe,可以查看pipe函数,如下:
在这里插入图片描述

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

1.1父进程和一个子进程之间的通信

接下来试着写一下,父进程向管道当中写“i am father”, 子进程从管道当中读出内容, 并且打印到标准输出;先创建管道, 进而创建子进程, 父子进程使用管道进行通信。

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <unistd.h>
using namespace std;

int main()
{
	//创建匿名管道
    int pipefd[2] = {0}; //文件描述符数组
    int n = pipe(pipefd);
    if (n < 0) //如果失败,则打印错误码
    {
        cout << "pipe error, " << errno << ": " << strerror(errno) << endl;
    }
    cout << "读端:" << pipefd[0] << endl;
    cout << "写端:" << pipefd[1] << endl; //打印出读写端的文件描述符
	//创建子进程
    pid_t id = fork(); //子进程中返回0,父进程返回子进程id,出错返会-1
    assert(id != -1);
    if (id == 0)
    {
        // child(子进程)
        close(pipefd[1]); //关闭不需要的文件描述符,子进程需要读,所以关闭管道写端
                          //pipefd数组中0表示读端;1表示写端
        char buffer[1024];//不管读数据还是写数据,都需要缓冲区,所以buffer相当于一个缓冲区
        while (true)
        {
            sleep(1);
            int n = read(pipefd[0], buffer, sizeof(buffer) - 1); //read函数
            //sizeof为什么-1? 从文件中读取数据到buffer中,因为c语言中字符串的结束
            //标志是'\0',而文件中的数据结束尾不需要'\0',所以buffer的内存要比读出
            //数据内存大'\0';相反,往文件中写入数据时,要注意去掉字符串末尾的'\0'//字符串末尾的'\0'在文件中会变成乱码(^@)
            if (n > 0)
            {
                buffer[n] = '\0';
                cout << "我是子进程,读取到父进程的数据: " << buffer << endl;
            }
            else if (n == 0)
            {
                cout << "我是子进程,读到了文件结尾" << buffer << endl;
                break;
            }
            else
            {
                cout << "我是子进程,读取异常" << endl;
            }
        }
        close(pipefd[0]); //关闭子进程中管道的读端
        exit(0);
    }
    // father(父进程)
    close(pipefd[0]); //父进程写入,关闭读端
    int cnt = 1; //用来记录写了多少次
    const string str = "i am father"; //要写入的内容
    char buffer[1024];
    while (true)
    {
        sleep(1);
        snprintf(buffer, sizeof(buffer), "%s, 计数器:%d", str.c_str(), cnt++);//将数据写入到buffer中
        write(pipefd[1], buffer, strlen(buffer));//再将buffer中的数据写入管道中
    }
    close(pipefd[1]); //关闭父进程中管道的写端
    
    return 0;
}

下面是一些open,write,close等函数和文件描述符等有关概念:
系统方面对文件的打开,读写和关闭(open,write,close等用法)
文件描述符
运行结果如下:
在这里插入图片描述
由下述代码看现象:

#include <iostream>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <cstdio>
#include <unistd.h>
using namespace std;

int main()
{
    int pipefd[2] = {0};
    int n = pipe(pipefd);
    if (n < 0)
    {
        cout << "pipe error, " << errno << ": " << strerror(errno) << endl;
    }

    pid_t id = fork();
    assert(id != -1);
    if (id == 0)
    {
        // child
        int cnt = 5;
        close(pipefd[1]);
        char buffer[1024];
        while (--cnt)
        {
            sleep(1);
            int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = '\0';
                cout << buffer << endl;
            }
            else if (n == 0)
            {
                cout << "我是子进程,读到了文件结尾" << buffer << endl;
                break;
            }
            else
            {
                cout << "我是子进程,读取异常" << endl;
            }
        }
        close(pipefd[0]);
        exit(0);
    }
    // father
    close(pipefd[0]);
    int cnt = 1;
    const string str = "i am father";
    char buffer[1024];
    while (true)
    {
        sleep(1);
        snprintf(buffer, sizeof(buffer), "%s, 计数器:%d", str.c_str(), cnt++);
        write(pipefd[1], buffer, strlen(buffer));
    }
    close(pipefd[1]);
    return 0;
}

在这里插入图片描述

由上可知:
写端一直写,读端关闭OS会杀死一直在写入的进程。
将上述代码进行小的修改,就可以得到如下现象,代码就不在展示了,四种现象如下:
1.如果read读取完毕了所有的管道数据,如果对方不发,那么就只能等待;
2.如果我们writer端将管道写满了,那么就不能再写。管道的内存是有一定大小的;
3.如果我关闭了写端,读取完毕管道数据,再读,就会read返回0,表明读到了文件结尾;
4.写端一直写,读端关闭,会发生什么呢?没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程! OS会通过信号来终止进程,13) SIGPIPE。

1.2父进程和多个子进程之间的通信

父进程写数据,子进程读取数据,父进程和多个子进程之间的通信,首先创建管道,代码如下:

void createProcess(vector<EndPoint> &end_points)
{
    for (int i = 0; i < CHILDNUMS; ++i)
    {
        // 1.先创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0); // pipe函数成功返回0
        (void)n;        // linux中,定义一个变量,但是没使用,所以强转为void。(gcc编译可能报错)
        // 2.创建子进程
        pid_t id = fork();
        assert(id != -1); // fork失败返回-1
        if (id == 0)
        {
            // 子进程
            // 关闭不需要的文件描述符,假设父进程写,子进程读
            close(pipefd[1]); // pipe中1 为写端
            char buffer[1024];
            while (true)
            {
                //...进行读取
                ......
                
            }
            close(pipefd[0]); // 关闭读端
            exit(0);          // 关闭读端,子进程退出
        }
        // 父进程
        close(pipefd[0]);
        end_points.push_back(EndPoint(id, pipefd[1])); //将文件描述符管理起来
    }
}

其中需要注意的是创建子进程,子进程是父进程的拷贝,所以当多次fork后,子进程中含有父进程指向管道的写端,如下图:
在这里插入图片描述
所以在创建管道时,需要关闭多余的管道,改进代码如下:

void createProcess(vector<EndPoint> &end_points)
{
    vector<int> fds;
    for (int i = 0; i < CHILDNUMS; ++i)
    {
        // 1.先创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0); // pipe函数成功返回0
        (void)n;        // linux中,定义一个变量,但是没使用,所以强转为void。(gcc编译可能报错)
        // 2.创建子进程
        pid_t id = fork();
        assert(id != -1); // fork失败返回-1
        if (id == 0)
        {
            //关闭子进程多余的fd
            for(auto &fd : fds)
                close(fd);
            // 子进程
            // 关闭不需要的文件描述符,假设父进程写,子进程读
            close(pipefd[1]); // pipe中1 为写端
            char buffer[1024];
            while (true)
            {
                //...进行读取
                ......
                
            }
            close(pipefd[0]); // 关闭读端
            exit(0);          // 关闭读端,子进程退出
        }
        // 父进程
        close(pipefd[0]);
        end_points.push_back(EndPoint(id, pipefd[1]));
        
        fds.push_back(pipefd[1]);
    }
}

完整代码如下:

#include <iostream>
#include <vector>
#include <string>
#include <string.h> //strlen
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;

#define CHILDNUMS 3 // 子进程的个数
class EndPoint
{
public:
    EndPoint(pid_t id, int fd)
        : _child_id(id), _write_fd(fd)
    {
    }

public:
    pid_t _child_id;
    int _write_fd;
};
void createProcess(vector<EndPoint> &end_points)
{
    vector<int> fds;
    for (int i = 0; i < CHILDNUMS; ++i)
    {
        // 1.先创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0); // pipe函数成功返回0
        (void)n;        // linux中,定义一个变量,但是没使用,所以强转为void。(gcc编译可能报错)
        // 2.创建子进程
        pid_t id = fork();
        assert(id != -1); // fork失败返回-1
        if (id == 0)
        {
            //关闭子进程多余的fd
            for(auto &fd : fds)
                close(fd);
            // 子进程
            // 关闭不需要的文件描述符,假设父进程写,子进程读
            close(pipefd[1]); // pipe中1 为写端
            char buffer[1024];
            while (true)
            {
                int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
                if (n > 0)
                {
                    buffer[n] = '\0';
                    cout << "我是pid=%d的子进程," << getpid() << ";接受到父进程的指令是:" << buffer << endl;
                }
                else if (n == 0)
                {
                    cout << "我是子进程pid=" << getpid() << ",读到了文件结尾,并且父进程写端已关闭" << endl;
                    break;
                }
                else
                {
                    cout << "我是子进程,读取异常" << endl;
                }
                sleep(1);
            }
            close(pipefd[0]); // 关闭读端
            exit(0);          // 关闭读端,子进程退出
        }
        // 父进程
        close(pipefd[0]);
        end_points.push_back(EndPoint(id, pipefd[1]));

        fds.push_back(pipefd[1]);
    }
}
void ctrlProcess(const vector<EndPoint> &end_points)
{
    // 对每个子进程下发任务
    char buffer[1024];
    int command = 0;
    int cnt = 0;
    while (true)
    {
        cnt %= end_points.size();
        cout << "输如命令号:";
        cin >> command;
        if (command == 0)
            break;
        snprintf(buffer, sizeof(buffer), "pid=%d的子进程处理%d任务", end_points[cnt]._child_id, command);
        write(end_points[cnt]._write_fd, buffer, strlen(buffer));
        ++cnt;
        sleep(1);
    }
}
void waitProcess(const vector<EndPoint> &end_points)
{
    // 需要让子进程全部退出 --- 只需要让父进程关闭所有的write_fd并且要回收子进程的僵尸状态
    for (int end = 0; end < end_points.size(); ++end)
    {
        cout << "父进程让子进程退出:" << end_points[end]._child_id << endl;
        close(end_points[end]._write_fd);

        waitpid(end_points[end]._child_id, nullptr, 0); //waitpid不需要查看子进程退出信息,所以传nullptr
        cout << "父进程回收了子进程:" << end_points[end]._child_id << endl;
    }
}
int main()
{
    cout << "命令号为0,则退出进程" << endl;
    // 需要记录子进程的pid和父进程的写端
    vector<EndPoint> end_points;
    // 1. 先进行构建控制结构, 父进程写入,子进程读取
    createProcess(end_points);
    // 2.控制每个子进程
    ctrlProcess(end_points);
    // 3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

运行结果如下:
在这里插入图片描述
管道的特点:

  1. 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;
  2. 通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道;
  3. 管道提供流式服务;
  4. 一般而言,进程退出,管道释放,所以管道的生命周期随进程;
  5. 内核会对管道操作进行同步与互斥;
  6. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道。

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

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

相关文章

代码随想录day4 | 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点 02.07.链表相交 142.环形链表II

文章目录 一、两两交换链表中的节点二、删除链表的倒数第N个节点三、链表相交四、环形链表 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题 02.07. 链表相交 142.环形链表II 一、两两交换链表中的节点 两两交换链表中的节点 注意是两两交换&#xff0c;采用虚拟…

Global symbol “%data“ requires explicit package name

Global symbol “%data” requires explicit package name 如图编写demo的时候出现了如图的问题&#xff0c;在网上查找到的原因是&#xff1a; 一&#xff0c;使用use strict; &#xff0c;修改其他代码&#xff0c;如下&#xff1a; 1&#xff0c;首先&#xff0c;检查你是不…

静态库和动态库的区别与优缺点

文章目录 静态库与动态库的区别动态库与静态库的优缺点 静态库与动态库的区别 静态库直接打包链接到可执行程序 动态库将不会链接到可执行文件 &#xff0c;执行文件运行时需要动态加载 动态库 &#xff0c;所以需要提前知道动态库路径&#xff0c;需要将路径保存到环境变量或…

亲测:2核4G云服务器性能测试能安装多少个网站?

2核4G服务器能安装多少个网站&#xff1f;2核4g配置能承载多少个网站&#xff1f;一台2核4G服务器可以安装多少个网站&#xff1f;阿腾云2核4G5M带宽服务器目前安装了14个网站&#xff0c;从技术角度是没有限制的&#xff0c;只要云服务器性能够用&#xff0c;想安装几个网站就…

【前端知识】React 基础巩固(二十六)——Portals 的使用

React 基础巩固(二十六)——Portals 的使用 Portals 通常&#xff0c;组件会渲染到 root 节点下。可使用 Portals 将组件渲染至其他节点。 添加 id 为 more、modal 的 div 元素 <div id"root"></div> <div id"more"></div> &l…

apache 工作模式 NPM Multi-processing module

Apache 三种工作模式 prefork work event 监控线程相当于 项目组长&#xff0c;面对客户&#xff0c;根据任务自行调度 团队成员。 Prefork模式为Apache默认使用的工作模式&#xff0c;在该模式下&#xff0c;采用的机制是预派生子进程的方式&#xff0c;使用单独的进程来…

设计模式之单例

文章目录 前言一、单例模式的基本定义二、懒汉式单例三、饿汉式单例四、懒汉式单例存在的线程安全问题总结 前言 单例模式是比较经典的设计模式&#xff0c;本篇我们将结合实际案例说明单例模式的实现原理&#xff0c;以及存在的问题。 一、单例模式的基本定义 单例模式&…

【前端知识】React 基础巩固(二十八)——StrictMode

React 基础巩固(二十八)——StrictMode StrictMode StrictMode 是一个用来突出显示应用程序中潜在问题的工具 与 Fragment 一样&#xff0c;StrictMode 不会渲染任何可见的 UI为后代出发额外的检测和警告严格模式检查仅在开发模式下运行&#xff0c;不影响生产构建 严格模式检…

Google DeepMind掌舵人Demis Hassabis专访:合并后「超级单元」内幕,以及如何开展下一代模型研究...

导读 自2023年以来&#xff0c;Google旗下两大顶级人工智能研究团队 DeepMind 与 Google Brain 被不断推上风口浪尖&#xff0c;如何应对OpenAI与微软强强联合&#xff1f;如何组织不被看好的 DeepMind 与 Google Brain 团队合并&#xff1f;Google和OpenAI都没有护城河吗&…

单片机第一季:零基础8——蜂鸣器

蜂鸣器是一种一体化结构的电子讯响器&#xff0c;采用直流电压供电&#xff0c;广泛应用于计算机、打印机、复印机、报警器、电子玩具、汽车电子设备、电话机、定时器等电子产品中作发声器件。 蜂鸣器主要分为压电式蜂鸣器和电磁式蜂鸣器两种类型。 想要压电式蜂鸣器发声&…

使用Gataway设置全局过滤器

说明&#xff1a;Gateway网关是所有微服务统一的入口&#xff0c;通过网关技术&#xff0c;可以实现请求转发、权限控制和限流。全局过滤器是Gateway请求转发、权限控制的一个体现。比如对所有请求进行过滤&#xff0c;判断请求是否携带有效的token&#xff0c;对请求放行或者拦…

Java 装箱拆箱原理 包装类型缓存池

JAVA中的基本数据类型 byteshortintlongfloatdoublebooleanchar 为了让上述基本数据类型可以转为对象&#xff0c;Java在1.5推出了一系列包装类&#xff0c;基本类和包装类互相转换的过程&#xff0c;称为装箱和拆箱 缓存池 缓存池也叫常量池。它是事先存储一些常量数据用以…

reverse

b,a,0x7d65都能轻易的转成字符串&#xff0c;就是这个负数需要看汇编&#xff0c;在汇编层面是加了两个数实现的减法效果。 a[galf,saE{,er_y,3r0v,}e] for each in a:print(each[::-1],end) # flag{Easy_rev0r3e}

华为OD机试真题 Java 实现【路灯照明问题】【2022Q4 100分】,感谢fly晨发现这个问题,并提供更优质的算法

目录 专栏导读一、题目描述二、输入描述三、输出描述四、解题思路特别鸣谢&#xff1a;感谢fly晨发现这个问题&#xff0c;并提供更优质的算法。解题思路如下&#xff1a; 五、Java算法源码六、效果展示1、输入2、输出3、思路 专栏导读 本专栏收录于《华为OD机试&#xff08;J…

【USRP X310】如何查找USRP X310的 LabVIEW 案例 Demo

环境 Windows 10LabVIEWUSRP 支持的硬件 USRP-2900USRP-2901USRP-2920USRP-2921USRP-2922USRP-2930USRP-2932USRP-2940 40 MHzUSRP-2940 120 MHzUSRP-2942 40 MHzUSRP-2942 120 MHzUSRP-2943 40 MHzUSRP-2943 120 MHzUSRP-2944USRP-2945USRP-2950 40 MHzUSRP-2950 120 MHzUS…

轻奢分销商城pc/h5小程序开源版开发

轻奢分销商城pc/h5小程序开源版开发 商城系统&#xff1b;包含小程序商城、H5商城、公众号商城、PC商城、App&#xff0c;支持秒杀、优惠券、积分、分销、会员等级。 功能列表&#xff1a; 用户注册和登录&#xff1a;用户可以通过手机号码或第三方登录方式注册和登录账号。…

RPM包详解以及如何制作RPM包

RPM包简介 RPM 全名 RedHat Package Managerment&#xff0c;一开始只是Red Hat Linux的软件包格式&#xff0c;它最初的名字为Red Hat软件包管理程序。从那以后&#xff0c;RPM逐渐称为许多其他Linux发行版所接受的一种软件包格式。Linux标准化规范将RPM作为其官方软件包格式…

gdb调试产生code文件以及遇到的“file format not recognized”问题解决

一、调试 core 文件的配置 1.1 开启core文件 —— ulimit 有时候&#xff0c;服务器程序运行一段时间后会突然崩溃&#xff0c;这并不是我们希望看到的&#xff0c;需要 解决这个问题。只要程序在崩溃的时候有 core 文件产生&#xff0c;就可以使用这个 core 文件 来定位崩溃…

数据结构(王道)——线性表的存储结构之双链表

双链表和单链表的对比 一、从无到有创建一个双链表及其基础操作 带头结点 的双链表 初始化 双链表的后插操作&#xff1a; 从后插可以拓展到按位序插入&#xff0c;和前插操作。 因为双链表的特性&#xff0c;如果按位序插入或者前插操作&#xff0c;只需要找到插入的位置的前…