【Linux】进程间通信(管道)

news2025/4/21 23:07:33

文章目录

  • 进程通信的目的
  • 进程间通信发展
  • 进程间通信分类
      • 管道
      • System V IPC
      • POSIX IPC
  • 管道
      • 什么是管道
      • 管道的读写规则
      • 管道的特点:
      • 匿名管道
      • 处理退出问题
      • 命名管道
        • 创建一个命名管道
        • 匿名管道与命名管道的区别
        • 命名管道的打开规则

进程通信的目的

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

进程间通信发展

管道
System V进程间通信
POSIX进程间通信

进程间通信分类

管道

匿名管道pipe
命名管道

System V IPC

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

POSIX IPC

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

管道

什么是管道

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

在这里插入图片描述

管道的读写规则

当没有数据可读时
O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
如果所有管道写端对应的文件描述符被关闭,则read返回0
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

在这里插入图片描述

管道的特点:

  • 1.单向通信
  • 2.管道的本质就是文件,因为fd的生命周期随进程,管道的生命周期是随进程的。
  • 3.管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用与父子通信 --pipe打开管道,并不清楚管道的名字,匿名管道。
  • 4.在管道通信当中,写入的次数,和读取的次数,不是严格匹配的,读写次数得多少没有强相关 —表现 --字节流
  • 5.具有一定的协同能力,让reader和writer能狗按照一定的步骤进行通信–自带同步机制。

匿名管道

#include<iostream>
#include<string>
#include<unistd.h>
#include<cerrno>
//将errno.h换成cerrno是因为后者的在c++当中的兼容性更好,并且更美观
#include<string.h>
#include<cassert>
#include<sys/types.h>
#include<sys/wait.h>

using namespace std;

int main()
{
    //1.创建管道
    int pipefd[2]={0};
    int n=pipe(pipefd);
    if(n<0)
    {
        cout<<"创建失败:"<<errno<<"->"<<strerror(errno)<<endl;
        return 1;
    }
    cout<<"pipefd[0]:"<<pipefd[0]<<endl;//可以通过0为嘴巴,就是读
    cout<<"pipefd[1]:"<<pipefd[1]<<endl;//可以通过1为笔,就是写

    //2.创建子进程
    pid_t m =fork();
    if(m==-1)//assert是代表意料之中的情况,就是能确定他就是没问题但是还是防了一手
    {         //if就是代表意料之外的情况,不知道情况是什么
        cout<<"创建失败"<<endl;
        return 1;
    }
    if(m==0)//子进程
    {
        //3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
        close(pipefd[0]);

        //4.进行通信,结合某种场景
        char buffer[1024];
        int cnt =1;
        const string namestr="hello 我是子进程";
        while(true)
        {
            // snprintf(buffer,sizeof(buffer),"%s,计数器:%d,pid:%d",namestr.c_str(),cnt++,getpid());
            // write(pipefd[1],buffer,strlen(buffer));
            // sleep(1);
            char x = 'X';
            write(pipefd[1], &x, 1);
            std::cout << "Cnt: " << cnt++<<std::endl;
            sleep(1);
        }

        exit(0);
    }


    //3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
    close(pipefd[1]);

    //4.进行通信,结合某种场景
    char buffer[1024];
    int cnt =0;
    while(true)
    {
        int n=read(pipefd[0],buffer,sizeof(buffer)-1);//这里减一是因为read它是按字节算的,他最多就1023,要把最后一和字节留下
        if(n>0)
        {
            buffer[1024]='\0';
            cout<<"我是父进程, child give me message: " << buffer<<endl;
        }
        else if(n == 0)
        {
            cout << "我是父进程, 读到了文件结尾" << endl;
            break;
        }
        else 
        {
            cout << "我是父进程, 读异常了" << endl;
            break;
        }
        sleep(1);
        if(cnt++ > 5) break;
    }
    close(pipefd[0]);
    int status = 0;
    waitpid(m, &status, 0);
    cout << "sig: " << (status & 0x7F) << endl;
    sleep(100);
    return 0;
}

//task.cc
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include "task.hpp"
using namespace std;

const int gnum = 3;
Task t;

