文章目录
- 一、进程间通信的目的
- 二、进程间通信的本质
- 三、管道
- 1、介绍
- 2、匿名管道
- 3、命名管道
一、进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
二、进程间通信的本质
让不同的进程看到同一份资源。
三、管道
1、介绍
管道(Pipe)是一种基本的进程间通信(IPC,Inter-Process Communication)机制。它允许一个进程的输出直接作为另一个进程的输入,从而实现数据的传递,这种机制在Unix和类Unix系统(如Linux)中非常常见。分为:匿名管道和命名管道。
2、匿名管道
(1)为什么叫匿名管道?
该管道不需要路径、不需要名字。
(2)匿名管道的特性
- 面向字节流。
- 单向数据通信
- 用于具有血缘关系的进程(一般用于父子)。
- 文件的生命周期随进程,管道也是。
- 管道自带同步互斥等机制机制。
- 在Linux系统中,匿名管道的大小通常是固定的,并且相对较小。一般来说,匿名管道的缓冲区大小限制在4KB左右,与PIPE_BUF常量(它定义了管道缓冲区的最小原子单位,并影响着进程间通过管道通信的行为)有关。
(3)创建匿名管道
功能:创建一无名管道。
函数原型:
int pipe(int fd[2]);
头文件:
#include <unistd.h>
参数:
fd:输出型参数,文件描述符数组, 其中fd[0]表示读端, fd[1]表示写端。
返回值:
成功返回0,失败返回错误代码。
使用:
通过匿名管道使父进程向子进程发送信息。
1 #include <iostream> 2 #include <string> 3 #include <cstdlib> 4 #include <unistd.h> 5 #include <sys/types.h> 6 #include <sys/wait.h> 7 8 int main() 9 { 10 // 1、创造管道 11 int p[2] = {0}; 12 int n = pipe(p); 13 if (n != 0) 14 { 15 std::cerr << "管道错误" << std::endl; 16 return 1; 17 } 18 19 // 2、创建子进程 20 pid_t id = fork(); 21 if(id < 0) 22 { 23 std::cerr << "创建子进程错误" << std::endl; 24 } 25 //3、进行通信 26 //父进程 - 写 27 else if(id > 0) 28 { 29 //关闭读端 30 close(p[0]); 31 32 std::string s = "i am father"; 33 write(p[1],s.c_str(),s.size()); 34 35 close(p[1]); 36 pid_t rid = waitpid(0,nullptr,0); 37 (void)rid; 38 39 } 40 //子进程 -读 41 else if(id == 0) 42 { 43 //关闭写端 44 close(p[1]); 45 46 //读 47 char buff[1024]; 48 int m = read(p[0],buff,1024); 49 buff[m] = 0; 50 std::cout<<buff<<std::endl; 51 52 close(p[0]); 53 exit(0); 54 } 55 56 return 0; 57 }
站在文件描述符角度-深度理解管道
站在内核角度-管道本质
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想。
管道读写的规则:
- 管道为空且正常:rand会阻塞。
- 管道为满且正常:write会阻塞。
- 管道写端关闭且读端继续:读端读到0,表示读到文件结尾。
- 管道写端继续且读端关闭:写入进程会被终止(os发送信号13终止程序)。
(4)使用匿名管道实现简单的进程池
进程池
进程池(Process Pool)是一种并发编程的模型,用于管理和复用多个进程,以提高系统的效率和性能。以下是关于进程池的详细解释:
一、定义与组成
定义:进程池是资源进程和管理进程组成的技术应用,通过预先创建好的空闲进程,管理进程会把工作分发到空闲进程来处理。
组成:进程池主要由资源进程和管理进程组成。资源进程是实际执行任务的进程,而管理进程则负责创建、管理和调度这些资源进程。
二、工作原理
任务分配:管理进程负责将任务分配给空闲的资源进程。这通常通过某种任务队列来实现,资源进程从队列中获取任务并执行。
进程交互:管理进程和资源进程之间需要交互,以传递任务、状态和结果等信息。这种交互通常通过IPC(进程间通信)、信号、管道等机制来实现。
资源回收:当资源进程完成任务后,管理进程会回收这些进程,以便它们可以被重新用于执行其他任务。
三、优势与应用
减少性能开销:进程池通过复用进程来减少因频繁创建和销毁进程而带来的性能开销。
提高并发性能:在处理大量并发任务时,进程池可以显著提高系统的并发性能。
易于管理:进程池提供了一种集中管理多个进程的方式,使得系统的可维护性和可扩展性得到提高。
代码实现:
对进程进行描述
Channel.hpp
#pragma once
#include <unistd.h>//先描述 -每一个进程
class Channel
{
public:Channel(int fd,pid_t id):_fd(fd),_id(id){}//执行任务void Send(int num){write(_fd,&num,sizeof(num));}int _fd;pid_t _id;
};
任务
Task.hpp
#pragma once
#include <iostream>
#include <unordered_map>
#include <functional>
#include <ctime>
#include <sys/types.h>
#include <unistd.h>using task_t = std::function<void()>;void func1()
{std::cout << "执行任务1" << std::endl;
}void func2()
{std::cout << "执行任务2" << std::endl;
}
void func3()
{std::cout << "执行任务3" << std::endl;
}
void func4()
{std::cout << "执行任务4" << std::endl;
}//任务
class Task
{
public://初始化Task(){srand(time(nullptr));mp[0] = func1;mp[1] = func2;mp[2] = func3;mp[3] = func4;}//获取任务号int SelectTask(){return rand() % mp.size();}//添加任务void AddTask(task_t t){mp[mp.size()] = t;}//执行任务void Excute(int number){if(mp.find(number) == mp.end())return;mp[number]();}private://编号与任务的隐式哈希std::unordered_map<int, task_t> mp;
};//全局
Task tk;//实行的方法
void Worker()
{while (true){int num;int n = read(0,&num,sizeof(num));if(n > 0){tk.Excute(num);}else if(n == 0){break;}}
}
进程池
ProcessPool.hpp
#pragma once
#include <string>
#include <vector>
#include <cstdlib>
#include <sys/wait.h>
#include "Task.hpp"
#include "Channel.hpp"//包装器
using work_t = std::function<void()>;//错误码
enum
{OK = 0,UsageError,PipeError,ForkError
};//进程池
class ProcessPool
{
public:ProcessPool(int num, work_t work = Worker): _num(num),_work(work){}// 初始化管道和子进程int InitProcessPool(){for (int i = 0; i < _num; i++){// 先创建管道int p[2] = {0};int n = pipe(p);if (n != 0){return PipeError;}// 创建子进程int id = fork();if (id == 0) // 子进程{// 将不用的端口关闭close(p[1]);// 将子进程继承下来的写端都关闭了for (auto &e : channels){close(e._fd);}// 重定向到0端口dup2(p[0], 0);// 执行任务_work();exit(0);}else if (id > 0) // 父进程{// 关掉不用的读端close(p[0]);// 对子进程管理channels.emplace_back(Channel(p[1], id));}else{return ForkError;}}return OK;}// 执行任务void DispatchTask(){// 轮询int count = 10;int index = 0;while (count--){// 获取任务int n = tk.SelectTask();std::cout << "n: " << n << std::endl;// 派发任务Channel channel = channels[index];std::cout << "index:" << index << " fd: " << channel._fd << " id: " << channel._id << std::endl;channel.Send(n);index++;index %= _num;sleep(2);}}// 结束任务void CleanProcessPool(){for (auto &e : channels){close(e._fd);waitpid(e._id,nullptr,0);}}void Debug(){std::cout << "num: " << _num << std::endl;for (auto &e : channels){std::cout << "fd: " << e._fd << "id: " << e._id << std::endl;}}private:int _num; // 子进程个数work_t _work; // 使用方法std::vector<Channel> channels; // 管理子进程和管道
};
Main.cc
#include"ProcessPool.hpp"int main(int argc,char *argv[])
{if(argc == 1){std::cout<<"ProcessPool -num"<<std::endl;return UsageError;}ProcessPool p(std::stoi(argv[1]));p.InitProcessPool();p.DispatchTask();p.CleanProcessPool();return 0;
}
3、命名管道
- 匿名管道只能使具有血缘关系的进程进行通信,而命名管道可以使同一台主机的不同进程进行通信,和匿名管道都是使用文件内核级缓冲区,不对磁盘进行刷新。
- 命名管道是一个特殊的文件,用于在不同进程之间进行数据交换和通信。
- 读写规则和匿名管道一样。
(1)为什么叫命名管道
具有真是路径+文件名(真实存在的文件)
(2)命名管道特点
- 具有实体文件。
- 双向通信能力:与只支持单向通信的匿名管道(仅存在于父子进程间)不同,命名管道允许两个进程进行双向通信。然而,这通常需要通过创建两个命名管道来实现,每个管道负责一个方向的数据传输。
- 命名管道保证数据按照写入的顺序被读取,即先进先出(FIFO)的原则。这确保了数据的完整性和一致性。
- 由于命名管道在文件系统中表现为文件,因此可以使用标准的文件权限机制来控制对管道的访问。这包括读权限、写权限和执行权限等。
- 在Linux系统中,匿名管道的大小通常是固定的,并且相对较小。一般来说,匿名管道的缓冲区大小限制在4KB左右,与PIPE_BUF常量(它定义了管道缓冲区的最小原子单位,并影响着进程间通过管道通信的行为)有关。
- 生命周期随系统存在。
(3)命名管道的创建
命名管道可以从命令行上创建,命令行方法是使用下面这个命令
mkfifo filename
命名管道也可以从程序里创建,相关函数有mkfifo
头文件
#include <sys/types.h>
#include <sys/stat.h>
函数原型:
int mkfifo(const char *filename,mode_t mode);
参数:
filename : 管道文件名。
mode: 管道文件权限。
返回值:
成功时,mkfifo返回0。
失败时,返回-1,并设置errno以指示错误类型。
命名管道删除的函数
头文件:
#include <unistd.h>
函数原型:
int unlink(const char *pathname);
参数:
pathname:指向要删除的文件的路径名的指针。
返回值
成功时返回 0。
失败时返回 -1,并设置 errno 以指示错误类型。
(4)命名管道文件打开规则
- 如果当前打开操作是为读而打开FIFO时,阻塞直到有相应进程为写而打开该FIFO。
- 如果当前打开操作是为写而打开FIFO时,阻塞直到有相应进程为读而打开该FIFO。
- 总结:需要两个进程以不同操作打开FIFO时,才会真正打开FIFO。
(5)用命名管道实现server&client通信
默认client创建管道文件并进行写操作。
server获取管道文件并进行读操作。
使用代码实现:
公共文件:
两个程序的公共代码
#pragma once#include <iostream>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>//公共文件名
const char* filename = "./fifo";//读取数据大小
const int size = 1024;//公共权限
const mode_t permissions = 0666;//写
const int o_wronly = O_WRONLY;//读
const int o_rdonly = O_RDONLY;//打开文件
int Open(int flag)
{int fd = open(filename,flag);if(fd < 0){std::cerr<<"打开文件失败"<<std::endl;}return fd;
}//关闭文件
void Close(int fd)
{if(fd > 0)close(fd);
}
Client.hpp
#include"Comm.hpp"//创建命名管道
class Init
{
public://创建管道文件Init(){//设置掩码umask(0);//创建管道文件int m = mkfifo(filename,permissions);if(m < 0){std::cerr<<"创建管道文件失败"<<std::endl;}else{std::cerr<<"创建管道文件成功"<<std::endl; }}//删除管道文件~Init(){int m = unlink(filename);if(m < 0){std::cerr<<"删除管道文件失败"<<std::endl;}else{std::cerr<<"删除管道文件成功"<<std::endl;}}
};Init it;class Client
{
public://打开文件 -- bool OpenFifo(){_fd = Open(o_wronly);if(_fd < 0){return false;}return true;}//写文件void WriteFifo(const std::string & in){write(_fd,in.c_str(),in.size());}//关闭文件void CloseFifo(){Close(_fd);}private:int _fd;
};
Client.cc
#include "Client.hpp"int main()
{Client client;if(!client.OpenFifo())return 1;while (true){std::cout << "Please Enter# ";std::string in;std::getline(std::cin, in);client.WriteFifo(in);}client.CloseFifo();return 0;
}
Server.hpp
#include"Comm.hpp"class Server
{
public://打开文件 -- bool OpenFifo(){_fd = Open(o_rdonly);if(_fd < 0)return false;return true;}//读文件int readFifo(std::string *out){char buffer[size];int n = read(_fd,buffer,sizeof(buffer));std::cout<<n<<std::endl;buffer[n] = 0;*out = std::string(buffer);return n;}//关闭文件void CloseFifo(){Close(_fd);}private:int _fd;
};
Server.cc
#include "Server.hpp"int main()
{Server server;if(!server.OpenFifo())return 1;while (true){std::string out;int n = server.readFifo(&out);if(n == 0)break;std::cout << out << std::endl;}server.CloseFifo();return 0;
}