一.介绍进程间通信
1.什么是进程间通信
进程间通信(Inter-Process Communication,简称 IPC) 是指在不同进程之间传递或交换信息的过程。在操作系统中,每个进程都有自己独立的内存空间(虚拟地址空间),这意味着一个进程无法直接访问另一个进程的内存数据,需要借助操作系统提供的 IPC 机制来实现。
2.为什么要进行进程间通信
为了打破这种隔离,让进程能够协同工作、共享数据或相互控制,就需要借助操作系统提供的 IPC 机制。
作用:
1.数据传输:一个进程需要把数据传输给另一个进程。
2.资源共享:多个进程共享同样的资源。
3.通知事件:一个进程需要向另外的进程发送消息,表示发生了某种事件(如子进程退出,通知父进程回收子进程)。
4.进程控制:有些进程需要完全控制其他进程的执行,此时控制进程希望能够 拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
3.进程间通信的本质:
让不同的进程看到同一份资源------内存。此内存是由操作系统提供的,并且由系统调用设置统一的接口。
4.发展历程
**管道:**匿名管道pipe, 命名管道
**System V IPC:**System V 消息队列,System V 共享内存 和 System V 信号量
**POSIX IPC:**消息队列 ,共享内存,信号量 ,互斥量 ,条件变量 和 读写锁
二.匿名管道
1. 基础概念
匿名管道是 Linux 最基础的 IPC 方式,由 pipe() 系统调用创建。 在内核中存在一块缓冲区,没有对应的磁盘文件、没有文件名、没有 inode,因此称为匿名管道,为半双工模式。
单向通信(半双工) :
数据只能从写端流向读端;如需双向通信,需要创建两根管道。
全双工: 通信双方可以同时收发数据。 A 发数据给 B 的同时,B 也可以发数据给 A,两条数据流互不干扰。
2.系统调用
#include <unistd.h>
int pipe(int pipefd[2]);
在内核创建一块缓冲区(匿名管道),返回一对文件描述符。
pipefd2:文件描述符
- fd 0:管道读端(read)
- fd 1:管道写端(write)
- 通常fd0=3,fd1=4,因为已经有三个默认打开的文件,所以只能排3和4.
注意:数据只能:fd 1 → fd 0。
返回值
- 成功:返回
0- 失败:返回
-1,同时设置errno
使用:


3.匿名管道的原理
我们从进程通信的本质出发,观察匿名管道是如何做到两个进程共享一块内存空间。
首先由一个进程创建一个管道文件并通过文件描述符来IO连接。

然后该进程再fork一个子进程,根据子进程的开始浅拷贝的特性,子进程会拥有和父进程相同的文件描述符,那么其二者都指向同一个匿名管道。这样通信的条件就形成了。

然后根据系统调用的条件(半双工模式),选择留下一个接收,一个发送,就完成了信息的传递。

4.父子进程使用匿名管道通信
子进程写,父进程读:
#include<iostream>
#include<unistd.h>
#include <sys/wait.h>
#include<cstring>
void Childwrite(int fd)
{
while(1)
{
char putbuff[1024];
snprintf(putbuff,sizeof(putbuff),"fd:%d,子进程pid:%d\n",fd,getpid());
write(fd,putbuff,sizeof(putbuff));
}
}
void Fatherread(int fd)
{
while(1)
{
char getbuff[1024];
read(fd,getbuff,sizeof(getbuff));
std::cout<<getbuff<<std::endl;
}
}
int main()
{
int fd[2];
int n=pipe(fd);
pid_t id=fork();
if(id==0)
{
close(fd[0]);
Childwrite(fd[1]);
close(fd[1]);
exit(0);
}
close(fd[1]);
Fatherread(fd[0]);
close(fd[0]);
wait(nullptr);
return 0;
}

