当前位置: 首页 > news >正文

【Linux通信篇】深入理解进程间通信——管道

---------------------------------------------------------------------------------------------------------------------------------

每日鸡汤:找一个对的人,然后好好去爱。一个你跟他在一起,然后又可以舒舒服服做自己的人。

---------------------------------------------------------------------------------------------------------------------------------

目录

一:进程间通信介绍

1.1、进程间通信是什么?

1.2、为什么要进程间通信?【目的】

1.3、如何实现进程间通信?【怎么办】

二:匿名管道

2.1 管道原理

2.2 管道接口使用

l2.3 管道编码实现

2.4 管道特征

2.5 管道的四种情况

2.6 管道的使用场景

2.6.1 命令行管道

2.6.2 基于管道的简易进程池

三:命名管道

3.1 mkfifo 函数接口

3.2 命名管道编码实现

四:结语


一:进程间通信介绍

1.1、进程间通信是什么?

进程间通信是两个或者多个进程实现数据层面的交互。因为进程独立性的存在,就导致了进程通信的成本比较高。

1.2、为什么要进程间通信?【目的】

一个进程向另一个进程发送基本数据、发送命令、一些协同、通知等等。就是要让不同的进程通信起来。

目的:

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

1.3、如何实现进程间通信?【怎么办】

因为两个进程天然都是独立的。那么要让它们通信起来,该怎么办呢?首先进程间通信的本质是必须要让不同的进程看到同一份资源。所谓资源,就是特定形式的内存空间。为了保证进程的独立性,一般就是操作系统来提供这个资源。进程要访问这个空间,进行通信,本质就是访问操作系统。进程代表的是用户,但是操作系统并不相信用户,这就使得我们在写代码的过程中不能直接去访问操作系统提供的资源,而是通过系统调用接口去创建、释放这个资源。因为操作系统内部可能会存在多个进程之间需要通信,所以这些资源页一定是很多分的。故操作系统需要对这些资源管理起来——"先描述,再组织",一般的操作系统,会有一个独立的通信模块,它隶属于文件系统——IPC模块。进程间通信是有标准的(system V标准和 posix 标准)system V 标准主要针对的是本机内部通信,而 posix 标准主要针对的是网络通信,但是凡是发展都是有一个过程的,在这两个标准没有出来之前,进程间通信的方式都是通过基于文件级别的通信方式【管道通信】。

  • 管道:匿名管道pipe、命名管道。
  • System V IPC:System V 消息队列、System V 共享内存、System V 信号量。
  • POSIX IPC:消息队列、共享内存、信号量、互斥量、条件变量、读写锁。

管道是 Unix 中最古老的进程间通信的方式。把一个进程连接到另一个进程的一个数据流称作管道。

二:匿名管道

2.1 管道原理

 如图,父进程创建子进程来演示管道的原理。因为进程间通信的本质前提就是需要先将不同的进程看到同一份资源,而管道本质就是一个内存级别的文件。即父进程创建一个子进程,那么父子进程就能看到同一份文件了,父进程通过往缓冲区内写数据,之后子进程再读取缓冲区中的数据,即可实现进程之间的通信。

小Dis:一个文件内部含有 int cnt 引用计数这一变量,所以当两个进程指针指向同一份文件时,一个进程关闭,并不会影响另一个进程交互该文件。

这只是管道通信的大致情况,那么实际上的管道通信是怎样的呢?

 首先,父进程使用读写方式分别打开同一个文件,如图,3号文件描述符以读的方式打开该文件,而4号文件描述符以写的方式打开该文件。之后父进程创建子进程,子进程继承了父进程一系列的文件资源。因为都是同一个文件,那么也就只有一个文件缓冲区,之后在该唯一的缓冲区内进行读写文件数据。父进程可以通过4号文件描述符向文件中进行写入,之后子进程再通过3号文件描述符向文件中进行读取数据。此时父子进程就实现了数据的传输交互。一般在进行通信之前,需要把不需要的文件描述符关闭,上图中,将父进程的不用的3号文件描述符关闭,将子进程的不用的4号文件描述符关闭。但是我们发现这种通信方式只适用于单向通信,所以我们就将它命名为管道。

