【Linux】进程间通信(二)命名管道(FIFO)实战指南:从指令操作到面向对象封装的进程间通信实现

文章目录


命名管道

(基于文件+inode的进程间通信方案)
首先我们要清楚,多个进程可以同时打开多个普通文件,OS会为每一个进程都创建一个struct file,但是多个进程共享同一份inode、文件缓冲区和操作方法集,所以只会加载一次文件的属性和内容到struct file的inode和文件缓冲区中,这和父进程打开一个匿名管道并fork一个子进程后父子进程的行为类似。
命名管道就是从上面的普通文件改造而来,最直观的区别就是进程对命名管道的文件缓冲区写数据时,数据不会刷新到磁盘中。

命名管道的操作

指令操作

下面是创建命名管道的指令:

为什么命管道叫做fifo呢?其实管道本质就是一个队列,因为它有先进先出的特性。

我们可以看到,mkfifo创建出来的文件的类型是p,也就是管道文件。

下面我们来尝试用管道来传输数据:

代码操作

Makefile

我们创建两个独立的文件:client.cpp,server.cpp分别表示客户端和服务端。接下来写Makefile:

这里Makefile其实不能实现我们想要的------client.cpp,server.cpp分别编译并生成两个可执行程序,最后只会生成一个可执行程序。

这是因为Makefile本身一次只会形成一个可执行程序,运行时会从上往下扫描,把遇到的第一个目标文件形成可执行程序。
所以我们需要先创建一个只有依赖关系没有依赖方法的伪目标all,它依赖两个可执行程序:client,server,这样Makefile从上往下扫描时遇到的第一个目标文件就是all,然后就会执行all依赖关系中的client,server,这样就能一次创建两个可执行程序了。

创建命名管道

下面是代码层面场景命名管道的库函数调用接口:

因为进程间通信需要不同的进程看到同一份资源,所以我们再创建一个common.hpp文件,把客户端和服务端共享的内容都放到common.hpp中。

cpp 复制代码
#ifndef __COMMON_HPP__
#define __COMMON_HPP__

#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

std::string fifoname = "fifo";
mode_t mode = 0666;

#endif

下面就需要创建管道了,我们让服务端创建。但是需要注意,创建管道文件是可能失败的,比如管道文件已存在时。

程序结束时还需要删除管道文件,删除文件需要用到unlink,它不仅是系统调用,也是一个指令,可以用unlink指令在命令行删除管道文件。

cpp 复制代码
#include "common.hpp"

int main()
{
    int n = mkfifo(fifoname.c_str(), mode);
    if(n == 0)
    {
        std::cout << "mkfifo suceessful" << std::endl;
    }
    else
    {
        std::cout << "mkfifo failed" << std::endl;
    }

    sleep(5);

    int m = unlink(fifoname.c_str());
    (void)m;
    return 0;
}

实现通信

我们准备实现让client发送数据然后server接受数据。下面先实现server端,我们已经创建好管道文件了,下面就需要调用文件的各种系统调用接口打开文件、读取文件、关闭文件,和我们在文件系统介绍的一摸一样,小编就不过多赘述了。

这里小编要补充几点:

1、有关命名管道的操作特点,在打开管道一端,但另一端未打开的时候,open操作会被阻塞,因为如果不阻塞直接打开就有可能读到0。

2、读到的数据我们用字符串数组暂存,并且读取时要预留一个位置给\0,所以read的第三个参数需要sizeof(buffer) - 1,读取完毕后自己手动在字符串末尾添加\0。

3、当read读到0时就意味着client退出了,这时我们server端也需要退出,所以需要对read的返回值进行特殊处理。

cpp 复制代码
//server.cpp
#include "common.hpp"

int main()
{
    // 1、创建管道文件
    int n = mkfifo(fifoname.c_str(), mode);
    if (n == 0)
    {
        std::cout << "mkfifo suceessful" << std::endl;
    }
    else
    {
        // std::cout << "mkfifo failed" << std::endl;
        perror("mkfifo");
        exit(1);
    }

    // 2、打开管道文件
    // 命名管道特点,在打开一端,但另一端未打开的时候,open操作会阻塞
    int fd = open(fifoname.c_str(), O_RDONLY);
    if (fd < 0)
    {
        perror("open");
        exit(2);
    }
    std::cout << "open file success" << std::endl;

    // 3、读取管道数据
    char buffer[SIZE] = {0};
    while (true)
    {
        buffer[0] = 0; // 清空字符串
        ssize_t num = read(fd, buffer, sizeof(buffer) - 1);
        if (num > 0) // read失败返回-1
        {
            buffer[num] = 0; // 保持C风格字符串,末尾加0
            std::cout << "client say# " << buffer << std::endl;
        }
        else if(num == 0)
        {
            std::cout << "clent quit, me too!" << std::endl;
            break;
        }
        else{
            // read错误
            break;
        }
        std::cout << "num: " << num << std::endl;
    }

    // 4、归还资源
    close(fd);
    int m = unlink(fifoname.c_str());
    (void)m;
    return 0;
}

然后实现client端,还是平常打开文件的逻辑,唯一需要注意是处理输入的时候不用cin,而用getline,因为getline可以读入空格。

cpp 复制代码
//client.cpp
int main()
{
    int fd = open(fifoname.c_str(), O_WRONLY);
    if(fd < 0)
    {
        perror("open");
        exit(1);
    }

    std::string message;
    while(true)
    {
        std::cout << "please enter# ";
        getline(std::cin, message); // getline可以读取空格
        write(fd, message.c_str(), message.size());
    }

    close(fd);

    return 0;
}

现在我们来总结一下:

