目录
今天我们来学习命名管道
由于匿名管道(pipe()
)无法在两个毫不相干的进程之间进行通信,只能用于 具有亲缘关系的进程(如父子进程、兄弟进程)。如果要让完全独立的进程通信,需要使用 命名管道(FIFO) 或 其他 IPC 机制(如共享内存、消息队列、套接字等)。因为他们彼此不知道管道的名字,不知道名字就无法只读文件名字背后的地址和inode,就进而不知道管道内部存储的信息。关掉有名字不就好了。没错命名管道可以完成不同进程(两个毫不相干进程)间的通信。
命名管道的创建
通信原理和匿名的完全一致,我们可以使用指令或者C语言函数进行创建。
mkfifo
是用于创建 命名管道(FIFO, First In First Out) 的命令或系统调用。这个不是系统函数,是C语言的内置函数,mkfifo xxx(xxx为命名管道的名字),就是直接创建了一个以xxx为名字的命名管道。
使用mkfifo创建了一个命名管道test,然后管道文件的标准就是后面有一个|,我们往里写入是不需要加|的,为了进行进程间的通信,我们需要再打开一个shell,然后有个shell进行写入,另一个看一下这个进程看能不能看到。
可以看到两个毫不相干的进程完成了进程间的通信,不知道你们有没有注意到当一方进行写入时,如果另一方不立刻进行读取,写的一方会卡住,因为命名管道(FIFO) 是 同步的:写端(echo)必须等待读端(cat 或其他进程)来读取数据,否则写操作会阻塞。就是不允许一直写而不读,终归来说就是写端这个进行一直保持挂起状态而不退出,这个是放在前提进行的特点,我们出现这种情况,要么就是另一端进行读取,要么就是将写入放到后端进行。
如上图可知,当程序处于后端后:
使用函数创建命名管道的通信
预备创建
使用命名管道进行通信,需要使用C语言函数mkfifo
mkfifo的第一个参数是创建管道的路径(包括文件名),第二个参数是管道的使用权限,返回值为0表示创建成功,-1表示失败,然后需要形成两个可执行程序,代表两个毫不相干的进程进行通信,然后编写makefile,同时编译两个进程。
所以很简单,我们定义clent,server两个进程,然后分别创建各自的.cc和.hpp,然后comm.hpp是两个进程的共有部分,都能相互看到的内容。让两个可执行程序的.hpp都引用头文件comm.hpp。
最后销毁管道我们使用函数unlink销毁管道,这个函数就只需要传递管道的路径,然后返回值如果是0就是销毁成功,-1就是销毁失败。但是这个函数不是真的去把管道删除了,只是删除了管道的名字和inode的映射关系,只是删除了管道的路径,一个文件没有了路径之前说了就没有存在的必要了,这样就会被操作系统给释放了。
接下来的设计我就不会明说了,因为都很简单!!!
makefile设计
为了同时可以编译两个可执行文件,需要重新定义一个伪目标all,然后让其依赖两个可执行,没有依赖方法,这样就可以了在预编译all的时候编译了server和clent。
server.hpp设计
#pragma once
#include"comm.hpp"
class Init
{
public:
Init()
{
umask(0);
int n = ::mkfifo(pipefile.c_str(), gmode);
if (n < 0)
{
cerr << "mkfifo error!" << endl;
return;
}
cout << "mkfifo success" << endl;
sleep(10);
}
~Init()
{
int n = unlink(pipefile.c_str());
if (n < 0)
{
cerr << "unlink error!" << endl;
return;
}
cout << "unlink success" << endl;
}
};
Init init;
class server
{
public:
server()
:_fd(gdefultfd)
{}
bool openpipe()
{
_fd = ::open(pipefile.c_str(), O_RDONLY);
if (_fd < 0)
{
cerr << "open fail!" << endl;
return false;
}
return true;
}
//string* :输出型参数
//const string& :输入型参数
//string & : 输入输出型参数
int recpipe(string* out) //server负责读取
{
char buffer[gsize];
ssize_t n = ::read(_fd, buffer, sizeof(buffer)-1);
if (n > 0)
{
buffer[n] = 0;
*out = buffer;
}
return n;
}
void closepipe()
{
if (_fd >= 0)
{
::close(_fd);
}
}
~server()
{
//closepipe();
}
private:
int _fd;
};
由于需要通信的所以需要先有一个进程先来创建这个管道,我们先让进程server来创建这个命名管道,如上封装在Init类里,然后像gmode(管道权限),pipefile(路径),gdefultfd(文件描述符的初始值),gsize(读取容量),都是放在了comm.hpp里面作为共有资源被看到。
server我们让其负责读数据的工作,所以以读的形式打开,然后repipe中read读到输出型参数out里面,然后out将来以引用传入,再打印出来就可以了。
然后这里为了使读入的是字符串是/0不被读取,不然会乱码而少读取一个字节,然后将最后一位的下一个置为0,使读入缓冲区时可以刷新。
clent.hpp设计
#pragma once
#include"comm.hpp"
class clent
{
public:
clent()
:_fd(gdefultfd)
{}
bool openpipe()
{
_fd = ::open(pipefile.c_str(), O_WRONLY);
if (_fd < 0)
{
cerr << "open fail!" << endl;
return false;
}
return true;
}
//string* :输出型参数
//const string& :输入型参数
//string & : 输入输出型参数
int sendpipe(string& in) //server负责读取
{
return ::write(_fd, in.c_str(), in.size());
}
void closepipe()
{
if (_fd >= 0)
{
::close(_fd);
}
}
~clent()
{
//closepipe();
}
private:
int _fd;
};
clent进程就负责写入管道了,以只写打开管道后使用write直接写入就可以了。其他和server都一样的。
comm.hpp设计
这部分就是进程间可以看到的公共的部分了,我们可能会质疑到这个gsize有范围,那我一次写入如果超过,那一次读取不就读取不完了吗,那剩余的怎么办?剩下没有读取的就会留到下一次读取的时候再读,这样相当于将单次读取拆开了。
server.cc设计
#include"server.hpp"
int main()
{
server ser;
ser.openpipe();
string message;
while (true)
{
if (ser.recpipe(&message) > 0)
{
cout << "clent say# " << message << endl;
}
else
{
break;
}
}
cout << "clent quit, me too " << endl;
ser.closepipe();
return 0;
}
就是执行打开,读取,然后关闭。
clent.cc设计
#include"clent.hpp"
int main()
{
clent cle;
cle.openpipe();
string message;
while(true)
{
cout << "please enter: ";
getline(cin, message);
cle.sendpipe(message);
}
cle.closepipe();
return 0;
}
就是依次执行打开,写入,关闭。
测试运行
如果已经存在管道fifo需要先删除,我们需要先让读端打开,然后再写入,这样比较合理,防止写入的信息读不到。
可以看到,写端正常写入的同时,读端正常读取,写端退出时读端由于没有可读的了,就也跟着退出并将管道销毁了。