class EndPoint
{
private:
    static int number;
public:
    pid_t _child_id;
    int _write_fd;
    std::string processname;
public:
    EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
    {
        //process-0[pid:fd]
        char namebuffer[64];
        snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
        processname = namebuffer;
    }
    std::string name() const
    {
        return processname;
    }
    ~EndPoint()
    {
    }
};

int EndPoint::number = 0;

// 子进程要执行的方法
void WaitCommand()
{
    while (true)
    {
        int command = 0;
        int n = read(0, &command, sizeof(int));
        if (n == sizeof(int))
        {
            t.Execute(command);
        }
        else if (n == 0)
        {
            std::cout << "父进程让我退出,我就退出了: " << getpid() << std::endl; 
            break;
        }
        else
        {
            break;
        }
    }
}

void createProcesses(vector<EndPoint> *end_points)
{
    vector<int> fds;
    for (int i = 0; i < gnum; i++)
    {
        // 1.1 创建管道
        int pipefd[2] = {0};
        int n = pipe(pipefd);
        assert(n == 0);
        (void)n;

        // 1.2 创建进程
        pid_t id = fork();
        assert(id != -1);
        // 一定是子进程
        if (id == 0)
        {
            for(auto &fd : fds) close(fd);

            // std::cout << getpid() << " 子进程关闭父进程对应的写端:";
            // for(auto &fd : fds)
            // {
            //     std::cout << fd << " ";
            //     close(fd);
            // }
            // std::cout << std::endl;
            
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向,可以不做
            dup2(pipefd[0], 0);//
            //这里重定向问题为什么read不能对应1?
            //因为写要对应读,读要对应写,0对应read,write对应1
            // 1.3.2 子进程开始等待获取命令
            WaitCommand();
            close(pipefd[0]);
            exit(0);
        }

        // 一定是父进程
        //  1.3 关闭不要的fd
        close(pipefd[0]);

        // 1.4 将新的子进程和他的管道写端,构建对象
        end_points->push_back(EndPoint(id, pipefd[1]));

        fds.push_back(pipefd[1]);
    }
}


int ShowBoard()
{
    std::cout << "##########################################" << std::endl;
    std::cout << "|   0. 执行日志任务   1. 执行数据库任务    |" << std::endl;
    std::cout << "|   2. 执行请求任务   3. 退出             |" << std::endl;
    std::cout << "##########################################" << std::endl;
    std::cout << "请选择# ";
    int command = 0;
    std::cin >> command;
    return command;
}

void ctrlProcess(const vector<EndPoint> &end_points)
{
    // 2.1 我们可以写成自动化的,也可以搞成交互式的
    int num = 0;
    int cnt = 0;
    while(true)
    {
        //1. 选择任务
        int command = ShowBoard();
        if(command == 3) break;
        if(command < 0 || command > 2) continue;
        
        //2. 选择进程
        int index = cnt++;
        cnt %= end_points.size();
        std::string name = end_points[index].name();
        std::cout << "选择了进程: " <<  name << " | 处理任务: " << command << std::endl;

        //3. 下发任务
        write(end_points[index]._write_fd, &command, sizeof(command));

        sleep(1);
    }
}

void waitProcess(const vector<EndPoint> &end_points)
{
    // 1. 我们需要让子进程全部退出 --- 只需要让父进程关闭所有的write fd就可以了!
    // for(const auto &ep : end_points) 
    // for(int end = end_points.size() - 1; end >= 0; end--)
    for(int end = 0; end < end_points.size(); end++)
    {
        std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
        close(end_points[end]._write_fd);

        waitpid(end_points[end]._child_id, nullptr, 0);
        std::cout << "父进程回收了子进程:" << end_points[end]._child_id << std::endl;
    } 
    sleep(10);

    // 2. 父进程要回收子进程的僵尸状态
    // for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
    // std::cout << "父进程回收了所有的子进程" << std::endl;
    // sleep(10);
}


