文章目录
- 一、管道
-
- [1. 匿名管道](#1. 匿名管道)
- [2. 命名管道](#2. 命名管道)
进程具有独立性,因此进程间通信的前提是两个进程能看到同一份资源
一、管道
对于进程打开的内存文件,操作系统是以引用计数的方式创建的 file 结构体,如果让两个进程与同一个 file 结构体关联,便可以让两个进程看到同一份资源
由于 file 结构体的缓冲区只有一个,因此只能让一个进程以写的方式,另一个进程以读的方式打开同一个文件,这样便实现了单向的进程间通信
操作系统提供了仅在内存中创建 file 结构体 (不在磁盘上创建对应的文件),这种特殊的文件称为管道文件,其中没有名字的称为 匿名管道,有名字的称为 命名管道
1. 匿名管道
进程创建匿名管道成功后,会以读和写两种方式打开管道文件,因此匿名管道通常用于父子进程的进程间通信
系统调用 pipe,头文件 unistd.h
- int pipe(int pipefd[2]),创建匿名管道
返回值:匿名管道创建成功返回 0,出错返回 -1,并且 errno 被设置为相应的出错信息
参数:pipefd 为输出型参数,pipefd[0] 存储读端文件描述符,pipefd[1] 存储写端文件描述符
父子进程通信的步骤:
- 父进程创建管道
父进程以读和写两种方式打开管道文件 - 创建子进程
子进程会拷贝父进程的文件描述符表,因此子进程也会以读和写两种方式打开同一个管道文件 - 父进程和子进程分别关闭自己不需要的读端或写端
一个进程向管道中写入,另一个进程从管道中读取
子进程向匿名管道写入,父进程从匿名管道读取
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
int main()
{
// 创建匿名管道
// pipefd[0] 表示读
// pipefd[1] 表示写
int pipefd[2] = { 0 };
if (pipe(pipefd) < 0)
{
// 创建匿名管道失败
cout << "匿名管道创建失败: " << errno << " " << strerror(errno) << endl;
exit(1);
}
// 创建子进程
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// 子进程向匿名管道写入,需要关闭读
close(pipefd[0]);
// 开始通信
int cnt = 0;
char buffer[64];
while (true)
{
snprintf(buffer, sizeof(buffer), "我是子进程,这是我给你发的第 %d 个信息", ++cnt);
// 向匿名管道中写入
write(pipefd[1], buffer, strlen(buffer));
cout << cnt << endl;
// sleep(1); // 让写端慢一点
// if (cnt == 3) break; // 模拟写端关闭
}
close(pipefd[1]);
exit(0);
}
// 父进程从匿名管道读取,需要关闭写
close(pipefd[1]);
// 开始通信
char buffer[64];
while (true)
{
// 从匿名管道中读取
// sleep(3); // 让读端慢一点
// sleep(3); break; // 模拟读端关闭
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
cout << buffer << endl;
}
else if (n == 0)
{
cout << "写端已经关闭,我读到文件结尾了" << endl;
break;
}
else
{
cout << "读取错误" << endl;
break;
}
}
close(pipefd[0]);
// 读端关闭,写端会收到 13 号信号 SIGPIPE
int status;
waitpid(id, &status, 0);
cout << "子进程退出信号: " << (status & 0x7F) << endl;
return 0;
}
- 放开子进程代码中的 sleep(1),让写端慢一点,匿名管道中没有数据时,读端会等待写端写入
- 放开父进程中的 sleep(3),让读端慢一点,匿名管道中写满数据时,写端会等待读端读取
- 放开子进程中模拟写端关闭的代码,并且让写端慢一点,写端关闭后,读端读取完匿名管道中的数据后,读端会读取到文件结尾
- 放开子进程中模拟读端关闭的代码,并且让写端慢一点,读端关闭时,此时匿名管道无意义,操作系统会向写端发送 SIGPIPE 13 号信号
命令行中的 | 即为匿名管道,| 会将前一个进程的标准输出重定向到匿名管道,后一个进程的标准输入重定向到匿名管道
通过匿名管道创建进程池:
cpp
// processpool.hpp
#include <iostream>
#include <vector>
#include <functional>
#include <cstring>
#include <cerrno>
#include <cassert>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
namespace starrycat
{
class EndPoint
{
public:
EndPoint()
{}
EndPoint(pid_t cid, int wfd) : _cid(cid), _wfd(wfd)
{}
~EndPoint()
{}
public:
pid_t _cid;
int _wfd;
};
const int NUM = 5;
class ProcessPool
{
using fun_t = std::function<void(int)>;
public:
ProcessPool(fun_t Command, int num = NUM)
{
std::vector<int> tmpfd; // 保存子进程不需要的父进程的文件描述符
for (int i = 0; i < num; ++i)
{
// 创建管道
int pipefd[2] = {0};
if (pipe(pipefd) < 0)
{
std::cout << "创建管道失败: " << errno << " " << strerror(errno) << std::endl;
exit(1);
}
// 创建子进程
int id = fork();
assert(id != -1);
if (id == 0)
{
// 子进程从管道中读取,需要关闭写
close(pipefd[1]);
for (auto e : tmpfd) close(e); // 关闭不需要的文件描述符
// 开始通信
while (true)
{
// 读取四字节整数的命令
int cmd = 0;
int n = read(pipefd[0], &cmd, sizeof(int));
if (n == sizeof(int))
{
// 测试
std::cout << getpid() << " ";
// 执行命令
Command(cmd);
}
else if (n == 0)
{
// 测试
std::cout << getpid() << " 读取到文件结尾了" << std::endl;
break;
}
else
{
std::cout << "读取异常" << std::endl;
break;
}
}
close(pipefd[0]);
exit(0);
}
// 父进程向管道中写入,需要关闭读
close(pipefd[0]);
_endPoints.push_back(EndPoint(id, pipefd[1]));
tmpfd.push_back(pipefd[1]);
}
}
ProcessPool(const ProcessPool &p) = delete;
ProcessPool &operator=(const ProcessPool &p) = delete;
~ProcessPool()
{
for (auto& e : _endPoints)
{
close(e._wfd);
waitpid(e._cid, nullptr, 0);
// 测试
std::cout << "等待子进程:" << e._cid << "成功" << std::endl;
sleep(1);
}
}
// 规定命令为四字节整数
void push(int command)
{
// 以轮训的方式调用子进程
static int index = 0;
write(_endPoints[index]._wfd, &command, sizeof(int));
index++;
index %= _endPoints.size();
}
private:
std::vector<EndPoint> _endPoints;
};
}
// myctrlprocess.cc
#include "processpool.hpp"
#include <iostream>
#include <string>
using namespace std;
// 子进程个数
const int num = 5;
void PrintLog()
{
cout << "打印日志任务,正在被执行..." << endl;
}
void InsertMySQL()
{
cout << "执行数据库任务,正在被执行..." << endl;
}
void NetQuest()
{
cout << "执行网络请求任务,正在被执行..." << endl;
}
void CommandError()
{
cout << "任务不存在" << endl;
}
void Command(int cmd)
{
switch (cmd)
{
case 0:
PrintLog();
break;
case 1:
InsertMySQL();
break;
case 2:
NetQuest();
break;
default:
CommandError();
break;
}
}
int main()
{
starrycat::ProcessPool ppool(Command);
int command = 0;
while (true)
{
cout << "请输入命令: ";
cin >> command;
// 测试
if (command == -1) break;
ppool.push(command);
sleep(1);
}
return 0;
}
2. 命名管道
命名管道支持两个毫不相关的进程通信,其使用和文件一样
系统调用 mkfifo,头文件 sys/types.h、sys/stat.h
- int mkfifo(const char *pathname, mode_t mode),创建命名管道
返回值:命名管道创建成功返回 0,出错返回 -1,并且 errno 被设置为相应的出错信息
参数:
- pathname 表示创建命名管道的路径名(如果只有文件名,则表示在进程所在的路径下创建)
- mode 表示创建命名管道的文件权限,受 umask 影响
客户端向命名管道写入,服务端从命名管道读取
cpp
// namepipe.h
#pragma once
// 命名管道文件名
const char* const fifoname = "fifo";
// client.cc
#include "namepipe.h"
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
// 以写方式打开命名管道
int wfd = open(fifoname, O_WRONLY);
if (wfd < 0)
{
cout << "打开文件失败" << errno << " " << strerror(errno) << endl;
exit(1);
}
// 开始通信
char buffer[1024];
while (true)
{
cout << "请输入: ";
char* str = fgets(buffer, sizeof(buffer), stdin);
if (str == NULL)
{
cout << "客户端退出" << endl;
break;
}
buffer[strlen(buffer) - 1] = '\0'; // 去掉输入的回车符
// 输入 quit 表示客户端退出
if(strcmp(buffer, "quit") == 0)
{
cout << "客户端退出" << endl;
break;
}
// 向命名管道写入
write(wfd, buffer, strlen(buffer));
}
close(wfd);
return 0;
}
// server.cc
#include "namepipe.h"
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;
int main()
{
// 创建命名管道
umask(0); // 设置进程的权限掩码
if (mkfifo(fifoname, 0666) < 0)
{
cout << "创建管道失败: " << errno << " " << strerror(errno) << endl;
exit(1);
}
// 以读方式打开命名管道
int rfd = open(fifoname, O_RDONLY);
if (rfd < 0)
{
cout << "打开文件失败" << errno << " " << strerror(errno) << endl;
}
// 开始通信
char buffer[1024];
while (true)
{
// 从命名管道读取
int n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
cout << "客户端: " << buffer << endl;
}
else if (n == 0)
{
cout << "客户端退出了,服务端也退出" << endl;
break;
}
else
{
cout << "读取异常" << endl;
break;
}
}
close(rfd);
unlink(fifoname); // 删除命名管道
return 0;
}
先启动 myserver,在启动 myclient
命名管道和匿名管道的通信特性是一样的
mkfifo 文件名
功能:创建命名管道