之前所学的进程,一般进程和进程之间是独立的关系,最多产生一些耦合,比如父进程创建子进程,父进程等待子进程等等。但实际在很多问题上进程和进程之间是需要相互协同的,那么进程间通信就是在不同进程之间传播或交换信息,接下来让我们一起走进进程间通信的大门!
可以形象理解成:两个人之间互相打电话,不管你们的目的是什么,是A打电话给B,叫他拿外套(进程控制),还是A通知B,今天晚上要开会(通知事件),本质都要“打电话”。
管道:
System V IPC:
POSIX IPC:
任何进程通信的手段:
其中,who命令和wc命令都是两个程序,当它们运行起来后就变成了两个进程,who进程通过标准输出将数据打到“管道”当中,wc进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,who命令用于查看当前云服务器的登录用户(一行显示一个用户),wc -l是计算行数的一个命令,说明当前只有一个用户在线,
pipe函数用于创建匿名管道,pipe函数的函数原型如下:
int pipe(int pipefd[2]);
pipe函数的参数是一个输出型参数,所以用户需要定义两个数组,并传过来,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
数组元素 | 代表含义 |
---|---|
pipefd[0] | 管道读端的文件 |
pipefd[1] | 管道写端的文件 |
pipe函数调用成功时返回0,调用失败时返回-1。 |
图上模拟的是子进程向管道写入数据,父进程从管道读取数据。在创建匿名管道实现父子进程间通信的过程中,需要pipe函数和fork函数搭配使用,具体步骤如下:
站在文件描述符角度-深度理解管道
所以,看待管道,就如同看待文件一样!管道的使用和文件一致,迎合了“Linux一切皆文件思想”。
当没有数据可读时
O_NONBLOCK disable: read调用阻塞,即进程暂停执行,一直等待有数据来为止;
O_NONBLOCK enable: read调用返回-1,errno值为EAGAIN。
当管道满的时候
O_NONBLOCK disable: write调用阻塞,直到有进程读走数据;
O_NONBLOCK enable: 调用返回-1,errno值为EAGAIN。
如果所有管道写端对应的文件描述符被关闭,则read返回0,表面读到文件结尾;
如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出。
当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性(即要么全做,要么都不做)。
当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。
下列代码演示:子进程通过写端写数据到管道,父进程读取管道数据。
实例代码:
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
//父进程需要以读写的方式打开管道文件,父进程和子进程之间进行相互通信,父进程打开的管道文件地址是会被子进程继承下来的
//所以它们指向的是同一份管道文件
//【一定要让不同的进程看到同一份资源】
//1.创建管道
int pipefd[2] = {0};//将其初始化为0
int n = pipe(pipefd);//pipefd是一个输出型参数
if(n<0)
{
std::cout<<"pipe error, "<<errno<<":"<<strerror(errno)<<std::endl;
}
//pipe函数创建管道文件,将数组pipefd初始化成3和4,并返回,因为0,1,2已被占用
std::cout<<"pipefd[0]: "<<pipefd[0]<<std::endl;//3->读端
std::cout<<"pipefd[1]: "<<pipefd[1]<<std::endl;//4->写端
//2.创建子进程
pid_t id = fork();
assert(id!=-1);//正常应该应用判断,我这里就断言
if(id==0) //子进程
{
//3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
//我们想让子进程进行写入,所以关闭的是pipefd[0]读,保留pipefd[1]写
close(pipefd[0]);
//4.开始通信--结合某种场景---子进程将数据写入管道
const std::string namestr = "嗨!我是子进程";
int cnt = 1;
char buffer[1024];
while(true)
{
//先向buffer字符缓冲区格式化写入数据
snprintf(buffer,sizeof(buffer),"%s,计数器:%d,我的PID:%d\n",namestr.c_str(),cnt++,getpid());
//然后再将缓冲区的数据写到管道里
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
close(pipefd[1]);
//子进程执行完退出
exit(0);
}
//父进程
//3.为实现单向通信,需要关闭不需要的文件描述符fd
close(pipefd[1]);
//4.开始通信--结合某种场景---父进程从管道中读取数据
char buffer[1024]={0};
while(true)
{
//父进程先把管道里的数据读出来,放到缓冲区中
int n = read(pipefd[0],buffer,sizeof(buffer)-1);
if(n>0)
{
buffer[n]='\0';
std::cout<<"我是父进程,子进程给我的消息是:"<<buffer<<std::endl;
}
else if(n==0)
{
std::cout<<"我是父进程,我读到文件结尾了"<<std::endl;
break;
}
else
{
std::cout<<"我是父进程,我读异常了"<<std::endl;
}
}
close(pipefd[0]);//关闭管道读端
//父进程等待子进程
waitpid(id,&status,0);
std::cout<<"sig: "<<(status & 0x7) << std::endl;//如果子进程一直写,但是父进程读取一段时间后退出
//OS就会杀掉子进程
return 0;
}
ctrlPipe:ctrlPipe.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -rf ctrlPipe
#pragma once
#include
#include
#include
#include
// typedef std::function func_t;
typedef void (*fun_t)(); //函数指针
void PrintLog()
{
std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}
void InsertMySQL()
{
std::cout << "执行数据库任务,正在被执行..." << std::endl;
}
void NetRequest()
{
std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}
// void ExitProcess()
// {
// exit(0);
// }
//约定,每一个command都必须是4字节
#define COMMAND_LOG 0
#define COMMAND_MYSQL 1
#define COMMAND_REQEUST 2
class Task
{
public:
Task()
{
funcs.push_back(PrintLog);
funcs.push_back(InsertMySQL);
funcs.push_back(NetRequest);
}
void Execute(int command)
{
if(command >= 0 && command < funcs.size()) funcs[command]();
}
~Task()
{}
public:
std::vector<fun_t> funcs;
};
#include
#include
#include
#include
#include
#include
#include
#include "Task.hpp"
using namespace std;
const int gnum = 3;
Task t;
class EndPoint
{
private:
static int number;
public:
pid_t _child_id;
int _write_fd;
std::string processname;
public:
EndPoint(int id, int fd) : _child_id(id), _write_fd(fd)
{
//process-0[pid:fd]
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);
processname = namebuffer;
}
std::string name() const
{
return processname;
}
~EndPoint()
{
}
};
int EndPoint::number = 0;
// 子进程要执行的方法
void WaitCommand()
{
while (true)
{
int command = 0;
int n = read(0, &command, sizeof(int));
if (n == sizeof(int))
{
t.Execute(command);
}
else if (n == 0)
{
std::cout << "父进程让我退出,我就退出了: " << getpid() << std::endl;
break;
}
else
{
break;
}
}
}
void createProcesses(vector<EndPoint> *end_points)
{
vector<int> fds;
for (int i = 0; i < gnum; i++)
{
// 1.1 创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
assert(n == 0);
(void)n;
// 1.2 创建进程
pid_t id = fork();
assert(id != -1);
// 一定是子进程
if (id == 0)
{
for(auto &fd : fds) close(fd);
// std::cout << getpid() << " 子进程关闭父进程对应的写端:";
// for(auto &fd : fds)
// {
// std::cout << fd << " ";
// close(fd);
// }
// std::cout << std::endl;
// 1.3 关闭不要的fd
close(pipefd[1]);
// 我们期望,所有的子进程读取"指令"的时候,都从标准输入读取
// 1.3.1 输入重定向,可以不做
dup2(pipefd[0], 0);
// 1.3.2 子进程开始等待获取命令
WaitCommand();
close(pipefd[0]);
exit(0);
}
// 一定是父进程
// 1.3 关闭不要的fd
close(pipefd[0]);
// 1.4 将新的子进程和他的管道写端,构建对象
end_points->push_back(EndPoint(id, pipefd[1]));
fds.push_back(pipefd[1]);
}
}
int ShowBoard()
{
std::cout << "##########################################" << std::endl;
std::cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << std::endl;
std::cout << "| 2. 执行请求任务 3. 退出 |" << std::endl;
std::cout << "##########################################" << std::endl;
std::cout << "请选择# ";
int command = 0;
std::cin >> command;
return command;
}
void ctrlProcess(const vector<EndPoint> &end_points)
{
// 2.1 我们可以写成自动化的,也可以搞成交互式的
int num = 0;
int cnt = 0;
while(true)
{
//1. 选择任务
int command = ShowBoard();
if(command == 3) break;
if(command < 0 || command > 2) continue;
//2. 选择进程
int index = cnt++;
cnt %= end_points.size();
std::string name = end_points[index].name();
std::cout << "选择了进程: " << name << " | 处理任务: " << command << std::endl;
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
sleep(1);
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
// 1. 我们需要让子进程全部退出 --- 只需要让父进程关闭所有的write fd就可以了!
// for(const auto &ep : end_points)
// for(int end = end_points.size() - 1; end >= 0; end--)
for(int end = 0; end < end_points.size(); end++)
{
std::cout << "父进程让子进程退出:" << end_points[end]._child_id << std::endl;
close(end_points[end]._write_fd);
waitpid(end_points[end]._child_id, nullptr, 0);
std::cout << "父进程回收了子进程:" << end_points[end]._child_id << std::endl;
}
sleep(10);
// 2. 父进程要回收子进程的僵尸状态
// for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
// std::cout << "父进程回收了所有的子进程" << std::endl;
// sleep(10);
}
// #define COMMAND_LOG 0
// #define COMMAND_MYSQL 1
// #define COMMAND_REQEUST 2
int main()
{
vector<EndPoint> end_points;
// 1. 先进行构建控制结构, 父进程写入,子进程读取 , bug?
createProcesses(&end_points);
// 2. 我们的得到了什么?end_points
ctrlProcess(end_points);
// 3. 处理所有的退出问题
waitProcess(end_points);
return 0;
}