[ Linux ] 进程间通信介绍 管道
创始人
2024-02-08 06:12:59

目录

0.进程间通信介绍

0.1通信背景

0.2进程间通信目的

1.管道

1.1 管道是什么

1.2 匿名管道

1.2.1管道通信的特点

1.2.2 匿名管道编码

父进程控制子进程的行为

进程池 -- 池化概念

1.3管道的特征总结

1.4命名管道

1.4.1创建一个命名管道

1.4.2 命名管道编码


0.进程间通信介绍

0.1通信背景

在之前我们学习进程时知道进程具有独立性,所以进程间交互数据的成本就变得非常高。进程之间为什么也进行进程间通信,这就需要谈谈进程间通信的目的了。但是进程具有独立性不是彻底独立,只管自己不理任何人,当然不是这样的。进程之间也是存在通信的。那么进程间通信的方式也有很多种,包括管道,System V IPC,POSIX IPC........那么今天我们先来看看进程间如何通过管道来进行通信。

0.2进程间通信目的

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

1.管道

1.1 管道是什么

管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。在Linux中管道分两种:匿名管道和命名管道。

1.2 匿名管道

假设现在在内存中有两个独立的进程,如果要想这两个进程之间通信,那么进程1可以先把数据拷贝到磁盘上,然后进程2再去读这个数据【如下图所示】....首先不考虑这种方法是否可行,但是比较好的让我们理解了一个道理:进程在通信之前,必须让不同的进程能够看到同一份资源(文件,内存块.....)

因此在通信之前,如何解决让进程先看到同一份资源呢?并且资源的不同决定了不同种类的通信方式!!!

因此我们正在学习的管道是提供共享资源的一种手段。

我们知道文件在内存和磁盘之间来回切换是非常耗费时间的,因此进程间通信大多都是内存级别的。意思就是在内存内部重建一块区域进行通信。

那么什么是管道呢?

在计算机通信中,我们把文件不再是一个磁盘文件,通过特定的接口表征自己的身份,说明他和磁盘脱离,自己读写数据是就在文件的内存缓冲区,完成数据交互,我们把这个文件叫做管道。因此我们说Linux下一切皆文件,管道也是文件。管道就是一个内存级文件。内容不需要刷新到磁盘中。

1.2.1管道通信的特点

在我们生活中遇到的管道有什么特点呢?那首先问那些都属于管道呢?天然气管道,水龙头管道等等.....那么这些管道大多数情况下都是单向的,并且这些管道都是传输资源的,在计算机中最重要的资源就是数据。

  1. 单向的
  2. 传输数据

那么我们如何来保证单向性呢?

我们来看下图,父进程和子进程通过管道完成进程通信如何保证单向性呢,我们刚刚提到了管道是一个文件,是数据的缓冲区,因此当父进程把需要通信的数据通过写的方式写入管道内时,子进程通过读的方式拿到这些资源即可完成父子间的通信。为了保证单向的,我们需要关闭父进程的读端,让父进程只能写,关闭子进程的写段,让子进程只能读。通过这样的方式我们就可以保证父进程只能写数据子进程只能读数据的单向性。

父进程必须以读写方式打开,这是因为子进程会继承下去,这样子进程就不用再打开了。那么谁决定父子关闭什么读写?这不是由管道决定的,这是由我们的需求所决定的。

那么我们如何来打开管道呢?难道要调用两次open()吗?当然不是了,因此操作系统提供了pipe()接口

1.2.2 匿名管道编码

认识pipe()接口,当我们调用piep时,底层会自动帮助我们把文件以读方式和写方式打开,而且我们会的到两个文件描述符,这两个文件描述符会写进pipefd数组内,因此这个数组是一个输出型参数。并且pipe是一个系统调用。返回0表示成功,返回-1表示失败。

接下来我们进行管道的代码:

下面这段代码是管道的创建和验证输出的数组是否使我们所想的两个文件描述符

#include 
#include 
#include using namespace std;int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0){cerr<<"pipe error"<

匿名管道代码演示