5.五种特性和四种情况
1.五种特性
1.匿名管道只能在有亲缘关系的进程间进行通信(父子,兄弟)
文件描述符只能被 fork( )继承,没有亲缘关系无法拿到同一个管道的读写端。无亲缘进程无法获取管道同一对读写 fd,不能通信。
2.只能进行单向通信,半双工工作模式。
3.管道文件,自带同步机制。
当写慢,读快时:读阻塞;
当写快,读慢时:管道文件很快被写满,写阻塞。
4.管道是面向字节流的。
不存在消息边界。多次写入的数据在内核缓冲区连续存放,读取时可能一次性读出多条消息,也可能拆分读出。业务层必须自行约定分包规则(定长、特殊分隔符、消息头携带长度)区分独立消息。
5.管道的生命周期随进程。
管道缓冲区存在内核内存,无磁盘实体;当所有持有管道 fd 的进程全部关闭两端描述符,内核自动释放,管道销毁。
2.四种情况
1.写慢,读快
现象:读进程读取速度 大于写进程写入速度,管道缓冲区很快被读空。
行为:read() 阻塞,读进程休眠,等待写端写入新数据,读的速度被拖慢。
写端:sleep 2秒,再写入。

2.写快,读慢
现象:写进程写入速度 大于读进程读取速度,内核环形缓冲区被填满。
行为:write() 阻塞,写进程休眠,等待读端读取数据腾出缓冲区空间。
3.写关闭,读继续
1.管道内残留的数据依旧可以正常 read 读取;
缓冲区数据全部读完之后,后续read()不再阻塞,直接返回 0(EOF);
子进程收到返回值 0,代表通信通道关闭,可以正常退出。
写三遍然后关闭

4.写继续,读关闭
内核向执行 write 的进程发送 SIGPIPE(管道破裂)信号,直接杀死写进程
我们使用waitpid来接收其的退出码来验证。


三.进程池实战

