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

Linux进程通信——匿名管道

目录

1、进程间通信基础概念

2、管道的工作原理

 2.1 什么是管道文件

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

3.1、pipe 系统调用

3.2  父进程调用 fork() 创建子进程

3.3. 父子进程的文件描述符共享

3.4. 关闭不必要的文件描述符

3.5 父子进程通过管道进行通信

父子进程通信的具体例子

 4.管道的四种场景 

4.1 场景一:父进程不写,子进程尝试读取

4.2 场景二:父进程不断写入,直到管道写满,子进程不读取

4.3 场景三:关闭写端,子进程读取数据

4.4 场景四:关闭读端,父进程写入数据

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

5.1 逻辑设计


🌇前言

在操作系统中,进程间通信(Interprocess Communication,简称IPC)是两个不同进程之间进行协同工作和信息交换的基础。IPC 允许不同的进程相互协调,协作完成任务。进程间通信的方式有很多种,而管道则是一种非常经典且常用的方式。本文将详细探讨 匿名管道,它在进程间通信中扮演着重要的角色。

🏙️正文

1、进程间通信基础概念

在深入探讨匿名管道之前,我们先来了解一些基本概念。进程间通信的目的是为了使多个独立的进程能够协同工作,进行信息交换。主要有四个目的:

  • 数据传输:不同进程之间需要传输数据。例如,将数据从客户端传送到服务器。

  • 资源共享:多个进程共享系统资源,保证高效使用。

  • 事件通知:某个进程需要通知其他进程某个事件的发生,例如进程的终止。

  • 进程控制:用于进程管理,协调进程的执行和资源的分配。

这些目的的核心是打破进程的独立性,让它们能够共享资源和信息,协同完成任务。 

2、管道的工作原理

管道是一种用于进程间通信的方式,它本质上是一个文件。无论是匿名管道还是命名管道,它们的原理都是通过文件描述符来共享数据。每个管道都有两个端口:一个是写端,另一个是读端。

管道最初是由 Unix 系统引入的,它允许具有“血缘关系”的进程(如父子进程)通过管道进行通信。管道的实现通常会涉及到内核为进程分配文件描述符。父进程在创建管道后,会为其子进程继承文件描述符,并通过关闭不需要的端口,确保通信的流向。

 2.1 什么是管道文件

管道文件是操作系统中用于实现进程间通信的特殊文件,具有以下几个显著特点:

  • 单向通信:管道是 单向 的通信方式,意味着数据只能从一个端流向另一个端。通常情况下,一个进程写数据到管道,而另一个进程从管道中读取数据。这种方式被称为“半双工通信”,如果需要实现双向通信,需要两个管道。

  • 基于文件的设计,管道本质上是内存中的文件; 管道文件并不是磁盘级别的文件,而是内存级别文件,管道文件没有自己的inode,也没有名字。过内存中的缓冲区进行存储,操作系统会将管道作为文件来处理。

  • 管道分为 匿名管道命名管道。匿名管道没有名字,是由操作系统在内存中创建的,仅限于有血缘关系(如父子进程或兄弟进程)的进程间通信。由于没有名字,匿名管道无法在进程间直接共享。

    与此不同,命名管道(FIFO)则拥有一个系统中的路径名,因此它可以被不具备血缘关系的进程之间共享。这使得命名管道的通信更加灵活。

  • 生命周期与进程绑定,管道的生命周期与创建它的进程生命周期紧密相关。当进程结束时,管道也会被操作系统回收。管道文件的生命周期由打开它的进程的生命周期决定,在进程终止时,管道的资源会被释放。

  • 内存缓冲区,管道的一个重要特点是,它在内存中创建一个缓冲区,用于存储待传输的数据。由于是内存中的缓冲区,管道中的数据并不会被持久化到磁盘中。这使得管道比磁盘文件更为高效,但数据在管道中的存储是临时的,不会在系统重启后保留

  • 阻塞行为与同步机制

    管道的通信遵循 阻塞 和 同步 机制。当读端尝试读取数据时,如果管道为空,进程会阻塞,直到写端写入数据。同样,写端如果尝试写入数据时,如果管道已满,进程也会阻塞,直到读端读取部分数据。

    这种阻塞行为本身提供了一定的同步机制。管道会保证写入数据的顺序,并且数据在被读取之前不会丢失。这使得进程间的通信是同步的,确保数据完整传输。

  • 管道大小限制,管道的大小在不同的操作系统和系统配置中可能有所不同。通常,管道大小会受到系统配置的限制。在 Linux 中,管道大小的默认值通常为 64KB(从 Linux 2.6.11 版本开始),不过在不同的系统或不同的内核版本中,管道的大小也可能有所变化

  • 在管道中,写入 与 读取 的次数并不是严格匹配的,此时读写次数没有强相关关系,管道是面向字节流读写的面向字节流读写又称为 流式服务:数据没有明确的分割,不分一定的报文段;与之相对应的是 数据报服务:数据有明确的分割,拿数据按报文段拿不论写端写入了多少数据,只要写端停止写入,读端都可以将数据读取。
  • 具有一定的协同能力让 读端 和 写端 能够按照一定的步骤进行通信(自带同步机制)当读端进行从管道中读取数据时,如果没有数据,则会阻塞,等待写端写入数据;如果读端正在读取,那么写端将会阻塞等待读端,因此 管道自带 同步与互斥 机制。

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