注意:这种通信方式只适用于具具备血缘关系的进程之间的通信。

2.2 管道接口使用

使用 pipe 函数系统调用接口来创建管道。

int pipe(int fd[2])

参数:输出型参数,具有两个元素的整形数组。将 两个文件描述符数字返回给用户。即 fd:文件描述符数组,其中fd[0]表示读端,fd[1]表示写端。

返回值:管道创建成功返回 0,失败返回 -1,设置错误码。

l2.3 管道编码实现

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

static const int N=2;

int main()
{
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if(n < 0)   return errno;
    std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;

    return 0;
}

说明 3 号文件描述符代表的是读端,4号文件描述符代表的是写端。

接下来让父进程创建子进程来进行父子进程通信。

#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

static const int N=2;

// child
void Writer(int wfd)
{
    std::string s = "Hello, I am child";
    pid_t self = getpid();
    int number = 0;
    char wbuffer[1024];
    while (true)
    {
        // 构建发送字符串
        wbuffer[0] = 0;
        snprintf(wbuffer, sizeof(wbuffer), "%s-%d-%d", s.c_str(), self, number++);
        // 发送给父进程
        write(wfd, wbuffer, strlen(wbuffer));
        sleep(1);
    }
}

// father
void Reader(int rfd)
{
    char rbuffer[1024];
    while (true)
    {
        rbuffer[0] = 0;
        ssize_t n = read(rfd, rbuffer, sizeof(rbuffer));
        if(n > 0)
        {
            rbuffer[n] = 0;
            std::cout << "father get a message from child: [" << getpid() << "]@ " << rbuffer << std::endl;
        }
        else if(n == 0)
        {
            std::cout << "father read file done" << std::endl;
            break;
        }
        else
            break;
    }
    
}

int main()
{
    int pipefd[N] = {0};
    int n = pipe(pipefd);
    if(n < 0)   return errno;
    
    // std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;

    // 让child 写,father 读
    pid_t id = fork();
    if(id < 0)
    {
        perror("fork");
        return errno;
    }
    else if(id == 0)
    {
        // child
        close(pipefd[0]);       // 子进程关闭读端

        Writer(pipefd[1]);      // 子进程的写操作

        close(pipefd[1]);
        exit(0);
    }
    // father
    close(pipefd[1]);       // 父进程关闭写端

    Reader(pipefd[0]);  // 父进程的读操作

    pid_t rid = waitpid(id, nullptr, 0);    // 阻塞等待
    if(rid < 0)
    {
        perror("waitpid");
        return errno;
    }
    close(pipefd[0]);   // 通信结束,关闭读端

    return 0;
}

我们在代码中只让子进程 sleep,父进程没有 sleep,并且发现父进程并没有一直在读取管道中的数据,即没有出现子进程正在写的同时,父进程读的情况。而是子进程写完之后父进程再来读取数据。所以,父子进程是会进行协同的,会有同步和互斥来保证管道文件的数据安全。

小Dis:管道是面向字节流的。

2.4 管道特征

  • 具有血缘关系的进程进行进程间通信。
  • 管道只能单向通信。
  • 父子进程是会进程协同的,进行同步与互斥,以保证管道文件中数据的安全。
  • 管道是面向字节流的。
  • 管道是基于文件的,而文件的生命周期是随进程的,所以进程如果退出了,管道文件也会被自动关闭释放掉。

2.5 管道的四种情况

  • 读写端正常,管道如果为空,读端就要堵塞。
  • 读写端正常,管道如果被写满,写端就要被阻塞。
  • 读端正常,写端关闭,读端就会读到0,表示读到了文件(pipe)结尾,不会被阻塞。
  • 写端正常,读端关闭,操作系统会通过 13 号信号把正在写入的进程杀掉。

2.6 管道的使用场景

2.6.1 命令行管道

这个管道与我们以前学习的 | 管道有什么关系呢?其实命令行中的 | 就是通过 pipe 来创建的。

