欢迎光临
我们一直在努力

【Linux】深度探秘命名管道:Linux 进程通信的无声桥梁

🏠 大家好,我是Yui_,一位努力学习C++/Linux的博主~💬 🍑 如果文章知识点有错误的地方,请指正!和大家一起学习,一起进步👀 🚀 如有不懂,可以随时向我提问,我会全力讲解~ 🔥 如果感觉博主的文章还不错的话,希望大家关注、点赞、收藏三连支持一下博主哦~! 🔥 你们的支持是我创作的动力! 🧸 我相信现在的努力的艰辛,都是为以后的美好最好的见证! 🧸 人的心态决定姿态! 💬 欢迎讨论:如有疑问或见解,欢迎在评论区留言互动。 👍 点赞、收藏与分享:如觉得这篇文章对您有帮助,请点赞、收藏并分享! 🚀 分享给更多人:欢迎分享给更多对 Linux 感兴趣的朋友,一起学习!

文章目录

  • 1.什么是命名管道
    • 1.2 命名管道的特点
  • 2. 创建命名管道
    • 2.1 在C程序中创建命名管道
  • 3. 命名管道的工作原理
    • 3.1 命名管道和匿名管道的区别与联系
      • 3.1.1 命名管道与匿名管道的区别
      • 3.1.2 命名管道和匿名管道的联系
  • 4.命名管道的简单应用
  • 5.总结

1.什么是命名管道

在 Unix/Linux 系统中,管道(Pipe)是一种重要的进程间通信(IPC,Inter-Process Communication)机制。除了前面介绍的匿名管道(Anonymous Pipe),系统还提供了命名管道(Named Pipe),通常称为 FIFO(First In, First Out)。命名管道通过一个在文件系统中存在的路径名来标识,使得不相关的进程之间也能通过它进行通信。 命名管道是一种特殊类型的文件,它在文件系统中有一个明确的名称,可以被多个进程打开和访问。与匿名管道不同,命名管道不局限于具有亲缘关系的进程(如父子进程),任何具有访问权限的进程都可以通过命名管道进行通信。 我们可以把命名管道看成”挂名“的匿名管道,把匿名管道加入文件系统中,但仅仅是挂个名而已,目的是为了人其他进程也能看到也看到这个文件(文件系统中的文件可以被所有的进程看到) 注意: 命名管道虽然能在文件系统被看到,但是它是没有Data block的,也就它不会存储在磁盘中,是一个内存文件。所以命名管道这个特殊文件大小为0 mkfifo

1.2 命名管道的特点

  • 持久性:命名管道存在于文件系统中,直到被删除。即使创建它的进程退出,命名管道仍然存在,等待其他进程的连接。
  • 双向通信:虽然管道本质上是半双工的(单向),但通过两个命名管道可以实现全双工通信。
  • 跨进程通信:命名管道允许不相关的进程之间进行通信,只需知道管道的路径即可。
  • 无需父子关系:任何进程都可以打开命名管道进行读写,不需要继承关系。

2. 创建命名管道

