Linux进程间通信【匿名管道】

news2024/12/23 19:12:42

✨个人主页: 北 海
🎉所属专栏: Linux学习之旅
🎃操作环境: CentOS 7.6 阿里云远程服务器

成就一亿技术人


文章目录

  • 🌇前言
  • 🏙️正文
    • 1、进程间通信相关概念
      • 1.1、目的
      • 1.2、发展
      • 1.3、分类
    • 2、什么是管道?
    • 3、管道的工作原理
    • 4、匿名管道的创建与使用
      • 4.1、pipe 函数
      • 4.2、实例代码演示
      • 4.3、管道读写规则
    • 5、管道的特点
    • 6、管道的四种特殊场景
      • 6.1、场景一
      • 6.2、场景二
      • 6.3、场景三
      • 6.4、场景四
    • 7、匿名管道的大小
    • 8、匿名管道实操-进程控制
      • 8.1、逻辑设计
      • 8.2、具体功能实现
        • 1、创建一批进程及管道
        • 2、任务类创建及任务等待
        • 3、子进程控制
        • 4、子进程回收
      • 8.3、效果演示
      • 8.4、注意事项
      • 8.5、完整源码
  • 🌆总结


🌇前言

进程间通信简称为 IPC(Interprocess communication),是两个不同进程间进行任务协同的必要基础。进行通信时,首先需要确保不同进程之间构建联系,其次再根据不同的使用场景选择不同的通信解决方案,本文主要介绍的通信解决方案为 匿名管道

通信


🏙️正文

1、进程间通信相关概念

在正式学习 匿名管道 之前,需要简单了解一下通信的相关概念

1.1、目的

进程间通信主要有以下四个目的:

  1. 数据传输不同进程间进行数据传输,比如此时我写的博客数据正在源源不断的上传至 CSDN 服务器中
  2. 资源共享多个进程之间需要共享资源,假设每个用户都是独立的进程,那么整个 C 站就是一个被共享的资源,用户之前可以共享其技术资源
  3. 事件通知一个进程向其他进程发送消息,通知处理相关事宜,比如 子进程终止时,需要通知父进程,回收其资源
  4. 进程控制有些进程需要起到 管理者 的作用,于是需要与被管理进程之间构建通信关系,进程任务下达及进程控制,并对进程状态进行实时监视

其实进程间通信的最终目的就是 打破各个独立进程之前的壁垒,进行任务协同

就好比 大航海时期 的冒险家们,克服困难、勇于开拓,打破了不同大陆间的隔离状态,最终将世界连为一个 “整体”,人类文明迎来了高速发展时期

  • 让世界连成一整体,开始了全球化发展
  • 有利于讲文明传播到世界各地,促进了世界各地的交流
  • 帮助不发达的地方引进新的文明成果

大航海时期

进程间具有独立性,这是原则
让进程间可以更好的协同工作,这是目的

因此进程间通信的本质就是 让不同的进程看到同一份 “资源”

  • 其中的 资源OS 直接或间接提供

无论后续的哪种进程间通信的解决方案,都要解决以下两个问题:

  1. 想办法让不同的进程看到同一份资源
  2. 让其中一方写入,另一方读取,完成通信;至于通信的目的及后续工作,需要结合具体场景分析

1.2、发展

进程间通信的发展可以简单概况为以下三个时期:

  • 管道时期(古老的通信方式)
  • System V 标准时期(本地化进程间通信)
  • POSIX 标准时期(网络中进程间通信)

管道可以说是十分古老且简单了,后来新出的 System V 标准丰富了进程间通信的方式,但奈何无法满足网络中的进程间通信需求,于是诞生了更好的 POSIX 标准

管道适合深入学习,探究进程间通信时的原理及执行流程

System V 标准如今比较少用了,但其通信速度极快的共享内存还是值得深入学习的

POSIXUnix 系统的一个设计标准,很多类 Unix 系统也在支持兼容这个标准,如 Linux , POSIX 标准具有跨平台性,就连 Windows 也对其进行了支持,后续学习 同步与互斥 时,所使用的信号量等都是出自 POSIX 标准,这是进程间通信的学习重点

