🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
文章目录
- 一、了解进行间通信
- ①进程间通信的必要性
- ②进程间通信的技术背景
- 二、管道
- ①管道原理
- 管道原理,三步走
- 管道pipe
- ②匿名管道
- 进程间通信demo代码(管道)
- ③管道特点总结
- ④简单的进程池--管道应用
- 完整代码
- ⑤命名管道
- 原理
- mkfifo
- 小实验
一、了解进行间通信
进程间通信,简称IPC(inter-process communication)
进程的运行具有独立性,带来的直接结果就是进程间想要通信的话,提高了难度,进程间通信的本值就是让不同的进程看到同一份资源(内存空间),并且这一份资源不能属于任何一个进程(就算是写入缓冲区,另一个进程再去读,也是读不到的,因为写时拷贝)
所谓的独立性,不是绝对的独立,而是大部分情况下,是独立的,该进程运行终止后,不会去引用其他进程
①进程间通信的必要性
如果是单进程,那么也就无法使用并发的能力,更无法实现多进程协同
进程间通信的目的就在于:传输数据,同步执行流,消息通知等等---->也就是进程间协同
1.数据传输:一个进程需要将它的数据发送给另一个进程(一个进程对数据加工成半成品,再交给另一个)
2.资源共享:多个进程之间共享同样的资源(某些资源就是需要他们共享)
3.通知事件:一个进程需要向另一个或一组进程发送信息,通知它发生了某种事件(比如进程终止时通知父进程)
4.进程控制:有些进程希望完全控制另一个进程的执行(比如Debug进程),此时控制进程希望能够拦截另一个进程的所有行为,并能够及时知道它的状态改变
进程间通信不是目的,而是手段,目的在于让多进程协同
②进程间通信的技术背景
1.进程是具有独立性的,进程地址空间+页表 保证进程运行的独立性
2.通信的成本比较高,设计的时候就把进程设计成了天然的独立性强的模块
二、管道
①管道原理
管道是当代Linux计算机上,比较简单的一种通信方式
什么是管道?
管道,有入口,有出口
管道的特带就是只能单向通信,管道内部传送的都是数据
管道就是用来传送数据的,至于什么数据,依据应用场景,并且管道是一种单向通信的方式。
管道原理,三步走
管道通信 背后是进程之间通过 管道进行通信,每一个进程的task_struct,里面有files_struct指针,这个结构体当中就有fd_array文件描述符表,找到对应的file_struct(这个也就是文件对应的结构体,里面可以找到inode,内核缓冲区)
过程:
1.分别以读和写的mode,打开一个文件
比如读对一个的fd文件描述符是3,写对应的fd是4
2.fork()创建子进程
创建新的task_struct,又因为进程的独立性,子进程也会有自己的文件描述符表,父进程的部分PCB会拷贝给子进程,像文件描述符这种东西也会。这时候,一个文件就被两个进程以读写的方式打开
这不就是让不同的进程看到同一份内存空间吗?管道底层就是通过文件的方式实现的
3.双方进程各自关闭自己不需要的文件描述符
这一步是在规定方向,比如我让父进程写,让子进程读,就保留父进程写的fd,关闭读的fd,子进程保留读的fd,关闭写的fd
以上的这种方式就叫做管道通信,所以管道其实就是文件,两个进程在通信的时候也完全没必要将内容刷新到磁盘,应为刷新到磁盘对于通信双方来说是没有任何意义的,这是纯内存级的通信,如果还要刷到外设,就太慢了,所以进程间通信是不需要持久化的,这样效率才是最高的,并且进程间通信的数据大部分属于临时数据
管道pipe
管道是Unix当中最古老的进程间通信方式(但不代表不存在了),我们把一个进程链接到另一个进程的数据流称为一个"管道"
其次,管道分为1.匿名管道 2.命名管道
②匿名管道
参数说明:
pipe是创建一匿名管道
需要一个数组 fd[0]是读端 fd[1]是写端
返回值:pipe成功返回0,失败返回错误码
pipe的三个过程在上面已经说过
1.父进程创建管道(打开一个文件流)
2.fork子进程(子进程复制部分父进程PCB,与管道建立联系)
3.父子进程关闭不需要fd,控制管道方向(其实不关也行,只要控制好代码就可以了,关了严谨一些,避免代码写错)
这种fork让子进程继承,能够让具有血缘关系的进程继承进程间通信,常用于父子进程
进程间通信demo代码(管道)
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstdio>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
int main()
{
int pipefd[2]={0];//pipe需要使用的数组
int n = pipe(pipefd);//床架管道成功
assert(n!=-1);
void(n);
//release模式下,assert直接就没了,那么n就只是被定义而没有被使用,就可以出现大量告警
//避免这些告警,那么void(n)一下,证明被使用过
#ifdef DEBUG//条件编译
pid_t id = fork();
assert(id>=0);//<0即fork错误
if(id==0)//子
{
close(pipdfd[1]);//关闭写
char buffer[1024];//读
while(true)
{
ssize_t read(pipefd[0],buffer,sizeof buffer-1);//留一个位置存'\0',系统调用
if(s>0)
{
buffer[s]=0;
//Father说的话
cout<<"child get a message"<<getpid()<<"]"<<"Father#"<<buffer<<endl;
{
}
exit(0);//子进程退出,文件描述符会被自动关闭,所以不用写close(pipefd[0]);
}
//父
close(pipdfd[0]);//关闭读
string message="我是父,我正在给你发消息";//发消息
int count=0;//记录消息条数
char send_buffer[1024];
while(true)
{
//构造一个变化的字符串
//写入send_buffer
snprintf(send_buffer,sizeof send_buffer,"%s[%d] : %d",message.c_str(),getpid)_,count++);
//写消息
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
}
pid_t ret = waitpid(id,nullptr,0);//阻塞子进程
assert(ret<0);
(void)ret;
close(pipefd[1]);//父进程并没有退出
return 0;
}
③管道特点总结
1.管道是一种进程间通信方式,最大的特点就是用来具有血缘关系的进程进程进程间通信,常用于父子通信
2.父进程在写的时候,按照1s的时间间隔打印,但是在子进程中,并没有按照子进程的规则来立马进行读取,也是难找1s的时间间隔读取—具有访问控制(子进程在等待父进程写入,这个访问控制是由于单向管道导致的。例如显示器也是一个文件,父子同时向显示器写的时候,并不会等待,还会互相干扰,这就是缺乏访问控制)
3.管道提供的是面向流式的服务(面向字节流),一般需要通过指定协议来进行数据区分(我可能读一次,就是别人上千次的写,这就是流式服务)
4.管道是基于文件的,文件的声明周期随进程,所以管道的生命周期是随进程的(需要所有进程都关闭,管道才会自动释放)
5.管道是单向通信的,就是半双工通信的一种特殊模式(要么在发,要么在收就是半双工)
①写快,读慢,写满就不能写了,管道是文件,缓冲区有固定大小
②写慢,读快,管道没有数据的时候,必须等待(具有访问控制)
③写关,读0(read返回0),表示读到结尾
④读关,写继续写,OS会终止写进程,因为写的数据没有任何意义,没人读
④简单的进程池–管道应用
过程描述
1.创建多个进程(进行for循环,先打开管道pipe,然后fork出子进程,建立同喜,while(true)让该子进程等待任务,for多少次就有多少继承,把子进程的pid和子进程的管道pipefd放进slots表中,也就是知道他们在哪里等命令就可以了)
2.父进程写命令进fd对应文件,子进程读取并执行,通过洗牌算法(分发算发),达到单机的负载均衡
3.派发任务,这里采用随机数方案(进程池)
4.关闭fd与继承退出
完整代码
//Task.hpp代码
#pragma once//避免重复包含头文件
#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <unordered_map>
#include <vector>
//typedef std::function<void> func;
using func = std::function<void()>;//包装器
//C++11的用法
std::vector<func> callbacks;//函数回调数组。条件调用以下方法
std::unordered_map<int,std::strig> desc;//方法描述
//定义四个任务!!
void readMySQL()
{//就取个名字,不访问MySQL
std::cout<<"sub process["<<getpid()<<"]执行访问数据库的任务"<<std::endl;
}
void execuleUrl()
{
std::cout<<"sub process["<<getpid()<<"]执行url解析"<<std::endl;
}
void cal()
{
std::cout<<"sub process["<<getpid()<<"]执行加密任务"<<std::endl;
}
void save()
{
std::cout<<"sub process["<<getpid()<<"]执行数据持久化任务"<<std::endl;
}
void load()
{
//装载 方法描述
//第一个参数是方法描述desc的对应下标
desc.insert({callback.size(),"readMySQL: 读取数据库"});//列表初始化转成pair
callbacks.push_back(readMySQL);
desc.insert({callback.size(),"execuleUrl: 进行Url解析"});
callbacks.push_back(execuleUrl);
desc.insert({callback.size(),"cal: 进行加密计算"});
callbacks.push_back(cal);
desc.insert({callback.size(),"save: 进行数据的保存"});
callbacks.push_back(save);
}
//ProcessPool.cpp代码
#include <iostream>
#include <unistd.h> //pipe
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <cstring>
#include <string>
#include <cassert>
#include <vector>
#include <utility>//实用工具 --> make_pair
#include <Task.hpp>
#include <cstdlib>
#include <ctime>
using namespace std;
#define PROCESS_NUM 5//五个进程
int waitCommand(int waitFd,bool &quit)
{//哪个fd的进程等待命名
uint32_t/*四个字节的命令*/ command=0;
//fd这个进程,读取命令,多长
ssize_t s= read(waitFd,&command,sizeof command);
if(s==0)//==0就是读到结尾
{
quit=true;
return -1;
}
assert(s==sizeof(unit32_t));
//必须是四个字节
return command;
}
void SendAndWakeUp(pid_t who,int fd,uint32_t command)
{//发送命令并唤醒子进程
write(fd,&command,sizeof(command));
cout<<"main process: call process "<<who<<"execute "<<desc[command]<<"through "<<fd<<endl;
}
int main()
{
load();//装载方法和方法的描述
vector<pair<pid_t,int>> slots;//pid pipefd;
//记录进程与pipefd的关系,便于从进程池中找到进程
//创建多个进程
for(int i=0;i<PROCESS_MIN;i++)
{
//创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
assert(n==0);
void(n);
//创建子进程
pid_t id = fork();
assert(id!=-1);
if(id==0)//父
{
close(pipefd[1]);
while(true)
{
//等命令
bool quit=false;
int command = waitCommand(pipefd[0],quit);
if(quit) break;
//执行命令
if(command>=0&&command<HandlerSize())
{
callbacks[command]();//调用对应回调函数
}
else
cout<<"非法command"<<command<<endl;
}
exit(1);
}
//father
close(pipefd[0]);
slots.push_back(make_pair(id,pipefd[1]));
//创建出一堆子进程,放入slots,然后通过slots选择给进程池中哪个进程发消息
}
srand((unsigned int)time(nullptr)^getpid()^23323123123L);//让数据更随机
//srand(time(0));
while(true)
{
it command= rand()%HanderSize();
int choice = rand()%slots.size();
SendAndMakeUp(slots[choice].first,slots[choice].second,command);
//PID,pipefd,command; 发送命令并唤醒子进程
sleep(1);
}
for(const auto slot:slots)
{//关闭fd,读到文件结尾,子进程退出
close(slot.second);
}
for(const auto slot:clots)
{//回收子进程信息
waitpid(slot.first,nullptr,0);
}
return 0;
}
⑤命名管道
原理
命名管道是通过一个进程创建一个文件流,用PCB找到files_struct结构体,然后通过fd_array找到对应文件的file_struct,让另一个进程来指向这个被打开的文件即可完成通信
但是,普通的文件,打开操作了之后都是会持久化的,那效率就太低了,因为进程间通信是纯内存级的,并且大部分数据都是临时数据,不需要写入磁盘,所以操作系统就提出可以在磁盘创建一种新的文件–管道文件,特点就是可以被打开使用,但是并不会进行数据的持久化(管道文件除了文件内容之外什么都有)
mkfifo
取名为make fifo ,因为管道本来就是先进先出的
mkfifo 文件名
//创建管道文件
mkfifo(path,mode);//路径和权限
//操作管道文件
int fd = open(path,读写方式);//O_RDONLY等等
//通信代码
...操作文件即可
//关闭文件
close(fd);
unlike(path);//删除管道文件
小实验
一个进程写入,此时出阻塞状态,对方还没有打开读
成功读取