2.6.2 基于管道的简易进程池

所谓进程池,就是提前将一个个的进程当作资源准备好,在需要的时候直接分配而不需要再调用 fork 函数创建子进程了,因为每次的调用fork函数创建子进程的工作量是很大的。

#include <iostream>
#include <vector>
#include <string>
#include <ctime>
#include <errno.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include "Task.hpp"

static const int processnum = 10; // 要创建的进程数量
std::vector<task_t> tasks;

// 先描述
class Channel
{
public:
    Channel(int cmdfd, pid_t slaverid, const std::string &processname)
        : _cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {
    }

public:
    int _cmdfd;               // 发布任务的文件描述符(负责写)
    pid_t _slaverid;          // 子进程的pid(负责读)
    std::string _processname; // 子进程的名字
};

// 子进程执行的功能,做的任务
void slaver()
{
    while (true)
    {
        int cmdcode = 0;
        int n = read(0, &cmdcode, sizeof(int));
        if(n == sizeof(int))
        {
            // 执行cmdcode对应的任务列表
            std::cout << "child-[" << getpid() << "]-slaver say@ get a cmdcode: " << cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size())
                tasks[cmdcode]();
        }
        else if(n == 0)
            break;
    }
    
}

// 初始化
void InitProcessPool(std::vector<Channel>* channels)
{
    for (size_t i = 0; i < processnum; i++)
    {
        // 创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        if(n < 0)
        {
            perror("pipe");
            return;
        }
        // 此时 pipefd[0] = 3, pipefd[1] = 4
        // 创建子进程
        pid_t id = fork();
        if(id == 0)
        {
            // child
            close(pipefd[1]);   // 关闭写端
            
            dup2(pipefd[0], 0); //标准输入0号文件描述符指向 读端管道文件
            slaver();           // 子进程执行的函数功能
            exit(0);
        }
        // father
        close(pipefd[0]);       // 关闭读端
        //添加channel字段
        std::string name = "process-" + std::to_string(i);
        channels->push_back(Channel(pipefd[1], id, name));
    }
}

// 控制子进程,给自己才能发布任务
void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 更新野区           #" << std::endl;
    std::cout << "# 3. 刷新金币             4. 获取点Juana        #" << std::endl;
    std::cout << "# 5.释放技能              0. 退出               #" << std::endl;
    std::cout << "################################################" << std::endl;
}
void CtrlSlaver(std::vector<Channel>& channels)
{
    int which = 0;
    int cnt = 10;
    while (cnt--)   // 轮询10次
    {
        // int select = 0;
        // Menu();
        // std::cout << "Please Enter@: ";
        // std::cin >> select;
        // if(select <= 0 || select > 5) break;
        // // 选择任务
        // int cmdcode = select-1;

        // 选择任务
        int cmdcode = rand() % tasks.size();

        // 轮询进程
        std::cout << "father say: cmdcode: " << cmdcode << ", already sendto: "
            << channels[which]._slaverid << ", processname: " << channels[which]._processname << std::endl;
        // 发送任务
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));
        which++;
        which %= channels.size();
        sleep(1);
    }
}

// 清理收尾
void QuitProcess(std::vector<Channel>& channels)
{
    for(auto& ch : channels)
    {
        close(ch._cmdfd);
    }
    sleep(3);
    for(auto& ch : channels)
    {
        waitpid(ch._slaverid, nullptr, 0);  // 等待子进程
    }
}


int main()
{
    LoadTask(&tasks);

    srand(time(0) ^ getpid() ^ 1023);
    // 再组织
    std::vector<Channel> channels;
    // 初始化
    InitProcessPool(&channels);

    // 控制子进程,给自己才能发布任务
    CtrlSlaver(channels);

    // 清理收尾
    QuitProcess(channels);
    return 0;
}

三:命名管道

管道应用(匿名管道)只能是具有血缘关系的进程才可进行进程间通信,那么如果是两个毫不相关的进程进行进程间通信呢?——命名管道。

进程间通信的前提就是先让两个不同的进程看到同一份资源。即看到同一份文件。

