Re:Linux系统篇(三十八)通信篇·三:深入浅出 Linux 命名管道:如何用 FIFO 实现跨进程通话与文件传输


◆ 博主名称: 小此方-CSDN博客 大家好,欢迎来到小此方的博客。
⭐️Linux系列个人专栏: 【主题曲】Linux
⭐️此方的GitHub: github_此方
⭐️ Re系列专栏:我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)


文章目录


概要&序論

  Hello,大家好,我是此方。上一篇我们讲了匿名管道,但是事实上匿名管道的局限性实在是太强了------两个通信的进程必须要有血缘关系 (最常见的就是父子关系)。很显然不能够满足我们的实际开发需求对不对。

  于是命名管道出现了。------他支持两个毫不相干的进程进行通信。 我们本文想讲的就是它。好,我们废话少说,开始吧。

一、引入命名管道

  你是怎么想到命名管道的 ?首先,还是一句最本质的------"进程间通信必须要让两个进程看到同一块资源。 ",这块资源在我们的命名管道任然指的是文件。但是不再是那个内存级管道文件。

  通过打开同一个路径下的同一个文件,就像两个进程在同一个显示器上打印/读取信息那样。两个进程就可以看到同一块资源。

  而文件有唯一的路径,有名字,于是我们把这个文件一个统一的名字------命名管道。

  解答疑问:两个进程打开同一个路径下的同一个文件,这个文件不会被加载两次吗?不会。内核不会做浪费时间和空间并且没有意义的事情。

  解答疑问:一个文件在内核中的映射实际上并不包括struct file ,只是inode ops(读写接口) 和缓冲区 。

二、创建命名管道

  命名管道文件和普通文件不同,它只进不出------只会被打开,不需要被刷新回到磁盘中。同时它要保证打印不乱序,而普通文件做不到。

2.1命令行创建命名管道并实现最简单的通信

  指令mkfifo + 文件名 可以创建一个命名管道。

  指令unlink + 文件名 可以删除一个命名管道。

2.2创建命名管道的系统调用

  系统调用:mkfifo() 第一个参数是带有路径的管道名称,第二个参数是权限掩码。 返回值依旧是成功返回0,失败返回 -1 并设置错误码。

  用两个通信例子讲一讲接口的使用,以及命名管道通信在实际运用中是怎样的。

三、实际案例

3.1实际案例一:打电话

3.1.1代码

Makefile:

bash 复制代码
ALL : Mr_List Mr_Zhang
Mr_List : Mr_List.cpp
	g++ -o $@ $^
Mr_Zhang : Mr_Zhang.cpp
	g++ -o $@ $^
.PHONY : clean 
clean : 
	rm -f Mr_List Mr_Zhang fifo

  共享文件:必要头文件+两者约定好的信道名称+用来打印错误的代码块。

cpp 复制代码
#ifndef __COMMON_NAMEDPIPE_
#define __COMMON_NAMEDPIPE_

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <cstdlib>
#include <string>
#include <fcntl.h>
#include <cstring>

#define PIPE_FILE "./fifo"

#define ERR_FIFO(m) do{ \   // 将多条语句打包,确保if-else安全展开
    perror(m) ;         \
    exit(EXIT_FAILURE) ;\    // 标准库宏,向系统报告程序异常退出
} while(false)          \
//有些初学者会把行延续符号打错,"/",
//只需要想想它和注释用的斜杠相反就行
#endif 

  我们约定张三负责说话,李四负责接收。这是张三是代码:

cpp 复制代码
#include "Common.hpp"
int main()
{
    umask(0);
    int n = mkfifo(PIPE_FILE, 0666);
    if (n == -1)
        ERR_FIFO("mkfifo");
    int wfd = open(PIPE_FILE, O_WRONLY);
    if (wfd == -1)
        ERR_FIFO("mkfifo");
    else
    {
        while (true)
        {
            std::cout<<"#PleaseEnter:";
            std::cout.flush();
            char buffer [1024] ;
            memset(buffer , 0 , sizeof(buffer));
            int m = read( 0 ,buffer ,sizeof(buffer));
            if(m < 0 )
                ERR_FIFO("read");
            else
            {
                int i = write(wfd , buffer ,m);
                if(i < 0 ) 
                    ERR_FIFO("write");
            }
        }
    }
    return 0;
}

  这是李四的代码:

