前引:在 Linux 多进程编程中,进程间通信(IPC)是实现 “进程协作、数据共享、行为同步” 的核心基石。当多个独立进程需要像 “齿轮” 一样精密配合时,匿名管道、命名管道、共享内存、消息队列等 IPC 机制,就是连接它们的关键 “纽带”。本文将从管道原理与操作入手,逐步延伸到进程池编写、共享内存实践,再触及消息队列、信号量的设计逻辑,最终探究内核对 IPC 资源的管理方式,甚至带您思考 “C 语言如何通过技巧实现多态思想与 IPC 的关联”,帮您系统构建 Linux IPC 知识体系!

注意:严重错误与讲解为本篇重点

目录

【一】VIsual Studio Code链接云主机

【二】进程通信介绍

【三】匿名管道

(1)介绍

(2)特点理解

(1)亲缘关系

(2)内存文件

(3)单向性

(4)生命周期

(5)字节流传输

(3)管道情况

【四】匿名管道运用

(1)打开读写端

(2)创建父子进程

(3)然后各自干自己的事

(4)首尾工作

(5)完整代码

【五】打造进程池

(1)创建任务

(2)父进程分发任务

(3)子进程读任务

(4)完成代码

(5)严重错误讲解与修复


【一】VIsual Studio Code链接云主机

之前咱们编辑文档都是采用 Vim,现在为了追求更极致的体验,我们切换为 VS Code,下面是安装以及链接云主机教程:https://tthfc.qichkj.top/code/

(1)首先在官网选择下载版本完成安装,接着打开VS Code的插件商店:搜索Remote - SSH安装

(2)先点击“+”号,然后在搜索框链接云主机:ssh root@公网IP,然后回车,选择第一个路径

(3)然后点击连接

(4)可以在C盘的config文件查看信息

(5)然后输入密码完成连接即可

随后连接云主机之后需要在VS code的插件商店安装下面几个常用的插件:

C/C++编译:

简体中文界面:

主题:

文件图标:

头文件自动包含:

C/C++扩展包:

【二】进程通信介绍

进程通信无非就是进程之间形成数据共享、交互、控制等等,但是有一个问题:进程之间是独立的

实现进程通信那么就需要“操作系统”作为第三方给我们提供接口来完成,就算是操作系统,形成通信的同时保证进程的数据安全,也需要去制定通信规则,目前进程间通信发展出三个规则体系:

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

【三】匿名管道

(1)介绍

什么是管道?将一个进程连接到另一个进程的数据流称为管道,我们今天学习匿名管道

匿名管道:对具有“亲缘关系”(比如父子进程、父孙进程...)进程之间实现通信,它不依赖文件                        名、路径(即不具体的文件),而操作内存文件,仅依赖文件描述符直接通信

由此可见:通过 pipe() 形成的管道,本身就是一个临时缓冲区,配合重定向将一个进程输出弄到                      管道(临时缓冲区),再从管道的读端获取管道内的数据

(2)特点理解
(1)亲缘关系

该种关系我们调用 fork()出来的即具有亲缘关系,但是完全单独的两个进程无法执行匿名管道

(2)内存文件

不是访问磁盘文件,即不需要路径、文件名等信息,只需要文件描述符直接访问内存文件

(3)单向性

例如:父进程通过 pipe()系统调用通过文件描述符打开了一个内存文件:

int pipe(int pipefd[2]);

参数:一个包含两个整数的数组,用于存储管道的读端和写端文件描述符:

  • pipefd[0]:固定为读端,用于通过 read() 系统调用读取管道数据
  • pipefd[1]:固定为写端,用于通过 write() 系统调用向管道写入数据

(理解:你给它一个包含两个整型元素的数组,它用返回值给你填充)

返回值:

  • 成功时返回 0pipefd 数组被正确填充
  • 失败时返回 -1

作用:

pipe() 的本质是让内核创建一个内存缓冲区(管道核心),将两个文件描述符与该缓冲区绑定

即[0]为一个文件描述符,指向该内存文件的读端;[1]为一个文件描述符,指向该内存文件的写端

例如:

此时我们如果创建一个子进程,那么会出现如下情况:父子进程读端与写端都高度重复

因为单向性即让两个进程满足:一个读一个写,例如:

下面是我经过试验总结的结论:

(1)父子进程可以同时进行读写,但是每个进程每次只能选择读或者写

(2)如果一个进程阻塞了(比如写满了,休眠....),另外一个进程可能阻塞(一直等它)

(3)文件缓冲区是有大小的,因此不可能一直循环写入,肯定会阻塞情况

(4)已被读取的数据会被自动清理

(4)生命周期

不管是内存文件还是磁盘文件,都是通过进程来执行,只要进程关闭,对应文件生命周期结束

(5)字节流传输

字节流传输即读和写都是按照单个字节为读写标准的,你写完之后我再读,但可能出现以下情况:

(1)无消息边界:比如你写100个字节是连续的,但是由于读端有限制,每30个字节进行读取

(2)顺序保证:读端根据写端按顺序读取

(3)管道情况

(1)读写正常,但文件缓冲区是空的,导致读端阻塞

(2)读写正常,管道如果被写满(例如:管道存储有大小、缓冲区也有大小),写端就阻塞

(3)读写正常,写端关闭,读端就会读取0,表明读到文件末尾,不会被阻塞

【四】匿名管道运用

现在我们来使用匿名管道,理清逻辑顺序:首先打开vs code来创建文件装代码,指令来执行

(1)打开读写端

通过 pipe()系统调用通过文件描述符打开了一个内存文件:

int pipe(int pipefd[2]);

参数:一个包含两个整数的数组,用于存储管道的读端和写端文件描述符:

  • pipefd[0]:固定为读端,用于通过 read() 系统调用读取管道数据
  • pipefd[1]:固定为写端,用于通过 write() 系统调用向管道写入数据

(理解:你给它一个包含两个整型元素的数组,它用返回值给你填充)

返回值:

  • 成功时返回 0pipefd 数组被正确填充
  • 失败时返回 -1

作用:

pipe() 的本质是让内核创建一个内存缓冲区(管道核心),将两个文件描述符与该缓冲区绑定

即[0]为一个文件描述符,指向该内存文件的读端;[1]为一个文件描述符,指向该内存文件的写端

pipe()系统调用之后,此时父进程的3号下标指向读端,4号下标指向写端

(2)创建父子进程

通过fork()之后,父子进程的读端和写端重叠,我们需要保持单向性,以父写子读为例!

关闭父进程的3号文件符描述下标,关闭子进程的4号描述符下标

(3)然后各干各的事

举例:

父子进程各自执行自己的函数,父进程向文件缓冲区写入,子进程来读

(4)首尾工作

子进程关闭,父进程回收子进程,完成收尾

(5)完整代码
#include<iostream>
#include<unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>

using namespace std;
#define MAX 5951


void Read(int pipefd)
{
    char buff[MAX]={0};
    while(1)
    {
        int pid=read(pipefd,buff,5951);
        if(pid==-1)
        {
            break;
        }
        cout<<buff<<endl;
        sleep(5);
    }
    return;
}
void Write(int pipefd)
{
    //向缓冲区写入内容
    int num=0;
    const char* ptr="Hello Linux";

    while(1)
    {
        int pid=write(pipefd,ptr,strlen(ptr));
        if(pid==-1)
        {
            break;
        }
        cout<<"正在写入"<<num++<<endl;
        sleep(5);
    }
    return;
}

int main()
{
    int pipefd[2]={0};
    //打开内存文件
    int pf= pipe(pipefd);
    if(pf==-1)
    {
        perror("pipe");
        return 1;
    }
    //创建父子进程
    int d=fork();
    if(d==0)//子进程
    {
        //关闭4号描述符下标
        close(pipefd[1]);  
        //执行函数
        Read(pipefd[0]);
        //关闭子进程
        exit(0);
    }
    //父进程
    //关闭3号描述符下标
    close(pipefd[0]);
    //执行函数
    Write(pipefd[1]);
    //回收子进程
    int pid = waitpid(-1,NULL,0);
    if(pid<=0)
    {
        perror("waitpid");
        return 1;
    }
    else
    {
        cout<<"回收成功"<<endl;
    }
    return 0;
}

【五】打造进程池

解释:通过一个父进程创建出一堆的子进程,父进程想给哪个子进程派发任务谁就执行

(通关秘籍:对于这种进程相关的,无非都是使用fork()出子进程,然后系统调用,再统一回收)

大框架:我们先把通用框架弄出来,然后以派发任务为主(注意:多个子进程描述符会逐步改变)

#define MAX 5

int main()
{
    //创建MAX个通信
    for(int i=0;i<MAX;i++)
    {
        //创建一个匿名管道通信
        int pipely[2]={0};
        int pid = pipe(pipely);
        if(pid==-1)
        {
            perror("pipe");
            return 1;
        }
        //分割父子进程
        int pid_t =fork();
        if(pid_t==0)//子
        {
            //子进程关闭写通道
            close(pipely[1]);

            //子进程执行任务

            //退出
            exit(0);
        }
        else if(pid_t>0)//父
        {
            //父进程关闭读通道
            close(pipely[0]);

            //父进程派发任务

            int count=MAX;
            while(count--)
            {
                //回收子进程
                int status;
                int pid=waitpid(-1,&status,0);
                if(pid>0)
                {
                    //如果子进程是被信号杀死
                    if(WIFSIGNALED(status)&&WTERMSIG(status))
                    {
                        //打印信号
                        std::cout<<"子进程"<<getpid()<<"被"<<WTERMSIG(status)<<"号信号杀死"<<std::endl;
                    }
                }
                else
                {
                    std::cout<<"没有子进程退出....."<<std::endl;
                }
            }
        }
        else
        {
            perror("fork");
            return 1;
        }
    }

    return 0;
}
(1)创建任务

我们通过函数指针来创建多个任务,随后放在vector容器里面:存储类型为 void (*)(void)

(2)父进程分发任务
void Write(int pipely)
{
    std::vector<void (*)(void)> task;
    Storage(task);

    srand(static_cast<unsigned int>(time(nullptr)));
    //调用函数写向显示器,注意之前子进程是使用dup2调整了描述符下标的
    int count=rand() % task.size();
    task[count]();
}
(3)子进程读任务
void Read(int pipely)
{
    char buff[200]={0};
    int pid =read(pipely,buff,20);
    if(pid==-1)
    {
        std::cout<<"读取失败!"<<std::endl;
        return;
    }
    std::cout<<"读取到了:"<<buff<<std::endl;
}
(4)完成代码

这里我增加了一点对管道的理解:将显示器输出到管道的写端,子进程再从管道读端读

因为调用 pipe() 实现的管道本身就形成一个临时缓冲区

头文件:

#pragma once

#include<iostream>
#include<unistd.h>
#include <sys/stat.h>
#include <stdio.h>
#include <sys/types.h>
#include<vector>
#include <sys/wait.h>
#include <cstring>
#include <cstdlib>
#include <ctime>

//创建任务函数
void Path1()
{std::cout<<"正在加载游戏"<<std::endl;}
void Path2()
{std::cout<<"请登录游戏"<<std::endl;}
void Path3()
{std::cout<<"正在执行游戏"<<std::endl;}
void Path4()
{std::cout<<"已退出游戏"<<std::endl;}

//创建函数指针
//void (*task_t)(void);
//存储任务
void Storage(std::vector<void (*)(void)>& task)
{
    task.push_back(Path1);
    task.push_back(Path2);
    task.push_back(Path3);
    task.push_back(Path4);
}

函数实现:

首先在下面的代码中是有四处严重错误的,我们先看错误代码,看你是否注意到:

#include"chi_pipeline.h"
#define MAX 5
#define MAX_READ sizeof(void (*)())

void Read(int pipely)
{
    char buff[200]={0};
    int pid =read(pipely,buff,20);
    if(pid==-1)
    {
        std::cout<<"读取失败!"<<std::endl;
        return;
    }
    std::cout<<"读取到了:"<<buff<<std::endl;
}

void Write(int pipely)
{
    std::vector<void (*)(void)> task;
    Storage(task);

    srand(static_cast<unsigned int>(time(nullptr)));
    //调用函数写向显示器,注意之前子进程是使用dup2调整了描述符下标的
    int count=rand() % task.size();
    task[count]();
}

int main()
{
    //创建MAX个通信
    for(int i=1;i<MAX;i++)
    {
        //创建一个匿名管道通信
        int pipely[2]={0};
        int pid = pipe(pipely);
        if(pid==-1)
        {
            perror("pipe");
            return 1;
        }
        //分割父子进程
        int pid_t =fork();
        if(pid_t==0)//子
        {
            //子进程关闭写通道
            close(pipely[1]);

            //子进程执行任务
            Read(pipely[0]);
            //退出
            exit(0);
        }
        else//父
        {
            //父进程关闭读通道
            close(pipely[0]);

            int fd = dup2(pipely[1],1);
            if(fd==-1)
            {
                std::cout<<"更新描述符失败"<<std::endl;
            }
            //父进程派发任务
            Write(pipely[1]);

            //回收子进程
            int status;
            int pid=waitpid(-1,&status,0);
            if(pid>0)
            {
                 //如果子进程是被信号杀死
                 if(WIFSIGNALED(status)&&WTERMSIG(status))
                 {
                    //打印信号
                    std::cout<<"子进程"<<getpid()<<"被"<<WTERMSIG(status)<<"号信号杀死"<<std::endl;
                 }  
            }
            else
            {
                 std::cout<<"没有子进程退出....."<<std::endl;
            }
        }
    }

    return 0;
}
(5)严重错误讲解与修复

(1)第一个是变量命令的问题,抛开变量名重复不说,fork()的返回类型应该是 pid_t 类型

(2)time随机种子的变化应该是 1秒 变化一次,而 for 循环变化太快了,导致每次抽取任务一样

(3)在父进程重定向显示器输出到管道的写端后,应该及时复原,否则出现如下情况:

         这里重定向的本质:终端输出->管道写端,执行第一次管道是没有问题的

         但是如果不复原管道,执行第二次 Write 时,父进程会把数据输出到第一次管道的写端,但           是第一次管道已经被关闭了,就导致数据写不进去

(4)waitpid()的第一次参数,-1 表示回收任意子进程,而如果之前的子进程正在执行任务,可           能会直接被 waitpid 回收,因此第一次参数应该调整为 d(回收当前循环的子进程)

所以我们的代码应该修改为如下这样:

效果展示:

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