目标结构:父进程通过管道分别与多个子进程进行通信,父进程写入任务码,子进程读取,然后子进程去执行。
父进程通过管道的机制打开多个子进程,并保持就绪(从进程状态来讲,就是进程阻塞),需要使用时就可以快速执行任务,不用从头开始。
代码实现:
代码结构设计:
思想:先描述,再组织
先描述:主体是管道(为什么?因为通过管理管道可以管理子进程(回顾前面四种情况)),所以我们需要描述单个管道的属性以及其的任务(其实就是根据自己的机制对子进程管理)
再组织:对多个管道进行组织,形成结构,方便多个进行协作(这里没有),方便进行查找想要的管道,然后对其进行操作。
最后对外提供统一的接口:掩盖细节,方便使用。
具体代码:
1.描述:
创建channel类:
int _wfd;//父进程的写管道,关闭时,管道只剩下子进程的读端,读完就会关闭子进程,从而实现对其的管理
pid_t _cpid;//子进程的pid
std::string _name;//这个管道的总体描述字符串
class channel
{
public:
channel(int wfd,pid_t cpid)
:_wfd(wfd)
,_cpid(cpid)
{
_name="子进程:"+std::to_string(_cpid)+"write_fd"+std::to_string(_wfd);
}
~channel()
{}
private:
int _wfd;//父进程的写管道
pid_t _cpid;//子进程的pid
std::string _name;//这个管道的总体描述字符串
};
方法:根据最后对外的接口来设计:
主要就是下面这些:配合完成:任务码读写,关闭管道和子进程
channel(int wfd,pid_t cpid)
:_wfd(wfd)
,_cpid(cpid)
{
_name="子进程:"+std::to_string(_cpid)+"write_fd"+std::to_string(_wfd);
}
void Close()
{
close(_wfd);
}
void Wait()
{
wait(NULL);
}
void Write(int code)//对管道进行写
{
write(_wfd, &code, sizeof(code));
}
2.组织管理管道
属性:多个channel组成的数组
std::vector<channel> _channels;
方法:
1.选择管道
需求:任务尽量平均给各个子进程
平均方式:随机选取
ChannelManger()
{
srand(time(nullptr));
}
int select_channel()
{
if (_channels.empty())
{
std::cerr << "错误:不存在可用子进程通道!" << std::endl;
return -1;
}
return rand()%_channels.size();
}
2.插入构造channel
void insert(int wfd,pid_t pid)
{
// 原地构造,避免拷贝
_channels.emplace_back(wfd, pid);
}
3.写入任务码
根据挑选的管道写入挑选出的任务码
void Cwrite(int Ccode,int Tcode)
{
_channels[Ccode].Write(Tcode);
}
4.关闭管道写端
按照我们的构想,只需要在父进程中关闭其对各个管道的写端就行了。
void Channelclose()
{
for(auto& ch:_channels)
{
ch.Close();
ch.Wait();
}
}
但是实际情况要实际分析,所以我们在具体创建管道和子进程时再进行讨论。
3.准备对外接口
1.需要任务管理,进行选择任务,执行任务
typedef void(*Task)();
class TaskManger
{
public:
TaskManger()
{
}
void insert(Task task)
{
_Ts.push_back(task);
}
int select_Task()
{
int Next=_Tflag;
_Tflag=(_Tflag+1)%_Ts.size();
return Next;
}
void Execute(int flag)
{
_Ts[flag]();
}
~TaskManger()
{}
private:
std::vector<Task> _Ts;
int _Tflag=0;
};
2.主体Processpool
2.1属性:
class Processpool
{
public:
Processpool()
{
//插入任务
_TM.insert(Print);
_TM.insert(Doneload);
_TM.insert(Upsend);
}
private:
ChannelManger _CM;//管道管理器
TaskManger _TM;//任务管理器
};
2.2创建进程进程池
bool Create()
{
for(int i=0;i<process_count;i++)
{
int pipefd[2];
int n=pipe(pipefd);//创建相应的管道
if(n==-1)
{
std::cout<<"pipe err"<<std::endl;
return false;
}
ssize_t pid=fork();//创建子进程
if(pid==0)
{
// _CM.Channelclose_1();
close(pipefd[1]);//关闭写端
Work(pipefd[0]);//等待任务码,有就执行任务
close(pipefd[0]);//关闭读端
exit(0);
}
else if(pid>0)//父进程
{
close(pipefd[0]);//关闭读端
_CM.insert(pipefd[1],pid);//插入初始化管道管理器
}
else//创建失败
{
std::cout<<"fork err"<<std::endl;
return false;
}
}
return true;
}
work函数(子进程等待任务码,并且执行):
void Work(int fd)
{
while(true)
{
int taskcode=0;
ssize_t n=read(fd,&taskcode , sizeof(taskcode));
if(n>0)
{
if(n==4)
{
_TM.Execute(taskcode);
}
else
{
continue;
}
}
else if(n==0)//读完了,然后父进程的写端关闭,就退出了work
{
std::cout<<"子进程退出"<<std::endl;
break;
}
else
{
std::cout<<"子进程read,err"<<std::endl;
break;
}
}
}
以上为对外接口create,和子进程干的事(读端)
2.3父进程写端
选择子进程,写入任务码
void Run()
{
int taskcode=_TM.select_Task();
int channelcode=_CM.select_channel();
std::cout<<"选中的子进程:"<<_CM.Get_name(channelcode)<<std::endl;
_CM.Cwrite(channelcode,taskcode);
std::cout<<"选中的任务码:"<<taskcode<<std::endl;
}
2.4退出------关闭父进程写端
我们按照理想的结构:父进程的写端关闭,管道内的内容被读完,那么管道就会关闭,子进程就会退出。
所以:
void Channelclose()
{
for(auto& ch:_channels)
{
ch.Close();//关闭写端
ch.Wait();//回收子进程信息
}
}
但是我们会发现最后面全部子进程阻塞了,没有一个退出

这个是怎么回事:我们都知道子进程创建时会拷贝父进程的信息------fd,那么父进程指向前面管道的写端也会被拷贝,所以最后形成的结构并不是我们想象的那样。
画图理解

