在虚拟宇宙中低语——进程间通信,Linux命名管道的前世今生

文章目录

🌌 序章

在操作系统的寂静星辰之间,进程如宇宙中的恒星各自运转,孤独却不懈。而在某一刻,它们渴望对话,渴望将彼此的思想穿越内存的风暴,投递至彼此心中。于是,进程间通信(IPC)如星际信使般诞生。而在众多信使中,有一种古老而优雅的方式被称为:命名管道(Named Pipe,亦称FIFO)。

这便是我们今天的主角------在系统的寂静深处低语的"命名管道"。

🌠 一、命名管道的宿命与哲学

命名管道,是一种半双工通信机制,允许两个不具亲缘关系的进程通过内核中一个具名的管道文件进行通信。

正如信使需要地址,命名管道也需栖身于文件系统中,等待两端的进程将心语倾诉或倾听。

匿名管道(pipe)不同,命名管道拥有一个路径名,存在于文件系统中,通常通过命令 mkfifo 或系统调用mkfifo()创建。即便通信双方毫无血缘(非父子进程),只要知晓这路径,便能互通心声。

那么如何给 匿名管道 起名字呢?

结合文件系统,给匿名管道这个纯纯的内存文件分配 inode,将文件名与之构建联系,关键点在于不给它分配 Data block,因为它是一个纯纯的内存文件,是不需要将数据刷盘到磁盘中的

可以将命名管道理解为 "挂名" 后的匿名管道,把匿名管道加入文件系统中,但仅仅是挂个名而已,目的就是为了让其他进程也能看到这个文件(文件系统中的文件可以被所有进程看到)

因为没有 Data block,所以命名管道这个特殊文件大小为 0

1.1、创建及简单使用

命令管道的创建依赖于函数 mkfifo,函数原型如下

bash 复制代码
#include <sys/types.h>
#include <sys/stat.h>

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

关于 mkfifo 函数

  • 对于参数1,既可以传递绝对路径 /home/xxx/namePipeCode/fifo,也可以传递相对路径 ./fifo,当然绝对路径更灵活,但也更长

  • 对于参数2,mode_t 其实就是对 unsigned int 的封装,等价于 uint32_t,而 mode 就是创建命名管道时的初始权限,实际权限需要经过 umask 掩码计算

不难发现,mkfifo mkdir 非常像,其实 mkfifo 可以直接在命令行中运行

创建一个名为fifo的命名管道文件

bash 复制代码
mkfifo fifo


p是管道文件,大小为0

这个管道文件也非常特殊:大小为 0,从侧面说明 管道文件就是一个纯纯的内存级文件,有自己的上限,出现在文件系统中,只是单纯挂个名而已

可以直接在命令行中使用命名管道:

  • echo 可以进行数据写入,可以重定向至 fifo
  • cat 可以进行数据读取,同样也可以重定向于 fifo

打开两个终端窗口(两个进程),即可进行通信

命名管道遵循管道的四种场景,因此在写端未写入数据时,读端会阻塞

当然也可以通过程序实现两个独立进程 IPC

