【Linux】 管道扩展 — 开始使用命名管道

时间:2024-05-31 22:09:22

在这里插入图片描述

送给大家一句话:
人生有六个字,前面三个是不害怕,后面三个是不后悔。 -- 董卿
????????????????????????????????

命名管道的功能实现

  • 1 命名管道的原理
  • 2 代码实现
    • 2.1 系统调用
    • 2.2 命名管道的封装
    • 2.3 开始使用
  • 3回归概念
  • Thanks♪(・ω・)ノ谢谢阅读!!!
  • 下一篇文章见!!!

1 命名管道的原理

命名管道时进程间通信的一种,那么原理也就是类似的:先让不同的进程看到同一份(操作系统)资源(“一段内存”)。

匿名管道是通过父子进程的继承关系来满足:父子进程可以看到同一段内存!这段内存会在子进程创建时的拷贝一份,所以并不需要名字,只需要通过pipefd[0] pipefd[1]来记录其读写端的文件描述符,然后在父子进程中关闭对应的文件描述符,达到单方向通信的需求!
在这里插入图片描述
根据匿名管道的底层,两个毫不相干的进程就无法通过匿名管道的方式来进行通信

那么两个毫不相干的进程如何才能看的同一片内存,才能共享一个文件缓冲区呢?当然就通过文件的路径(唯一性)来打开!

当两个进程打开同一个文件时,他们共享该文件的内核缓冲区。为了我们的通信效率,肯定不能把缓冲区的数据刷新到硬盘中。所以这个文件必须是一个特殊的文件,只用于通信的需求!!!

这个文件就是命名管道!!!

2 代码实现

2.1 系统调用

匿名管道的创建是通过系统调用:pipe(int pipefd[2]) 来建立,同样命名管道的创建也有对应的指令:mkfifo

MKFIFO(1)                                                                 User Commands                                                                MKFIFO(1)

NAME
       mkfifo - make FIFOs (named pipes)

SYNOPSIS
       mkfifo [OPTION]... NAME...

DESCRIPTION
       Create named pipes (FIFOs) with the given NAMEs.

       Mandatory arguments to long options are mandatory for short options too.

       -m, --mode=MODE
              set file permission bits to MODE, not a=rw - umask

       -Z     set the SELinux security context to default type

       --context[=CTX]
              like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX

       --help display this help and exit

       --version
              output version information and exit

我们使用一下来看看:
在这里插入图片描述
这个文件类型是p不同于-(普通文件)和d(目录),p表示管道文件,显然它是有名字的!
我们来尝试通信一下:
在这里插入图片描述
此时两个不同的进程就可以进行通信!!!
我们在让两个进程保持一直通信的状态,这样读端可以一直获取数据!
在这里插入图片描述

当我们突然关闭右侧读端时,左边的写端就直接退出来了!这是因为当读端退出了,操作系统会自动释放写端进程,操作系统不会做无用功(不会在一个没有读取的管道文件了一直写入)

当然这样的通信也就只能用来演示,我们先要通过命名管道来使我们创建的两个毫不相干的进程完成通信工作,接下来我们就来实现!

2.2 命名管道的封装

首先我们来认识一下创建管道的系统调用:

MKFIFO(3)                                                           Linux Programmer's Manual                                                          MKFIFO(3)

NAME
       mkfifo, mkfifoat - make a FIFO special file (a named pipe)

SYNOPSIS
       #include <sys/types.h>
       #include <sys/stat.h>

       int mkfifo(const char *pathname, mode_t mode);

       #include <fcntl.h>           /* Definition of AT_* constants */
       #include <sys/stat.h>

       int mkfifoat(int dirfd, const char *pathname, mode_t mode);

   Feature Test Macro Requirements for glibc (see feature_test_macros(7)):

       mkfifoat():
           Since glibc 2.10:
               _POSIX_C_SOURCE >= 200809L
           Before glibc 2.10:
               _ATFILE_SOURCE

int mkfifo(const char *pathname, mode_t mode);
第一个参数const char *pathname 是要建立的管道文件的路径与文件名,第二个参数mode_t mode 表示文件权限

返回值的意义:

ETURN VALUE
On success mkfifo() and mkfifoat() return 0. In the case of an error, -1 is returned (in which case, errno is set appropriately).
创建成功返回 0 失败返回 -1!

通过这些我可以先搭建一个基础类,可以创建管道文件!

  • 成员

    1. 管道文件名 const std::string _fifo_path
    2. 文件描述符 _fd 默认为-1
    3. 操作者类型 _id 1 /2
  • 构造函数 --> 创建管道 CreateNamePipe(const std::string &path )
    使用函数 int mkfifo(const char *pathname, mode_t mode); 文件名为 path(需要确定下来 保证引用该头文件的都可以获取) 权限为0666
    为 0 正常建立 , 为 -1 建立失败 测试创建是否成功

  • 析构函数 --> 删除管道 RemoveNamedPipe(const std::string &path )
    使用unlink(path.c_str()) 删除管道

#pragma once