具体流程:

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

3.1、pipe 系统调用

匿名管道的创建通过 pipe() 系统调用来实现。该函数会创建一个管道,并返回两个文件描述符:一个用于读,另一个用于写。函数原型如下:

#include <unistd.h>int pipe(int pipefd[2]);

 传入一个大小为2的整型数组作为输出型参数操作系统就会生成一个管道文件,并且让进程以读写的方式分别打开进程,并且将进程的读管道文件的文件标识符写道pipe[1],写管到文件描述符写道pipe[1]之中。

int pipefd[2];
pipe(pipefd);  // 创建管道
3.2  父进程调用 fork() 创建子进程

当父进程调用 fork() 时,操作系统会创建一个新的子进程。子进程会继承父进程的文件描述符表,因此,父子进程可以共享父进程所创建的管道文件描述符。也就是说,父进程和子进程都会拥有相同的管道读端和写端。

pid_t pid = fork();
3.3. 父子进程的文件描述符共享

父进程和子进程共享管道的读写端口意味着:

  • 父进程子进程 都可以操作 pipefd[0]pipefd[1]但它们之间的角色(读或写)通常是根据进程的需求来确定的。

  • 父进程和子进程在创建时各自拥有自己的 进程资源但文件描述符表会被子进程继承,指向相同的管道内存资源。

3.4. 关闭不必要的文件描述符

由于管道是单向通信的,所以为了避免数据混乱,父进程和子进程通常会关闭不必要的文件描述符。例如,如果父进程要写数据到管道而子进程读取数据,父进程应该关闭管道的读端,子进程应该关闭管道的写端

// 父进程关闭管道的读端,子进程关闭管道的写端
close(pipefd[0]);  // 父进程关闭读端
close(pipefd[1]);  // 子进程关闭写端
3.5 父子进程通过管道进行通信
  • 父进程写数据:父进程通过管道的写端 pipefd[1] 向管道中写入数据。

    write(pipefd[1], "Hello from parent", 17);
    

    子进程读数据:子进程通过管道的读端 pipefd[0] 从管道中读取数据。

    char buf[128];
    read(pipefd[0], buf, sizeof(buf));
    
  • 父进程写入的数据会通过管道传递给子进程。

  • 子进程从管道中读取数据,通常会按顺序接收父进程写入的数据。

父子进程通信的具体例子

下面是一个完整的示例代码,展示了父子进程如何通过管道进行通信:

#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;// 读取异常(消息过短)elsecout << "子进程读取数据量为:" << 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.管道的四种场景 

4.1 场景一:父进程不写,子进程尝试读取

情况描述:

  • 父进程没有写入数据到管道。

  • 子进程尝试从管道中读取数据。

结果:

  • 由于管道为空,子进程在尝试读取时会进入阻塞状态。

  • 只有当父进程开始向管道中写入数据后,子进程才会成功读取数据。