思路:创建 服务端 server 客户端 client 两个独立的进程,服务端 server 创建并以 的方式打开管道文件,客户端 client 的方式打开管道文件,打开后俩进程可以进程通信,通信结束后,由客户端关闭 写端(服务端 读端 读取到 0 后也关闭并删除命令管道文件

注意:

  • 当管道文件不存在时,文件会打开失败,因此为了确保正常通信,需要先运行服务端 server 创建管道文件
  • 服务端启动后,因为是读端,所以会阻塞等待 客户端(写端)写入数据
  • 客户端写入数据时,因为 '\n' 也被读取了,所以要去除此字符
  • 通信结束后,需要服务端主动删除管道文件
bash 复制代码
unlink 命令管道文件名	//删除管道文件

为了让服务端和客户端能享有同一个文件名,可以创建一个公共头文件 common.h,其中存储 命名管道文件名及默认权限等公有信息

公共资源 common.h

bash 复制代码
#pragma once

#include <iostream>
#include <string>

std::string fifo_name = "./fifo";   //管道名
uint32_t mode = 0666;   //权限

服务端(写端) server.cc 提供文件拷贝服务

bash 复制代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 服务端
    // 1、打开文件
    int wfd = open(fifo_name.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、打开源文件
    FILE *fp = fopen("file.txt", "r");
    if (fp == NULL)
    {
        cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、读取源文件数据
    char buff[1024];
    int n = fread(buff, sizeof(char), sizeof(buff), fp);

    //IPC区域
    // 4、写入源文件至命名管道
    write(wfd, buff, strlen(buff));
    cout << "服务端已向管道写入: " << n << "字节的数据" << endl;
    //IPC区域

    fclose(fp);
    fp = nullptr;
    close(wfd);
    return 0;
}

客户端(读端) client.cc 从服务端中拷贝文件(下载)

bash 复制代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 客户端
    // 1、创建命名管道文件
    int ret = mkfifo(fifo_name.c_str(), mode);
    if (ret < 0)
    {
        cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、以读的方式打开管道文件
    int rfd = open(fifo_name.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、打开目标文件
    FILE *fp = fopen("target.txt", "w");
    if (fp == NULL)
    {
        cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    //IPC区域
    // 4、读取数据
    char buff[1024];
    int n = read(rfd, buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if (n > 0)
        cout << "客户端已从管道读取: " << n << "字节的数据" << endl;
    else if (n == 0)
        cout << "写端关闭,读端读取到0,终止读端" << endl;
    else
        cout << "读取异常" << endl;
    //IPC区域

    //5、写入目标文件,完成拷贝
    fwrite(buff, sizeof(char), strlen(buff), fp);
    cout << "客户端已成功从服务端下载(拷贝)了文件数据" << endl;

    fclose(fp);
    close(rfd);
    unlink(fifo_name.c_str()); // 删除命名管道文件
    return 0;
}

注:strcasecmp 是一个字符串比较函数,无论字符串大小写,都能进行比较

所以 挂了名之后的命名管道是如何实现独立进程间 IPC 的呢?

1.2、命名管道的工作原理

把视角拉回文件系统:当重复多次打开同一个文件时,并不会费力的打开多次,而且在第一次打开的基础上,对struct file结构体中的引用计数 ++,所以对于同一个文件,不同进程打开了,看到的就是同一个

具体例子:显示器文件(stdout)只有一个吧,是不是所有进程都可以同时进行写入?

同理,命名管道文件也是如此,先创建出文件,在文件系统中挂个名,然后让独立的进程以不同的方式打开同一个命名管道文件 ,比如进程 A 以只读的方式打开,进程 B 以只写的方式打开,那么此时进程 B 就可以向进程 A 写文件,即 IPC

因为命名管道适用于独立的进程间 IPC,所以无论是读端和写端,进程 A、进程 B 为其分配的 fd 是一致的,都是 3

如果是匿名管道,因为是依靠继承才看到同一文件的,所以读端和写端fd不一样

所以 命名管道 匿名管道 还是有区别的

1.3、命名管道与匿名管道的区别

不同点:

  • 匿名管道只能用于具有血缘关系的进程间通信;而命名管道不存在该限制,谁都可以用
  • 匿名管道直接通过pipe函数创建使用;而命名管道需要先通过 mkfifo 函数创建,然后再通过open打开使用
  • 出现多条匿名管道时,可能会出现写端 fd 重复继承的情况;而命名管道不会出现这种情况
  • 在其他方面,匿名管道与命名管道几乎一致

两个都属于管道家族,都是最古老的进程间通信方式,都自带同步与互斥机制,提供的都是流式数据传输

2、命名管道的特点及特殊场景

命名管道的特点及特殊场景与匿名管道完全一致,这里简单回顾下,详细内容可跳转至 《Linux进程间通信【匿名管道】》

2.1、特点

可以简单总结为:

  • 管道是半双工通信
  • 管道生命随进程而终止
  • 命名管道任意多个进程间通信
  • 管道提供的是流式数据传输服务
  • 管道自带 同步与互斥 机制

2.2、四种特殊场景

四种场景分别为

  • 管道为时,读端阻塞,等待写端写入数据
  • 管道为时,写端阻塞,等待读端读取数据
  • 进程通信时,关闭读端,OS 发出 13 号信号 SIGPIPE 终止写端进程
  • 进程通信时,关闭写端,读端读取到 0 字节数据,可以借此判断终止读端

3、命名管道实操

以下是一些使用命名管道实现的简单小程序,主要目的是为了熟悉命名管道的使用

3.1、实现文件拷贝

下载应用的本质是在下载文件,将服务器看作写端,自己的电脑看作读端,那么下载这个动作本质上就是 IPC,不过是在网络层面实现的

我们可以利用 命名管道实现不同进程间 IPC,即进程从文件中读取并写入一批数据,另一个进程一次读取一批数据并保存至新文件中,这样就实现了文件的拷贝

目标:利用命名管道,向空文件 target.txt 中写入数据,即拷贝源文件 file.txt

公共资源 common.h

bash 复制代码
#pragma once

#include <iostream>
#include <string>

std::string fifo_name = "./fifo";   //管道名
uint32_t mode = 0666;   //权限

服务端(写端)server.cc提供文件拷贝服务

bash 复制代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 服务端
    // 1、打开文件
    int wfd = open(fifo_name.c_str(), O_WRONLY);
    if (wfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、打开源文件
    FILE *fp = fopen("file.txt", "r");
    if (fp == NULL)
    {
        cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、读取源文件数据
    char buff[1024];
    int n = fread(buff, sizeof(char), sizeof(buff), fp);

    //IPC区域
    // 4、写入源文件至命名管道
    write(wfd, buff, strlen(buff));
    cout << "服务端已向管道写入: " << n << "字节的数据" << endl;
    //IPC区域

    fclose(fp);
    fp = nullptr;
    close(wfd);
    return 0;
}

客户端(读端)client.cc从服务端中拷贝文件(下载)

bash 复制代码
#include <iostream>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "common.h"

using namespace std;

int main()
{
    // 客户端
    // 1、创建命名管道文件
    int ret = mkfifo(fifo_name.c_str(), mode);
    if (ret < 0)
    {
        cerr << "mkfifo fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 2、以读的方式打开管道文件
    int rfd = open(fifo_name.c_str(), O_RDONLY);
    if (rfd < 0)
    {
        cerr << "open fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    // 3、打开目标文件
    FILE *fp = fopen("target.txt", "w");
    if (fp == NULL)
    {
        cerr << "fopen fail! errno: " << errno << " | " << strerror(errno) << endl;
        exit(0);
    }

    //IPC区域
    // 4、读取数据
    char buff[1024];
    int n = read(rfd, buff, sizeof(buff) - 1);
    buff[n] = '\0';

    if (n > 0)
        cout << "客户端已从管道读取: " << n << "字节的数据" << endl;
    else if (n == 0)
        cout << "写端关闭,读端读取到0,终止读端" << endl;
    else
        cout << "读取异常" << endl;
    //IPC区域

    //5、写入目标文件,完成拷贝
    fwrite(buff, sizeof(char), strlen(buff), fp);
    cout << "客户端已成功从服务端下载(拷贝)了文件数据" << endl;

    fclose(fp);
    close(rfd);
    unlink(fifo_name.c_str()); // 删除命名管道文件
    return 0;
}

拷贝结果:成功拷贝

此时 服务端是写端,客户端是读端,实现的是 下载服务;当 服务端是读端,客户端是写端时,实现的就是 上传服务,搞两条管道就能模拟实现简单的 数据双向传输服务

注意: 创建管道文件后,无论先启动读端,还是先启动写端,都要阻塞式的等待另一方进行交互

3.2、实现进程控制

在 Linux 匿名管道 IPC 中,我们实现了一个简易版的进程控制程序,原理是通过多条匿名管道实现父进程对多个子进程执行任务分配

匿名管道用于有血缘关系间 IPC,命名管道也可以

所以我们可以把上一篇文章中的 匿名管道换为命名管道,一样可以实现通信

任务池 Task.hpp

bash 复制代码
#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>
#include <unistd.h>

using namespace std;

void PrintLOG()
{
    cout << "PID: " << getpid() << " 正在执行打印日志的任务..." << endl;
}

void InsertSQL()
{
    cout << "PID: " << getpid() << " 正在执行数据库插入的任务..." << endl;
}

void NetRequst()
{
    cout << "PID: " << getpid() << " 正在执行网络请求的任务..." << endl;
}

class Task
{
public:
    Task()
    {
        // 装载任务
        _tt = {{"打印日志", PrintLOG}, {"数据库插入", InsertSQL}, {"网络请求", NetRequst}};
    }

    // 展示任务
    void showTask()
    {
        cout << "目前可用任务有:[";
        for (auto e : _tt)
            cout << e.first << " ";
        cout << "]" << endl;
        cout << "输入 退出 以终止程序" << endl;
    }

    // 执行任务
    void Execute(const string &task)
    {
        if (_tt.count(task) == 0)
        {
            cerr << "没有这个任务:" << task << endl;
        }
        else
        {
            _tt[task](); // 函数对象调用
        }
    }

private:
    unordered_map<string, function<void(void)>> _tt;
};

控制程序 namePipeCtrl.cc 包括进程、管道创建,任务执行与进程等待

bash 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Task.hpp"

using namespace std;

enum
{
    NAME_SIZE = 64
};

// 子进程基本信息类
class ProcINfo
{

public:
    ProcINfo(pid_t pid = pid_t(), int wfd = int())
        : _pid(pid), _wfd(wfd), _num(_cnt++)
    {
        char buff[NAME_SIZE] = {0};
        snprintf(buff, NAME_SIZE, "Process %d | pid:wfd [%d:%d]", _num, _pid, _wfd);
        _name = string(buff);
    }

    pid_t _pid;
    int _wfd;
    int _num;
    string _name;
    static int _cnt;
};

int ProcINfo::_cnt = 0;

// 进程控制类
class ProcCtrl
{
public:
    ProcCtrl(int num = 3, mode_t mode = 0666)
        : _num(num), _mode(mode)
    {
        // 根据 _num 创建命名管道及子进程
        CreatPipeAndProc();
    }

    ~ProcCtrl()
    {
        waitProc();
    }

    // 创建管道及进程
    void CreatPipeAndProc()
    {
        // 因为是继承的,所以也要注意写端重复继承问题
        vector<int> fds;
        for (int i = 0; i < _num; i++)
        {
            // 步骤:创建管道,存入 _vst
            char pipeNameBUff[NAME_SIZE]; // 管道名缓冲区
            snprintf(pipeNameBUff, NAME_SIZE, "./fifo-%d", i);

            int ret = mkfifo(pipeNameBUff, _mode);
            assert(ret != -1);
            (void)ret;

            _vst.push_back(string(pipeNameBUff));

            // 创建子进程,让子进程以只读的方式打开管道文件
            pid_t id = fork();
            if (id == 0)
            {
                // 子进程内
                // 先关闭不必要的写端
                for (auto e : fds)
                    close(e);

                // 打开管道文件,并进入任务等待默认(读端阻塞)
                int rfd = open(_vst[i].c_str(), O_RDONLY);
                assert(rfd != -1);
                (void)rfd;

                waitCommand(rfd);

                close(rfd); // 关闭读端
                exit(0);
            }

            // 父进程以写打开管道,保存 fd 信息
            int wfd = open(_vst[i].c_str(), O_WRONLY);
            assert(wfd != -1);
            (void)wfd;

            // 注册子进程信息
            _vpt.push_back(ProcINfo(id, wfd));
            fds.push_back(wfd);
        }
    }

    // 子进程等待任务派发
    void waitCommand(int rfd)
    {
        while (true)
        {
            char buff[NAME_SIZE] = {0};
            int n = read(rfd, buff, sizeof(buff) - 1);

            buff[n] = '\0';

            if (n > 0)
            {
                Task().Execute(string(buff));
            }
            else if (n == 0)
            {
                cerr << "读端读取到 0,写端已关闭,读端也即将关闭" << endl;
                break;
            }
            else
            {
                cerr << "子进程读取异常!" << endl;
                break;
            }
        }
    }

    // 展示可选进程
    void showProc()
    {
        cout << "目前可用进程有:[";
        int i = 0;
        for (i = 0; i < _num - 1; i++)
            cout << i << "|";
        cout << i << "]" << endl;
    }

    // 下达任务给子进程
    void ctrlProc()
    {
        while (true)
        {
            cout << "==========================" << endl;
            int n = 0;
            do
            {
                showProc();
                cout << "请选择子进程:> ";
                cin >> n;
            } while (n < 0 || n >= _num);

            Task().showTask();
            string taskName;
            cout << "请选择任务:> ";
            cin >> taskName;

            if (taskName == "退出")
                break;

            // 将信息通过命名管道写给子进程
            cout << "选择进程 ->" << _vpt[n]._name << " 执行 " << taskName << " 任务" << endl;
            write(_vpt[n]._wfd, taskName.c_str(), taskName.size());
            sleep(1);
        }
    }

    // 关闭写端、删除文件、等待子进程退出
    void waitProc()
    {
        for (int i = 0; i < _num; i++)
        {
            close(_vpt[i]._wfd);               // 关闭写端
            unlink(_vst[i].c_str());           // 关闭管道文件
            waitpid(_vpt[i]._pid, nullptr, 0); // 等待子进程
        }

        cout << "所有子进程已回收" << endl;
    }

private:
    vector<ProcINfo> _vpt; // 子进程信息表
    vector<string> _vst;   // 命名管道信息表
    int _num;              // 子进程数/命名管道数
    mode_t _mode;          // 命名管道文件的权限
};

int main()
{
    ProcCtrl p1;
    p1.ctrlProc();
    return 0;
}

关于 父子进程间使用命名管道通信 值得注意的问题:

  • 在命名管道创建后,需要先创建子进程,让子进程打开读端或写端,然后才让父进程打开写端或读端,这是因为假如先让父进程打开写端或读端,那么此时父进程就会进入阻塞状态,导致无法创建子进程,自然也就无法再打开【读端或写端】;
  • 所以正确做法是先让子进程打开,即使子进程【阻塞】了,父进程也还能运行。不要让【阻塞】阻碍子进程的创建

子进程继承都存在的问题:写端重复继承,因此需要关闭不必要的写端 fd

关于问题一的理解可以看看下面这两张图:

正确用法: 先创建子进程,让子进程打开【读端或写端】,再让父进程打开【写端或读端】

小结

以上就是本次关于 Linux进程间通信之命名管道的全部内容了,作为匿名管道的兄弟,命名管道具备匿名管道的大部分特性,使用方法也基本一致,不过二者在创建和打开方式上各有不同:匿名管道简单,但只能用于具有血缘关系进程间通信,命名管道虽麻烦些,但适用于所有进程间通信场景。

本篇关于命名管道的介绍就暂告段落啦,希望能对大家的学习产生帮助,欢迎各位佬前来支持斧正!!!

相关推荐
love530love1 分钟前
【笔记】在 MSYS2(MINGW64)中正确安装 Rust
运维·开发语言·人工智能·windows·笔记·python·rust
啵啵学习3 分钟前
Linux 里 su 和 sudo 命令这两个有什么不一样?
linux·运维·服务器·单片机·ubuntu·centos·嵌入式
半桔37 分钟前
【Linux手册】冯诺依曼体系结构
linux·缓存·职场和发展·系统架构
网硕互联的小客服1 小时前
如何利用Elastic Stack(ELK)进行安全日志分析
linux·服务器·网络·安全
数字芯片实验室1 小时前
寄存器模型生成:从手工到自动化
运维·自动化
Yungoal1 小时前
php & apache构建 Web 服务器
服务器·php·apache
冰橙子id2 小时前
linux——磁盘和文件系统管理
linux·运维·服务器
咕噜企业签名分发-淼淼2 小时前
应用app的服务器如何增加高并发
运维·服务器
b***25112 小时前
18650锂电池组点焊机:高效组装锂电池的关键工具|比斯特自动化
运维·自动化
leagsoft_10032 小时前
联软NSPM自动化策略管理 助力上交所加速国产化替代提升运维效率
运维·网络·自动化