#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <cstdio>
#include <unistd.h>
//文件名
const std::string path = "./myfifo";
//管道类
class NamedPipe
{
public:
    NamedPipe(const std::string fifo_path)
        : _fifo_path(fifo_path)
    {
        int n = mkfifo(_fifo_path.c_str(), 0666);
        if(n != 0)
        {
            perror("mkfifo");
        }

    }
    ~NamedPipe()
    {
        sleep(10);
        unlink(_fifo_path.c_str());
    }

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

这样就可以通过类的实例化对象来建立管道!!!

接下来我们进行打开文件函数的书写:

  1. 首先,命名管道是文件,打开文件需要open接口,管理管道由操作者来控制。使用者只能使用不能管理管道的创建与关闭

  2. 表明身份的宏定义:----- 权限不同

    • greater 1 创建者 :只有创建者才可以建立删除管道
    • user 2 使用者 :只需要初始化其管道,不需要再建立
  3. 打开管道 OpenForRead / OpenForWrite —> OpenNamedPipe()
    打开管道的本质就是打开文件 _fd = open()
    注意打开的方式与操作者的类型紧密相关 所以进行飞封装!通过内部传参来保证权限的正确

private:
    bool OpenNamedPipe(int mode)
    {
        //sleep(2);
        std::cout << "OpenNamedPipe : " ;
        _fd = open(_fifo_path.c_str(), mode);
        if (_fd < 0)
        {
            std::cout << "false" << std::endl;
            return false;
        }
        std::cout << "true" << std::endl;
        return true;
    }
public:
// 打开文件
    int OpenForRead()
    {
        return OpenNamedPipe(Read);
    }
    int OpenForWrite()
    {
        return OpenNamedPipe(Write);
    }

打开文件之后就是进行读取或者写入,我们在写一下相应的函数:

  • 读取 ReadNamedPipe(std::string *out)
    设置缓冲区
    从管道里读取 向缓冲区写入数据
    命名管道对于读端而言 , 如果我们打开文件,但是写端还没有,就会阻塞在open调用中,等待写端进入

  • 写入 WriteNamedPipe(const std::string& in)
    向文件描述符里面进行写入

 	// 读取文件
    int ReadNamedPipe(std::string *out)
    {
        char buffer[128];
        int n = read(_fd, buffer, sizeof(buffer));
        buffer[n] = 0;
        *out = buffer;
        return n;
    }
    // 写入文件
    int WriteNamedPipe(const std::string &in)
    {
        int n = write(_fd, in.c_str(), in.size());

        return n;
    }

这样我们的封装就完成了,NamedPipe具有以下功能:

  1. 通过文件路径和操作者权限建立实例化对象
  2. 按照需求调用:OpenForRead() / OpenForWrite()打开文件
  3. 进行写入和读取WriteNamedPipe / ReadNamedPipe

2.3 开始使用

模拟客户端和服务器的通信过程:客户端写入数据,服务器读取数据

client.cc
#include"namedPipe.hpp"

int main()
{
    NamedPipe fifo(path , user);
    if(fifo.OpenForWrite())
    {
        std::cout << "client open named pipe done" << std::endl;
        while(true)
        {
            std::cout << "Please Enter>" ;
            std::string in ;
            getline(std::cin , in);
            fifo.WriteNamedPipe(in);
        }
    }
    return 0;
}
server.cc
#include"namedPipe.hpp"

int main()
{
    NamedPipe fifo(path , greater);
    //服务端进行读取
    if(fifo.OpenForRead())
    {
        std::cout << "server open named pipe done" << std::endl;
        sleep(3);
        while(true)
        {
            std::string out ;
            int n = fifo.ReadNamedPipe(&out);
            if(n > 0)
            {
                std::cout << "client say >" << out << std::endl;
            }
            else if (n == 0)
            {
                std::cout << "client quit , server too!" << out << std::endl;
                break;
            }
            else
            {
                perror("Write");
                break;
            }
        }
    }

    return 0;
}
来看效果:

在这里插入图片描述
这个效果很好玩呢!!!

注意:

  1. 对于读端来说,如果我们打开文件,但是写端还没有进入,那么就会阻塞在open()调用中!直到写端打开—进程同步!!!
  2. 当读端退出时,写端再次写入数据时会直接退出!操作系统不会做无用功!!!(直接把broken pipe坏的管道 进行杀掉!会发送对应的13号信号SIGPIPE
  3. 管道别写满 && read fd 不读且没有关闭 :
    管道被写满,写进程会被阻塞,写条件不具备-- wait 等待条件具备(读取走一部分数据才能继续写)
  4. 如果管道内部是空的 && write fd没有关闭:
    读取条件不具备,读取进程会被阻塞 – wait 等待条件具备(写入了数据)

3回归概念

总结一下,命名管道的通信原理依然是:让两个不同的进程看到同一份资源(通过文件路径)

匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用命名管道(FIFO文件)来做这项工作.

Thanks♪(・ω・)ノ谢谢阅读!!!

下一篇文章见!!!