1、client和server是如何看到同一份资源的?因为命名管道不同于匿名管道,它有文件系统路径标识,所以当server和client通过路径+文件名打开的文件时就能通过路径解析找到唯一的文件inode,进而保证不同的进程打开的是同一个文件。

2、为什么fifo叫命名管道?因为命名管道本身就有名字,并且也有inode,open打开文件时如果打开的是命名管道就会对其做特殊处理,我们作为程序员不用操心。

以面向对象封装命名管道

1、构造函数中不写创建管道逻辑,析构函数中不写关闭管道逻辑,而是将创建管道和关闭管道和关闭文件描述符单独写成三个方法,因为客户端和服务端都会使用命名管道,服务端既要读取数据又要打开管道、打开文件、关闭管道、关闭文件描述符,而客户端只打开文件、关闭文件,这样解耦合方便服务端、客户端各自调用自己需要的接口。

2、封装Close时添加一个文件描述符默认值判断defaultfd,defaultfd默认为-1,打开管道成功了将defaultfd改为管道的fd,关闭管道后将defaultfd重新置为-1,当Close的参数为-1时表示程序没有打开管道文件或者已经将管道删除了,这时直接return,避免对无效 fd 执行 close 导致的系统错误、资源污染。 3、实现面向对象代码时对于参数传递的最佳实践如下:

输入参数:const+&

输出参数:*

输入输出参数:&

源码

cpp 复制代码
//common.hpp
#ifndef __COMMON_HPP__
#define __COMMON_HPP__

#include <stdio.h>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>

std::string fifoname = "fifo";
mode_t mode = 0666;

#define SIZE 128 //缓冲区大小

#endif
cpp 复制代码
//NamedPipe.hpp
#pragma once

#include "common.hpp"

const int defaultfd = -1;

class NamedPipe
{
public:
    NamedPipe(const std::string name) : _name(name), _fd(defaultfd)
    {
    }

    // 创建管道
    bool Create()
    {
        int n = mkfifo(_name.c_str(), mode);
        if (n == 0)
        {
            std::cout << "mkfifo suceessful" << std::endl;
        }
        else
        {
            // std::cout << "mkfifo failed" << std::endl;
            perror("mkfifo");
            return false;
        }
        return true;
    }

    bool OpenForRead()
    {
        _fd = open(_name.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            perror("open");
            return false;
        }
        std::cout << "open file success" << std::endl;
        return true;
    }

    bool OpenForWrite()
    {
        _fd = open(_name.c_str(), O_WRONLY);
        if (_fd < 0)
        {
            perror("open");
            return false;
        }
        return true;
    }

    // 输出型参数
    bool Read(std::string *out)
    {
        char buffer[SIZE] = {0};
        ssize_t num = read(_fd, buffer, sizeof(buffer) - 1);
        if (num > 0) // read失败返回-1
        {
            buffer[num] = 0; // 保持C风格字符串,末尾加0
            *out = buffer;
        }
        else if (num == 0)
        {
            return false;
        }
        else
        {
            return false;
        }
        return true;
    }

    // 输入型参数
    void Write(const std::string &in)
    {
        write(_fd, in.c_str(), in.size());
    }

    // 关闭管道文件描述符(本代码示例中服务端、客户端都需要关闭)
    void Close()
    {
        if (_fd == defaultfd)
        {
            return; // 直接return,避免执行无效操作
        }
        int n = close(_fd);
        if (n < 0)
            perror("close");
        _fd = -1;
    }

    // 归还管道文件
    void Remove()
    {
        int m = unlink(_name.c_str());
        (void)m;
    }

    ~NamedPipe()
    {
    }

private:
    std::string _name; // 管道文件名
    int _fd;           // 管道文件描述符
};
cpp 复制代码
//server.cpp
#include "NamedPipe.hpp"

int main()
{
    std::string fifoname = "fifo";
    NamedPipe np(fifoname);
    // 1、创建管道文件
    np.Create();

    // 2、打开管道文件
    np.OpenForRead();

    // 3、读取管道数据
    std::string message;
    while (true)
    {
        bool res = np.Read(&message);
        if (res)
        {
            std::cout << "client say# " << message << std::endl;
        }
        else
        {
            break;
        }
    }

    // 4、归还资源
    np.Close();
    np.Remove();

    return 0;
}
cpp 复制代码
//client.cpp
#include "NamedPipe.hpp"

int main()
{
    NamedPipe np(fifoname);

    np.OpenForWrite();

    std::string message;
    while(true)
    {
        std::cout << "please enter# ";
        getline(std::cin, message); // getline可以读取空格
        np.Write(message);
    }

    np.Close();

    return 0;
}

总结

命名管道主要用于在毫无关系的进程之间进行文件级进程通信。其他特点匿名、命名管道相同。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的赞和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

相关推荐
此生只爱蛋2 小时前
【Linux】自定义协议+序列和反序列化
linux·服务器·网络
山川而川-R2 小时前
ubuntu摄像头型号匹配不上_11-6
linux·windows·ubuntu
小年糕是糕手2 小时前
【数据结构】常见的排序算法 -- 选择排序
linux·数据结构·c++·算法·leetcode·蓝桥杯·排序算法
huangyuchi.2 小时前
【Linux网络】Socket编程实战,基于UDP协议的Dict Server
linux·网络·c++·udp·c·socket
Maple_land9 小时前
Linux复习:冯·诺依曼体系下的计算机本质:存储分级与IO效率的底层逻辑
linux·运维·服务器·c++·centos
李的阿洁10 小时前
k8s中的容器服务
linux·容器·kubernetes
Macbethad10 小时前
用流程图去描述一个蓝牙BLE数字钥匙的初始化连接过程
服务器·网络·流程图
谢景行^顾10 小时前
数据结构知识掌握
linux·数据结构·算法