目录
前言:
正文
1.命名管道
1.1创建及使用
1.2命名管道的工作原理
1.3命名管道与匿名管道的区别
2.命名管道特点及应用场景
2.1特点
2.2场景
3.命名管道实操
3.1实现文件拷贝
3.2实现进程控制
总结:
前言:
管道中除了匿名管道还有一个命名管道,命名管道有自己的名字,自带同步与互斥机制、数据单向流通,可以实现毫不相干的两个独立进程间通信。
正文
1.命名管道
让匿名管道有名字的属性,就会使其变成命名管道。
- 结合文件系统,给匿名管道这个纯纯的内存文件分配
inode
,将文件名与之构建联系,关键点在于不给它分配Data block
,因为它是一个纯纯的内存文件,是不需要将数据刷盘到磁盘中的
1.1创建及使用
命名管道的创建可以使用函数 mkfifo 原型如下:
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
关于mkfifo函数
对于参数1,既可以传递绝对路径 /home/xxx/namePipeCode/fifo,也可以传递相对路径 ./fifo,当然绝对路径更灵活,但也更长
对于参数2,mode_t 其实就是对 unsigned int 的封装,等价于 uint32_t,而 mode 就是创建命名管道时的初始权限,实际权限需要经过 umask 掩码计算
不难发现,mkfifo 和 mkdir 非常像,其实 mkfifo 可以直接在命令行中运行
创建一个名为 fifo 的命名管道文件
mkfifo fifo
成功解锁了一种新的特殊类型文件:p
管道文件
这个管道文件也非常特殊:大小为 0,从侧面说明 管道文件就是一个纯纯的内存级文件,有自己的上限,出现在文件系统中,只是单纯挂个名而已
可以直接在命令行中使用命名管道:
echo 可以进行数据写入,可以重定向至 fifo
cat 可以进行数据读取,同样也可以重定向于 fifo
打开两个终端窗口(两个进程),即可进行通信
当然也可以通过程序实现两个独立进程 IPC
思路:创建 服务端 server 和 客户端 client 两个独立的进程,服务端 server 创建并以 读 的方式打开管道文件,客户端 client 以 写 的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭 写端(服务端 读端 读取到 0 后也关闭并删除命令管道文件)
注意:
- 当管道文件不存在时,文件会打开失败,因此为了确保正常通信,需要先运行服务端
server
创建管道文件 - 服务端启动后,因为是读端,所以会阻塞等待 客户端(写端)写入数据
- 客户端写入数据时,因为
'\n'
也被读取了,所以要去除此字符 - 通信结束后,需要服务端主动删除管道文件
公共资源
common.h
#pragma once
#include <iostream>
#include <string>
std::string fifo_name = "./fifo"; //管道名
uint32_t mode = 0666; //权限
服务端
server.cc
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"
using namespace std;
int main()
{
// 服务端
// 1、创建命名管道文件
int ret = mkfifo(fifo_name.c_str(), mode);
if (ret < 0)
{
cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl;
exit(0);
}
// 2、以读的方式打开文件
int rfd = open(fifo_name.c_str(), O_RDONLY);
if (rfd < 0)
{
cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
exit(0);
}
// 3、读取数据
while (true)
{
char buff[64];
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = '\0';
if (n > 0)
{
cout << "Server get message# " << buff << endl;
}
else if (n == 0)
{
cout << "写端关闭,读端读取到0,终止读端" << endl;
break;
}
else
{
cout << "读取异常" << endl;
break;
}
}
close(rfd);
unlink(fifo_name.c_str()); //删除命名管道文件
return 0;
}
客户端
client.cc
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"
using namespace std;
int main()
{
// 客户端
// 1、打开文件
int wfd = open(fifo_name.c_str(), O_WRONLY);
if (wfd < 0)
{
cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
exit(0);
}
// 2、写入数据,进行通信
char buff[64] = {0};
while (true)
{
cout << "Client send message# ";
fgets(buff, sizeof(buff) - 1, stdin);
buff[strlen(buff) - 1] = '\0'; // 去除 '\n'
if (strcasecmp("exit", buff) == 0)
break;
write(wfd, buff, strlen(buff));
}
close(wfd);
return 0;
}
注:strcasecmp
是一个字符串比较函数,无论字符串大小写,都能进行比较
1.2命名管道的工作原理
把视角拉回文件系统:当重复多次打开同一个文件时,并不会费力的打开多次,而且在第一次打开的基础上,对 struct file
结构体中的引用计数 ++
,所以对于同一个文件,不同进程打开了,看到的就是同一个
具体例子:
显示器文件(stdout)只有一个吧,是不是所有进程都可以同时进行写入?
同理,命名管道文件也是如此,先创建出文件,在文件系统中挂个名,然后让独立的进程以不同的方式打开同一个命名管道文件,比如进程 A 以只读的方式打开,进程 B 以只写的方式打开,那么此时进程 B 就可以向进程 A 写文件,即 IPC
因为命名管道适用于独立的进程间 IPC
,所以无论是读端和写端,进程 A
、进程 B
为其分配的 fd
是一致的,都是 3
- 如果是匿名管道,因为是依靠继承才看到同一文件的,所以读端和写端
fd
不一样
1.3命名管道与匿名管道的区别
- 匿名管道只能用于具有血缘关系的进程间通信;而命名管道不讲究,谁都可以用
- 匿名管道直接通过
pipe
函数创建使用;而命名管道需要先通过mkfifo
函数创建,然后再通过open
打开使用 - 出现多条匿名管道时,可能会出现写端
fd
重复继承的情况;而命名管道不会出现这种情况
2.命名管道特点及应用场景
2.1特点
- 管道是半双工通信
- 管道生命随进程而终止
- 命名管道任意多个进程间通信
- 管道提供的是流式数据传输服务
- 管道自带 同步与互斥 机制
2.2场景
- 管道为空时,读端阻塞,等待写端写入数据
- 管道为满时,写端阻塞,等待读端读取数据
- 进程通信时,关闭读端,
OS
发出13
号信号SIGPIPE
终止写端进程 - 进程通信时,关闭写端,读端读取到
0
字节数据,可以借此判断终止读端
3.命名管道实操
3.1实现文件拷贝
我们可以利用 命名管道实现不同进程间 IPC
,即进程从文件中读取并写入一批数据,另一个进程一次读取一批数据并保存至新文件中,这样就实现了文件的拷贝
目标:利用命名管道,向空文件 target.txt
中写入数据,即拷贝源文件 file.txt
公共资源 common.h
#pragma once
#include <iostream>
#include <string>
std::string fifo_name = "./fifo"; //管道名
uint32_t mode = 0666; //权限
服务端(写端)
server.cc
提供文件拷贝服务
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"
int main()
{
//服务端
// 打开文件
int wfd = open(fifo_name.c_str(), O_WRONLY);
if(wfd < 0)
{
std::cerr <<"open fail! errno:"<< errno << "| "<<strerror(errno)<<std::endl;
exit(0);
}
//打开源文件
FILE *fp = fopen("file.txt","r");
if(fp == NULL)
{
std::cerr <<"Fopen fail! errno:"<< errno << "| "<<strerror(errno)<<std::endl;
exit(0);
}
//读取数据
char buff[1024] ;
int n = fread(buff,sizeof(char),sizeof(buff),fp);
// IPC
//将源文件写入命名管道
write(wfd,buff,strlen(buff));
std::cout << "服务端已向管道写入: " << n << "字节的数据" <<std:: endl;
fclose(fp);
fp = nullptr;
close(wfd);
return 0;
}
客户端(读端)
client.cc
从服务端中拷贝文件(下载)
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"
int main()
{
//创建管道文件
int ret = mkfifo(fifo_name.c_str(),mode);
if(ret<0)
{
std::cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << std::endl;
exit(0);
}
//打开管道文件
int rfd = open(fifo_name.c_str(),O_RDONLY);
if(rfd<0)
{
std::cerr << "open fail! errno: " << errno << " | " << strerror(errno) << std::endl;
exit(0);
}
// 打开目标文件
FILE *fp = fopen("target.txt","w");
if(fp == NULL)
{
std::cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << std::endl;
exit(0);
}
// 进行拷贝 ipc-communication
char buff[1024];
int n = read(rfd,buff,sizeof(buff)-1);
buff[n] = '\n';
if (n > 0)
std::cout << "客户端已从管道读取: " << n << "字节的数据" << std::endl;
else if (n == 0)
std:: cout << "写端关闭,读端读取到0,终止读端" <<std:: endl;
else
std::cout << "读取异常" << std::endl;
fwrite(buff,sizeof(char),strlen(buff),fp);
std::cout << "客户端已成功从服务端下载(拷贝)了文件数据" <<std:: endl;
//关闭管道/文件
fclose(fp);
fp = nullptr;
close(rfd);
unlink(fifo_name.c_str());
return 0;
}
Makefile 文件
.PHONY:all
all:client server
server:server.cc
g++ -o $@ $^ -std=c++11
client:client.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f client server
运行结果如下图所示:
此时 服务端是写端,客户端是读端,实现的是 下载服务;当 服务端是读端,客户端是写端时,实现的就是 上传服务,搞两条管道就能模拟实现简单的 数据双向传输服务
注意: 创建管道文件后,无论先启动读端,还是先启动写端,都要阻塞式的等待另一方进行交互
3.2实现进程控制
在 Linux
匿名管道 IPC
中,我们实现了一个简易版的进程控制程序,原理是通过多条匿名管道实现父进程对多个子进程执行任务分配
匿名管道用于有血缘关系间 IPC
,命名管道也可以
所以我们可以把上一篇文章中的 匿名管道换为命名管道,一样可以实现通信
task.hpp
#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>
#include <unistd.h>
using namespace std;
void PrintLOG()
{
cout << "PID: " << getpid() << " 正在执行打印日志的任务…" << endl;
}
void InsertSQL()
{
cout << "PID: " << getpid() << " 正在执行数据库插入的任务…" << endl;
}
void NetRequst()
{
cout << "PID: " << getpid() << " 正在执行网络请求的任务…" << endl;
}
class Task
{
public:
Task()
{
// 装载任务
_tt = {{"打印日志", PrintLOG}, {"数据库插入", InsertSQL}, {"网络请求", NetRequst}};
}
// 展示任务
void showTask()
{
cout << "目前可用任务有:[";
for (auto e : _tt)
cout << e.first << " ";
cout << "]" << endl;
cout << "输入 退出 以终止程序" << endl;
}
// 执行任务
void Execute(const string &task)
{
if (_tt.count(task) == 0)
{
cerr << "没有这个任务:" << task << endl;
}
else
{
_tt[task](); // 函数对象调用
}
}
private:
unordered_map<string, function<void(void)>> _tt;
};
控制程序
namePipeCtrl.cc
包括进程、管道创建,任务执行与进程等待
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Task.hpp"
using namespace std;
enum
{
NAME_SIZE = 64
};
// 子进程基本信息类
class ProcINfo
{
public:
ProcINfo(pid_t pid = pid_t(), int wfd = int())
: _pid(pid), _wfd(wfd), _num(_cnt++)
{
char buff[NAME_SIZE] = {0};
snprintf(buff, NAME_SIZE, "Process %d | pid:wfd [%d:%d]", _num, _pid, _wfd);
_name = string(buff);
}
pid_t _pid;
int _wfd;
int _num;
string _name;
static int _cnt;
};
int ProcINfo::_cnt = 0;
// 进程控制类
class ProcCtrl
{
public:
ProcCtrl(int num = 3, mode_t mode = 0666)
: _num(num), _mode(mode)
{
// 根据 _num 创建命名管道及子进程
CreatPipeAndProc();
}
~ProcCtrl()
{
waitProc();
}
// 创建管道及进程
void CreatPipeAndProc()
{
// 因为是继承的,所以也要注意写端重复继承问题
vector<int> fds;
for (int i = 0; i < _num; i++)
{
// 步骤:创建管道,存入 _vst
char pipeNameBUff[NAME_SIZE]; // 管道名缓冲区
snprintf(pipeNameBUff, NAME_SIZE, "./fifo-%d", i);
int ret = mkfifo(pipeNameBUff, _mode);
assert(ret != -1);
(void)ret;
_vst.push_back(string(pipeNameBUff));
// 创建子进程,让子进程以只读的方式打开管道文件
pid_t id = fork();
if (id == 0)
{
// 子进程内
// 先关闭不必要的写端
for (auto e : fds)
close(e);
// 打开管道文件,并进入任务等待默认(读端阻塞)
int rfd = open(_vst[i].c_str(), O_RDONLY);
assert(rfd != -1);
(void)rfd;
waitCommand(rfd);
close(rfd); // 关闭读端
exit(0);
}
// 父进程以写打开管道,保存 fd 信息
int wfd = open(_vst[i].c_str(), O_WRONLY);
assert(wfd != -1);
(void)wfd;
// 注册子进程信息
_vpt.push_back(ProcINfo(id, wfd));
fds.push_back(wfd);
}
}
// 子进程等待任务派发
void waitCommand(int rfd)
{
while (true)
{
char buff[NAME_SIZE] = {0};
int n = read(rfd, buff, sizeof(buff) - 1);
buff[n] = '\0';
if (n > 0)
{
Task().Execute(string(buff));
}
else if (n == 0)
{
cerr << "读端读取到 0,写端已关闭,读端也即将关闭" << endl;
break;
}
else
{
cerr << "子进程读取异常!" << endl;
break;
}
}
}
// 展示可选进程
void showProc()
{
cout << "目前可用进程有:[";
int i = 0;
for (i = 0; i < _num - 1; i++)
cout << i << "|";
cout << i << "]" << endl;
}
// 下达任务给子进程
void ctrlProc()
{
while (true)
{
cout << "==========================" << endl;
int n = 0;
do
{
showProc();
cout << "请选择子进程:> ";
cin >> n;
} while (n < 0 || n >= _num);
Task().showTask();
string taskName;
cout << "请选择任务:> ";
cin >> taskName;
if (taskName == "退出")
break;
// 将信息通过命名管道写给子进程
cout << "选择进程 ->" << _vpt[n]._name << " 执行 " << taskName << " 任务" << endl;
write(_vpt[n]._wfd, taskName.c_str(), taskName.size());
sleep(1);
}
}
// 关闭写端、删除文件、等待子进程退出
void waitProc()
{
for (int i = 0; i < _num; i++)
{
close(_vpt[i]._wfd); // 关闭写端
unlink(_vst[i].c_str()); // 关闭管道文件
waitpid(_vpt[i]._pid, nullptr, 0); // 等待子进程
}
cout << "所有子进程已回收" << endl;
}
private:
vector<ProcINfo> _vpt; // 子进程信息表
vector<string> _vst; // 命名管道信息表
int _num; // 子进程数/命名管道数
mode_t _mode; // 命名管道文件的权限
};
int main()
{
ProcCtrl p1;
p1.ctrlProc();
return 0;
}
关于 父子进程间使用命名管道通信 值得注意的问题:
在命名管道创建后,需要先创建子进程,让子进程打开【读端或写端】,然后才让父进程打开【写端或读端】,这是因为假如先让父进程打开【写端或读端】,那么此时父进程就会进入【阻塞】状态,导致无法创建子进程,自然也就无法再打开【读端或写端】;所以正确做法是先让子进程打开,即使子进程【阻塞】了,父进程也还能运行。不要让【阻塞】阻碍子进程的创建
子进程继承都存在的问题:写端重复继承,因此需要关闭不必要的写端 fd
总结:
命名管道部分还有很多,结合bash 等,等后续补充。