// #define COMMAND_LOG 0
// #define COMMAND_MYSQL 1
// #define COMMAND_REQEUST 2
int main()
{
    vector<EndPoint> end_points;
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    createProcesses(&end_points);

    // 2. 我们的得到了什么?end_points
    ctrlProcess(end_points);

    // 3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}
#pragma once

#include <iostream>
#include <vector>
#include <unistd.h>


typedef void (*fun_t)(); //函数指针

void PrintLog()
{
    std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}

void InsertMySQL()
{
    std::cout << "执行数据库任务,正在被执行..." << std::endl;
}

void NetRequest()
{
    std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}

//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2

class Task
{
public:
    Task()
    {
        funcs.push_back(PrintLog);
        funcs.push_back(InsertMySQL);
        funcs.push_back(NetRequest);
    }
    void Execute(int command)
    {
        if(command >= 0 && command < funcs.size()) funcs[command]();
    }
    ~Task()
    {}
public:
    std::vector<fun_t> funcs;
};

处理退出问题

上述代码当中的最后一部分是处理退出,而我用的方法是将父进程的写端进行关闭,子进程的读端也就读不到东西了所以也就子进程就关闭了。
在这里插入图片描述
上面图片的代码就是错误的代码,在将父进程的写端和子进程的读端进行分开处理就是可以的,但是放到一起就出现问题了。原因是在父进程创建第二个子进程时需要继承给子进程文件描述符表,然后出现的问题就是子进程会将父进程的读端一并继承下去,也就导致有多个进程指向进程读端,也就导致读端关闭不上,所以子进程也就无法退出。而为什么分开是可以的就是因为它是将所有的父进程指向的读端全部关闭也就不存在上述问题了。解决方法1:可以倒着关闭先关闭最后一个读端这样就不会出现上述问题了。
在这里插入图片描述

在这里插入图片描述
第二种解决办法就是将父进程的继承下来不需要的写端进行关闭
在这里插入图片描述

命名管道

//server.cc
#include"comm.hpp"
#include<sys/types.h>
#include<sys/stat.h>
#include<cerrno>
#include<string.h>
#include<fcntl.h>

int main()
{   // 1. 创建管道文件,我们今天只需要一次创建
    //这个设置并不影响系统的默认配置,只会影响当前进程
    umask(0);
    int n= mkfifo(fifoname.c_str(),0666);
    if(n<0)
    {
        std::cout<<"error:"<<errno<<strerror(errno)<<std::endl;
        return 1;    
    }
    std::cout<<"create fifo file success"<<std::endl;
    // 2. 让服务端直接开启管道文件
    int rfd=open(fifoname.c_str(),O_RDONLY);
    if(rfd<0)
    {
        std::cout<<"open file fail"<<std::endl;
        return 2;
    }
    std::cout<<"open fifo success"<<std::endl;

    //3.正常通信
    char buffer[NUM];
    while(true)
    {
        buffer[0] = 0;
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);//注意
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "client# " << buffer << std::endl;
            // printf("%c", buffer[0]);
            // fflush(stdout);
        }
        else if(n == 0)
        {
            std::cout << "client quit, me too" << std::endl;
            break;
        }
        else 
        {
            std::cout << errno << " : " << strerror(errno) << std::endl;
            break;
        }
    }

    // 关闭不要的fd
    close(rfd);
    unlink(fifoname.c_str());
    return 0;
}
//client.cc
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    //1. 不需创建管道文件,我只需要打开对应的文件即可!
    int wfd = open(fifoname.c_str(), O_WRONLY);
    if(wfd < 0)
    {
        std::cerr << errno << ":" << strerror(errno) << std::endl;
        return 1;
    }

    // 可以进行常规通信了
    char buffer[NUM];
    while(true)
    {
        std::cout << "请输入你的消息# ";
        char *msg = fgets(buffer, sizeof(buffer), stdin);
        assert(msg);
        (void)msg;
    
        buffer[strlen(buffer) - 1] = 0;
        //防止出现空行,因为fgets会将\n读进去,所以,将\n变成\0
        //这里不用担心空串,因为怎么都会输入\n的
        // abcde\n\0
        // 012345
        if(strcasecmp(buffer, "quit") == 0) break;
        //用于判断结束,该关键字是不区分大小写的比较

        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n >= 0);
        (void)n;
    }

    close(wfd);

    return 0;
}
//comm.hpp
#include<iostream>
#include<unistd.h>
#include<string>


#define NUM 1024

