【进程间通信:管道】(一)

简介: 【进程间通信:管道】(一)

1 进程间通信介绍

1.1 进程间通信目的

数据传输:一个进程需要将它的数据发送给另一个进程 。

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

1.2 进程间通信发展

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

1.3 进程间通信分类

管道

匿名管道pipe

命名管道

System V IPC

System V 消息队列

System V 共享内存

System V 信号量

POSIX IPC

消息队列

共享内存

信号量

互斥量

条件变量

读写锁

2 管道

2.1 什么是管道

管道是 Unix 中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个 “ 管道".

比如我们常见的命令 | , 我们知道其实我们执行的命令在linux上本质是执行一个进程,管道也分为匿名管道和命名管道,像上面这种没有名字的就叫做匿名管道。

2.2 匿名管道

2.2.1 匿名管道的使用

在文件描述符得时候我们讲过,子进程会继承父进程的文件描述符,但是子进程并不会去拷贝父进程的文件,也就是子进程与父进程其实看到的是同一份文件,这就具备了进程间通信的前提:两个进程看到了同一份资源。我们就能够根据我们的需求来让父子进程完成我们的任务(比如让父进程写文件,子进程从文件中读取)

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

我们可以用pipe函数来帮助我们创建匿名管道(大家一定要注意,使用匿名管道的前提是在父进程创建子进程前就已经把管道打开了,这样子进程才能够继承父进程的文件描述符)

1cf452a6f57e4700b97cb27900d7f096.png


实例代码:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<cerrno>
#include<cstring>
#include<string>
using namespace std;
int main()
{
    int pipefd[2]={0};
    int n=pipe(pipefd);
    if(n<0)
    {
        cout<<"error"<<":"<<strerror(errno)<<endl;
        return 1;
    }
    pid_t id=fork();
    if(id==0)
    {
        //child 子进程读取,父进程写入
        close(pipefd[1]);
        char buffer[1024];
        while(true)
        {
            int n=read(pipefd[0],buffer,9);
            if(n>0)
            {
                buffer[n]='\0';
                cout<<"child :"<<buffer<<endl;
            }
            else if(n==0)
            {
                cout<<"read file end"<<endl;
            }
            else 
            {
                cout<<"read error"<<endl;
            }
        }
        close(pipefd[0]);
        exit(0);
    }
    //parent 子进程读取,父进程写入
    close(pipefd[0]);
    const char* str="hello bit";
    while(true)
    {
        write(pipefd[1],str,strlen(str));
    }
    close(pipefd[1]);
    int status=0;
    waitpid(id,&status,0);
    cout<<"singal:"<<(status&0x7f)<<endl;
    return 0;
}

这样我们就编写完成了一份基本的用匿名管道进行通信的方法。

代码中值得注意的细节有:

  1. 系统规定数组下标为0表示读端,数组下标为1表示写端
  2. 父子进程一个完成写入,一个完成读取,在写入前应当关闭读端,同理在读取前应当关闭写段。

2.2.2 使用匿名管道创建进程池

我们可以用一个匿名管道来做一些比较优雅的事情:比如创建一个进程池,用一个父进程管理多个子进程:

contralProcess.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);
            // 1.3 关闭不要的fd
            close(pipefd[1]);
            // 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
            // 1.3.1 输入重定向,可以不做
            dup2(pipefd[0], 0);
            // 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);
}
int main()
{
    vector<EndPoint> end_points;
    // 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
    createProcesses(&end_points);
    // 2. 我们的得到了什么?end_points
    ctrlProcess(end_points);
    // 3. 处理所有的退出问题
    waitProcess(end_points);
    return 0;
}

Task.hpp:

#pragma once
#include <iostream>
#include <vector>
#include <unistd.h>
#include <unordered_map>
// typedef std::function<void ()> func_t;
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;
};

不知道大家注意到了没有一个问题:我们在创建子进程时先做的工作是先从vector中读取数据来关闭的,这个vector中存放的究竟是什么呢?

我们来思考下:当我们父进程第一次fork后,父进程使用了下标为4的文件描述符,第一个子进程使用了下标为3的文件描述符,父进程通过pipefd[1]向第一个管道里面写入数据,子进程通过pipefd[0]在管道里面读取数据,但是当我们第二次创建子进程的时候子进程会继承父进程的文件描述符,也就是说,第二次的子进程居然也继承了父进程第一次打开的下标为4的文件描述符,那么这样做的危害是什么?如果我们通过先的关闭第一个管道的写端,然后再回收第一个子进程时,第一个子进程会一直阻塞在那里,为什么呢?因为第二个子进程中继承父进程的写端还指向第一个子进程的读端,也就是我们如果只先关闭了第一个管道的写端是不行的,第一个子进程并没有结束,因为他的读端还指向后面所有的子进程,这就导致第一个子进程回收时一直阻塞在那里。后面进程的分析方法同理:


1c429d66f71e45aa8e882a4ca4c8a8b6.png

解决方法有:我们可以先统一将所有管道的写端关闭,然后再一个一个回收。这样所有进程的写端都被关闭了,自然就成功退出了。还可以从最后一个管道的写端开始关闭,边关闭边回收,由于最后一个子进程的读端只指向最后一个管道的写端,所以能够正常退出。

但是这样写终归治标不治本,这时因为fork创建子进程时子进程已经把前面进程的文件描述符给继承下来了,有没有方法在创建子进程是就把继承父进程的文件描述符给关闭了呢?答案是有的,上面我们提到的vector就是能够很好的处理,我们每次创建了子进程后,就把对应的管道的写端给保留下来(保留到vector中),然后每次创建时就先关闭之前继承的文件描述符给关闭就行了。(这里面的关系有点复杂,大家一定要自己下去好好总结)

所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了 “Linux 一切皆文件思想 ” 。


目录
相关文章
|
15天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
6月前
|
存储 负载均衡 Linux
【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(下)
【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(下)
|
6月前
|
消息中间件 Unix Linux
【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(上)
【Linux 系统】进程间通信(匿名管道 & 命名管道)-- 详解(上)
|
2月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
2月前
|
SQL 网络协议 数据库连接
已解决:连接SqlServer出现 provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程【C#连接SqlServer踩坑记录】
本文介绍了解决连接SqlServer时出现“provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程”错误的步骤,包括更改服务器验证模式、修改sa用户设置、启用TCP/IP协议,以及检查数据库连接语句中的实例名是否正确。此外,还解释了实例名mssqlserver和sqlserver之间的区别,包括它们在默认设置、功能和用途上的差异。
|
3月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
161 3
|
3月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
67 0
|
3月前
|
Linux C语言
【C语言】进程间通信之命名管道fifo
【C语言】进程间通信之命名管道fifo
40 0
|
3月前
|
C语言
【C语言】进程间通信之管道pipe
【C语言】进程间通信之管道pipe
78 0
|
3月前
|
Python
Python IPC深度探索:解锁跨进程通信的无限可能,以管道与队列为翼,让你的应用跨越边界,无缝协作,震撼登场
【8月更文挑战第3天】Python IPC大揭秘:解锁进程间通信新姿势,让你的应用无界连接
26 0

热门文章

最新文章

相关实验场景

更多