#include 
#include 
#include 
#include 
#include 
#include using namespace std;//演示pipe管道通信的基本过程 -- 匿名管道
int main()
{//1.创建管道int pipefd[2] = {0};if(pipe(pipefd) != 0){cerr<<"pipe error"<0){//读取成功buffer[s] = '\0';cout<<"子进程收到消息,消息的内容是:"<< buffer < 0 ){cout<<"父进程等待成功"<

至此父进程的数据就发给了子进程,子进程也能够接受父进程发送来的数据。在上述代码中,我们在父进程中带了sleep(1),让父进程每间1秒向管道内写入一个数据,那么子进程没有带sleep(1),为什么子进程也会随之休眠一秒呢?为了更好的看到这个现象,我们在父子进程里面带上时间戳,写写日志。

我们通过测试观察到子进程代码没有任何的休眠,子进程会随着父进程的节奏读取,那么我们可以得出结论:当父进程没有写入数据的时候,子进程在等!所以,父进程写入之后,子进程才能read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主!

那么,父进程和子进程读写的时候是有一定的顺序性的!当父进程向管道写入的时候,子进程才可以读!

  • 管道内部,没有数据,reader就必须阻塞等待(read时等待)
  • 管道内部,如果数据写满,writer就必须阻塞等到(writer时等待)

因此pipe内部是自带访问控制机制的以及存在同步和互斥机制的。

所谓的阻塞等待的本质是将当前的tast_struct 放入等待队列中,将PCB的状态由R->S/D/T

父进程控制子进程的行为

假设父进程想让我的子进程做父进程想让子进程做的行为,以及父进程想控制一批(多个)子进程.....该如何来写呢???

我们先写一段父进程控制子进程的行为的代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include using namespace std;// 定义一个函数指针
typedef void (*functor)();vector functors; //方法的集合// for debug
unordered_map info;void f1() { cout << "这是一个处理日志的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]" << endl; }void f2() { cout << "这是一个处理数据备份的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]" << endl; }void f3() { cout << "这是一个处理网络连接的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]" << endl; }void loadFunctor()
{info.insert({functors.size(), "处理日志任务"});functors.push_back(f1);info.insert({functors.size(), "数据备份任务"});functors.push_back(f2);info.insert({functors.size(), "处理网络连接任务"});functors.push_back(f3);
}int main()
{// 0. 加在任务列表loadFunctor();// 1.创建管道int pipefd[2] = {0};if (pipe(pipefd) != 0){cerr << "pipe error" << endl;return 1;}// 2.创建子进程pid_t id = fork();if (id < 0){cerr << " fork error" << endl;return 2;}else if (id == 0){// child read// 关闭不需要的fdclose(pipefd[1]);while (true){uint32_t operatorType = 0;//如果有数据就读取 如果没有数据就阻塞等待ssize_t s = read(pipefd[0], &operatorType, sizeof(uint32_t));if(s == 0) {cout << "我是子进程,我要退出了" <

通过这份代码父进程控制了子进程的行为,往后我们只需要修改functors里面的方法,就可以让子进程执行指定的任务。

进程池 -- 池化概念

那么如果是一个父进程想要控制一批子进程呢??? 父进程怎么样把一批任务交给子进程呢?


因此,我们有多少个进程,我们就创建多少个管道,父进程可以通过对指定管道写入特定的任务,让指定的子进程做对应的事情,这样我们就引入了一个池化的概念!那么我怎么如何书写对应的代码呢?

int processNum  = 5;int main()
{for(int i = 0;i

上面这份代码就成功的将每一次创建的子进程和父进程都独立的进行了控制,因此父进程在不断的循环,给子进程指派任务的时候需要知道给哪一个进程指派,指派什么任务,通过什么指派呢? 因此我们接下来需要做的就是解决这些问题。因此我们需要一节pair结构

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include using namespace std;// 定义一个函数指针
typedef void (*functor)();vector functors; //方法的集合// for debug
unordered_map info;void f1() { cout << "这是一个处理日志的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]\n\n" << endl; }void f2() { cout << "这是一个处理数据备份的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]\n\n" << endl; }void f3() { cout << "这是一个处理网络连接的任务,执行的进程id: [" << getpid() << "]"<< "执行的时间是:[" << time(nullptr) << "]\n\n" << endl; }void loadFunctor()
{info.insert({functors.size(), "处理日志任务"});functors.push_back(f1);info.insert({functors.size(), "数据备份任务"});functors.push_back(f2);info.insert({functors.size(), "处理网络连接任务"});functors.push_back(f3);
}//第一个int32_t:进程pid
//第二个int32_t:该进程对应的管道写端fd
typedef pair elem;
int processNum = 5;void work(int blockFd)
{cout << "进程 [" << getpid() << "] 开始工作" << endl;//进行while (true){//阻塞等待 获取任务信息uint32_t operatorCode = 0;ssize_t s = read(blockFd, &operatorCode, sizeof(uint32_t));if (s == 0)break;assert(s == sizeof(uint32_t));(void)s;//处理任务if (operatorCode < functors.size()){functors[operatorCode]();}}cout << "进程 [" << getpid() << "] 结束工作" << endl;
}
// [子进程的pid,子进程的管道fd]
void sendTask(const vector & processFds)
{srand((long long)time(nullptr));while (true){sleep(1);//选择一个进程 选择进程是随机的 没有压着一个进程给任务 -- 随机的//较为均匀的将任务给所有的子进程 --- 负载均衡uint32_t pick = rand() % processFds.size();// 选择任务uint32_t task = rand() % functors.size();//把任务给一个指定的进程write(processFds[pick].second, &task, sizeof(task));//打印对应的提示信息cout << "父进程指派任务 --> " << info[task] << "给进程:" << processFds[pick].first<< "编号:" << pick << endl;}
}int main()
{loadFunctor();vector assignMap;for (int i = 0; i < processNum; ++i){//定义保存管道fd的对象int pipefd[2] = {0};//创建管道pipe(pipefd);pid_t id = fork();if (id == 0){//子进程读取close(pipefd[1]);//子进程执行work(pipefd[0]);close(pipefd[0]);exit(0);}//父进程做得事情close(pipefd[0]);elem e(id, pipefd[1]);assignMap.push_back(e);}cout << "create all process success !" << endl;//父进程,派发任务sendTask(assignMap);// 回收资源for (int i = 0; i < processNum; ++i){if (waitpid(assignMap[i].first, nullptr, 0) > 0)cout << "wait for : pid =  " << assignMap[i].first << " wait success"<< "number " << i << endl;close(assignMap[i].second);}return 0;
}

我们发现此时父进程5个子进程随机的派发不同的任务,这就是一种进程池。至此匿名管道全部写完。

其中,我们在shell命令行中写的 | 就是匿名管道

1.3管道的特征总结

  1. 管道只能用来进行具有血缘关系的进程之间,进行进程间通信,常用语父子通信。
  2. 管道只能单向通信(由内核设计实现)半双工的一种有特殊情况
  3. 管道自带同步机制(pipe满,write等;pipe空,read满) -- 自带访问控制
  4. 管道是面向字节流的 -- 先写的字符 一定是先被读取的 没有格式边界 需要用户来自定义区分内容的边界 [sizeof(uint32_t)]
  5. 管道的生命周期随进程 -- 管道是文件 -- 进程退出了 曾经打开的文件也会退出

1.4命名管道

我们刚刚提到的都是父子间(血缘)通信,如果我们想要两个毫不相干的两个进程之间通信,应该怎么办。因此我们接下来要讲的就是命名管道。命名管道和之前的匿名管道最大的区别就是可以让任意两个进程之间通信。

1.4.1创建一个命名管道

  • 创建一个命名管道可以在命令上创建 使用如下这个命令

mkfifo filename

这个myfifo就是一个管道文件,前面以p开头。以p开头就是管道,假如我们现在要在左侧想管道内部写入一些东西,在右侧实时查看,当我们左侧回车按下时候,右侧立马出现了“aaaaaa”

但是这样还是不能很好的观察现象,我们在右侧写一个实时的查看脚本,让一直想管道文件内部写入 bbbbb

while :; do echo "bbbbb" ; sleep 1; done >> myfifo

  • 命名管道也可以在程序中创建,相关函数

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

我们发现命名管道是带路径的,这有什么作用呢? 其实命名管道是通过一个fifo文件,由于这个文件存在路径,我们都知道路径具有唯一性,因此通过路径我们进程都可以看到这一份资源。

1.4.2 命名管道编码

由于我们知道命名管道可以实现两个不想关的进程完成通信,因此我们接下来将写两个文件(进程),让这两个文件进行通信。我们想让clientFifo.cpp这个进程和severFifo.cpp这个进程通过命名管道通信,该怎么写呢?

//comm.h
#pragma once 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include #define IPC_PATH "./.fifo"// makefile
.PHONY:all
all: clientFifo severFifoclientFifo:clientFifo.cppg++ -Wall -o $@ $^ -std=c++11severFifo:severFifo.cppg++ -Wall -o $@ $^ -std=c++11.PHONY:clean
clean:rm -f clientFifo severFifo .fifo//clientFifo.cpp
//写入
#include "comm.h"using namespace std;int main()
{int pipeFd = open(IPC_PATH,O_WRONLY);if(pipeFd < 0){cerr<<"Open : " << strerror(errno) < 0 ){buffer[s] = '\0';cout<<" 客户端-->服务器#  " << buffer << endl;}else if(s == 0){cout<< "客户端退出了,我也退出了";break;}else{//do nothingcout << "read error" <<  strerror(errno) <

通过这个代码我们成功的在两个毫无相关的进程之间完成了管道通信。

(本篇完)

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...