![]()
文章目录
- 进程通信的目的
- 进程间通信发展
- 进程间通信分类
- 管道
- System V IPC
- POSIX IPC
- 管道
- 什么是管道
- 管道的读写规则
- 管道的特点:
- 匿名管道
- 处理退出问题
- 命名管道
- 创建一个命名管道
- 匿名管道与命名管道的区别
- 命名管道的打开规则
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
管道
System V进程间通信
POSIX进程间通信
匿名管道pipe
命名管道
System V 消息队列
System V 共享内存
System V 信号量
消息队列
共享内存
信号量
互斥量
条件变量
读写锁
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为
当没有数据可读时
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将不再保证写入的原子性。
- 1.单向通信
- 2.管道的本质就是文件,因为fd的生命周期随进程,管道的生命周期是随进程的。
- 3.管道通信,通常用来进行具有“血缘”关系的进程,进行进程间通信。常用与父子通信 --pipe打开管道,并不清楚管道的名字,匿名管道。
- 4.在管道通信当中,写入的次数,和读取的次数,不是严格匹配的,读写次数得多少没有强相关 —表现 --字节流
- 5.具有一定的协同能力,让reader和writer能狗按照一定的步骤进行通信–自带同步机制。
#include
#include
#include
#include
//将errno.h换成cerrno是因为后者的在c++当中的兼容性更好,并且更美观
#include
#include
#include
#include
using namespace std;
int main()
{
//1.创建管道
int pipefd[2]={0};
int n=pipe(pipefd);
if(n<0)
{
cout<<"创建失败:"<<errno<<"->"<<strerror(errno)<<endl;
return 1;
}
cout<<"pipefd[0]:"<<pipefd[0]<<endl;//可以通过0为嘴巴,就是读
cout<<"pipefd[1]:"<<pipefd[1]<<endl;//可以通过1为笔,就是写
//2.创建子进程
pid_t m =fork();
if(m==-1)//assert是代表意料之中的情况,就是能确定他就是没问题但是还是防了一手
{ //if就是代表意料之外的情况,不知道情况是什么
cout<<"创建失败"<<endl;
return 1;
}
if(m==0)//子进程
{
//3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[0]);
//4.进行通信,结合某种场景
char buffer[1024];
int cnt =1;
const string namestr="hello 我是子进程";
while(true)
{
// snprintf(buffer,sizeof(buffer),"%s,计数器:%d,pid:%d",namestr.c_str(),cnt++,getpid());
// write(pipefd[1],buffer,strlen(buffer));
// sleep(1);
char x = 'X';
write(pipefd[1], &x, 1);
std::cout << "Cnt: " << cnt++<<std::endl;
sleep(1);
}
exit(0);
}
//3.关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipefd[1]);
//4.进行通信,结合某种场景
char buffer[1024];
int cnt =0;
while(true)
{
int n=read(pipefd[0],buffer,sizeof(buffer)-1);//这里减一是因为read它是按字节算的,他最多就1023,要把最后一和字节留下
if(n>0)
{
buffer[1024]='\0';
cout<<"我是父进程, child give me message: " << buffer<<endl;
}
else if(n == 0)
{
cout << "我是父进程, 读到了文件结尾" << endl;
break;
}
else
{
cout << "我是父进程, 读异常了" << endl;
break;
}
sleep(1);
if(cnt++ > 5) break;
}
close(pipefd[0]);
int status = 0;
waitpid(m, &status, 0);
cout << "sig: " << (status & 0x7F) << endl;
sleep(100);
return 0;
}
//task.cc
#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);//
//这里重定向问题为什么read不能对应1?
//因为写要对应读,读要对应写,0对应read,write对应1
// 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;
}
#pragma once
#include
#include
#include
typedef void (*fun_t)(); //函数指针
void PrintLog()
{
std::cout << "pid: "<< getpid() << ", 打印日志任务,正在被执行..." << std::endl;
}
void InsertMySQL()
{
std::cout << "执行数据库任务,正在被执行..." << std::endl;
}
void NetRequest()
{
std::cout << "执行网络请求任务,正在被执行..." << std::endl;
}
//约定,每一个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;
};
上述代码当中的最后一部分是处理退出,而我用的方法是将父进程的写端进行关闭,子进程的读端也就读不到东西了所以也就子进程就关闭了。
上面图片的代码就是错误的代码,在将父进程的写端和子进程的读端进行分开处理就是可以的,但是放到一起就出现问题了。原因是在父进程创建第二个子进程时需要继承给子进程文件描述符表,然后出现的问题就是子进程会将父进程的读端一并继承下去,也就导致有多个进程指向进程读端,也就导致读端关闭不上,所以子进程也就无法退出。而为什么分开是可以的就是因为它是将所有的父进程指向的读端全部关闭也就不存在上述问题了。解决方法1:可以倒着关闭先关闭最后一个读端这样就不会出现上述问题了。
//server.cc
#include"comm.hpp"
#include
#include
#include
#include
#include
int main()
{ // 1. 创建管道文件,我们今天只需要一次创建
//这个设置并不影响系统的默认配置,只会影响当前进程
umask(0);
int n= mkfifo(fifoname.c_str(),0666);
if(n<0)
{
std::cout<<"error:"<<errno<<strerror(errno)<<std::endl;
return 1;
}
std::cout<<"create fifo file success"<<std::endl;
// 2. 让服务端直接开启管道文件
int rfd=open(fifoname.c_str(),O_RDONLY);
if(rfd<0)
{
std::cout<<"open file fail"<<std::endl;
return 2;
}
std::cout<<"open fifo success"<<std::endl;
//3.正常通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);//注意
if(n > 0)
{
buffer[n] = 0;
std::cout << "client# " << buffer << std::endl;
// printf("%c", buffer[0]);
// fflush(stdout);
}
else if(n == 0)
{
std::cout << "client quit, me too" << std::endl;
break;
}
else
{
std::cout << errno << " : " << strerror(errno) << std::endl;
break;
}
}
// 关闭不要的fd
close(rfd);
unlink(fifoname.c_str());
return 0;
}
//client.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "comm.hpp"
int main()
{
//1. 不需创建管道文件,我只需要打开对应的文件即可!
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
std::cerr << errno << ":" << strerror(errno) << std::endl;
return 1;
}
// 可以进行常规通信了
char buffer[NUM];
while(true)
{
std::cout << "请输入你的消息# ";
char *msg = fgets(buffer, sizeof(buffer), stdin);
assert(msg);
(void)msg;
buffer[strlen(buffer) - 1] = 0;
//防止出现空行,因为fgets会将\n读进去,所以,将\n变成\0
//这里不用担心空串,因为怎么都会输入\n的
// abcde\n\0
// 012345
if(strcasecmp(buffer, "quit") == 0) break;
//用于判断结束,该关键字是不区分大小写的比较
ssize_t n = write(wfd, buffer, strlen(buffer));
assert(n >= 0);
(void)n;
}
close(wfd);
return 0;
}
//comm.hpp
#include
#include
#include
#define NUM 1024
const std::string fifoname = "./fifo";
uint32_t mode = 0666;
//makefile
//两个文件联合编译
.PHONY:all
all:client server
client:client.cc
g++ -o $@ $^ -std=c++11
server:server.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm client server
命名管道可以从命令行上创建,命令行方法是使用下面这个命令
$ mkfifo filename
命名管道也可以从程序里创建,相关函数有
int mkfifo(const char *filename,mode_t mode);
创建命名管道
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}
匿名管道由pipe函数创建并打开。
命名管道由mkfifo函数创建,打开用open
FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完
成之后,它们具有相同的语义。
如果当前打开操作是为读而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO