本篇要引入的内容是命名管道
前面的总结中已经搞定了匿名管道,但是匿名管道有一个很严重的问题,它只允许具有血缘关系的进程进行通信,那如果是两个不相关的进程进行通信,此时应该如何处理?此时就可以采用的是命名管道
从名字上能看出来,它既然叫命名管道,就说明它是有名字的
系统默认是支持指令级别的命名管道的,例如在bash中的竖划线实际上就是指令级别的匿名管道,而命名管道当然也是有指令级别的
fifo指令
从指令的角度来讲,使用fifo命令就可以创建出一条命名管道,例如使用mkfifo filename,就可以在当前目录下创建出一个命名管道:
从上述的指令中可以看到,确确实实一个命名管道被创建出来了,并且在权限前面的p也证明这是一个管道文件
为什么说它是一个命名管道?第一个是说它有文件名,其实还有一层原因是因为它存在路径,只要有路径和文件名,未来就可以通过路径和文件名找到这个文件,既然可以找到文件,就可以借助这个文件进行进程间的通信,那么下面来看如何进行进程间的通信
上述是最简单的管道通信示意图,基本原理就是一个进程把内容放入到管道中,另外一个进程从管道中读取信息,这就是最基本的原理
这个管道和匿名管道的一个区别是,匿名管道是内存级的文件,简单来说就是不会在磁盘上创建信息,操作系统关闭这个文件也就随之消失了,而命名管道是一个磁盘级的文件,不会随着操作系统的关闭而消失,所以可以把这个文件当做是一个正常的文件来看待
那么和正常文件的其中一个区别是,向管道中输入文件后会发现,此时文件的大小依旧是0:
出现这样的原因是,虽然它是一个磁盘级的文件,但是在实际的数据通信的过程,这个文件必然是要读端和写端分别对两个程序进行开放,那么既然这个文件已经被打开了,那么它的数据就没有必要向磁盘中刷新,所以磁盘中这个文件此时还是没有数据的,而对于其他的文件,都会及时的向磁盘中做刷新,以保存到磁盘中
管道的原理
下面讨论的内容是关于管道文件的原理,现在有两个进程a和进程b,通信的本质是让两个进程看到同一份资源,这样就可以借助这个同一份资源实现进程之间的通信了,这是在最初就创建出的观点,那么对于命名管道来说,如何能让两个资源都看到我呢?怎么能保证呢?其实借助的就是路径名和文件名的唯一性,这样从宏观上来讲就能保证看到的是同一份资源,换个角度,从微观上讲,看到的真的是同一份资源吗?答案也是肯定的,下面给出具体的解释
首先,文件是存在于磁盘中的,现在a进程有它对应的PCB,有自己的文件描述符表,b进程也有自己的PCB和文件描述符表,而现在如果a进程打开了这个路径中名字为某个名字的命名管道文件,操作系统为了方便管理信息,就要为这个文件创建一个文件结构体来管理这个文件对象,然后再将文件描述符分配给a进程的文件描述符表当中,同时也有文件对应的文件缓冲区,如果这是一个普通文件,那么未来就可以借助这个文件缓冲区将内容刷新到磁盘中或者是把磁盘中的内容读取到内存中,这些都是可以理解的,那么下一个要探讨的问题是,如果此时还有一个进程b把这个文件打开了,那么操作系统是否还会做出同样的内容呢?会不会继续加载这个文件对应的文件缓冲区,然后再创建等等一系列步骤呢?答案显然是不会的,因为操作系统是一个非常讲究效率的模块,它不会做出任何违背效率的事,所以文件的内容都存储在内存中,属性也已经加载好了,那么操作系统就不会重新加载了,所以两个进程打开了同一个文件,文件对应的缓冲区,内容和属性这些内容都是不用再重新加载的,但是还会有对应的文件结构体,用来描述这个文件进行读写到什么位置,这些还是会对应的进行加载的
用下图来对上述的这一系列原理做出一个解释:
从这个图也能看出来,其实命名管道和匿名管道的原理基本上是一样的,没什么区别,操作系统判别到底是不是一个文件的标准就是看路径+文件名,有了文件名就有了inode,于是就有了这上述的一系列逻辑,就做到了,让进程a和进程b都看到了同一份资源,进而借助缓冲区完成了进程之间的通信
其实相比起匿名管道来说,命名管道反而更加简单,所以有了前面进程池的代码基础,实现也不算很难:
// comm.h
#pragma once
#define FILENAME "fifo"
// client.cc
#include
#include
#include
#include
#include
#include
#include
#include "comm.h"
int main()
{
int wfd = open(FILENAME, O_WRONLY);
if (wfd < 0)
{
std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
return 1;
}
std::cout << "open fifo success... write" << std::endl;
std::string message;
while (true)
{
std::cout << "Please Enter# ";
std::getline(std::cin, message);
ssize_t s = write(wfd, message.c_str(), message.size());
if (s < 0)
{
std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
break;
}
}
close(wfd);
std::cout << "close fifo success..." << std::endl;
return 0;
}
// server.cc
#include
#include
#include
#include
#include
#include
#include
#include "comm.h"
bool MakeFifo()
{
int n = mkfifo(FILENAME, 0666);
if (n < 0)
{
std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
return false;
}
std::cout << "mkfifo success... read" << std::endl;
return true;
}
int main()
{
Start:
int rfd = open(FILENAME, O_RDONLY);
if (rfd < 0)
{
std::cerr << "errno: " << errno << ", errstring: " << strerror(errno) << std::endl;
if (MakeFifo())
goto Start;
else
return 1;
}
std::cout << "open fifo success..." << std::endl;
char buffer[1024];
while (true)
{
ssize_t s = read(rfd, buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
std::cout << "Client say# " << buffer << std::endl;
}
else if (s == 0)
{
std::cout << "client quit, server quit too!" << std::endl;
break;
}
}
close(rfd);
std::cout << "close fifo success..." << std::endl;
return 0;
}