IPC进程间通信-管道

24 篇文章 1 订阅

🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
在这里插入图片描述

一、了解进行间通信

进程间通信,简称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);//删除管道文件

小实验

在这里插入图片描述
一个进程写入,此时出阻塞状态,对方还没有打开读
在这里插入图片描述

成功读取

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪皮兄弟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值