首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >初识Linux · 命名管道

初识Linux · 命名管道

作者头像
_lazy
发布2024-11-19 10:29:00
发布2024-11-19 10:29:00
9670
举报
文章被收录于专栏:Initial programmingInitial programming

前言:

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

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

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

代码编写

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

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

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

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

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

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

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

代码语言:javascript
复制
#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:

代码语言:javascript
复制
#include "namedPipe.hpp"
int main()
{
    CreateFifo();

    return 0;
}

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

代码语言:javascript
复制
.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++语言吗?所以我们不妨封装一个对象:

代码语言:javascript
复制
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:

代码语言:javascript
复制
#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,初始化的时候总得初始化上去吧?

代码语言:javascript
复制
    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;
        }
    }

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

我们先来操作打开管道:

代码语言:javascript
复制
    bool OpenforRead()
    {
        return OpenNamePipe(Read);
    }
    bool OpenforWrite()
    {
        return OpenNamePipe(Write);
    }

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

代码语言:javascript
复制
private:
    bool OpenNamePipe(int mode)
    {
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
            return false;
        return true;
    }

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

代码语言:javascript
复制
    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:

代码语言:javascript
复制
#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:

代码语言:javascript
复制
#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端是直接关闭的,这是上文匿名管道的知识点,实际上也是一种进程间同步!!

感谢阅读!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 前言:
  • 代码编写
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档