所以,命名管道通信是通过使用路径+文件名的方案让不同的进程看到同一份文件资源,进而实现不同进程间的通信的。故有路径+文件名。即叫做命名管道。

3.1 mkfifo 函数接口

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • 参数 pathname:文件路径
  • 参数 mode:命名管道的文件权限 

3.2 命名管道编码实现

// comm.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define FILE_FIFO "./myfifo"
#define MODE 0666

enum{
    FIFO_CREATE_ERR = 1,
    FIFO_DELETE_ERR,
    FIFO_OPEN_ERR
};

class Init
{
public:
    Init()
    {
        // 创建管道
        int n = mkfifo(FILE_FIFO, MODE);
        if(n < 0)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        // 删除管道
        int m = unlink(FILE_FIFO);
        if(m < 0)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};
// client.cc
#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    int fd = open(FILE_FIFO, O_WRONLY);     // 以写方式打开管道文件
    if(fd < 0)
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }
    cout << "client open fifo_file done" << endl;

    string line;
    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, line);

        write(fd, line.c_str(), line.size());
    }
    close(fd);
    return 0;
}

 客户端主要完成的工作是:打开信道——开始通信——关闭信道。

#include "comm.hpp"

using namespace std;

int main()
{
    // 创建信道
    Init init;
    // 打开信道
    int fd = open(FILE_FIFO, O_RDONLY);     // 只读的方式打开
    if(fd < 0)
    {
        cout << "server open fifo_file error" << endl;
        exit(FIFO_OPEN_ERR);
    }
    cout << "server open fifo_file success" << endl;

    // 开始通信
    while (true)
    {
        char buffer[1024] = {0};
        int x = read(fd, buffer, sizeof(buffer));
        if(x > 0)
        {
            buffer[x] = 0;
            cout << "I am server, client say# " << buffer << endl;
        }
        else if(x == 0)
        {
            cout << "client quit, me too" << endl;
            break;
        }
        else
            break;
    }
    close(fd);
    // 结束之后自动退出管道
    return 0;
}

服务端主要完成的工作是:创建信道——打开信道——开始通信——关闭信道——删除信道。

四:结语

今天的分享到这里就结束了,如果觉得文章还可以的话,就一键三连支持一下欧。各位的支持就是捣蛋鬼前进的动力。

相关文章:

  • 第八课:性能优化与高并发处理方案
  • Debian二次开发一体化工作站:提升科研效率的智能工具
  • NVIDIA显卡驱动、CUDA、cuDNN 和 TensorRT 版本匹配指南
  • 【大模型】WPS 接入 DeepSeek-R1详解,打造全能AI办公助手
  • 【实战篇】【DeepSeek 全攻略:从入门到进阶,再到高级应用】
  • 《几何原本》命题I.23
  • MySQL表的内外连接
  • 每日一题——三道链表简单题:回文,环形合并有序
  • 【STM32】ADC功能-单通道多通道(学习笔记)
  • 【网络编程】WSAAsyncSelect 模型
  • Manus 与鸿蒙 Next 深度融合:构建下一代空间计算生态
  • QwQ-32B 开源!本地部署+微调教程来了
  • 当前主流的大模型训练与推理框架的全面汇总
  • 同步,异步,并发,并行
  • [AtCoder Beginner Contest 396] E - Min of Restricted Sum
  • python下比pygame启动更快的MP3播放方法~
  • Remosaic 算法
  • 打造私人专属AI = 个人知识库 + 本地化部署deepseek模型 / deepseek官方模型(CherryStudio版)
  • 洛谷 P2234:[HNOI2002] 营业额统计 ← STL set
  • STM32DMA串口传输实验(标准库)
  • 常熟港口建设费申报网站/怎么做好seo推广
  • 手机云电脑/seo下拉优化
  • 适合大学生做兼职的网站有哪些/谷歌sem推广
  • 我想阻止一个网站要怎么做/站长工具使用
  • 建筑做文本网站/网络营销课程报告
  • 安陆网站建设/免费外贸接单平台