加上wait阻塞,所以从前往后关闭写端的方法使得进程全部阻塞。那么我们该如何解决呢?
解决方案:
方案一:
在子进程创建的时候对其拷贝父进程的各个写端进行删除。然后再进行从前往后的删除。
方案二:
从后往前删除
完整代码:
processpoll.hpp
#pragma once
#include<iostream>
#include<vector>
#include<unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<cstdlib>
#include<time.h>
#include"Task.hpp"
class channel
{
public:
channel(int wfd,pid_t cpid)
:_wfd(wfd)
,_cpid(cpid)
{
_name="子进程:"+std::to_string(_cpid)+"write_fd"+std::to_string(_wfd);
}
void Close()
{
close(_wfd);
}
void Wait()
{
wait(NULL);
}
void Print()
{
std::cout<<_name<<std::endl;
}
void Write(int code)
{
write(_wfd, &code, sizeof(code));
}
int Get_wfd()
{
return _wfd;
}
pid_t Get_cpid()
{
return _cpid;
}
std::string& Get_NAME()
{
return _name;
}
~channel()
{}
private:
int _wfd;
pid_t _cpid;
std::string _name;
};
class ChannelManger
{
public:
ChannelManger()
{
srand(time(nullptr));
}
void insert(int wfd,pid_t pid)
{
// 原地构造,避免拷贝
_channels.emplace_back(wfd, pid);
}
int select_channel()
{
if (_channels.empty())
{
std::cerr << "错误:不存在可用子进程通道!" << std::endl;
return -1;
}
return rand()%_channels.size();
}
std::string& Get_name(int num)
{
return _channels[num].Get_NAME();
}
void Prints()
{
for(int i=0;i<_channels.size();i++)
{
_channels[i].Print();
}
}
void Cwrite(int Ccode,int Tcode)
{
_channels[Ccode].Write(Tcode);
}
//方案一,子进程创建时让子进程自己关加给其它的写端
void Channelclose_1()
{
for(auto& ch:_channels)
{
ch.Close();
}
}
void Channelclose()//再父进程全部关
{
for(auto& ch:_channels)
{
ch.Close();
ch.Wait();
}
}
void Channelclose_2()
{
for(int i=_channels.size()-1;i>=0;i--)
{
_channels[i].Close();
_channels[i].Wait();
}
}
~ChannelManger()
{}
private:
std::vector<channel> _channels;
};
const int process_count=5;
class Processpool
{
public:
Processpool()
{
_TM.insert(Print);
_TM.insert(Doneload);
_TM.insert(Upsend);
}
void Work(int fd)
{
while(true)
{
int taskcode=0;
ssize_t n=read(fd,&taskcode , sizeof(taskcode));
if(n>0)
{
if(n==4)
{
_TM.Execute(taskcode);
}
else
{
continue;
}
}
else if(n==0)
{
std::cout<<"子进程退出"<<std::endl;
break;
}
else
{
std::cout<<"子进程read,err"<<std::endl;
break;
}
}
}
bool Creat()
{
for(int i=0;i<process_count;i++)
{
int pipefd[2];
int n=pipe(pipefd);
if(n==-1)
{
std::cout<<"pipe err"<<std::endl;
return false;
}
ssize_t pid=fork();
if(pid==0)
{
// _CM.Channelclose_1();
close(pipefd[1]);
Work(pipefd[0]);
close(pipefd[0]);
exit(0);
}
else if(pid>0)
{
close(pipefd[0]);
_CM.insert(pipefd[1],pid);
}
else
{
std::cout<<"fork err"<<std::endl;
return false;
}
}
return true;
}
void Debug()
{
_CM.Prints();
}
void Run()
{
int taskcode=_TM.select_Task();
int channelcode=_CM.select_channel();
std::cout<<"选中的子进程:"<<_CM.Get_name(channelcode)<<std::endl;
_CM.Cwrite(channelcode,taskcode);
std::cout<<"选中的任务码:"<<taskcode<<std::endl;
}
void over()
{
_CM.Channelclose();
}
~Processpool()
{}
private:
ChannelManger _CM;
TaskManger _TM;
};
main.cpp
#include"processpool.hpp"
int main()
{
Processpool pp;
pp.Creat();
pp.Debug();
int cnt=3;
while(cnt--)
pp.Run();
pp.over();
return 0;
}
task.hpp
#include<vector>
#include<iostream>
void Print(){std::cout<<"我在打印"<<std::endl;}
void Doneload(){std::cout<<"我在下载"<<std::endl;}
void Upsend(){std::cout<<"我在上传"<<std::endl;}
typedef void(*Task)();
class TaskManger
{
public:
TaskManger()
{
}
void insert(Task task)
{
_Ts.push_back(task);
}
int select_Task()
{
int Next=_Tflag;
_Tflag=(_Tflag+1)%_Ts.size();
return Next;
}
void Execute(int flag)
{
_Ts[flag]();
}
~TaskManger()
{}
private:
std::vector<Task> _Ts;
int _Tflag=0;
};
结果:
