目录
进程间通信的本质:让不同的进程看到同一份资源!
什么是管道通信
管道是Unix中最古老的进程间通信的形式。它是一种基于文件的通信形式,我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。
管道文件是一种纯内存文件,不需要刷新到磁盘。管道只允许单向通信,如果要双向通信的话,需要建立两个管道,“互相读写”!
管道通信的特点
管道通信时可能会遇到以下四种情况:
- 1、正常情况,如果管道没有数据了,读端必须等待,直到有数据为止(写端写入数据)
- 2、正常情况,如果管道被写满了,写端就必须等待,直到有空间为止(读端读走数据)
- 3、写端关闭,读端如果一直读的话,就会导致 read 返回值为0,表示读到了文件结尾为止
- 4、读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标文件发送 SIGPIPE(13) 信号,终止目标进程。
管道在通信时有以下五种状态
- 1、匿名管道,可以允许有血缘关系的进程之间进行进程间通信,常用于父子,但匿名管道的通信也仅限于此!
- 2、匿名管道,默认给读写端要提供同步机制
- 3、是面向字节流的(面向字节流读取, 跟写的时候的格式无关, 读取的时候只跟字节数有关)
- 4、管道的声明周期是跟随进程的
- 5、管道是单向通信的,它是半双工通信的一种特殊情况。
匿名管道
匿名管道没有文件名,只有文件描述符。可以调用 pipe 函数来创建匿名管道
其中,传进去一个数组,pipefd[0] 表示读端文件描述符,pipedf[1] 表示写端文件描述符。
返回值:创建成功返回0,创建失败返回-1,并设置错误码!
当用父子进程之间创建管道来通信时:
上述的管道特点、匿名管道的原理等。都可以使用下面的代码来测试(代码有超详细注释)
下面这段代码创建了一个简单的单向通信的管道
#include <iostream>
#include <unistd.h> // 操作系统调用接口, 只能 .h 结尾(规定的)
#include <cassert> // 以 c 开头 + C语言头文件(不加 h) 就可以重新封装一套 C 语言头文件 #include<cstdio> #include<cstring>
#include <sys/types.h>
#include <cstdio>
#include <cstring>
#include <sys/wait.h>
using namespace std;
#define MAX 1024
int main()
{
int pipefd[2] = {0};
// 第一步:建立管道
int n = pipe(pipefd); // 管道创建成功 等于 0 创建成功,创建失败返回 -1
assert(n == 0); // 只在 debug 下存在 assert 用于意料之外的问题!
(void)n; // 假装使用一下, 防止 n 没有被使用而报警
// cout << pipefd[0] << " " << pipefd[1] << endl; // 使用 pipe 函数后 pipe[0], pipe[1] 分别自动被设置为 3,4 (未被使用的两个最小的文件描述符)
// pipefd[0] 读端 pipefd[1] 写端
// 第二步:创建子进程
pid_t id = fork();
if (id < 0)
{
// 创建失败
perror("fork");
return 1;
}
// 第三步:父子关闭不需要的文件描述符,形成单向通信的信道
// 子进程写入,父进程读取
if (id == 0)
{
// 子进程
close(pipefd[0]);
int cnt = 10;
// 只向管道写入
while(cnt)
{
char message[MAX];
snprintf(message, sizeof(message) - 1, "Hello father, I am child, pid: %d, cut: %d", getpid(), cnt);
cnt--;
write(pipefd[1], message, strlen(message));
sleep(1);
}
// TODO
exit(0);
}
// 父进程
close(pipefd[1]); // 父进程关闭写端
char buffer[MAX];
while(true)
{
ssize_t n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 一会要给最后一个位置加 '\0',方便 C/C++ 使用(万一读满了, 就最多读 MAX - 1 个)
// 面向字节流读取, 跟写的时候的格式无关, 读取的时候只跟字节数有关
if(n > 0)
{
buffer[n] = 0; // 自己维护字符串(为 C++ 准备的)
cout << getpid() << " :child say:" << buffer << " to me !" <<endl;
}
if(n == 0) break; // 说明写端关闭了(不是写端不写,而是关闭了)读端就会关闭
}
// 子进程退出后才能等待到
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
// 等待成功
cout << "wait success, child exit sig: " << (status &0x7F) << endl; // 退出信号 :status 的低 7 位
}
return 0;
}
使用管道技术设计一个简单的进程池
进程池(Process Pool)是一种并发编程的技术,它允许创建一组预先分配的子进程,这些子进程可以被重复地使用来执行任务。
通常情况下,每个进程都会拥有一个独立的地址空间和资源,这会导致进程的创建和销毁需要耗费大量的时间和资源。而进程池技术通过预先创建一组子进程,这些子进程会在一个池中等待任务分配。当有任务需要执行时,只需要将任务交给其中一个空闲的子进程即可,不需要重复地创建和销毁进程。
进程池通常由一个主进程(也称为管理进程)和一组子进程组成。主进程负责管理子进程的创建、销毁和任务分配,而子进程则负责执行实际的任务。
进程池的主要优势:提高效率、简化编程、提高可扩展性,进程池主要适用于CPU密集型的任务,即任务主要涉及到计算和数据处理。
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <functional>
#include <ctime>
#include <vector>
#include <string>
#include <sys/types.h>
#include <sys/wait.h>
typedef std::function<void()> task_t;
void Download()
{std::cout << "我是一个下载任务" << std::endl;}
void PrintLog()
{std::cout << "我是一个打印日志的任务" << std::endl;}
void PushVideoStream()
{std::cout << "这是一个推送视频流的任务" << std::endl;}
// void ProcessExit()
// {exit(0);}
class Init
{
public:
// 任务码
const static int g_download_code = 0;
const static int g_printlog_code = 1;
const static int g_pushVideostream_code = 2;
// 任务集合
std::vector<task_t> tasks;
public:
Init()
{
tasks.push_back(Download);
tasks.push_back(PrintLog);
tasks.push_back(PushVideoStream);
srand(time(nullptr) ^ getpid());
}
bool CheckSafe(int code)
{
if(code >= 0 && code < tasks.size()) return true;
else return false;
}
void RunTask(int code)
{
return tasks[code]();
}
int SelectTask()
{
int code = rand() % tasks.size();
return code;
}
std::string ToDesc(int code) //将任务码转化为任务字符传串
{
switch (code)
{
case g_download_code:
return "g_download_code";
case g_printlog_code:
return "g_printlog_code";
case g_pushVideostream_code:
return "g_pushVideostream_code";
default:
return "Unknow";
}
}
};
Init init; // 定义对象
const int num = 5;
static int number = 1;
class channel // 先描述, 再组织
{
public:
channel(int fd, pid_t id)
: ctrlfd(fd),
workerid(id)
{
name = "channel-" + std::to_string(number++);
}
public:
int ctrlfd;
pid_t workerid;
std::string name;
};
void Work()
{
while (true)
{
int code = 0;
ssize_t n = read(0, &code, sizeof(code)); // n 表示每次读到的字节个数
if (n == 0) break; // 等于0 说明啥都没读到退出子进程并回收
(void)n;
if (!init.CheckSafe(code))
continue; // code 不合法, 不在任务范围之内
init.RunTask(code);
}
}
void PrintDebug(const std::vector<channel> &c)
{
for (const auto &channel : c)
{
std::cout << channel.ctrlfd << "," << channel.workerid << "," << channel.name << std::endl;
}
}
void CreateChannels(std::vector<channel> *c)
{
std::vector<int> old; // 用来保存父进程的写端文件描述符
for (int i = 0; i < num; i++)
{
// 1. 先创建管道
int pipefd[2];
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 2. 创建子进程
pid_t id = fork();
assert(id != -1);
// 3. 构建单向信道
if (id == 0) // child
{
if(!old.empty())
{
for(int i = 0; i < old.size(); i++)
{
close(old[i]); // 关闭继承过来的父进程的文件描述符
}
}
close(pipefd[1]);
dup2(pipefd[0], 0); // 管道直接切换为从标准输入中读(重定向)
Work();
exit(0);
}
// father
close(pipefd[0]);
old.push_back(pipefd[1]); //保存这次进程的写端, 方便下次的子进程关闭这些从父进程中继承过来的不需要要的端口
c->push_back(channel(pipefd[1], id));
// std::cout << id << std::endl;
}
}
void SendCommand(std::vector<channel> &c, bool flag, int num = 0)
{
int quit = 0;
int pos = 0;
while (true)
{
// 1、选择任务
int command = init.SelectTask();
// 2、选择信道
const auto &channel = c[pos++];
pos %= c.size();
// debug
std::cout << "send command" << 1 << "in" << channel.name <<"worker is" << std::endl;
sleep(10);
// 3、发送任务
write(channel.ctrlfd, &command, sizeof(command)); // 发送任务
// 4、判断是否要退出
if (!flag)
{
num--;
if (num <= 0)
break;
}
//sleep(1);
}
std::cout << "SendCommand done......" << std::endl;
}
void ReleaseChannels(std::vector<channel>& c)
{
// version 2
int num = c.size() - 1;
for(; num >= 0; num --)
{
close(c[num].ctrlfd);
pid_t rid = waitpid(c[num].workerid, nullptr, 0);
}
// version 1
// // 回收资源,想让子进程退出,释放资源,只需要关闭写端
// for (const auto &channel : c)
// {
// close(channel.ctrlfd);
// // pid_t rid = waitpid(channel.workerid, nullptr, 0); 如果这样做就会出现, 其他子进程的文件描述符还指向写端,导致写端无法正常关闭, 进而导致 wait 读不到数据
// }
// // 回收子进程
// for (const auto &channel : c)
// {
// pid_t rid = waitpid(channel.workerid, nullptr, 0);
// if (rid == channel.workerid)
// {
// std::cout << "wait child" << channel.workerid << "success" << std::endl;
// }
// }
}
const bool g_always_loop = true;
int main()
{
std::vector<channel> channels;
CreateChannels(&channels);
// PrintDebug(channels);
SendCommand(channels, !g_always_loop, 1000);
ReleaseChannels(channels);
// sleep(10);
return 0;
}
命名管道
匿名管道没有名字,可以让有血缘关系的进程,通过继承的属性,进行管道通信,而命名管道则是可以让两个好不相干的进程进行管道通信!
1、两个进程中的其中一个进程调用系统接口 mkfifo 创建命名管道文件。
创建成功返回0,创建失败返回 - 1,并设置错误码 。
2、使用这个管道文件进行通信,一个进程为读端,一个进程为写端。
一个进程使用 write 接口往这个管道文件写,一个进程使用 read 接口从这个管道文件里读,就形成一个管道式的通信。