前言
命名管道是什么呢?
管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
命名管道是一种特殊类型的文件。
一、命名管道
在学习命名管道前我们先看看在命令行创建命名管道的函数mkfifo:
fifo的意思就是first in first out也就是先进先出的意思,比如我们直接在目录下创建一个命名管道文件:
在文件的权限部分的第一个P代表的是管道文件,下面我们讲讲命名管道的原理:
同样有两个进程,上面的是父进程下面是子进程,父进程的一个3号文件描述符表中记录一个文件的地址,这也是被父进程打开的文件,当我们创建一个子进程时,想让子进程打开和父进程打开的相同的那个文件,对于操作系统来说是不会重新再创建一个相同的文件的,操作系统会查询子进程要打开的文件是否已经被打开,如果找到这个被打开的文件就把这个文件的地址填入子进程的文件描述符表中,这样子进程就指向父进程打开的这个文件了,并且在文件中会有像ret这样的变量,当我们有文件描述符指向这个文件时这个变量就会++这也就是引用计数,关闭文件后就会--。那么如何保证两个毫不相关的进程看到的是同一个文件并打开呢?其实很简单,因为文件的唯一性是用路径表示的,只要让不同的进程通过文件路径+文件名看到同一个文件并打开,就是看到了同一个资源,这就具备了进程间通信的前提。
接下来我们用代码演示命名管道,首先需要创建两个文件client.cc写客户端代码,serve.cc写服务端代码,因为这次我们要实现两个可执行程序,所以我们需要在makefile中生成两个可执行程序,makefile代码如下图:
.PHONY后面加上all,就是说我的目标文件是all,我们让all只有依赖关系没有依赖方法,这样就会去找server和client的依赖关系,就生成了两个可执行程序。下面我们正式编写代码,还记得我们刚开始讲的mkfifo函数吗?此函数的参数需要路径和选项(下面有C库中的mkfifo函数的说明),对于路径我们直接搞一个const string类型的字符串来保存路径,因为服务端和客户端需要打开同一份文件所以我们再创建一个公共的hpp头文件,把我们刚刚定义的路径放进去:
下面我们再看一下C库中的mkfifo函数说明:
我们可以看到此函数有两个参数,第一个参数是路径,第二个参数是mode,mode是什么呢?mode_t类型又是什么呢?实际上mode_t就是一种无符号整数,我们在讲文件的时候提到过,就是一种让你控制要创建的文件初始是什么权限,我们默认将权限给成0666。
下面我们编写服务端的代码,不知道大家还记不记得之前说过的,对于权限我们给的是0666但是经过权限掩码的影响会变成其他的,所以我们如果不想被权限掩码所影响就将默认的权限掩码设置为0。
因为我们在创建管道文件的时候会有可能失败,所以我们用if语句判断一下,函数返回值如果等于0就是成功否则就是失败,失败我们就打印对应的错误然后返回1.下一步就是让服务端开启管道文件,开启后就可以正常通信了:
int main() { //1.创建管道文件,我们今天只需要一次创建 umask(0); //这个设置并不影响系统的默认设置,只会影响当前进程 int n = mkfifo(fifoname.c_str(),mode); if (n!=0) { std::cout<<errno<<" : "<<strerror(errno)<<std::endl; return 1; } std::cout<<"creat fifo file sucess"<<std::endl; // 2.让服务端直接开启管道文件 int rfd = open(fifoname.c_str(),O_RDONLY); if (rfd<=0) { std::cout<<errno<<" : "<<strerror(errno)<<std::endl; return 2; } std::cout<<"open fifo success , begin ipc"<<std::endl; return 0; }
开启管道文件很简单,就是打开我们创建的管道文件,这里只需要以只读方式打开就可以。同样要判断打开失败的情况,成功后我们就打印打开管道文件成功。下面我们实现开始正常通信的代码:
int main() { //1.创建管道文件,我们今天只需要一次创建 umask(0); //这个设置并不影响系统的默认设置,只会影响当前进程 int n = mkfifo(fifoname.c_str(),mode); if (n!=0) { std::cout<<errno<<" : "<<strerror(errno)<<std::endl; return 1; } std::cout<<"creat fifo file sucess"<<std::endl; // 2.让服务端直接开启管道文件 int rfd = open(fifoname.c_str(),O_RDONLY); if (rfd<=0) { std::cout<<errno<<" : "<<strerror(errno)<<std::endl; return 2; } std::cout<<"open fifo success , begin ipc"<<std::endl; //3.正常通信 char buffer[NUM]; while (true) { buffer[0] = 0; ssize_t n = read(rfd,buffer,sizeof(buffer)-1); if (n>0) { buffer[n] = 0; std::cout<<"client# "<<buffer<<std::endl; } else if (n==0) { std::cout<<"client quit,me to"<<std::endl; break; } else { std::cout<<errno<<" : "<<strerror(errno)<<std::endl; break; } } //关闭不要的fd close(rfd); return 0; }
当我们正常通信的时候需要从缓冲区读数据所以先创建一个缓冲区,缓冲区大小设置为宏放在公共头文件中,因为我们把读到的数据当字符串看,所以在调用read函数的时候要让sizeof-1不要读\0,然后把缓冲区初始化一下对于C语言,直接在0位置放个\0就会认为是空字符串。然后我们判断函数返回值,如果已经读到数据结尾我们就在最后的位置放一个\0,因为我们打印字符串的时候是按照C语言的标准打印,而C语言字符串必须以\0结尾,因为服务端接收客户端发来的消息,所以在打印字符串前面加上客户端的名称。当返回值等于0说明客户端不在写东西了,客户端已经退出了,客户端都退出了就让服务端也退出,else就是读取失败,打印失败原因即可。通信结束后我们关闭管道文件即可。下面我们实现客户端:
#include <iostream> #include <cerrno> #include <cstring> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <fcntl.h> #include "comm.hpp" #include <assert.h> int main() { //1.不需要创建管道文件,只需要打开对应的文件即可 int wfd = open(fifoname.c_str(),O_WRONLY); if (wfd<0) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; return 1; } close(wfd); return 0; }
客户端不需要创建管道文件,因为服务端已经创建了所以我们和服务端一样打开即可,打开后因为我们的客户端要写入消息所以以只写方式打开,当打开函数的返回值小于0直接打印报错信息,接下来我们实现通信方式:
int main() { //1.不需要创建管道文件,只需要打开对应的文件即可 int wfd = open(fifoname.c_str(),O_WRONLY); if (wfd<0) { std::cerr<<errno<<":"<<strerror(errno)<<std::endl; return 1; } //可以进行常规通信了 char buffer[NUM]; while (true) { std::cout<<"请输入你的消息# "; char* msg = fgets(buffer,sizeof(buffer),stdin); assert(msg); (void)msg; buffer[strlen(buffer)-1] = 0; if (strcasecmp(buffer,"quit")==0) { break; } ssize_t n = write(wfd,buffer,strlen(buffer)); assert(n>=0); (void)n; } close(wfd); return 0; }