0


初识Linux · 命名管道


前言:

有了前文匿名管道的基础,我们介绍匿名管道的时候就轻松许多了,匿名管道和命名管道的区别主要是在于,匿名管道不需要文件路径,并且匿名管道常用于父子进程这种具有血缘关系的场景,使用命名管道的时候,我们常常用于的情况是两个进程毫无联系,使这两个毫无关系的进程可以进行通信。

对于匿名管道来说,我们知道文件对象以及文件对象里面的文件对象里面属性集合,操作集合都不会重新创建,对于命名管道来说也是一样的,所以对于内核级别的文件缓冲区也是这个样子的,OS就没有必要创建两个了,毕竟浪费空间时间的事OS可不想做。

以上其实算是对于命名管道的原理的部分的简单介绍,其实和匿名管道差不多,本文的主要内容其实还是命名管道的代码编写。


代码编写

那么准备工作是先创建三个文件,分别表示客服端,服务端,以及创建管道的文件,创建命名管道之后,让另外两个进程分别打开管道。

那么我们的第一个任务是了解创建命名管道的函数->mkfifo:

直接man mkfifo查询到的是1号手册的mkfifo,那么我们可以使用试试:

创建了对应管道文件之后,我们可以发现几个特征点,它的名字后面带有| 代表管道的意思,并且,它的文件类型部分的字母对应的是p,也就是Pipe部分了,这个其实在文件权限部分介绍过。

还有一个有意思的点是在于……留个关子。

那么这是命令行部分创建命名管道,我们是要直接应用于代码层面,所以介绍3号手册的函数mkpipe:

对应n个头文件,对于返回值来说的话,如果创建管道成功的话,返回的值是0,出错了,返回的值就是-1,并且错误码被设置,所以如果返回出错我们可以打印对应的错误码看看,我们现在不妨试试:

#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <cstdio>
#include <cerrno>

const std::string path = "./mypipe";

int CreateFifo()
{
    int n = mkfifo(path.c_str(),0666);
    if(n < 0)
    {
        perror("mkfifo");
    }

    return n;
}

这里使用的头文件相对来说也是比较多的,毕竟涉及到了string mkfifo perror,所以C++的头文件有,C++版的C语言头文件也是有的,在namedpipe文件里面实现好了该函数之后,我们转到server.cc文件里面进行调用,其实在client.cc里面调用都可以,毕竟之后不过就是一个进程作为读端,一个进程作为写端,所以任意调用,这里使用server.cc:

#include "namedPipe.hpp"
int main()
{
    CreateFifo();

    return 0;
}

这里需要插一个题外话了,我们经常是需要Make文件的,但是makefile好像只能编译一个文件?所以我们需要对原来的makefile进行简单的修改:

.PHONY:all
all:client server

client:client.cc
    g++ -o $@ $^ -std=c++11
server:server.cc
    g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
    rm -rf server client

因为makefile是从上到下查找的,所以我们形成一种依赖关系,从而实现两种文件的编译即可。

那么现在我们尝试编译一下server.cc文件:

也是成功创建了,那么我们再运行一下试试?

也就成功的报错了,提示说文件已经存在了。

那么创建了管道文件我们总得删除管道吧?

可以使用函数unlink:

直接给对应的文件路径就可以了。

可是问题来了,我们现在能保证创建多个管道,但是每次创建管道都要使用函数,每次还要手动的调用,难道这不是很麻烦吗?我们使用的语言难道不是面向对象的C++语言吗?所以我们不妨封装一个对象:

class namepipe
{
public:
    namepipe(const std::string fifo_path)
        :_fifo_path(_fifo_path)
    {}

    ~namepipe()
    {
        int res = unlink(_fifo_path.c_str());
        if(res != 0)
        {
            perror("unlink");
        }
    }

private:
    const std::string _fifo_path;
    int _fd;
    int _id;
};

像这样,封装一个对象的好处是,我们不用自己手动的去销毁管道,因为实例化的对象出了main函数的栈帧自己就调用对应的析构函数了。

那么我们可以这个基础之上,进行一些细节的补充,比如是谁调用的?所以我们可以定义一个宏,表示是谁定义的,然后用宏来初识_id:

#define Creater 1
#define User 2
#define Read O_RDONLY
#define Write O_WRONLY
class namepipe
{
public:
    namepipe(const std::string fifo_path, int who)
        : _fifo_path(_fifo_path), _id(who)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);
            if (res != 0)
            {
                perror("mkfifo");
            }
            std::cout << "Creater create named pipe" << std::endl;
        }
    }

    ~namepipe()
    {
        if (_id == Creater)
        {

            int res = unlink(_fifo_path.c_str());
            if (res != 0)
            {
                perror("unlink");
            }
            std::cout << "Creater free named pipe" << std::endl;
        }
    }

private:
    const std::string _fifo_path;
    int _fd;
    int _id;
};

我们使用宏分别创建者的好处还有就是,client调用的时候我们就可以直接通过宏判断是谁创建的,这样就可以省略不必要的构造。

可是现在有一个问题就是,我们已经知道谁创建的,知道了对应的路径,可是打开的文件呢?这是非常重要的,所以我们引入一个变量,_fd,那么可以给一个默认的文件描述符,比如-1,初始化的时候总得初始化上去吧?

    namepipe(const std::string fifo_path, int who,int fd = DefaultFd)
        : _fifo_path(_fifo_path), _id(who),_fd(DefaultFd)
    {
        if (_id == Creater)
        {
            int res = mkfifo(_fifo_path.c_str(), 0666);
            if (res < 0)
            {
                perror("mkfifo");
            }
            std::cout << "Creater create named pipe" << std::endl;
        }
    }

对于构造函数和析构函数这里已经差不多了,那么剩下的就是文件打开,怎么操作管道的事儿了。

我们先来操作打开管道:

    bool OpenforRead()
    {
        return OpenNamePipe(Read);
    }
    bool OpenforWrite()
    {
        return OpenNamePipe(Write);
    }

打开管道我们通过宏的不同传参,就可以保证管道的打开是通过读的方式打开的还是写的方式打开的:


private:
    bool OpenNamePipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

但是毕竟涉及到了_fd的修改,所以我们不希望直接可以调用,那么将它私有是最好的选择,这是打开方式的方法,最后的就是写入读取的方法了:

    int ReadNamePipe(std::string *out)
    {
        char buffer[BaseSize];
        int n = read(_fd, buffer, sizeof(buffer));
        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    int WriteNamePipe(std::string &in)
    {
        return write(_fd,in.c_str(),in.size());
    }

此时两个一整个管道相关的操作就完成了。

对于server:

#include "namedPipe.hpp"

int main()
{
    // CreateFifo();

    namepipe fifo(path, Creater);

    if (fifo.OpenforRead())
    {
        std::cout << "server open name pipe done" << std::endl;
        sleep(3);
        while (true)
        {
            std::string message;
            int n = fifo.ReadNamePipe(&message);
            if (n > 0)
            {
                std::cout << "Client Say> " << message << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "Client quit, Server Too!" << std::endl;
                break;
            }
            else
            {
                std::cout << "fifo.ReadNamedPipe Error" << std::endl;
                break;
            }
        }
    }
    return 0;
}

对于client:

#include "namedPipe.hpp"

int main()
{
    namepipe fifo(path, User);
    if (fifo.OpenforWrite())
    {
        std::cout << "client open named pipe done" << std::endl;
        while (true)
        {
            std::cout << "Please enter> ";
            std::string message;
            std::getline(std::cin, message);
            fifo.WriteNamePipe(message);
        }
    }
    return 0;
}

试试结果?

符合预期。

如果我们运行./server的时候,不打开client,会发现./server也不会有后续动作,并且如果我们直接关掉写端,./server端是直接关闭的,这是上文匿名管道的知识点,实际上也是一种进程间同步!!


感谢阅读!

标签: linux 运维 服务器

本文转载自: https://blog.csdn.net/2301_79697943/article/details/143576909
版权归原作者 _lazy. 所有, 如有侵权,请联系我们删除。

“初识Linux · 命名管道”的评论:

还没有评论