形象化理解:

  • 这就像一个垃圾桶,子进程是倒垃圾的工作人员,而父进程是往垃圾桶里扔垃圾。如果垃圾桶为空,子进程(倒垃圾的人)就无法工作,必须等待父进程(扔垃圾的人)开始丢垃圾,才能开始工作。

4.2 场景二:父进程不断写入,直到管道写满,子进程不读取

情况描述:

  • 父进程持续向管道写入数据,直到管道被写满。

  • 子进程不进行读取操作。

结果:

  • 当管道的缓冲区满了,父进程会被阻塞,无法继续写入数据,直到子进程读取数据。

  • 这是因为管道有大小限制,管道满时,写端无法继续写入,必须等待管道中有空间才能继续写入。

形象化理解:

  • 就像垃圾桶满了后,不能继续往里面丢垃圾,必须等到垃圾桶被清空(子进程读取数据)之后,才能继续丢垃圾。

4.3 场景三:关闭写端,子进程读取数据

情况描述:

  • 父进程写入数据到管道,并关闭写端。

  • 子进程从管道中读取数据,并在读取到末尾时判断写端是否关闭。

结果:

  • 当父进程关闭写端后,子进程可以继续读取管道中的数据,直到数据读取完。

  • 子进程在读取到数据末尾时会收到 read的,表示已经没有更多数据可读取,且写端已关闭。

形象化理解:

  • 这类似于垃圾桶的垃圾已经被倒空,子进程(倒垃圾的人)会看到垃圾桶已经没有垃圾了。即使它继续尝试“倒垃圾”,也不会有新的垃圾,显示读取到了文件末尾。

4.4 场景四:关闭读端,父进程写入数据

情况描述:

  • 父进程是写端,子进程是读端。父进程写入数据。

  • 父进程在读取五次后关闭读端。

结果:

  • 当关闭读端后,写端(父进程)会收到 SIGPIPE 信号,通常导致进程终止。

  • 因为操作系统会发现,写端已没有可用的读取端(读端关闭了),它会强制终止写端进程以防止资源浪费。

形象化理解:

这就像垃圾桶的“倒垃圾的人”(写端)发现没有“垃圾桶”(读端)可以丢垃圾,因此操作系统会终止写端,避免无意义的行为继续发生。


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

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

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

5.1 逻辑设计

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

1.创建一批进程及管道

首先需要先创建一个包含进程信息的类,最主要的就是子进程的写端 fd,这样父进程才能通过此 fd 进行数据写入
循环创建管道、子进程,进行相应的管道链接操作,然后子进程进入任务等待状态,父进程将创建好的子进程信息注册
假设子进程获取了任务代号,那么应该根据任务代号,去执行相应的任务,否则阻塞等待
注意: 因为是创建子进程,所以存在关系重复继承的情况,此时应该统计当前子进程的写端 fd,在创建下一个进程时,关闭无关的 fd

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

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

完整代码如下:

Task.hpp

#pragma once 
#include<iostream>
#include<vector>typedef void (*task_t)();void task1()
{std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}
void LoadTask(std::vector<task_t>& task)
{task.push_back(task1);task.push_back(task2);task.push_back(task3);task.push_back(task4);
}

 processpool.cc