cpp 复制代码
#include "Common.hpp"
int main()
{
    int rfd = open(PIPE_FILE ,O_RDONLY );
    if(rfd == -1) 
        ERR_FIFO("open");
    while(true){
        char buffer[1024] ;
        memset(buffer , 0  , sizeof(buffer)) ;
        std::cout<< "#PleaseWait ..."<<std::endl;
        int n = read(rfd , buffer ,sizeof(buffer)-1) ; 
        if(n < 0 )
            ERR_FIFO("read") ;
        else if(n == 0 ){
            std:: cout<<"Mr_Zhang Hang Up The Telephone!"<<std::endl;
            break;
        }
        else{
            buffer[n] = '\0' ;
            std :: cout << "Mr_Zhang say :"<<buffer ;
        }    
    }
    return 0 ;
}

3.1.2测试

3.2实际案例二:传输文件内容

  这个代码更加简单,上面的代码我们采用了面向过程,接下来这个是面向对象,逻辑类似。

  Makefile文件:

bash 复制代码
ALL : Client Server 
Client : Client.cpp
	g++ -o  $@ $^
Server : Server.cpp
	g++ -o $@  $^
.PHONY: clean
clean :
	rm -f Client Server 

  程序的主体部分,包含这个命名管道被创建、销毁、打开(读/写)、读和写的各种必要的方法。

  当然还有一种设计方案是去掉Create和Destory方法,把他们放在构造和析构函数中,但是这样就必须把其他的方法额外封装在一个opt类中了(因为创建管道的任务只需要通信双方的其中一人完成。

cpp 复制代码
#ifndef __COMMON_NAMEDPIPE_
#define __COMMON_NAMEDPIPE_

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <cstdlib>
#include <string>
#include <fcntl.h>
#include <cstring>
#define PIPE_FILE "./fifo"

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
                            \
    } while (false)

class NamedPipe
{
public:
    void Create(std ::string file = PIPE_FILE)
    {
        umask(0);
        int n = mkfifo(file.c_str(), 0666);
        if (n == -1)
            ERR_EXIT("mkfifo");
    }
    void Destory(std ::string file = PIPE_FILE)
    {
        int n = unlink(file.c_str());
        if (n == -1)
            ERR_EXIT("unlink");
    }
    void OpenForRead(std ::string file = PIPE_FILE)
    {
        _pipefd = open(file.c_str(), O_RDONLY);
        if (_pipefd == -1)
            ERR_EXIT("open_read");
    }
    void OpenForWrite(std ::string file = PIPE_FILE)
    {
        _pipefd = open(file.c_str(), O_WRONLY);
        if (_pipefd == -1)
            ERR_EXIT("open_write");
    }
    void Read(int outfd)
    {
        //读取管道内容到外部文件中
        char _buffer[1024];
        while(read(_pipefd, _buffer, sizeof(_buffer)) != 0)
        {
            int n = write(outfd , _buffer , sizeof(_buffer));
            if(n < 0 )
                ERR_EXIT("write") ;
            memset(_buffer , 0 , sizeof(_buffer));
        }
    }
    void Write(int infd)
    {
        char _buffer[1024];
        //将文件中的内容读取到缓冲区buffer
        while (read(infd, _buffer, sizeof(_buffer)) != 0)
        {
            int n = write(_pipefd, _buffer, sizeof(_buffer));
            if (n < 0)
                ERR_EXIT("write");
            memset(_buffer , 0 , sizeof(_buffer));
        }
    }

private:
    int _pipefd;
};

#endif

  服务端读取Source文件中的内容,然后用命名管道发送给客户端。

cpp 复制代码
#include "Common.hpp"
int main()
{
    //读取管道内容到缓冲区
    NamedPipe pipe ;
    pipe.OpenForRead(PIPE_FILE);
    int outfd = open("destnation.txt",O_WRONLY);
    if(outfd == -1) 
        ERR_EXIT("open");
    pipe.Read(outfd) ;
    //缓冲区内容写入一个Object.txt文件。
    return 0 ;
}
cpp 复制代码
#include "Common.hpp"
int main()
{
    //从文件Source.txt中读取内容到缓冲区
    NamedPipe pipe ;
    pipe.Create(PIPE_FILE) ;
    pipe.OpenForWrite(PIPE_FILE);
    int infd = open("Source.txt" , O_RDONLY);
    if( infd == -1) 
        ERR_EXIT("open");
    pipe.Write(infd);
    pipe.Destory() ;
    return 0 ;
}

好的本期内容就到这里,如果对你有帮助,还不要忘记点赞三联支持。我是此方,我们下期再见。bye!