1、进程间通信介绍
之前所学都是单个进程,多个进程之间如何运转?
1、目的
数据传输:一个进程需要将它的数据发送给另一个进程
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止
时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
一个进程的所有陷入和异常,并能够及时知道它的状态改变。
进程是有独立性的,这增加了通信的成本。要让两个不同的进程进行通信的前提条件就是让两个进程看到同一份资源。这是操作系统直接或间接提供的。
对于任何通信手段,都要先让不同的进程看到同一份资源,然后一方写入,一方读取来完成通信。
2、发展
管道
System V进程间通信
POSIX进程间通信
2、管道
1、原理
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”
可以同时创建多个进程,通过管道连接,它们的父进程也都一样,bash。
管道也是文件,一个文件以写方式打开管道,将自己的标准输出重定向到管道,另一头的文件以读方式打开管道,将自己的标准输入重定向到管道。
文件进程开始时,结构体里有文件描述符的表,指向各个文件,除此之外,还会打开一个匿名管道,这是一个操作系统提供的纯粹的内存文件,不需要刷新内容到磁盘。通过进程调用,这个匿名管道会通过读和写打开同一个文件;当前进程会fork一下,把自己的文件描述符表拷贝给子进程,具体的内容会有对应更改,那打开的文件等需要拷贝吗?不拷贝。但没关系,因为文件描述表的原因,子进程依然指向父进程创建好的文件,包含那个匿名管道。这其实也相当于一个浅拷贝。父子进程指向的文件一样。所以父子都指向那个匿名管道,一方更改,一方就能得到新数据;不过这时候管道只支持单向通信,接下来要做的是确定数据流向,关闭不需要的fd。操作系统会把子进程的写方式和父进程的读方式都关掉,这样父写子读,就形成了一个管道。
管道确实只能单向通信,如果想要双向,就定义两个管道。
2、简单模拟实现
简单的代码,子进程向父进程发消息,就是一种管道。整个步骤就是创建管道,创建子进程,关闭不需要的fd,开始通信。
创建管道用pipe函数,int pipe(int fd[2]),用到unistd.h头文件。pipefd是输出型参数,系统会创建管道,然后把读和写端的文件描述符传给pipefd数组,数组中就有两个整型了。pipe函数失败返回-1,错误码被设置,成功返回0。
#include <iostream>
#include <unistd.h>
#include <string>
int main()
{
//一定要保证不同进程看到同一份资源
int pipefd[2] = {
0};
//1、创建管道
int n = pipe(pipefd);
if(n < 0)
{
std::cout << "pipe error, " << errno << ":" << strerror(errno) << std::ednl;
return 1;
}
std::cout << "pipefd[0]: " << pipefd[0] << std::endl;//读端
std::cout << "pipefd[0]: " << pipefd[1] << std::endl;//写端
//2、创建子进程
pid_t id = fork();
assert(id != -1)//断言或者判断都行
//子进程
if(id == 0)
{
//3、关闭不需要的fd,让父进程进行读取,让子进程进行写入
close(pipdfd[0]);
//4、开始通信
const std::string namestr = "hello, 我是子进程";//要传变化的数据,来证明是通信过来的数据
int cnt = 1;
char buffer[1024];
while(true)
{
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];
while(true)
{
int n = read(pipefd[0], buffer, sizeof(buffer) - 1);
if(n > 0)
{
buffer[n] = '\0';
std::cout << "我是父进程, child give me message: " << buffer << std::endl;
}
}
close(pipefd[0]);
return 0;
}
3、总结
系统是可以分得清管道文件和普通文件的。
特点
1、单向通信,一种半双工,意思为双方交替着工作;全双工则是双方可以同时工作
2、管道本质是文件,因为文件描述符和文件的生命周期随进程,所以管道的生命周期也随进程。父子进程退出,那么之前关掉的文件描述符又会回到之前的位置
3、父子进程能够通信也是有继承存在于其中的。管道通信通常用于具有“血缘”关系的进程,常用于父子进程。兄弟进程之间也可以通信。pipe打开管道时不需要关心打开了哪个文件,只要得到两个文件描述符就好,因为pipe打开的是匿名管道
4、管道通信中,写入的次数和读取的次数不是严格匹配的,可能写了7个但是只读了1次把它们都读取了。读写没有强相关。读取是面向字节流的,读取只看应读的字节数
5、管道具有一定的协同能力,让读和写能按照一定的步骤进行通信----自带同步机制
场景
1、如果我们read读取完了所有的管道数据,对方不写入,读取方只能等待
2、write端写满后(一般是65535/65536,大约是64kb),就不会继续写了,等到读端读取。可以在子进程那里不写sleep,而父进程sleep(10),也就是子进程疯狂写,而父进程缓慢读,看实际现象
3、如果关闭了写端,读取完管道数据,再读就会返回0,表明读到了文件结尾
4、写端一直写,读端关闭,那么写入就变得没有意义;操作系统不会维护无意义,低效率,浪费资源的进程,所以进程会直接杀掉这个进程。系统会通过信号来终止进程,SIGPIPE -13关闭进程
管道在读写有单次读写的数据量,是一个宏PIPE_BUF,可以用man 7来查看。管道在单次写入的数据量应当小于这个PIPE_BUF,它的大小是4096字节,小于的时候写入操作就是原子的,这个之后会解释什么是原子。
3、控制进程——匿名管道
一个父进程可以有多个子进程,每个子进程都通过管道和父进程联系,如果父进程不往管道里写数据,子进程就阻塞,而写了数据,就会让子进程重新进入运行状态。父进程可以通过向子进程写入特定的消息,唤醒子进程,甚至让子进程定向的执行某种任务。父进程对自己创建的管道和进程会先描述再组织。
子进程在创建时会继承父进程的文件描述符,所以第一个父进程会有好多个子进程连接到它,这就混乱了。实际要实现的结构是一对一,一对父子进程间有自己独立的管道,而不受其他进程影响。
一步步来设计这个结构
先写一个开头
#include <iostream>
#include <string>
#include <unistd.h>
#include <cassert>
using namespace std;
const int gnum = 5;//子进程数量
int main()
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
//1.3 关闭不要的fd
close(pipefd[1]);
//...
close(pipefd[0]);
exit(0);
}
//父进程,写
//1.3 关闭不要的fd
close(pipefd[0]);
}
return 0;
}
每一次循环,pipefd都需要重新创建,都归为0,这样对于父进程来说,就像是在做完全重复的动作一样,它分不清子进程和对应的管道。所以我们需要让父进程不论在哪里都能够知道哪个管道是哪个子进程的。这里我们就写一个类,每次循环结束后都往由这个类构造的vector里插入,这样每次通信的结果也就保留了下来。
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;
const int gnum = 5;//子进程数量
class EndPoint
{
public:
pid_t child_id;//哪一个子进程
int _write_fd;//哪一个管道
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
int main()
{
vector<EndPoint> end_pints;
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
//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]));
}
//2. 循环结束,我们已经拿到了所有子进程和他的管道
return 0;
}
现在把这一部分放到一个函数createProcess中,函数参数就是那个vector。以及还要写控制子进程等待输入的函数WaitCommand。
现在的代码
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
using namespace std;
const int gnum = 5;//子进程数量
class EndPoint
{
public:
pid_t child_id;//哪一个子进程
int _write_fd;//哪一个管道
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
}
~EndPoint()
{
}
};
void WaitCommand()
{
;
}
void createProcess(vector<EndPoint>* end_points)
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
//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]));
}
}
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
return 0;
}
新开一个后缀为.hpp的文件,里面写上子进程执行的任务。
Task.hpp
#pragma once
#include <iostream>
#include <vector>
typedef void(*fun_t)();//函数指针
void PrintLog()
{
std::cout << "打印日志任务,正在被执行..." << 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_REQUEST 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;
};
回到CtrlProcess.cc文件中
void WaitCommand()
{
while(true)
{
int command;
int n = read(0, &command, sizeof(char));
if(n == sizeof(int))
{
t.Execute(command);
}
else if(n == 0)//子进程要退出
{
break;
}
else//有异常,就需要
{
break;
}
}
}
定义一个全局的Task对象t,并且规定好每个输入的命令都是以4字节为单位的。这些都写完后,父子进程已经建立好了管道,也管理好了所有的管道,那么走完CreateProcess函数后,父进程继续执行,这时候就可以给子进程发消息了。可以在Task里每个任务打印的一句话中打印上对应子进程的pid,头文件是unistd.h。
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
int num = 0;
while(true)
{
//1. 选择任务
int command = COMMAND_LOG;
//2. 选择进程
int index = rand() % end_points.size();
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
sleep(1);
}
return 0;
}
继续完善代码。让子进程按照顺序来选择任务,在main里修改;每个子进程都取个名字;控制输入的命令数字,不对就重新输入,输入3就整体退出;最后要处理所有的退出问题,如果父进程写了一部分数据然后退出,那么子进程读完后就也会退出,在waitcommand函数中,子进程读到0就会break,然后会到createProcess函数中,子进程会结束自己的读端,所以我们只需要关闭父进程的写端即可,然后回收子进程。
整体代码
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#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;//哪一个管道
string processname;
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);//格式化形式输入到buffer中
processname = buffer;
}
string name() const
{
return processname;
}
~EndPoint()
{
}
};
int EndPoint::number = 0;
void WaitCommand()
{
while(true)
{
int command;
int n = read(0, &command, sizeof(char));
if(n == sizeof(int))
{
t.Execute(command);
}
else if(n == 0)
{
cout << "父进程让我退出,我就退出: " << getpid() << endl;
break;
}
else
{
break;
}
}
}
void createProcess(vector<EndPoint>* end_points)
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
//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]));
}
}
int ShowBoard()
{
cout << "*******************************************" << endl;
cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << endl;
cout << "| 2. 执行请求任务 3. 退出 |" << endl;
cout << "*******************************************" << endl;
cout << "请选择: ";
int command = 0;
cin >> command;
return command;
}
void ctrlProcess(const vector<EndPoint> &end_pints)
{
//2.1 我们可以写成自动化的,也可以写成交互式的
int num = 0;
int cnt = 0;
while(true)
{
//1. 选择任务
int command = ShowBoard();
if(command == 3) break;//输入3就退出,再回到main处
if(command < 0 || command > 2)
{
cout << "没有对应任务" << endl;
continue;
}
//2. 选择进程
int index = cnt++;//按照顺序来选择任务
cnt %= end_points.size();
string name = end_points[index].name();
cout << "选择了进程: " << name << " | 处理任务" << command << endl;
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(const auto &ep : end_points) close(ep._write_fd);
cout << "父进程让所有的子进程全部退出" << endl;
sleep(10);
//2. 父进程要回收子进程的僵尸状态
for(const auto &ep : end_points) waitpid(ep._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程" << endl;
sleep(10);
}
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
ctrlProcess(&end_points);
//3. 处理所有的退出问题
waitProcess(end_points);
return 0;
}
现在waitProcess函数是有问题的。函数把两个循环变成一个循环,子进程退出后就直接回收,这样就会发现程序没法退出了。这个问题出现在createProcess函数中。
void createProcess(vector<EndPoint>* end_points)
{
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
//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]));
}
}
虽然看似循环,每次建立一个子进程,建立对应的管道,但是fork的子进程会继承父进程打开的文件,也就是上一次的管道就被继承了,子进程就会指向这个管道,依次类推,整个结构根本不独立。针对这一个问题,第一种办法就是倒着退出回收,子进程退出,就会把自己的读写端都关闭,也就不指向上面的管道了
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(int end = end_points.size() - 1; end >= 0; end--)
{
cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
//2. 父进程要回收子进程的僵尸状态
waitpid(end_points[end]._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
}
sleep(10);
}
但我们还是要构建出来理想的结构,就要从create那里开始改造。每次循环父进程的写端并没有close,在这次循环的最后我们把这个写端给保存起来,等到下一个子进程时,他会继承上面的父进程的写端,那就在子进程做事之前把储存的之前的写端全都close就可以了。
void createProcess(vector<EndPoint>* end_points)
{
vector<int> fds;
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
//这里可以只写一个close,其他的打印语句是为了更好地观察
cout << getpid() << " 子进程关闭父进程对应的写端: ";
for(auto &fd : fds)
{
cout << fd << " ";
close(fd);
}
cout << 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]);
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(int end = end_points.size() - 1; end >= 0; end--)
{
cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
//2. 父进程要回收子进程的僵尸状态
waitpid(end_points[end]._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
}
sleep(10);
}
全部代码
#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <cassert>
#include <sys/wait.h>
#include <sys/types.h>
#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;//哪一个管道
string processname;
public:
EndPoint(int id, int fd):_child_id(id), _write_fd(fd)
{
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "process-%d[%d:%d]", number++, _child_id, _write_fd);//格式化形式输入到buffer中
processname = buffer;
}
string name() const
{
return processname;
}
~EndPoint()
{
}
};
int EndPoint::number = 0;
void WaitCommand()
{
while(true)
{
int command;
int n = read(0, &command, sizeof(char));
if(n == sizeof(int))
{
t.Execute(command);
}
else if(n == 0)
{
cout << "父进程让我退出,我就退出: " << getpid() << endl;
break;
}
else
{
break;
}
}
}
void createProcess(vector<EndPoint>* end_points)
{
vector<int> fds;
//1、先进行构建控制结构,1父多子,每个子都有自己的管道
//父写子读
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)//子进程,读
{
cout << getpid() << " 子进程关闭父进程对应的写端: ";
for(auto &fd : fds)
{
cout << fd << " ";
close(fd);
}
cout << 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()
{
cout << "*******************************************" << endl;
cout << "| 0. 执行日志任务 1. 执行数据库任务 |" << endl;
cout << "| 2. 执行请求任务 3. 退出 |" << endl;
cout << "*******************************************" << endl;
cout << "请选择: ";
int command = 0;
cin >> command;
return command;
}
void ctrlProcess(const vector<EndPoint> &end_pints)
{
//2.1 我们可以写成自动化的,也可以写成交互式的
int num = 0;
int cnt = 0;
while(true)
{
//1. 选择任务
int command = ShowBoard();
if(command == 3) break;//输入3就退出,再回到main处
if(command < 0 || command > 2)
{
cout << "没有对应任务" << endl;
continue;
}
//2. 选择进程
int index = cnt++;//按照顺序来选择任务
cnt %= end_points.size();
string name = end_points[index].name();
cout << "选择了进程: " << name << " | 处理任务" << command << endl;
//3. 下发任务
write(end_points[index]._write_fd, &command, sizeof(command));
}
}
void waitProcess(const vector<EndPoint> &end_points)
{
//1. 让子进程全部退出 --- 只需要让父进程关闭所有的写端
for(int end = end_points.size() - 1; end >= 0; end--)
{
cout << "父进程让所有的子进程全部退出: " << end_points[end]._child_id << endl;
close(end_points[end]._write_fd);
//2. 父进程要回收子进程的僵尸状态
waitpid(end_points[end]._child_id, nullptr, 0);
cout << "父进程回收了所有的子进程: " << end_points[end]._child_id << endl;
}
sleep(10);
}
int main()
{
vector<EndPoint> end_pints;
createProcess(&end_points);
//2. 循环结束,我们已经拿到了所有子进程和他的管道
ctrlProcess(&end_points);
//3. 处理所有的退出问题
waitProcess(end_points);
return 0;
}
4、命名管道
匿名管道有局限性,只能用于有血缘关系的进程之间进行通信,要让两个陌生的进程之间通信就需要用到命名管道。
创建命名管道要用到mkfifo命令,括号里就是命名管道参数,fifo意思就是先进先出
它的文件类型以p开头,说明是一个管道文件。
现在写个命令echo “字符串” >fifo,往这里面写内容,但是光标会停在下一行开头,一直闪烁,这是因为fifo文件只是一个符号,向里面写的东西不会真实存在在磁盘中,只写到管道文件中,我们可以用cat命名读,它默认从显示器读,cat < fifo就会从管道文件读,然后打印到显示器上。即使写一个一直输出内容的代码,也可以另开一个窗口用cat来获取内容。
1、原理
在之前基础IO博客中的硬链接部分写到过引用计数这个东西,命名管道里也会用到引用计数。在磁盘中的一个文件,如果不打开,就待在磁盘里;打开后就会有自己的文件结构体,里面有各项参数,其中就有引用计数;操作者创建进程后,会有文件描述符表等东西,进程可以打开在内存中的文件,此时引用计数就变成了1;如果再开一个进程,打开同样的文件,系统不会在开一个这个文件的结构体,而是这两个进程指向同一个文件结构体,计数变为2,进程一个个消失,计数就从2变为0。但两个进程如何保证打开同一个文件?要想让文件唯一,文件路径+文件名都相同就可以。
2、模拟实现
创建管道文件,让读写端进程分别按照自己的需求打开文件,然后开始通信。
我们定义两个文件,要形成两个可执行程序需要这样写。
comm.hpp
#pragma once
#include <iostream>
#include <string>
#define NUM 1024
const std::string fifoname = "./fifo";
uint32_t mode = 0666;
Makefile
.PHONY:all
all:Server Client
Server:Server.cc
g++ -0 Server Server.cc
Client:Client.cc
g++ -0 Client Client.cc
.PHONY:clean
clean:
rm -f Server Client
Server.cc
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;
int main()
{
//1、创建管道文件,只需要一次创建
umask(0);//不影响系统默认设置,只会影响当前进程
int n = mkfifo(fifoname.c_str(), mode);
if(n != 0)
{
cout << errno << " : " << strerror(errno) << endl;
return 1;
}
//2、让服务端开启管道文件
cout << "create fifo success, begin ipc" << endl;
int rfd = open(fifoname.c_str(), O_RDONLY);
if(rfd < 0)
{
cout << errno << " : " << strerror(errno) << endl;
return 2;
}
cout << "open fifo success, begin ipc" << endl;
//3、通信
char buffer[NUM];
while(true)
{
buffer[0] = 0;//C风格的字符串只要第一个位置是0,那就是空字符串
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);//不一定会发字符串,也有可能发4字节数据流,只是这里就看作字符串
if(n > 0)
{
buffer[n] = 0;
cout << "client# " << buffer << endl;
}
else if (n == 0)//写端关闭,读端就会读到0
{
cout << "client quit, me too" << endl;
break;
}
else
{
cout << errno << " : " << strerror(errno) << endl;
break;
}
}
//关闭不要的fd
close(rfd);
unlink(fifoname.c_str());//它会自己去除所有引用计数,退出管道文件
return 0;
}
Client.cc
#include <iostream>
#include <cstdio>
#include <cerrno>
#include <cassert>
#include <cstring>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"
using namespace std;
int main()
{
//1、需不需要创建管道文件?不需要,只需要打开对应的文件即可
int wfd = open(fifoname.c_str(), O_WRONLY);
if(wfd < 0)
{
cerr << errno << " : " << strerror(errno) << endl;
return 1;
}
//通信
char buffer[NUM];
while(true)
{
cout << "请输入你的消息: ";
char* msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减1,因为C语言接口会自动处理成字符串,如果是系统接口就需要减1
//但有时候会混淆,所以也可以无脑减1,代码风格统一。
assert(msg);
(void)msg;
ssize_t n = write(wfd, buffer, sizeof(buffer));//虽然这是系统接口,不加1就得不到最后的\0,我们只需要获取字符串内容即可
assert(n > 0);
(void)n;
}
close(wfd);
return 0;
}
打开两个窗口来观察Client和Server。开始运行后,Server会卡在打开管道这里,因为它只开读端,需要Client开启写端才会继续运行,也就是一开始只有create file,等到我们./client后,Client.cc开始运行,那么Server那里就会打印open file…;程序是可以正常通信的,但是如果每输入一条语句就空格一下,Server也是会收到这个空格,就会导致每一行下面都有一个空行,所以我们在Client那里更改一下。
cout << "请输入你的消息: ";
char* msg = fgets(buffer, sizeof(buffer), stdin);//sizeof(buffer)不用减1,因为C语言接口会自动处理成字符串,如果是系统接口就需要减1
//但有时候会混淆,所以也可以无脑减1,代码风格统一。
assert(msg);
(void)msg;
buffer[strlen(buffer) - 1] = 0;
ssize_t n = write(wfd, buffer, sizeof(buffer));//虽然这是系统接口,不加1就得不到最后的\0,我们只需要获取字符串内容即可
assert(n > 0);
(void)n;
如果输入空串,Server就会退出,这个因为上面代码中最后一个assert会断言失败,那么Server收到后n就是0,就会退出,所以那里写成assert(n >= 0)。
下面的链接中修改了一下,改成不需要打空格也能自动打印。
结束。