POSIX 标准支持网络中通信,比如 套接字(socket 就在此标准中

1.3、分类

根据不同发展时期的标准,可以将进程间通信的解决方案划分为以下几种:

管道:

  • 匿名管道
  • 命名管道

System V 标准:

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

POSIX 标准:

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

显然,随着时代进步,技术也在不断迭代发展,新标准在替代旧标准时,也必然新增时代特色需求,比如 上世纪末的网络;如今人工智能的迅猛发展也给传统从业者敲响了警钟,只有在自我发展中寻求变革,才能 hold 住新时代发展的浪潮


2、什么是管道?

管道是 Unix 系统 IPC (进程间通信)中 最古老 的方式,其历史最早可追溯至 1964年10月11日

管道的历史
出自 《UNIX PIPES 管道原稿》 — 陈皓

在命令行中输入 | 即可使用管道

创建两个睡眠时间较长的 后台进程

sleep 10000 | sleep 20000 &

注:& 表示令当前进程变为后台进程

图示

可以看出,两个 sleep 进程的 PPID 一致,同时 PID 连续,因此这两个进程是兄弟关系

管道分为 匿名管道命名管道,两者绝大部分原理、特点都一致,本文主要介绍 匿名管道,同时适用于 命名管道 的知识点统一称为 管道

Linux 中一切皆文件,所以管道本质上就是一个文件


3、管道的工作原理

管道的工作原理其实很简单:打开一个文件,让两个进程分别享有读端与写端 fd,对文件进行操作即可

图解

命名管道和匿名管道基本原理都差不多,但命名管道更强大,能实现两个毫不相干的进程间通信

具体在 OS 中的体现:在文件的结构体 files_struct 中,存在一个特殊的成员 struct file *fd_array[],这是一个指针数组,其中存储的是指向不同文件的指针

//Linux内核源码(部分)
struct files_struct {
  /*
   * read mostly part
   */
	atomic_t count;
	struct fdtable __rcu *fdt;
	struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
	spinlock_t file_lock ____cacheline_aligned_in_smp;
	int next_fd;
	unsigned long close_on_exec_init[1];
	unsigned long open_fds_init[1];
	struct file __rcu * fd_array[NR_OPEN_DEFAULT];	//文件指针数组
};

此时父进程可以打开匿名管道文件,fork 子进程后,子进程继承原有的 文件系统 关系,与父进程共享同一份文件资源,然后父子进程分别关闭 读端与写端,实现匿名管道的单向关系,即可正常进行通信

具体流程:

  1. 父进程创建匿名管道,同时以读、写的方式打开匿名管道,此时会分配两个 fd
  2. fork 创建子进程,子进程拥有自己的进程系统信息,同时会继承原父进程中的文件系统信息,此时子进程和父进程可以看到同一份资源:匿名管道 pipe
  3. 因为子进程继承了原有关系,因此此时父子进程对于 pipe 都有读写权限,需要确定数据流向,关闭不必要的 fd,比如父进程写、子进程读,或者父进程读、子进程写都可以

图示

注意:

  • fork 创建子进程后,子进程会继承原父进程中的文件系统信息,这也就是父子进程都会同时向屏幕打印信息的原理,因为此时它们操作的是同一个文件!
  • 父进程需要以读写的方式打开匿名管道 pipe,这样子进程在继承时,才不会发生权限丢失
  • 创建出的匿名管道文件 pipe 虽然属于文件系统,但它是一个特殊文件,一个由 OS 提供的纯纯的内存文件,不需要将数据冲刷至磁盘中,只需要承担进程间通信任务即可
  • 管道是一种半双工、单流向的通信方式,因为 pipe 只有一个缓冲区,所以这种方式才被叫做 管道通信

4、匿名管道的创建与使用

4.1、pipe 函数

匿名管道是通过 pipe 函数创建的,其函数原型如下所示

图示

#include <unistd.h>

int pipe(int pipefd[2]);

关于 pipefd 数组

数组元素含义
pipefd[0]表示 匿名管道的 读端
pipefd[1]表示 匿名管道的 写端

巧记:

  • pipefd[0] -> 0 -> 嘴巴 -> 读书 -> 读端
  • pipefd[1] -> 1 -> 钢笔 -> 写字 -> 写端

关于返回值:创建匿名管道成功,返回 0,失败返回 -1,并设置错误码

实际在使用此函数时,需要先创建好大小为 2pipefd 数组,然后将其传入函数,成功创建匿名管道后,pipefd 数组中存储的就是 匿名管道的读端和写端 fd

4.2、实例代码演示

下面通过一个简单的程序,演示 匿名管道函数 pipe 的使用

使用匿名管道步骤

  • 创建匿名管道
  • 创建子进程
  • 关闭不需要的 fd
  • 开始通信
#include <iostream>
#include <cassert>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int main()
{
    // 1、创建匿名管道
    int pipefd[2]; // 数组
    int ret = pipe(pipefd);
    assert(ret == 0);
    (void)ret; // 防止 release 模式中报警告

    // 2、创建子进程
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程内
        close(pipefd[1]); // 3、子进程关闭写端

        // 4、开始通信
        char buff[64]; // 缓冲区
        while (true)
        {
            int n = read(pipefd[0], buff, sizeof(buff) - 1);    //注意预留一个位置存储 '\0'
            buff[n] = '\0';

            if (n >= 5 && n < 64)
            {
                // 读取到了信息
                cout << "子进程成功读取到信息:" << buff << endl;
            }
            else
            {
                // 未读取到信息
                if (n == 0)
                    cout << "子进程没有读取到信息,通信结束!" << endl;
                // 读取异常(消息过短)
                else
                    cout << "子进程读取数据量为:" << n << " 消息过短,通信结束!" << endl;
                break;
            }
        }

        close(pipefd[0]); // 关闭剩下的读端
        exit(0);          // 子进程退出
    }

    // 父进程内
    close(pipefd[0]); // 3、父进程关闭读端

    char buff[64];

    // 4、开始通信
    srand((size_t)time(NULL)); // 随机数种子
    while (true)
    {
        int n = rand() % 26;
        for (int i = 0; i < n; i++)
            buff[i] = (rand() % 26) + 'A'; // 形成随机消息
        buff[n] = '\0';                    // 结束标志

        cout << "=============================" << endl;
        cout << "父进程想对子进程说: " << buff << endl;
        write(pipefd[1], buff, strlen(buff)); // 写入数据

        if (n < 5)
            break; // 消息过短时,不写入

        sleep(1);
    }

    close(pipefd[1]); // 关闭剩下的写端

    // 父进程等待子进程结束
    int status = 0;
    waitpid(id, &status, 0);

    // 通过 status 判断子进程运行情况
    if ((status & 0x7F))
    {
        printf("子进程异常退出,core dump: %d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
    }
    else
    {
        printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
    }

    return 0;
}

图解

站在 文件描述符 的角度理解上述代码:

图解

站在 内核(管道本质) 的角度理解上述代码:

图解

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

4.3、管道读写规则

管道是一种 半双工、单向流 的通信方式,因此在成功创建匿名管道后,需要两个待通信的进程都能获得同一个 pipefd 数组

这就是匿名管道比较特殊的地方了:匿名管道只支持具有血缘关系的进程通信,如 父子进程、兄弟进程等,因为只有 继承 了,才能共享到 同一个 pipefd 数组

当通信双方都获得 pipefd 数组后,需要根据情况关闭不需要的 fd,确保 单流向 的原则

注:命名管道可以支持不具有血缘关系进程间通信

关于匿名管道还有一个函数:pipe2 (了解),比 pipe 函数多一个参数2 flags,可以使匿名管道在发生特殊情况时,作出不同的动作,当 flags0 时,pipe2 等价于 pipe

管道的读写规则:

PIPE_BUF 为管道大小,Linux 中为 4096 字节

  • 当要写入的数据量不大于 PIPE_BUF 时,Linux 将保证写入的原子性
  • 当要写入的数据量大于 PIPE_BUF 时,Linux 将不再保证写入的原子性

原子性:不存在中间状态,确保数据的安全性


5、管道的特点

管道 主要有以下几个特点:

1.单向通信,管道是半双工的一种特殊情况

  • 管道就像单行道,只允许数据单向流通,即通知,如果想要实现两个进程间相互进行通信,需要创建两条管道,管道1:父进程写,子进程读;管道2:子进程写,父进程读

图解

2.管道的本质是文件,因为 fd 的生命周期随进程而终止,所以管道的生命周期也是随着进程而结束的

  • 当进程终止运行时,管道资源会被 OS 回收

3.匿名管道常用来进行具有 “血缘” 关系的进程,进行进程间通信(常用于父子进程间通信)

  • pipe 打开管道,并不清楚管道的名字等信息,这种管道称为 匿名管道,因此 匿名管道 只能用于有血缘关系的进程 IPC,因为 需要通过 fork 继承匿名管道信息

4.在管道中,写入读取 的次数并不是严格匹配的,此时读写次数没有强相关关系,管道是面向字节流读写的

  • 面向字节流读写又称为 流式服务:数据没有明确的分割,不分一定的报文段;与之相对应的是 数据报服务:数据有明确的分割,拿数据按报文段拿
  • 不论写端写入了多少数据,只要写端停止写入,读端都可以将数据读取

5.具有一定的协同能力,让 读端写端 能够按照一定的步骤进行通信(自带同步机制)

  • 当读端进行从管道中读取数据时,如果没有数据,则会阻塞,等待写端写入数据;如果读端正在读取,那么写端将会阻塞等待读端,因此 管道自带 同步与互斥 机制

可以简单总结为:

  1. 管道是半双工通信
  2. 管道生命随进程而终止
  3. 匿名管道只支持具有血缘关系的进程间通信,而命名管道无所谓
  4. 管道提供的是流式数据传输服务
  5. 管道自带 同步与互斥 机制

6、管道的四种特殊场景

管道还存在四种特殊场景:管道为空、管道为满、写端关闭、读端关闭,四种场景对应四种不同的特殊情况,都可以通过代码进行演示

注意: 当前大部分场景中,子进程为读端,父进程为写端

6.1、场景一

父进程不写,此时管道为空,子进程尝试读取

伪代码段
// 父进程不写(空),子进程读

//子进程(尝试读取)
int cnt = 1;
while (true)
{
    char ch;
    read(pipefd[0], &ch, 1);
    cout << "已读取 " << cnt++ << " 字节的数据" << endl;
}

//父进程(不写)
while (true) {}

图解
结果:因为管道为空,因此子进程无法读取,即 读端阻塞

只有当写端写入数据后,读端才能正常读取

6.2、场景二

父进程不断写入,直到管道写满,子进程不读取

伪代码段
//父进程写(满),子进程不读

//子进程(不读)
while (true) {}

//父进程(不断写入)
int cnt = 1; // 计数器
while (true)
{
    char ch = 'x';
    write(pipefd[1], &ch, 1); // 写入数据
    cout << "已写入 " << cnt++ << " 字节的数据" << endl;
}

图解
结果:在一段时间后,管道被写满,写端无法写入数据,进入阻塞状态

只有当读端尝试将管道中的数据读走一部分后,写端才能继续写入

形象化理解
管道为空:垃圾桶为空时,你不会去到垃圾(写入),因为没有垃圾,需要等有垃圾了才去倒(读端阻塞)
管道为满:垃圾桶中的垃圾装满时,无法再继续扔垃圾(写端阻塞),需要等把垃圾倒了(读取),才能继续扔垃圾

图示

6.3、场景三

在通信的过程中,关闭写端,只保留读端

伪代码段
//写端写入一段信息后,就关闭

//子进程正常读取,并且对读取到的数据量进行判断
char buff[64];
while(true)
{
    int n = read(pipefd[0], buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if(n == 0)
        cout << "写端已关闭,读取数据量为: " << n << " 字节" << endl;
    else
        cout << "成功读取到信息: " << buff << endl;

    sleep(1);
}

//父进程只写入一次数据,然后关闭写端
char buff[] = "Hello pipe!";
write(pipefd[1], buff, strlen(buff)); // 写入数据

close(pipefd[1]); // 关闭剩下的写端

图解
结果:关闭写端后,读端会将匿名管道中的数据读取完后,再读,会读到 0,表示已读到文件末尾

如何理解?

  • 因为管道是单流向通信,写端都关闭了,证明不会再有数据写入,因此当读端把剩余数据都读取后,每次都是读取 0 字节数据,表明此时已经读到了结尾,读端也可以结束读取了

6.4、场景四

在通信过程中,关闭读端,只保留写端

注:这里将角色变换一下,方便父进程捕捉到子进程的退出信号

切换:父进程 -> 读端,子进程 -> 写端

伪代码段
//读端读取一段时间后,就关闭

//子进程不断写入
while (true)
{
    char buff[] = "Hello pipe!";
    write(pipefd[1], buff, strlen(buff)); // 写入数据
    sleep(1);
}

//父进程在读取五次信息后,就终止读取,关闭读端
char buff[64];
int cnt = 1;
while (true)
{
    int n = read(pipefd[0], buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if (n == 0)
        cout << "写端已关闭,读取数据量为: " << n << " 字节" << endl;
    else
        cout << "成功读取到信息: " << buff << endl;

    // 读取五次后,关闭读端
    if (cnt++ == 5)
        break;
}

close(pipefd[0]);

// 父进程等待子进程结束
int status = 0;
waitpid(id, &status, 0);
printf("子进程异常退出,core dump: %d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));

结果
结果:OS 不允许任何浪费资源的行为存在,如果关闭了读端,那么证明写端写了也没有,即没有存在的意义,于是 OS 会发出 13 号信号,终止写端进程

通过指令查看信号表

kill -l

结果

以上就是管道的四种特殊场景,不仅适用于匿名管道,同时也适用于命名管道


7、匿名管道的大小

既然管道能被写满,那么管道的大小究竟是多少?

一、通过 man 手册查询相关信息

man 7 pipe

接着输入 /pipe capacity 即可搜索出管道的大小

结果

文档解释:Linux 2.6.11 之前,管道大小为一个系统页的大小(比如在 i386 平台中,管道大小为 4096 字节,即 4kb),从 Linux 2.6.11 开始,管道大小的容量统一为 65536 字节,即 64kb

因为在 Linux 2.6.11 版本中,对管道进行更新,采取了新的解决方案

图示
原文链接:Circular pipes

可以通过指令查看当前系统的内核版本号

uname -a

图示

二、通过指令查看当前系统资源的限制情况

ulimit -a

图示
当前系统中,限制单条管道大小为 512 * 8 = 4096 字节

可以前往 /usr/src/kernels/内核版本信息/include/linux/pipe_fs_i.h 这个文件中,查看当前系统的 管道条目数,比如我当前的系统中,管道条目数为 16,因此管道的大小上限为 4096 * 16 = 65536 字节

图示

此时可以猜测:新的管道解决方案中,为所有的管道分配了一块定额空间,可用的 16 条管道中,可以根据自己的需要,获取大小,极大提高了效率

三、通过程序验证

这个前面就已经验证过了,不断往管道中写数据,直到管道被写满

每次写入 1 字节的数据,可以看到最终写了 65536 字节的数据

图解

总之,从 Linux 2.6.11 版本开始,管道大小上限为 64kb


8、匿名管道实操-进程控制

匿名管道作为 IPC 的其中一种解决方案,那么肯定有它的实战价值

场景:父进程创建了一批子进程,并通过多条匿名管道与它们链接,父进程选择某个子进程,并通过匿名管道与子进程通信,并下达指定的任务让其执行

8.1、逻辑设计

首先创建一批子进程及匿名管道 -> 子进程(读端)阻塞,等待写端写入数据 -> 选择相应的进程,并对其写入任务编号(数据)-> 子进程拿到数据后,执行相应任务

图示

8.2、具体功能实现

下面来看看具体功能实现(部分细节可能未展示,详细实现可以看源码)

1、创建一批进程及管道

创建一批进程及管道

  • 首先需要先创建一个包含进程信息的类,最主要的就是子进程的写端 fd,这样父进程才能通过此 fd 进行数据写入
  • 循环创建管道、子进程,进行相应的管道链接操作,然后子进程进入任务等待状态,父进程将创建好的子进程信息注册
  • 假设子进程获取了任务代号,那么应该根据任务代号,去执行相应的任务,否则阻塞等待

注意: 因为是创建子进程,所以存在关系重复继承的情况,此时应该统计当前子进程的写端 fd,在创建下一个进程时,关闭无关的 fd

具体体现为:每次都把 写端 fd 存储起来,在确定关系前 “清理” 干净

图解

关于上述操作的危害,需要在编写完进程等待函数后,才能演示其作用

#define NAME_SIZE 64
// 封装一个包含各种必备信息的类

class ProcInfo
{
public:
    ProcInfo(pid_t id = pid_t(), int fd = int())
        : _childID(id), _wfd(fd), _num(++_count)
    {
        char buff[NAME_SIZE];
        snprintf(buff, sizeof buff, "Process %d [%d:%d]", _num, _childID, _wfd);
        _name = string(buff);
    }

    ~ProcInfo()
    {
        _childID = _wfd = 0;
    }

    pid_t _childID;  // pid
    int _wfd;        // 写端 fd
    string _name;    // 进程名
    int _num;   //编号
    static int _count; // 计数
};

int ProcInfo::_count = 0; // 静态成员初始化

void CreateProcessAndPipe(vector<ProcInfo> &PP, int ppNum = 3)
{
    vector<int> fds;    //存储继承中不需要的写端 fd
    for(int i = 0; i < ppNum; i++)
    {
        //首先创建管道
        int pipefd[2];
        int ret = pipe(pipefd);
        assert(ret != -1);
        (void)ret;

        //然后创建子进程
        int id = fork();
        assert(id != -1);
        (void)id;

        if(id == 0)
        {
            //子进程内

            //需要先关闭之前子进程遗留的写端fd
            for(auto e : fds)
                close(e);

            close(pipefd[1]);   //子进程关闭写端
            waitCommand(pipefd[0]);  //子进程等待命令

            close(pipefd[0]);
            exit(0);
        }

        //父进程内
        close(pipefd[0]);   //父进程关闭读端
        PP.push_back(ProcInfo(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}

2、任务类创建及任务等待

子进程在创建完成后,需要进入一个 等待阶段 -> 读端阻塞,同时当子进程读取到相应的 指令 时,需要执行相应任务,这里将封装成了一个类,并通过对象调用函数

ctrlProc.cc

void waitCommand(int rfd)
{
    while(true)
    {
        //读端尝试读取信息
        int command = 0; 
        int n = read(rfd, &command, sizeof(command));
        if(n != 0)
        {
            TaskPools().Execute(command);
        }
        else
        {
            cout << "当前子进程读取任务失败,已退出!" << endl;
            break;
        }
    }
}

Task.hpp

#pragma once

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

using namespace std;

void PrintLOG()
{
  cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl;
}

void InsertSQL()
{
  cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl;
}

void NetRequst()
{
  cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl;
}

typedef void(*func_t)();

//任务池
class TaskPools
{
public:
    TaskPools()
    {
    	//装载任务
        _vft.push_back(PrintLOG);
        _vft.push_back(InsertSQL);
        _vft.push_back(NetRequst);
    }

    ~TaskPools()
    {}

    void Execute(int num)
    {
        //根据编号,执行任务
        if(num < 0 || num > _vft.size())
            cout << "没有这个任务" << endl;
        else
            _vft[num]();
    }

private:
    vector<func_t> _vft;    //可用任务表
}; 

3、子进程控制

当所有子进程都完成注册后(统计至 PP),可以让用户输入下标选择程序、输入任务编号选择任务、或者输入程序退出

注意: 因为当前子进程编号从 1 开始,所以在进行下标访问时,需要 -1 避免越界

void showTask()
{
    cout << "**************************" << endl;
    cout << "* 0.日志打印  1.数据插入 *" << endl;
    cout << "* 2.网络请求  3.退出程序 *" << endl;
    cout << "**************************" << endl;
}

void CtrlProcess(vector<ProcInfo> &PP)
{
    while (true)
    {
        // 展示当前可用的进程
        int index = 0;
        do
        {
            cout << "当前可选择进程:";
            for (int i = 1; i <= PP.size(); i++)
                cout << i << " ";
            cout << endl;

            cout << "请选择进程: ";
            cin >> index;
        } while (index < 1 || index > PP.size());

        int taskNum = 0;
        do
        {
            showTask(); // 展示可选任务
            cout << "请选择任务: ";
            cin >> taskNum;
        } while (taskNum < 0 || taskNum > 3);

        // 分配任务
        if(taskNum == 3)
            break;

        cout << "已选择: " << PP[index - 1]._num << " 号进程 | " << PP[index - 1]._name << endl;
        write(PP[index - 1]._wfd, &taskNum, sizeof(taskNum));
        sleep(1);   //执行完任务后,睡一会
    }
}

此时已经可以把任务跑起来了

图示

现在就是万事俱备,只欠回收

4、子进程回收

子进程回收十分简单,因为已经在 PP 中存储了各个子进程的 PID,只需要遍历等待回收即可

void WaitProcess(vector<ProcInfo> &PP)
{
    // 遍历回收就好了
    for (auto e : PP)
    {
        close(e._wfd);  //关闭写端,读端读取到 0 自动结束阻塞
        int status = 0;
        waitpid(e._childID, &status, 0);

        // 通过 status 判断子进程运行情况
        if ((status & 0x7F))
        {
            printf("子进程异常退出,core dump: %d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
        }
        else
        {
            printf("子进程 %s 正常退出,退出码:%d\n", e._name.c_str(), (status >> 8) & 0xFF);
        }
    }

    cout << "所有子进程都已回收" << endl;
}

此时可以验证一下之前的 存在多个写端的问题

首先正常跑(有解决方案的前提下)

结果

然后删除原来的解决方案:vector<int> fds

结果

所以 关闭不必要的 fd 还是很重要的,尤其是在这种涉及 继承 的场景中

8.3、效果演示

下面通过一个动图看看整个程序的运行情况

结果

8.4、注意事项

总体来说,在使用这个小程序时,以下关键点还是值得多注意的

  • 注册子进程信息时,存储的是 写端 fd,目的是为了通过此 fd 向对应的子进程写数据,即使用不同的匿名管道
  • 创建管道后,需要关闭父、子进程中不必要的 fd
  • 需要特别注意父进程写端 fd 被多次继承的问题,避免因写端没有关干净,而导致读端持续阻塞
  • 关闭读端对应的写端后,读端会读到 0,可以借助此特性结束子进程的运行
  • 在选择进程 / 任务 时,要做好越界检查
  • 等待子进程退出时,需要先关闭写端,子进程才会退出,然后才能正常等待

8.5、完整源码

整个程序的完成源码如下所示:

ctrlProc.cc

#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp" //任务所需头文件

using namespace std;

#define NAME_SIZE 64

// 封装一个包含各种必备信息的类
class ProcInfo
{
public:
    ProcInfo(pid_t id = pid_t(), int fd = int())
        : _childID(id), _wfd(fd), _num(++_count)
    {
        char buff[NAME_SIZE];
        snprintf(buff, sizeof buff, "Process %d [%d:%d]", _num, _childID, _wfd);
        _name = string(buff);
    }

    ~ProcInfo()
    {
        _childID = _wfd = 0;
    }

    pid_t _childID;    // pid
    int _wfd;          // 写端 fd
    string _name;      // 进程名
    int _num;          // 编号
    static int _count; // 计数
};

int ProcInfo::_count = 0; // 静态成员初始化

void waitCommand(int rfd)
{
    while (true)
    {
        // 读端尝试读取信息
        int command = 0;
        int n = read(rfd, &command, sizeof(command));
        if (n != 0)
        {
            TaskPools().Execute(command);
        }
        else
        {
            cout << "当前子进程读取任务失败,已退出!" << endl;
            break;
        }
    }
}

void CreateProcessAndPipe(vector<ProcInfo> &PP, int ppNum = 3)
{
    vector<int> fds; // 存储继承中不需要的写端 fd
    for (int i = 0; i < ppNum; i++)
    {
        // 首先创建管道
        int pipefd[2];
        int ret = pipe(pipefd);
        assert(ret != -1);
        (void)ret;

        // 然后创建子进程
        int id = fork();
        assert(id != -1);
        (void)id;

        if (id == 0)
        {
            // 子进程内

            // 需要先关闭之前子进程遗留的写端fd
            for (auto e : fds)
                close(e);

            close(pipefd[1]);       // 子进程关闭写端
            waitCommand(pipefd[0]); // 子进程等待命令

            close(pipefd[0]);
            exit(0);
        }

        // 父进程内
        close(pipefd[0]); // 父进程关闭读端
        PP.push_back(ProcInfo(id, pipefd[1]));
        fds.push_back(pipefd[1]);
    }
}

void showTask()
{
    cout << "**************************" << endl;
    cout << "* 0.日志打印  1.数据插入 *" << endl;
    cout << "* 2.网络请求  3.退出程序 *" << endl;
    cout << "**************************" << endl;
}

void CtrlProcess(vector<ProcInfo> &PP)
{
    while (true)
    {
        // 展示当前可用的进程
        int index = 0;
        do
        {
            cout << "当前可选择进程:";
            for (int i = 1; i <= PP.size(); i++)
                cout << i << " ";
            cout << endl;

            cout << "请选择进程: ";
            cin >> index;
        } while (index < 1 || index > PP.size());

        int taskNum = 0;
        do
        {
            showTask(); // 展示可选任务
            cout << "请选择任务: ";
            cin >> taskNum;
        } while (taskNum < 0 || taskNum > 3);

        // 分配任务
        if (taskNum == 3)
            break;

        cout << "已选择: " << PP[index - 1]._num << " 号进程 | " << PP[index - 1]._name << endl;
        write(PP[index - 1]._wfd, &taskNum, sizeof(taskNum));
        sleep(1); // 执行完任务后,睡一会
    }
}

void WaitProcess(vector<ProcInfo> &PP)
{
    // 遍历回收就好了
    for (auto e : PP)
    {
        close(e._wfd);  //关闭写端,读端读取到 0 自动结束阻塞
        int status = 0;
        waitpid(e._childID, &status, 0);

        // 通过 status 判断子进程运行情况
        if ((status & 0x7F))
        {
            printf("子进程异常退出,core dump: %d   退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
        }
        else
        {
            printf("子进程 %s 正常退出,退出码:%d\n", e._name.c_str(), (status >> 8) & 0xFF);
        }
    }

    cout << "所有子进程都已回收" << endl;
}

int main()
{
    // 1、创建一批进程及匿名管道
    vector<ProcInfo> PP;
    CreateProcessAndPipe(PP);

    // 2、进程控制
    CtrlProcess(PP);

    // 3、进程回收
    WaitProcess(PP);

    return 0;
}

Task.hpp

#pragma once

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

using namespace std;

void PrintLOG()
{
  cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl;
}

void InsertSQL()
{
  cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl;
}

void NetRequst()
{
  cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl;
}

typedef void(*func_t)();

//任务池
class TaskPools
{
public:
    TaskPools()
    {
        _vft.push_back(PrintLOG);
        _vft.push_back(InsertSQL);
        _vft.push_back(NetRequst);
    }

    ~TaskPools()
    {}

    void Execute(int num)
    {
        //根据编号,执行任务
        if(num < 0 || num > _vft.size())
            cout << "没有这个任务" << endl;
        else
            _vft[num]();
    }

private:
    vector<func_t> _vft;    //可用任务表
}; 

Makefile

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

.PHONY:clean
clean:
	rm -r ctrlProc

🌆总结

以上就是本次关于 Linux 进程间通信之匿名管道的全部内容了,在本文中,我们首先学习了什么是 IPC,以及 IPC 的发展历史及分类;然后从 管道 中的 匿名管道 入手,介绍了 管道 的各种特性、场景及 匿名管道 的使用;最后通过一个简单的 匿名管道 进程控制程序,将 匿名管道 IPC 这种方法的知识整体运用了一遍,第一次接触这种多进程程序,还是值得一写的


星辰大海

相关文章推荐

Linux基础IO【软硬链接与动静态库】

Linux基础IO【深入理解文件系统】

Linux【模拟实现C语言文件流】

Linux基础IO【重定向及缓冲区理解】

Linux基础IO【文件理解与操作】

===============

Linux【模拟实现简易版bash】

Linux进程控制【进程程序替换】

Linux进程控制【创建、终止、等待】

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

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

相关文章

Redi事务,数据持久化

4.其他数据功能 4.1pubsub发布订阅 Redis 发布订阅(pub/sub)是一种消息通信模式&#xff1a;发送者(pub)发送消息&#xff0c;订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。 下面示例展示了频道 channel1 &#xff0c; 以及订阅这个频道的三个客户端 —— c…

旧改周报--深圳7大项目获进展:中海、星河等主导

2023年第22周&#xff0c;深圳市共发布拆除重建类权利人核实公示1项&#xff0c;规划公告1项&#xff0c;规划修改&#xff08;草案&#xff09;公示2项&#xff0c;规划&#xff08;修改&#xff09;公告1项&#xff0c;实施主体公示1项&#xff0c;建设用地批复1项&#xff0…

win7安装visual studio 2015出现安装包丢失或损坏

winr 输入 certmgr.msc 查看有没有选中的两个证书&#xff0c;如果没有需要从其他电脑导入&#xff0c;然后直接点击安装界面重试&#xff0c;即可继续安装

【SpringMVC】RESTful案例

1、Rest风格 对于Rest风格&#xff0c;我们需要学习的内容包括: REST简介REST入门案例REST快速开发案例:基于RESTful页面数据交互 1. REST简介 REST&#xff08;Representational State Transfer&#xff09;&#xff0c;表现形式状态转换,它是一种软件架构风格 当我们想表示…

优秀的测试开发应该具备的六大能力有哪些?

目录 前言 什么是测试开发工程师&#xff1f; 测试开发的六大能力 前言 从我工作中接触到的测试开发&#xff0c;以及面试测试开发候选人时问的问题&#xff0c;我将自己对测试开发这个岗位的理解&#xff0c;总结了如下六点能力。 我个人认为&#xff0c;具备如下六点能力…

数据结构之二叉树,实现二叉树的创建与遍历,以及二叉树的一些练习题

目录 目录 一、二叉树的创建与遍历 1.创建二叉树 构建出来的树如图&#xff1a; 2.二叉树的销毁 3.二叉树的前序遍历[Leetcode144.力扣] 4.二叉树的中序遍历 5.二叉树的后序遍历 二、二叉树的实现 1.获取树中节点的个数 2.获取叶子节点的个数 3.获取第K层节点的个数…

Redis实现分布式锁的原理:常见问题解析及解决方案、源码解析Redisson的使用

0、引言&#xff1a;分布式锁的引出 锁常常用于多线程并发的场景下保证数据的一致性&#xff0c;例如防止超卖、一人一单等场景需求 。通过加锁可以解决在单机情况下安全问题&#xff0c;但是在集群模式下就不行了。集群模式&#xff0c;即部署了多个服务器、并配置了负载均衡后…

记录使用Echarts-gl实现3D地图

一、前言 最近项目需要做个大屏展示的&#xff0c;开始做了第一版用户觉得地图太过于单调了&#xff0c;给我发了一个视频&#xff0c;让我参考着做。我看着视频上的地图旋转了方向、地图有标记、看着像是3D的&#xff08;视频上的地图使用多个图层叠加起来、CSS样式做了旋转&…

Nginx网络服务——location规则与rewrite重写

Nginx网络服务——location规则与rewrite重写 一、Nginx中location与rewrite1.location与rewrite常用的正则表达式2. location与rewrite的联系和区别 二、location的匹配规则1.location 的匹配分类2.location 常用的匹配规则3.location 优先级4.location匹配规则优先通用的总结…

【知识图谱搭建到应用】--知识存储--04

文章目录 Mysqljenafuseki数据存储数据建模数据映射注意事项 py2neoneo4jPy2neo与Neo4j的版本问题Py2neo导入三元组数据批量导入csv文件 rdflib库 前面几篇在讲述骗理论的内容&#xff0c;本片主要描述如何将清洗过的结构化数据存储在转换成三元组并存储起来&#xff0c;并于后…

ChatGPT与软件架构(4) - 架构师提示工程指南

架构师可以通过各种类型的对话提示&#xff0c;提升驱动ChatGPT对话输出的质量&#xff0c;更好的利用AI能力辅助架构设计。原文: Software Architects’ Guide to Enhancing ChatGPT Interactions With Prompt Types Robert Stump Unsplash 前言 随着ChatGPT等人工智能语言模型…

12.数据结构之AVL树

前言 提到平衡二叉查找树&#xff0c;不得不提二叉查找树。二叉查找树&#xff0c;说简单点&#xff0c;其实就是将我们的数据节点&#xff0c;有序的维护为一个树形结构。这样我们查的时候&#xff0c;那么我们查找某个节点在不在集合中的时间复杂度实际上就是树的高度。如果…

华为OD机试真题 Java 实现【玩牌高手】【2023 B卷 100分】,附详细解题思路

一、题目描述 给定一个长度为n的整型数组&#xff0c;表示一个选手在n轮内可选择的牌面分数。选手基于规则选牌&#xff0c; 请计算所有轮结束后其可以获得的最高总分数。 选择规则如下&#xff1a; 在每轮里选手可以选择获取该轮牌面&#xff0c;则其总分数加上该轮牌面分…

python笔记 第二章 变量

系列文章目录 第一章 初识python 文章目录 2.1变量2.1.1变量的作用2.1.2定义变量标识符命名习惯使用变量 2.2 认识bugDebug工具Debug工具使用步骤: 2.3 数据类型 2.1变量 目标 变量的作用定义变量认识数据类型 2.1.1变量的作用 变量就是一个存储数据的的时候当前数据所在的…

Java基础——堆和栈、static关键字、静态变量和成员变量的区别

Java程序运行顺序&#xff1a;Java应用程序—虚拟机—操作系统—硬件 Java中栈内存用来存储局部变量和方法调用&#xff0c;堆内存用来存储Java中的对象&#xff0c;成员变量、局部变量、类变量指向的对象都存储在堆内存中。 static关键字&#xff1a; 随着类的加载而加载优先…

ISATAP隧道配置与验证

ISATAP隧道配置与验证 【实验目的】 熟悉IPv6ISATAP隧道的概念。 掌握IPv6和IPv4共存的实现方法。 掌握IPv6 ISATAP地址编址规则。 掌握IPv6 ISATAP隧道的配置。 验证配置。 【实验拓扑】 设备参数如下表所示。 设备 接口 IP地址 子网掩码 默认网关 R1 S0/0 192.…

【内存管理大猫腻:从“越界”到“内存泄漏”应有尽有】

本章重点 什么是动态内存 为什么要有动态内存 什么是野指针 对应到C空间布局&#xff0c; malloc 在哪里申请空间 常见的内存错误和对策 C中动态内存“管理”体现在哪 什么是动态内存 动态内存是指在程序运行时&#xff0c;根据需要动态分配的内存空间。 #include <stdio.h&…

1.链表的实现:不带哨兵

一、链表linked list 1.定义 链表是数据元素的线性集合&#xff0c;其每个元素都指向下一个元素&#xff0c;元素存储上并不连续,链表逻辑连续。 2.分类 ①单向链表&#xff1a;每个元素只知道其下一个元素是谁 ②双向链表: 每个元素知道其上一个元素和下一个元素 ③循环链…

Java - Stream流详解

文章目录 前言 大家好,好久不见了,最近由于实训的影响导致拖更了,在更新这一次估计javaSE基本上就算是完结了,还有一些落下的后面也会补上的,下次见面就是数据结构了 尽情期待吧!那么就让我们步入Stream流的学习吧! 一、Stream流是什么&#xff1f; Stream流是Java 8中的一个…

【openEuler 20.03 TLS编译openGauss2.0.0源码】

openEuler 20.03 TLS编译openGauss2.0.0源码 一、安装环境二、安装前准备二、安装步骤 一、安装环境 项目Value操作系统openEuler 20.03 64bit with ARMopenGauss2.0.0openGauss-third_party2.0.0 二、安装前准备 项目Value购买华为ECS鲲鹏 8vCPU32G 100M/s带宽 openEuler 2…