【进程间通信:管道】

news/2024/10/23 5:32:54/

目录

1 进程间通信介绍

1.1 进程间通信目的

1.2 进程间通信发展  

1.3 进程间通信分类

 2 管道

2.1 什么是管道

2.2 匿名管道

2.2.1 匿名管道的使用

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

2.3 管道读写规则

2.4 匿名管道特点

2.5 命名管道

2.5.1 概念

2.5.2 使用


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

 实例代码:

#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 关闭不要的fdclose(pipefd[1]);// 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取// 1.3.1 输入重定向,可以不做dup2(pipefd[0], 0);// 1.3.2 子进程开始等待获取命令WaitCommand();close(pipefd[0]);exit(0);}// 一定是父进程//  1.3 关闭不要的fdclose(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_pointsctrlProcess(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 2class 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的文件描述符,那么这样做的危害是什么?如果我们通过先的关闭第一个管道的写端,然后再回收第一个子进程时,第一个子进程会一直阻塞在那里,为什么呢?因为第二个子进程中继承父进程的写端还指向第一个子进程的读端,也就是我们如果只先关闭了第一个管道的写端是不行的,第一个子进程并没有结束,因为他的读端还指向后面所有的子进程,这就导致第一个子进程回收时一直阻塞在那里。后面进程的分析方法同理:

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

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

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

2.3 管道读写规则

当没有数据可读时:

  • O_NONBLOCK disableread调用阻塞,即进程暂停执行,一直等到有数据来到为止。
  • O_NONBLOCK enableread调用返回-1errno值为EAGAIN
当管道满的时候:
  • O_NONBLOCK disable write调用阻塞,直到有进程读走数据
  • O_NONBLOCK enable:调用返回-1errno值为EAGAIN
  • 如果所有管道写端对应的文件描述符被关闭,则read返回0
  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

 前面的都很好理解,这里提一嘴什么是原子性:原子性就是假如我们往管道中写入一句"hello world"时,如果能够将其完整写进去时进行写入,否则就不进行写入,也就是程序的执行只能够有两种结果:数据全部写入和数据全部都没写入,不存在着写入一半的情况。

2.4 匿名管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

 

2.5 命名管道

2.5.1 概念

管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用 FIFO 文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件。

2.5.2 使用

我们知道匿名管道是具有血缘关系的进程建立通信的方式,而命名管道则是可以让没有血缘关系的进程建立通信。那么如何让没有血缘关系的进程看到同一份资源呢?

我们首先来看看这样一个命令:

 我们通过mkfifo命令创建了一个叫做fifo的管道文件,当我们将字符串输出重定向到该文件时我们发现光标卡在了这里不动了,而当我们去读取的时候才会显现,当我们终止掉时两边都已经结束了:

 这是在命令行上创建的命名管道。而这种管道是内存级别的文件,是不会刷新到磁盘上的。生成的命名管道文件只是内核缓冲区的一个标识,用于让多个进程找到同一个缓冲区。匿名管道和命名管道的本质都是内核中的一块缓冲区。再来回答如何让不同的进程看到同一份资源,我们可以采用文件路径+文件名来作为唯一标识该文件的方法来创建文件作为一个命名管道。

除了用命令行式的方法,我们还可以用系统调用:

int mkfifo(const char *filename,mode_t mode);

 通过这个我们可以实现一个简单的服务端与客户端进行通信的程序:

server.cc:

#include<iostream>
#include<cstring>
#include<string>
#include<cerrno>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
using namespace std;int main()
{const string fileName("myfile");umask(0);int n=mkfifo(fileName.c_str(),0666);if(n<0){cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;return 1;}cout<<"server creat fifo success"<<endl;int rop=open(fileName.c_str(), O_RDONLY);if(rop<0){cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;return 1;}cout<<"server open fifo success,begin ipc"<<endl;char buffer[1024];while(true){buffer[0]=0;int n=read(rop,buffer,sizeof(buffer)-1);if(n>0){buffer[n]=0;cout<<buffer<<endl;}else if(n==0){cout<<"client exit,server also exit"<<endl;break;}else{cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;return 1;}}close(rop);unlink(fileName.c_str());return 0;
}

client.cc:

#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string>
#include<cerrno>
#include<cstring>
using namespace std;
int main()
{const string fileName("myfile");int wop=open(fileName.c_str(),O_WRONLY);if(wop<0){cerr<<"strerrno:"<<errno<<strerror(errno)<<endl;return 1;}string myinfo;while(true){cout<<"请输入你的消息"<<endl;getline(cin,myinfo);write(wop,myinfo.c_str(),myinfo.size());myinfo[strlen(myinfo.c_str())-1]=0;}close(wop);return 0;
}

