命名管道的创建和通信实现

发布于:2025-03-11 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

命名管道的创建

使用函数创建命名管道的通信

预备创建

makefile设计

server.hpp设计

clent.hpp设计

comm.hpp设计

server.cc设计

clent.cc设计

测试运行


今天我们来学习命名管道

由于匿名管道pipe()无法在两个毫不相干的进程之间进行通信,只能用于 具有亲缘关系的进程(如父子进程、兄弟进程)。如果要让完全独立的进程通信,需要使用 命名管道(FIFO)其他 IPC 机制(如共享内存、消息队列、套接字等)。因为他们彼此不知道管道的名字,不知道名字就无法只读文件名字背后的地址和inode,就进而不知道管道内部存储的信息。关掉有名字不就好了。没错命名管道可以完成不同进程(两个毫不相干进程)间的通信。

命名管道的创建

通信原理和匿名的完全一致,我们可以使用指令或者C语言函数进行创建。

mkfifo 是用于创建 命名管道(FIFO, First In First Out) 的命令或系统调用。这个不是系统函数,是C语言的内置函数,mkfifo xxx(xxx为命名管道的名字),就是直接创建了一个以xxx为名字的命名管道。

使用mkfifo创建了一个命名管道test,然后管道文件的标准就是后面有一个|,我们往里写入是不需要加|的,为了进行进程间的通信,我们需要再打开一个shell,然后有个shell进行写入,另一个看一下这个进程看能不能看到。

可以看到两个毫不相干的进程完成了进程间的通信,不知道你们有没有注意到当一方进行写入时,如果另一方不立刻进行读取,写的一方会卡住,因为命名管道(FIFO)同步的写端(echo)必须等待读端(cat 或其他进程)来读取数据,否则写操作会阻塞。就是不允许一直写而不读,终归来说就是写端这个进行一直保持挂起状态而不退出,这个是放在前提进行的特点,我们出现这种情况,要么就是另一端进行读取,要么就是将写入放到后端进行。

如上图可知,当程序处于后端后:

使用函数创建命名管道的通信

预备创建

使用命名管道进行通信,需要使用C语言函数mkfifo

mkfifo的第一个参数是创建管道的路径(包括文件名),第二个参数是管道的使用权限,返回值为0表示创建成功,-1表示失败,然后需要形成两个可执行程序,代表两个毫不相干的进程进行通信,然后编写makefile,同时编译两个进程。

所以很简单,我们定义clent,server两个进程,然后分别创建各自的.cc和.hpp,然后comm.hpp是两个进程的共有部分,都能相互看到的内容。让两个可执行程序的.hpp都引用头文件comm.hpp。

最后销毁管道我们使用函数unlink销毁管道,这个函数就只需要传递管道的路径,然后返回值如果是0就是销毁成功,-1就是销毁失败。但是这个函数不是真的去把管道删除了,只是删除了管道的名字和inode的映射关系,只是删除了管道的路径,一个文件没有了路径之前说了就没有存在的必要了,这样就会被操作系统给释放了。

接下来的设计我就不会明说了,因为都很简单!!!

makefile设计

为了同时可以编译两个可执行文件,需要重新定义一个伪目标all,然后让其依赖两个可执行,没有依赖方法,这样就可以了在预编译all的时候编译了server和clent。

server.hpp设计

#pragma once
#include"comm.hpp"
class Init
{
public:
    Init()
    {
        umask(0);
        int n = ::mkfifo(pipefile.c_str(), gmode);
        if (n < 0)
        {
            cerr << "mkfifo error!" << endl;
            return;
        }
        cout << "mkfifo success" << endl;
        sleep(10);

    }
    ~Init()
    {
        int n = unlink(pipefile.c_str());
        if (n < 0)
        {
            cerr << "unlink error!" << endl;
            return;
        }
        cout << "unlink success" << endl;
    }
};

Init init;

class server
{
public:
    server()
    :_fd(gdefultfd)
    {}
    bool openpipe()
    {
        _fd = ::open(pipefile.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            cerr << "open fail!" << endl;
            return false;
        }
        return true;
    }
    //string* :输出型参数
    //const string& :输入型参数
    //string & : 输入输出型参数
    int recpipe(string* out) //server负责读取
    {
        char buffer[gsize];
        ssize_t n = ::read(_fd, buffer, sizeof(buffer)-1);
        if (n > 0)
        {
            buffer[n] = 0;
            *out = buffer;
        }
        return n;
    }
    void closepipe()
    {
        if (_fd >= 0)
        {
            ::close(_fd);
        }
    }
    ~server()
    {
        //closepipe();
    }
private:
    int _fd;
};

由于需要通信的所以需要先有一个进程先来创建这个管道,我们先让进程server来创建这个命名管道,如上封装在Init类里,然后像gmode(管道权限),pipefile(路径),gdefultfd(文件描述符的初始值),gsize(读取容量),都是放在了comm.hpp里面作为共有资源被看到。

server我们让其负责读数据的工作,所以以读的形式打开,然后repipe中read读到输出型参数out里面,然后out将来以引用传入,再打印出来就可以了。

然后这里为了使读入的是字符串是/0不被读取,不然会乱码而少读取一个字节,然后将最后一位的下一个置为0,使读入缓冲区时可以刷新。

clent.hpp设计

#pragma once
#include"comm.hpp"
class clent
{
public:
    clent()
    :_fd(gdefultfd)
    {}
    bool openpipe()
    {
        _fd = ::open(pipefile.c_str(), O_WRONLY);
        if (_fd < 0)
        {
            cerr << "open fail!" << endl;
            return false;
        }
        return true;
    }
    //string* :输出型参数
    //const string& :输入型参数
    //string & : 输入输出型参数
    int sendpipe(string& in) //server负责读取
    {
        return ::write(_fd, in.c_str(), in.size());
    }
    void closepipe()
    {
        if (_fd >= 0)
        {
            ::close(_fd);
        }
    }
    ~clent()
    {
        //closepipe();
    }
private:
    int _fd;
};

clent进程就负责写入管道了,以只写打开管道后使用write直接写入就可以了。其他和server都一样的。

comm.hpp设计

这部分就是进程间可以看到的公共的部分了,我们可能会质疑到这个gsize有范围,那我一次写入如果超过,那一次读取不就读取不完了吗,那剩余的怎么办?剩下没有读取的就会留到下一次读取的时候再读,这样相当于将单次读取拆开了。

server.cc设计

#include"server.hpp"
int main()
{
    server ser;
    ser.openpipe();
    string message;
    while (true)
    {
        if (ser.recpipe(&message) > 0)
        {
            cout << "clent say# " << message << endl;
        }
        else
        {
            break;
        }
    }
    cout << "clent quit, me too " << endl;
    ser.closepipe();
    return 0;
}

就是执行打开,读取,然后关闭。

clent.cc设计

#include"clent.hpp"
int main()
{
    clent cle;
    cle.openpipe();
    string message;
    while(true)
    {
        cout << "please enter: ";
        getline(cin, message);
        cle.sendpipe(message);
    }
    cle.closepipe();
    return 0;
}

就是依次执行打开,写入,关闭。

测试运行

如果已经存在管道fifo需要先删除,我们需要先让读端打开,然后再写入,这样比较合理,防止写入的信息读不到。

可以看到,写端正常写入的同时,读端正常读取,写端退出时读端由于没有可读的了,就也跟着退出并将管道销毁了。