const std::string fifoname = "./fifo";
uint32_t mode = 0666; 
//makefile
//两个文件联合编译
.PHONY:all
all:client server

client:client.cc
	g++ -o $@ $^ -std=c++11

server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm client server

创建一个命名管道

命名管道可以从命令行上创建,命令行方法是使用下面这个命令

$ mkfifo filename

命名管道也可以从程序里创建,相关函数有

int mkfifo(const char *filename,mode_t mode);

创建命名管道

int main(int argc, char *argv[])
{
 mkfifo("p2", 0644);
 return 0;
}

匿名管道与命名管道的区别

匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完
成之后,它们具有相同的语义。

命名管道的打开规则

如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO

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

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

相关文章

应用层协议 —— websocket

websocket介绍 websocket是从HTML5开始支持的一种网页端和服务端保持长连接的消息推送机制。 传统的web程序都是属于“一问一答”的形式&#xff0c;即客户端给服务器发送了一个HTTP请求&#xff0c;服务器给客户端返回一个HTTP响应。这种情况下服务器属于被动的一方&#xff…

前端食堂技术周刊第 86 期:Remix 拥抱 RSC、2023 React 生态系统、从 0 实现 RSC、字节跳动 Mobile DevOps 工程实践

美味值&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f;&#x1f31f; 口味&#xff1a;椰子水 食堂技术周刊仓库地址&#xff1a;https://github.com/Geekhyt/weekly 本期摘要 Remix 拥抱 RSCWebContainers 原生支持 npm、yarn 和 pnpm2023 React 生态系…

MySQL InnoDB集群部署及管理全教程

MySQL InnoDB 集群提供完整的高可用性 MySQL 的解决方案。通过使用MySQL Shell附带的AdminAPI&#xff0c;您可以轻松 配置和管理至少三个MySQL服务器的组 实例以充当 InnoDB 集群。 InnoDB 集群中的每个 MySQL 服务器实例都运行 MySQL 组复制&#xff0c;提供复制机制 InnoDB…

LoadRunner 2023 下载和安装

下载 LoadRunner目前最新的版本是2023版&#xff0c;需要到Micro Focus公司的官网注册账号然后申请下载&#xff0c;比较麻烦&#xff0c;这里我把大家常用的社区版本&#xff0c;搬运到阿里云盘上&#xff0c;供下载&#xff1a; https://www.aliyundrive.com/s/WtHSzD4MrXw …

面试了十几家软件测试公司谈谈我最近面试的总结

由于互联网裁员&#xff0c;最近在 bosss 上投了些简历&#xff0c;测试开发岗&#xff0c;看看目前市场情况。 虽然都在说大环境不好&#xff0c;失业的人很多&#xff0c;我最近约面试的还是比较多的&#xff0c;说说最近的体会吧&#xff0c;希望能给大家提供价值。 1、20K…

教你制作一个简单的进销存管理软件,值得收藏!

首先要制作进销存软件&#xff0c;要具体了解进销存到底是什么含义&#xff0c;这三个字分别代表什么流程&#xff0c;在整个进销存管理中的组成。再根据不同的流程制作进销存软件相对应的部分—— 01进销存的定义 “进”——采购 采购是进销存管理的重要组成部分&#xff0…

微信开放平台第三方开发,注册试用小程序,一整套流程

大家好&#xff0c;我是小悟 对服务商来说&#xff0c;试用小程序的好处不言而喻&#xff0c;主打一个先创建后认证的流程。只需要提供小程序名称和openid便可快速注册一个试用小程序&#xff0c;在认证之前&#xff0c;有效期14天&#xff0c;大致流程如下。 注册试用小程序 …

HCIA-RS实验-配置DHCP

什么是DHCP DHCP是动态主机配置协议&#xff08;Dynamic Host Configuration Protocol&#xff09;的缩写&#xff0c;它是一种网络协议&#xff0c;用于自动分配IP地址、子网掩码、网关以及DNS服务器等网络参数给计算机&#xff0c;从而简化了网络管理和配置。 DHCP服务器的…

robotframework+python接口自动化的点滴记录