#include "Task.hpp"
#include <string>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <cassert>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>using namespace std;const int processnum =4;
vector<task_t> tasks; //把所有的任务,装进去class channel
{public:channel(int &cmdfd ,int &mypid,string &name):_name(name),_cmdfd(cmdfd),_mypid(mypid){}public:string _name;//子进程的名字int _cmdfd;//发信号的文件描述符  int _mypid;//我的PID
};void Menu()
{std::cout << "################################################" << std::endl;std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;std::cout << "#                         0. 退出               #" << std::endl;std::cout << "#################################################" << std::endl;
}void slaver(int pool)
{while(true){int cmdcode=0;//通过调用码,去领任务int n=read(pool,&cmdcode,sizeof(int));cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " <<  cmdcode << endl;if(cmdcode>0&&cmdcode<=tasks.size()) tasks[cmdcode-1]();if(n==0) break;}
}void InitProcesspool(vector<channel>&channels)
{vector<int> d;//把父进程的所有打开的写管道存储到里面for(int i=1;i<=processnum;i++){int pipeid[2];int n=pipe(pipeid);assert(!n);int pid=fork();if(pid==0){cout<<"创建的"<< i<<"号子进程"<<endl;for(auto &t:d){close(t);//关掉所有不相关的管道}close(pipeid[1]);//关闭写管道slaver(pipeid[0]);//读操作// close(pipeid[0]);//多此一举cout<<"关闭的"<< i<<"号子进程"<<endl;exit(0);}close(pipeid[0]);string name="创建的子进程"+to_string(i);channels.push_back({pipeid[1],pid,name});//那个管道发数据记录下来,d.push_back(pipeid[1]);//把一会发数据的管道号记下来sleep(1);}
}void setslaver(vector<channel>&channels)
{int which=0;int cnt=4;while(cnt--){int slect=0;Menu();cin>>slect;     if(!slect) break;   // rand((void)time(nullptr));// int i=srand()%5;cout<<"farher message"<<channels[which]._name<<endl;write(channels[which]._cmdfd,&slect,sizeof(int));     which++;which%=channels.size();sleep(1);}
}void Quitpool(vector<channel>&channels)
{for(auto& t:channels)//关闭所有的写管道{close(t._cmdfd);waitpid(t._mypid,nullptr,0);}
}int main()
{vector<channel> channels;//把打开的子进程装进来LoadTask(tasks);InitProcesspool(channels);//创建子进程setslaver(channels);//发配任务,采用轮询Quitpool(channels);//关闭写管道,等到read=0子进程退出,全部关闭return 0;
}

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

注册子进程信息时,存储的是 写端 fd,目的是为了通过此 fd 向对应的子进程写数据,即使用不同的匿名管道

创建管道后,需要关闭父、子进程中不必要的 fd

需要特别注意父进程写端 fd 被多次继承的问题,避免因写端没有关干净,而导致读端持续阻塞关闭读端对应的写端后,读端会读到 0,可以借助此特性结束子进程的运行

在选择进程 / 任务 时,要做好越界检查

等待子进程退出时,需要先关闭写端,子进程才会退出,然后才能正常等待。

http://www.lryc.cn/news/585581.html

相关文章:

  • 《打破预设的编码逻辑:Ruby元编程的动态方法艺术》
  • C语言/Keil的register修饰符
  • ​老电影画质为何会模糊?要如何修复呢?
  • 【数据结构与算法】206.反转链表(LeetCode)
  • 力扣-21.合并两个有序链表
  • 力扣-160.相交链表
  • MongoDB(一)
  • “28项评测23项SOTA——GLM-4.1V-9B-Thinking本地部署教程:10B级视觉语言模型的性能天花板!
  • 【SpringBoot】 整合MyBatis+Postgresql
  • 瀚高数据库提交数据后,是否需要COMMIT(APP)
  • 微信小程序核心知识点速览
  • Android simpleperf生成火焰图
  • 《数据库》MySQL备份回复
  • 神经网络的参数初始化
  • 鸿蒙app 开发中的Record<string,string>的用法和含义
  • Ubuntu 24.04上安装 Intelligent Pinyin 中文输入法
  • 模拟心电图采样数据
  • 【SpringAI】6.向量检索(redis)
  • 用FunctionCall实现文件解析(一):环境准备与基础知识
  • 如何使用Fail2Ban阻止SSH暴力破解
  • vue3+express联调接口时报“\“username\“ is required“问题
  • 获取华为开源3D引擎 (OpenHarmony),把引擎嵌入VUE中
  • Spring Boot中请求参数读取方式
  • AIC8800M40低功耗wifi在ARM-LINUX开发板上做OTA的调试经验
  • (六)复习(OutBox Message)
  • ParaCAD 笔记 png 图纸标注数据集
  • 设计模式(结构型)-适配器模式
  • jenkins部署springboot+Docker项目
  • 力扣网编程134题:加油站(双指针)
  • [实战]调频三角波和锯齿波信号生成(完整C代码)