Makefile:

.PHONY:all
all:client serverclient:client.ccg++ -o $@ $^ -std=c++11
server:server.ccg++ -o $@ $^ -std=c++11.PHONY:clean
clean:rm -rf client server

效果:

由于我们是在服务端建立好的匿名管道,所以当我们退出时最好在服务端中干掉管道文件。

顺便提问一下:多个进程在通过管道通信时,删除管道文件则无法继续通信吗?

显然不是的,由于管道文件只是起一个标识作用,之前已经打开管道的进程依旧可以正常通信。


http://www.ppmy.cn/news/312981.html

相关文章

【收藏】通信知识分类整理

5G专题 有史以来最强的5G入门科普&#xff01; 超简单&#xff01;学习5G的正确姿势&#xff01; 深度解析&#xff1a;5G与未来天线技术&#xff08;转载&#xff09; 5G核心网&#xff0c;到底长啥样&#xff1f;从2G到5G&#xff0c;核心网&#xff0c;你到底经历了什么&am…

元宇宙系统全面学习线路

问题导读 1.元宇宙已经发展了多少年&#xff1f; 2.元宇宙是什么&#xff1f; 3.元宇宙有哪些概念? 4.元宇宙支持技术有哪些&#xff1f; 5.元宇宙和VR有什么区别&#xff1f; 6.元宇宙有哪些机会&#xff1f; 7.元宇宙如何开发&#xff0c;有哪些赛道&#xff1f; 元宇宙的发…

云计算与大数据技术应用 第二章

大数据技术概述 大数据技术的产生 大数据的基本概念 1、什么是大数据&#xff1f;大数据&#xff08;Big data或Megadata&#xff09;&#xff1a;大数据&#xff0c;或称巨量数据、海量数据、大资料&#xff0c;指的是所涉及的数据量规模巨大到无法通过人工&#xff0c;在合…

我在华为做外包的真实经历!

1 写在前面 我将用系列文章&#xff0c;回顾十年程序生涯&#xff0c;一方面是对职场生涯的阶段性总结&#xff0c;另一方面希望这些经历&#xff0c;对大家往后职场生涯有所启发。 我很庆幸一路走来皆是自己的选择&#xff0c;虽然也走了不少弯路&#xff0c;但那是我选择的生…

安图恩频道一直连接服务器,DNF里最让大家不爽的事,安图恩频道什么时候能换服务器...

原标题&#xff1a;DNF里最让大家不爽的事&#xff0c;安图恩频道什么时候能换服务器 玩DNF的时候你经历过绝望吗&#xff1f; 我想&#xff0c;最大的绝望是打团掉线&#xff0c;或者网络波动让你不能安心打团。 进跨区组队进团&#xff0c;一心欢喜的买票等开&#xff0c;却没…

dnf服务器老是连接中断,《dnf》老是网络连接中断怎么办 网络总是中断解决办法...

导 读 DNF老是网络连接中断的原因有很多&#xff0c;本地网络极不稳定&#xff0c;.与科技组队&#xff0c;官方维护失误&#xff0c;导致正常数据被判定异常&#xff0c;强力秒杀&#xff0c;导致被判定数据异常。长时间挂机都可能造成&#xff0c;请排除以上原因。 详细答案&…

dnf服务器显示1234567,DNF:旭旭宝宝色盲被实锤,普雷这个机制有点意思

DNF&#xff1a;旭旭宝宝“色盲”被实锤&#xff0c;普雷这个机制有点意思 虽然现在普雷团队副本在国服的DNF里面还没有出现&#xff0c;但是游戏策划为了让我们国服的DNF玩家能够更早熟悉普雷副本里面的怪物机制&#xff0c;所以弄出了一个“普雷妮”&#xff0c;让我们可以提…

dnf超时空漩涡副本路线流程图_DNF超时空漩涡怎么打 队伍配置攻坚路线兵营boss攻略...

虽然超时空漩涡的机制与超时空之战相同&#xff0c;但是难度的原因目前并不适合中路强推&#xff0c;对于第一梯队进攻超时空漩涡团本的大佬们来说&#xff0c;怪物机制和攻坚方法都已经非常了解了&#xff0c;不出现意外(网络波动&#xff0c;队友掉线&#xff0c;怪物出现BUG…