进程间通信
进程为什么要通信?
进程也是需要某种协同的,所以如何协同的前提条件,需要通信,数据是有类别的,别人传递的信息对你来说,可能是通知就绪的、单纯要传递给我的数据、控制相关的信息...
事实:进程具有独立性,进程=内核数据结构+代码和数据
创建子进程时各自创建自己的内核数据结构,代码和数据遵守写时拷贝,所以进程是一定独立的!!!
那么该如何通信?
进程如何通信?
1.进程间通信,成本可能会稍微高一些!
2.进程间通信的前提,先让不同的进程,看到同一份(操作系统)资源(“一段内存”)
a、一定是某一个进程先需要通信,让OS创建一个共享资源
b、OS必须提供很多的系统调用 --- OS创建的共享资源的不同,系统调用接口的不同,进程间通信会有不同的种类!!!
进程通信的常见方式是什么?
system V --- 本地通信
这是我们都要遵守的一个标准,世界上各种专利就有各种标准!!!
方式:1.消息队列 2.共享内存 3.信号量
何不直接复用内核代码直接通信呢?
利用管道:1.匿名管道 2.命名管道
匿名管道
下面我们创建父子两进程!!!
两进程序都需同时进行读和写的操作,当我们创建子进程时,子进程需不需要再创建一份文件系统呢?不需要,我们说进程是独立的,我们并没有说过文件系统是具有独立性的!!!
所以OS就会让父子进程共用同一份文件系统!!!
理解一种现象:为什么父子进程会向同一个显示器端打印数据!
进程默认会打开标准输入输出0/1/2...怎么做到的?
我们创建的父进程是bash的子进程,bash打开了所有的子进程也就打开了,我们只要做好约定即可!!!与硬链接的引用计数类似,这里也有引用计数!!!所以子进程关闭close()0/1/2,不影响父进程继续使用显示器文件!!!
我们让父进程关闭写操作,子进程关闭读操作,这样,父进程进行只读,子进程进行写!!!
形成了一种单向的通信 --- 管道
我们再想一个问题,经过缓冲区时需不需要再刷新到磁盘上呢???
不需要,这不是多此一举吗?OS不喜欢做这样的事情,怎么简单怎么来!!!
父子既然要关闭不需要的fd,为什么曾经要打开呢?可以不关闭吗?
理解一个系统调用接口。
参数为输出型参数!!!
不需要文件路径和文件名,匿名管道
下面创建管道,管道有多大???在Ubuntu 20.04管道大小是64kb
父进程读子进程写
1.父进程创建管道
2.父进程fork出子进程
3.父进程关闭fd[0],子进程关闭fd[1]
父子既然要关闭不需要的fd,为什么曾经要打开呢?可以不关闭吗?
为什么曾经要打开是因为为了让子进程继承下去!!!
可以不关闭吗?可以,但建议关闭,万一误写了呢!!!
我们一直在做的事情进程间通信,进程间通信是有成本的!!!
进场间通信本质:让不同的进程看到了同一份资源
思考两点:1.如果我想双向通信呢?两个管道
2.为什么要单向通信,因为单向通信简单,只让单向通信,由此产生管道!!!
实现单向通信的测试代码
#include<iostream>
#include <unistd.h>
#include<cerrno> // errno.h
#include<cstring> // string.h
#include<sys/wait.h>
#include<sys/types.h>
//fork之后子进程是能拿到父进程的数据的 --- 通信吗?写时拷贝,对方都看不到
//char buffer[1024];//不行的
const int size = 1024;
std::string getOtherMessage()
{
static int cnt = 0;
std::string messageid = std::to_string(cnt);
cnt++;
pid_t self_id = getpid();
std::string stringpid = std::to_string(self_id);
std::string message = "messageid:";
message += messageid;
message += "my pid is:";
message += stringpid;
return message;
}
//子进程进行写入
void SubProcessWrite(int wfd)
{
int pipesize = 0;
std::string message = "father,I am you son process!";
char c = 'A';
while(true)
{
std::cout << "++++++++++++++++++++++++++++++++++++++++++++++++" << std::endl;
std::string info = message + getOtherMessage();//这条消息就是子进程发给父进程的消息
write(wfd, info.c_str(), info.size());//写入管道的时候,没有写入\0,有没有必要?没有必要!
std::cerr << info << std::endl;
sleep(1);//子进程慢点写
// write(wfd, &c, 1);
// std::cout << "pipesize:" << ++pipesize << "write charator is:" << c++ << std::endl;
// if(c == 'G') break;
// sleep(1);
}
}
//父进程进行读取
void FatherProcessRead(int rfd)
{
char inbuffer[size]; //c99后支持
while(true)
{
sleep(2);
std::cout << "--------------------------------------------" << std::endl;
ssize_t n = read(rfd, inbuffer, sizeof(inbuffer) - 1);
if(n > 0)
{
inbuffer[n] = 0;//这个位置给为'\0'
std::cout << inbuffer << std::endl;
}
else if(n == 0)
{
//如果read的返回值是0,表示写端直接关闭了,我们读到了文件的结尾
std::cout << "client quit,father get return val:" << n << "father quit too!" << std::endl;
break;
}
else
{
std::cerr << "read error" << std::endl;
break;
}
//sleep(1);
//break;
}
}
int main()
{
//1.创建管道
int pipefd[2];
int n = pipe(pipefd);//输出型参数, rfd wfd
if(n != 0)
{
std::cerr << "errno:" << errno << " : " << "errstring: " << strerror(errno) << std::endl;
return 1;
}
//pipefd[0] -> 0 -> r(嘴巴读) pipefd[1] -> 1 -> w(笔写)
std::cout << "pipefd[0]: " << pipefd[0] << ", pipefd[1]: " << pipefd[1] << std::endl;
sleep(1);
//2.创建子进程
pid_t id = fork();
if(id == 0)
{
std::cout << "子进程关闭不需要的fd了,准备发消息了" << std::endl;
// 子进程 -- write
// 3.关闭不需要的fd
close(pipefd[0]);
//if(fork() > 0) exit(0);
SubProcessWrite(pipefd[1]);
close(pipefd[1]);
exit(0);
}
std::cout << "父进程关闭不需要的fd了,准备收消息了" << std::endl;
sleep(1);
//父进程 --- read
//3.关闭不需要的fd
close(pipefd[1]);
FatherProcessRead(pipefd[0]);
//std::cout << "5s,father close rfd" << std::endl;
//sleep(5);
close(pipefd[0]);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
std::cout << "wait child process done,exit sig:" << (status&0x7f) << std::endl;
std::cout << "wait child process done,exit code(ign):" << ((status >> 8)&0xff) << std::endl;
}
return 0;
}
管道的4种情况
- 如果管道内部是空的 && write fd没有关闭,读取条件不具备,该进程会被阻塞,父进程wait,等待读取条件具备再写入数据。
- 如果管道被写满 && read fd不读且没有关闭,管道被写满,写进程会被阻塞(管道被写满,写条件不具备)父进程wait等待写条件具备,读取数据。
- 管道一直在读 && 写端关闭了wfd,读端read返回值会读到0,表示读到了文件结尾。
- rfd直接关闭,写端wfd一直在进行写入?写端操作会被操作系统直接使用13号信号关掉。相当于进程出现了异常
管道的5种特征
- 匿名管道:只用来具有血缘关系的进程之间,进行通信,常用与父子进程之间通信。
- 管道内部,自带进程之间同步的机制 -- > 多执行流执行代码的时候,具有明显的顺序性!!
- 管道文件的生命周期是随进程的!!!
- 管道文件在通信的时候,是面向字节流的。(现阶段不易理解)write的次数和读取的次数不是一一匹配的
- 管道的通信方式,是一种特殊的半双工模式(像两个人说话,都可以说话,但只有一方说时另一方就只能听),还有一种叫全双工(像我们日常生活的吵架)。
atomic:原子的
意思是小于PIPE_BUF时写入是安全的!!!
Linux下PIPE_BUF是4096字节。
进程池
由一个父进程通过管道来控制多个子进程父进程写入,子进程读取。
master向哪个管道进行写入 ,就是唤醒哪个子进程执行任务
固定长度的4字节长度的数组下标 --- 任务码
父进程要进行后端任务 划分的负载均衡
轮询方案!!!
如果管道里面没有数据,worker进程就在阻塞等待,等待任务的到来。
ProcessPool.cc文件
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "Task.hpp"
// master
class Channel
{
public:
Channel(int wfd, pid_t id, const std::string &name)
: _wfd(wfd)
, _subprocessid(id)
, _name(name)
{
}
int GetWfd() { return _wfd; }
pid_t GetProcessid() { return _subprocessid; }
std::string GetName() { return _name; }
void CloseChannel()
{
close(_wfd);
}
void Wait()
{
pid_t rid = waitpid(_subprocessid, nullptr, 0);
if (rid > 0)
{
std::cout << "wait " << rid << " success" << std::endl;
}
}
~Channel()
{
}
private:
int _wfd;
pid_t _subprocessid;
std::string _name;
};
// 形参类型和命名规范
// const &:输出
//&: 输入输出
//*:输出型参数
// task_t task回调函数
void CreateChannelAndSub(int num, std::vector<Channel> *channels, task_t task)
{
for (int i = 0; i < num; i++)
{
// 1.创建管道
int pipefd[2] = {0};
int n = pipe(pipefd);
if (n < 0)
exit(1);
// 2.创建子进程
pid_t id = fork();
if (id == 0)
{
if(!channels->empty())
{
//第二次之后,创建的管道
for(auto &channel : *channels)
channel.CloseChannel();
}
// child read
close(pipefd[1]);
dup2(pipefd[0], 0); // 将管道的读端重定向到标准输入
task();
close(pipefd[0]);
exit(0);
}
// 构建一个channel名字
std::string channel_name = "Channel-" + std::to_string(i);
// 父进程
// father write
close(pipefd[0]);
// a. 子进程的pid,b. 父进程关心的管道的w端
channels->push_back(Channel(pipefd[1], id, channel_name));
}
}
int NextChannel(int channelnum)
{
static int next = 0;
int channel = next;
next++;
next %= channelnum;
return channel;
}
void SendTaskCommand(Channel &channel, int taskcommand)
{
write(channel.GetWfd(), &taskcommand, sizeof(taskcommand));
}
void ctrlProcessOnce(std::vector<Channel> &channels)
{
sleep(1);
// a.选择一个任务
int taskcommand = SelectTask();
// b.选择一个信道和进程
int channel_index = NextChannel(channels.size());
// c.发送任务
SendTaskCommand(channels[channel_index], taskcommand);
std::cout << "taskcommand:" << taskcommand << "channel:" << channels[channel_index].GetName() << "sub process:" << channels[channel_index].GetProcessid() << std::endl;
}
void ctrlProcess(std::vector<Channel> &channels, int times = -1)
{
if (times > 0)
{
while (times--)
{
ctrlProcessOnce(channels);
}
}
else
{
while (true)
{
ctrlProcessOnce(channels);
}
}
}
void CleanUpChannel(std::vector<Channel> &channels)
{
// int num = channels.size() - 1;
// while(num >= 0)
// {
// channels[num].CloseChannel();
// channels[num--].Wait();
// }
for (auto &channel : channels)
{
channel.CloseChannel();
channel.Wait();
}
// for (auto &channel : channels)
// {
// channel.Wait();
// }
}
//./processpool 5
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage:" << argv[0] << "processnum" << std::endl;
return 1;
}
int num = std::stoi(argv[1]);
LoadTask();
std::vector<Channel> channels;
// 1.创建信道和子进程
CreateChannelAndSub(num, &channels, work);
// 2.通过channel控制子进程
ctrlProcess(channels, 5);
// 3.回收管道和子进程 a.关闭所有的写端 b.回收子进程
CleanUpChannel(channels);
return 0;
}
Task.hpp文件
#pragma once
#include <iostream>
#include <ctime>
#include <cstdlib>
#include <sys/types.h>
#include <unistd.h>
#define TaskNum 3
typedef void (*task_t)(); // task_t 函数指针类型
void Print()
{
std::cout << "I am print task" << std::endl;
}
void DownLoad()
{
std::cout << "I am a download task" << std::endl;
}
void Flush()
{
std::cout << "I am a flush task" << std::endl;
}
task_t tasks[TaskNum];
void LoadTask()
{
srand(time(nullptr) ^ getpid() ^ 17777);
tasks[0] = Print;
tasks[1] = DownLoad;
tasks[2] = Flush;
}
void ExcuteTask(int number)
{
if (number < 0 || number > 2)
return;
tasks[number]();
}
int SelectTask()
{
return rand() % TaskNum;
}
void work()
{
while(true)
{
int command = 0;
int n = read(0, &command, sizeof(command));
if(n == sizeof(int))
{
std::cout << "pid is:" << getpid << "handler task" << std::endl;
ExcuteTask(command);
}
else if(n == 0)
{
std::cout << "sub process:" << getpid << "quit" << std::endl;
break;
}
}
}