创建命名管道有两种方法:

  • 直接在命令行上创建。
  • 在程序中创建。
  • 在命令行创建:

    mkfifo mypipe

    效果如下:

    ubuntu@VM-20-9-ubuntu:~/pipeTest/namePipe$ mkfifo mypipe
    ubuntu@VM-20-9-ubuntu:~/pipeTest/namePipe$ ls -l
    total 0
    prw-rw-r– 1 ubuntu ubuntu 0 Nov 15 19:56 filename
    prw-rw-r– 1 ubuntu ubuntu 0 Nov 15 20:21 mypipe

    2.1 在C程序中创建命名管道

    为了在C程序中创建命名管道,我们需要用到的函数也是mkfifo。 mkfifo:

    头文件:

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

    格式:

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

    参数介绍:

    • pathname
      • 命名管道的路径名。
      • 在文件系统中创建一个文件,代表命名管道。
    • mode
      • 管道文件的权限(类似于文件的权限),是一个 mode_t 类型值。
      • 常见值:
        • 例如 0666,允许所有用户读写。
      • mode 会受到 进程的 umask 设置 的影响。 为了防止mode的设置被umask影响,可以事先将umsak设置为0,umask(0). 返回值: 成功:0 失败:-1,并设置error 下面我来实现一个进程向另一个进程发送信息: 客户端:client.cc

    //客户端向服务端发送消息
    #include <iostream>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/stat.h>
    #include <cstring>
    #include <fcntl.h>
    #include "common.hpp"

    using namespace std;

    int main()
    {
    int mf = mkfifo(pipePath,md);
    if(mf<0)
    {
    perror("mkfifo");
    exit(1);
    }
    //打开管道
    int fd = open(pipePath,O_WRONLY);//以写方式打开
    if(fd < 0)
    {
    perror("open");
    exit(1);
    }
    //char data[SIZE];
    string data;
    while(true)
    {
    //开始写入数据
    cout<<"cilent message# ";
    getline(cin,data);
    if(data == "exit")
    break;
    ssize_t n = write(fd,data.c_str(),data.size());
    if(n<0)
    {
    perror("write");
    exit(1);
    }

    }
    //退出客户端
    cout<<"exit"<<endl;
    close(fd);

    return 0;
    }

    服务端:server.cc

    //服务端接受客户端的信息
    #include <iostream>
    #include <sys/types.h>
    #include <unistd.h>
    #include <sys/stat.h>
    #include <cstring>
    #include <fcntl.h>
    #include "common.hpp"

    using namespace std;

    int main()
    {
    //打开管道
    int fd = open(pipePath,O_RDONLY);//以读的方法
    if(fd < 0)
    {
    perror("open");
    exit(1);
    }
    char data[SIZE];
    while(true)
    {
    int n = read(fd,data,sizeof(data)1);
    if(n<0)
    {
    perror("read");
    exit(1);
    }
    else if(n == 0)
    {
    //写端关闭
    cout<<"写端关闭"<<endl;
    break;
    }
    data[n] = 0;
    cout<<"client say:"<<data<<endl;
    }

    close(fd);
    if (unlink(pipePath) == 1) {
    perror("unlink failed");
    return 1;
    }//关闭管道
    return 0;
    }

    公共文件:common.hpp

    #pragma once
    #define SIZE 1024
    //存储管道的路径+名字
    const char* pipePath = "./myPipe";
    //也可以用绝对路径,当然相对路径更简单

    //设置mask
    mode_t md = 0666;

    效果图: 效果图

    3. 命名管道的工作原理

    再次回到文件系统:当重复多次打开一个文件时,并不会费力的打开多次,而是在第一次的基础上对struct_file结构体中的引用计数自增1,所以对于同一个文件,不同的进程打开了,看到的就是同一个。 这也是管道实现的本质:让不同的进程看到同一块空间。 命名管道的工作原理

    因为命名管道适用于独立的进程IPC,所以无论是读端还是写端,进程A,进程B为其分配的文件描述符都是3。 我们知道如果是匿名管道,因为是依靠继承才看到同一文件的,所以读写端的fd是不一样的。

    3.1 命名管道和匿名管道的区别与联系

    3.1.1 命名管道与匿名管道的区别

    • 匿名管道只能用于具有血缘关系的进程间通信;而命名管道就不一样了,无论有没有血缘关系都可以。
    • 匿名管道是通过pipe函数创建出来了;而命名管道需要先通过mkfifo函数创建,然后再通过open打开使用。
    • 当出现多条匿名管道时,可能会出现写端fd被重复继承的情况;而命名管道就不会出现这种情况。

    3.1.2 命名管道和匿名管道的联系

    • 两个都是属于管道,都是操作系统中最古老的进程通信方式,都自带有同步和异步机制,提供的都是流式数据传输 与匿名管道相同的也有在这4个场景下的处理
  • 管道为空时,读端堵塞,等待写端写入数据。
  • 管道已满时,写端堵塞,等待读端读取数据。
  • 进程通信时,关闭读端,OS发出13号信息SIGPIPE终止写端进程。
  • 进程通信时,关闭写端,读端读取到0字节数据,可以凭借这个特征来终止进程。
  • 4.命名管道的简单应用

    我们可以利用命名管道来实现一些简单的功能,加强我们对命名管道的理解。现在我们打算实现的文件拷贝小程序。 我们可以利用命名管道实现不同进程间IPC,也就是一个进程读取文件中的内容然后写进管道当中,然后另一个进程在通过管道将数据读出保存到新的文件,如此一来就是实现了一个进程的文件拷贝功能。 这也是在网络上下载应用的方式,因为下载应用的本质就是下载文件,我们将服务器看作写端,自己的电脑看作读端,那么下载这个动作的本质就是IPC,不过是在网络层面实现的。 公共区域common.hpp:

    #pragma once
    #include <unistd.h>
    #include <sys/types.h>
    #include <cstdlib>
    #include <iostream>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <cstring>
    #define SIZE 1024
    //打开拷贝的文件路径
    const char* filePath = "./file.txt";
    //要打开的管道路径
    const char* pipePath = "./pipe";
    //mask码设置
    mode_t md = 0666;

    服务端server.cc

    /**
    * 服务端通过命名管道将本地文件拷贝到客户端
    * 先打开命名管道,再打开需要被拷贝的文件,将文件通过命名管道发送给另一个程序
    *
    */

    #include "common.hpp"

    int main()
    {
    //创建命名管道
    if(mkfifo(pipePath,md) == 1)
    {
    perror("mkfifo");
    exit(1);
    }
    //打开管道
    int fd = open(pipePath,O_WRONLY);
    if(fd == 1)
    {
    perror("open");
    exit(1);
    }
    //打开需要被拷贝的文件
    FILE* fp = fopen(filePath,"r");
    if(fp == nullptr)
    {
    perror("fread");
    exit(1);
    }
    //将文件传给管道
    char buf[SIZE];
    size_t bytes = 0;
    while(true)
    {
    size_t n = fread(buf,1,sizeof(buf),fp);
    if(n>0)
    {
    //写入管道
    ssize_t sst = write(fd,buf,n);
    if(sst == 1)
    {
    perror("write");
    close(fd);
    fclose(fp);
    exit(1);
    }
    bytes+=n;
    }
    // 检查是否到达文件末尾或出错
    if (n < sizeof(buf)) {
    if (feof(fp)) {
    break; // 文件读取完毕
    }
    if (ferror(fp)) {
    perror("fread");
    fclose(fp);
    close(fd);
    exit(1);
    }
    }
    }
    std::cout<<"传递字节数"<<bytes<<std::endl;
    fclose(fp);
    close(fd);
    unlink(pipePath);
    return 0;
    }

    客户端client.cc

    //客户端接受服务端的文件
    /**
    * 客户端接受服务端的文件
    * 打开命名管道,开始读取服务端传递给客户端的信息
    */

    #include "common.hpp"

    int main()
    {

    //打开管道,读取管道数据
    int fd = open(pipePath,O_RDONLY);
    if(fd == 1)
    {
    perror("open");
    exit(1);
    }

    //将读取的数据打印到显示屏
    char buf[SIZE];
    ssize_t n = read(fd,buf,sizeof(buf)1);
    if(n == 1)
    {
    perror("read");
    exit(1);
    }
    std::cout<<"文件内容:\\n"<<buf<<"读取字节数:"<<n<<std::endl;

    close(fd);
    return 0;

    }

    运行结果: 运行结果

    此时服务端是写端,客户端是读端,实现的是下载服务;当服务端是读端,客户端是写端是,实现的就是上传服务,搞两条管道就能实现模拟简单的数据双向传输服务。 注意:创建管道文件后,无论先启动读端还是启动写端,都要堵塞式的等待另一方进行交互。

    往期Linux文章:Linux专栏

    5.总结

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

    赞(0)
    未经允许不得转载:171主机测评 » 【Linux】深度探秘命名管道:Linux 进程通信的无声桥梁
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址