命名管道是一种在操作系统中用于进程间通信的机制,它允许不同的进程之间通过管道进行数据交换。与匿名管道相比,命名管道具有更多的灵活性和功能。在本博客中,我们将深入探讨命名管道的概念、用途以及如何在编程中使用它们。
(一)什么是命名管道
命名管道是一种具有独特标识符(通常是文件路径)的管道。它允许不同进程通过该标识符进行通信。与无名管道不同,命名管道存在于文件系统中,使得它们能够跨越进程和甚至计算机边界进行通信。
- 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
- 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
- 命名管道是一种特殊类型的文件
(二)命名管道原理理解
2.1 创建一个命名管道
1️⃣命名管道可以从命令行上创建,命令行方法是使用下面这个命令
mkfifo filename
- 我们不了解的话可以通过man手册去对这个函数进行学习:
示例演示:
【解释说明】
- 我们可以先手动的创建一个管道文件,给它起一个 fifo 的文件名,创建好之后我们就看到了当前目录下存在了一个文件叫做 fifo ;
- 其次我们可以发现它的文件类型前面以P开头,当大家看到P开头的,会能想到什么?在之前我给大家在讲我们Linux基础命令的时候说过一个话题叫做文件类型:以 - 开头普通文件、以D开头为目录文件、以L开头为链接文件L开头的叫做软链接、这里以P开头叫做管道文件,这时候在磁盘上存在了一个管道文件
接下来,我准备向文件中写入数据:
【解释说明】
- 在我们的理解中把它写到文件当中,此时就相当于当我一敲回车,echo对应的这个东西就会变成进程;
- 然后,执行我们向显示器当中打印,经过重定向,它最终不向显示器文件打印,而向管道文件中打印,所以底层作为重定向是没问题的;
- 紧接着我们就尝试去写了,但当前呢它卡在这里的,什么都没做,我们再看一下当前这个管道文件里,当前显示的是零,好像没有写入啊;
- 这是因为管道文件有种特殊特性,虽然在磁盘当中创建了这个 fifo,但它仅仅是一种符号,那么对于这种符号呢,将来你向这个文件里写入的消息,并没有或者并不会刷新落实到磁盘上,而是只帮我们在这里直接 echo,然后写入管道文件当中,但是管道文件当前是内存级的,所以你的大小没有变。
接下来,我们在尝试一下输出重定向操作:
【解释说明】
- 对于 cat 指令默认就是从显示器当中进行读取,你输什么它给你打印什么;
- 而 cat fifo 可以直接从管道文件当中输出重对象,将曾经他向管道里写的东西写到它里面;
- 另外cat也是一条命令,它一旦启动就会变成进程,所以这是一个进程,这俩进程没有任何关系,然后我们这个hello world 的消息最终就打印了出来,这种通讯方式我们就称之为叫做命名管道。
接下来,我再改一下代码逻辑,再给大家分析一波:
【解释说明】
- 在进行显示的时候,我让它每隔一秒进行向我们显示器当中打印一条hello world;
- 但我不想往显示器打印,我想让往管道打印,此时上述那一堆就对应的一个进程,向我们对应的管道文件当中写入;
- 那么对于 fifo 此时呢,我们就看到本来应该显示到这个显示器文件这个终端的字符串,最终经过管道被重定向到了右端。
2️⃣ 命名管道也可以从程序里创建,相关函数有:
int mkfifo(const char *filename,mode_t mode);
2.2 命名管道原理
【解释说明】
- 现在假设左边框框里,它是我们对应的操作系统内部的一大堆的数据结构,和它对应的磁盘组成;
- 如果磁盘里有一个文件,现在有一个问题,如果我有一个进程要打开它,这是个进程,打开一个文件,有自己的文件描述符表,然后012定义的是标准输入标准输出标准错误这;
- 此时,对应的要打开我们对应的这个文件,怎么打开呢?我曾经讲过一个文件如果不打开,它就在磁盘上静静的躺着,就有了我们之前讲的分区,然后格式化,然后有文件系统等;
- 紧接着,对应的这个磁盘文件,它里面还可以去包含文件相关的属性inode,还有它对应的缓冲区等等,一旦它被打开了,首先要在操作系统内为该文件创建对应的struct file对象,然后这个struct file对象它有对应的自己的缓冲区;
- 在结构体当中,有指针指向它的缓冲区,实际上它里面严格上讲应该是指向它inode,然后进一步指向缓冲区
【解释说明】
- 我们让父进程创建子进程的时候,子进程会继承父进程的文件描述符表;
- 所以对于子进程,它直接继承父进程的文件描表之后,它会把这个值拷下来,拷下来之后,他俩就指向同一个文件了;
上诉是第一种情况,假设如果又来了一个进程,他也要有自己的文件描述符表,紧接着有一个小问题?
- 如果这个进程此时也打开磁盘中的这个文件了,还用不用再在操作系统内部给他创建一个对应的struct file 结构体,然后给他创建一个缓冲区,然后这样去搞呢?
【解释说明】
- 其实大可不必;
- 因为在正常的情况下,对应的文件的属性的值呢数字是一样的,所以操作系统内根本就不需要维护对应的两个一样的结构体;
【解释说明】
- 所以对于我们来讲,实际上当你想打开这个文件时,当新进程想打开其他文件时,新进程先做的第一件事情是在所有已经打开的文件列表里去找这个文件是否已经被打开了,没有被打开则创建,如果已经打开了;
- 直接把对应的我们struct file对象的地址填入到对应的下标里,而对应的这个struct file对象里面包含一个叫做ref,我们称之为引用计数;
- 这个引用计数指向的时候,默认是1,再有进程打开它就变成2了,当你关闭这个文件时,它就把这个ref进行减减操作,由2再变成对应的1,当你再关闭它才由1变成0,然后操作系统才释放它。
【注意事项】
不过如果这个文件是一个普通文件,将来你写的时候数据要定期刷新到磁盘里面,可是今天我们的目的是想让这两个进程,通过对应的缓冲区让他们俩之间通信,好让他们俩直接通信,那么我们的文件呢就不应该把这个数据刷到磁盘上,所以未来呢,我们如果创建了一个文件,这种文件必须有一个特性,就叫做是内存级
【解释说明】
- 也就是说对于这种文件只需要把数据有一个进程写进来,另一个进程再从缓冲区中读,此时不需要做这个刷盘动作;
- 如果做了刷盘操作:一导致速度慢了,二也没必要,所以我们就由此诞生了一种文件,这种文件就叫做管道文件,或者叫做命名管道文件,它就是一个文件,只不过这个文件呢,没有所谓的data block,它就是在磁盘当中的一种
(三)代码示例
3.1命名管道的打开规则
如果当前打开操作是为读而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
- O_NONBLOCK enable:立刻返回成功
如果当前打开操作是为写而打开FIFO时
- O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
- O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
3.2 实现server&client通信
【client.cc】
#include <iostream> #include <cstdio> #include <cerrno> #include <cstring> #include <cassert> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include "common.hpp" 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){ system("stty raw"); int c = getchar(); system("stty -raw"); ssize_t n = write(wfd, (char*)&c, sizeof(char)); assert(n >= 0); (void)n; } //关闭不要的fd close(wfd); return 0; }
【server.cc】
#include <iostream> #include <cerrno> #include <cstring> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include "common.hpp" 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 << "create fifo file success" << 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" << std::endl; // 3.正常通信 char buffer[NUM]; while (true) { buffer[0] = 0; ssize_t n = read(rfd,buffer,sizeof(buffer)); if(n > 0){ buffer[n] = 0; printf("%c", buffer[0]); fflush(stdout); } else if(n == 0){ std::cout << "client quit, me too" << std::endl; break; } else { std::cout << errno << " : " << strerror(errno) << std::endl; break; } } // 4.关闭不要的fd close(rfd); unlink(fifoname.c_str()); return 0; }
【common.hpp】
#pragma once #include <iostream> #include <string> #define NUM 1024 const std::string fifoname = "./fifo"; uint32_t mode = 0666;
(四)小结
以上便是关于命名管道的全部知识内容了,接下来简单小结命名管道以及匿名管道和命名管道之间的区别!!
命名管道:
- 基本概念: 命名管道是一种通过文件系统路径标识的通信管道,允许不同进程之间进行通信。
- 创建方式: 在Unix/Linux系统中,可以使用
mkfifo
函数创建命名管道;在Windows系统中,可以使用CreateNamedPipe
函数。 - 通信方向: 命名管道同样是单向的,但可以被用于任意两个进程之间的通信,甚至是不同计算机之间。
- 生命周期: 命名管道的生命周期不受限于创建它的进程,可以长时间存在于文件系统中。
- 灵活性: 命名管道相对于匿名管道更灵活,适用于更复杂的进程通信场景,包括跨越进程和计算机的通信。
匿名管道与命名管道的区别:
- 匿名管道由pipe函数创建并打开。
- 命名管道由mkfifo函数创建,打开用open
- FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。
本文内容便讲解结束,感谢大家的观看和支持!!