在robotframeworkpython框架上写了两三天的接口自动化&#xff0c;做了一些笔记。 1.在断言的时候经常由于数据类型导致较验不通过&#xff0c;值得注意的是&#xff0c;在定义常量或者变量的时候&#xff0c;使用${}代表int类型&#xff0c;例如${2}就代表数字2&#xff0c;另…

Qt学习09:其他基本小控件

文章首发于我的个人博客&#xff1a;欢迎大佬们来逛逛 文章目录 QSpinBoxQDateTimeEditQComboBoxQSliderQRubberBand QSpinBox 微调框&#xff0c;可以通过点击增加减小或者输入来调整数据。 继承自&#xff1a;QAbstractSpinBox 同时这个类还具有Double类型的版本。 常用操…

搭建Scala开发环境

一、Windows上安装Scala 1、到Scala官网下载Scala Scala2.13.10下载网址&#xff1a;https://www.scala-lang.org/download/2.13.10.html 单击【scala-2.13.10.msi】超链接&#xff0c;将scala安装程序下载到本地 2、安装Scala 双击安装程序图标&#xff0c;进入安装向导&…

笔试强训错题总结(二)

笔试强训错题总结&#xff08;二&#xff09; 选择题 下列哪一个是析构函数的特征&#xff08;&#xff09; A. 析构函数定义只能在类体内 B. 一个类中只能定义一个析构函数 C. 析构函数名与类名不同 D. 析构函数可以有一个或多个参数 析构函数可以在类中声明&#xff0c…

np.arange()用法+reshape+np.dot()

1.np.arange()用法 np.arange()函数返回一个有终点和起点的固定步长的排列 # 参数个数情况&#xff1a; np.arange()函数分为一个参数&#xff0c;两个参数&#xff0c;三个参数三种情况 # 1&#xff09;一个参数时&#xff0c;参数值为终点&#xff0c;起点取默认值0&#xff…

SpringBootSecurity 简单明了

在autoConfiguration Jar的imports文件里面有 SecurityFilterAutoConfiguration类&#xff0c;这样springboot会自己加载这个类。 该类的作用是向容器内部注入一个RegisterBean叫DelegatingFilterProxyRegistrationBean&#xff0c;由于它同时实现了ServletContextInitializer接…

Redis问题处理

1、jemalloc/jemalloc.h&#xff1a;没有那个文件或目录 解决方法&#xff1a; 正确解决办法(针对2.2以上的版本) 清理上次编译残留文件&#xff0c;重新编译 make distclean && make

【学术小白如何写好论文】文献综述

文章目录 一、前言1.目的2.作用 二、切入角度三、写作方法 一、前言 前言&#xff1a;在撰写这部分的时候&#xff0c;我们首先要明确文献综述的目的是什么&#xff0c;作用是什么。 1.目的 梳理前人研究的脉络找出前人研究的不足 2.作用 让本研究更充实&#xff0c;告诉读者…

路径规划算法:基于蛾群优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于蛾群优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于蛾群优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化算法蛾群…

SpringSecurity实现前后端分离登录token认证详解

目录 1. SpringSecurity概述 1.1 权限框架 1.1.1 Apache Shiro 1.1.2 SpringSecurity 1.1.3 权限框架的选择 1.2 授权和认证 1.3 SpringSecurity的功能 2.SpringSecurity 实战 2.1 引入SpringSecurity 2.2 认证 2.2.1 登录校验流程 2.2.2 SpringSecurity完整流程 2.2.…

翻译的技巧

400字左右的文章中划出5个句子&#xff0c; 30分钟内将其翻译成中文&#xff0c;分值10分。文章的题材大多是有关政治、经济、文化、教育、科普以及社会生活&#xff0c;议论文为主&#xff0c;说明文为辅&#xff0c;结构严谨&#xff0c;逻辑性强&#xff0c;长难句较多。不仅…

基于深度学习的高精度人脸口罩检测识别系统(PyTorch+Pyside6+YOLOv5模型)

摘要&#xff1a;基于深度学习的高精度人脸口罩检测识别系统可用于日常生活中或野外来检测与定位人脸口罩目标&#xff0c;利用深度学习算法可实现图片、视频、摄像头等方式的人脸口罩目标检测识别&#xff0c;另外支持结果可视化与图片或视频检测结果的导出。本系统采用YOLOv5…