命名管道:跨进程通信的终极指南

命名管道

前面我们讲的是匿名管道可以通过父子继承或者有血缘关系的进程之间,实现访问到同一块资源空间的目的进而能够通信,那么现在的命名管道没有血缘关系,应该怎么做到通信的首要条件呢?

下面让我们来仔细的谈论一下

1.多个进程(进程 A、进程 B)先后打开同一个磁盘文件a/b/c.txt时,磁盘上的文件内容、inode 只会在内存中加载 一份,不会重复加载两次,只有struct file会加载2份因为两个进程可以各自维护自己的读写位置,互不干扰,靠的就是各自独立的struct file

2.我们前面说过要想要实现通信的首要条件就是得访问到同一块资源, 那么在这里是怎么做的呢?

就是依靠路径唯一性:不同进程打开同一条文件路径,内核通过路径解析找到同一个 inode,最终绑定同一份内核缓冲资源,从而让多个进程操作同一份文件。

3.所以对于命名管道来说和普通文件一样拥有路径、文件名(1,2点同样试用),存放在文件系统中,满足「路径唯一性,不同的是管道文件只在磁盘上存inode标识(属性)不会存数据,数据全部都在内存中,所以不会刷盘。

小知识点为什么管道文件要做到不刷新磁盘呢?

因为管道数据只用来在两个进程之间临时传递,数据读完就没用了 ,不需要长期保存到磁盘,所以管道在磁盘不会存数据,所有收发数据全程只存放在内核内存缓冲区

结论:所以命名管道允许无亲缘关系的不同进程 ,通过打开同一个管道文件路径的方式,完成进程间通信。

总结三种文件

类型 是否占用磁盘 数据存放位置 进程间通信适用场景
普通文件 c.txt 是,数据持久化在磁盘 内核页缓存 + 磁盘 多进程读写共享文件(不适合高频实时 IPC)
命名管道 FIFO 仅占 inode,无磁盘数据 内核缓冲区 任意进程间简单 IPC
匿名管道 pipe 无磁盘项 内核缓冲区 仅父子 / 兄弟亲缘进程

指令mkfifo

用来创建命名管道(FIFO 文件)

复制代码
# 在当前目录创建命名管道 myfifo
mkfifo myfifo
# 在/tmp下创建
mkfifo /tmp/my_pipe

用法eg:

终端 1(读进程,阻塞等待数据)

复制代码
cat test_fifo
# 进程执行open读端,阻塞,等待另一端写进程打开管道

终端 2(写进程,往管道发数据)

复制代码
echo "hello fifo" > test_fifo
# 数据写入内核管道缓冲区,被终端1的cat读取打印

删除命名管道

命名管道像删除普通文件一样不用了要移除,而匿名管道关闭了所有的fd后自动就删除了,不需要手动删除

复制代码
rm/unlink test_fifo

函数 mkfifo()

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

int mkfifo(const char *pathname, mode_t mode);
  • pathname :要创建的命名管道文件路径 / 文件名(如"./myfifo""/tmp/test_fifo");
  • mode :管道文件权限,和open权限位规则一致,常用八进制权限:
    • 0644:所有者读写,组 / 其他只读
    • 0666:所有用户读写(实际最终权限会受进程umask掩码影响)

返回值

  • 成功:返回 0
  • 失败:返回 -1,同时设置errno(常见错误:文件已存在、路径无写权限等)

代码实现

server.cc(服务端提供创建管道的任务):

复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include"commen.hpp"
using namespace std;
int main()
{
    //1.创建管道
    int m=mkfifo(FIFO_FIFE,0666);
    if(m<0)
    {
        cout<<"mkfifo error"<<endl;
    }
    else if(m==0)
    {
     cout<<"mkfifo sucess"<<endl;
    }
    //2.打开管道
    int fd=open(FIFO_FIFE,O_RDONLY);//打开读端
    if(fd<0)
    {
        cout<<"open error"<<endl;
    }
    cout<<"open sucess"<<endl;
    //3.从管道读内容
    while(1)
    {
        char buffer[1024];
        memset(buffer,0,sizeof(buffer));
        ssize_t n=read(fd,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n]=0;
            cout<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"读到结尾了,break"<<endl;
            break;
        }
        else
        {
            cout<<"read error"<<endl;
            break;
        }
    }
    close(fd);//break后第执行close关掉读端
    //4.与匿名管道不一样,这里删除命名管道
    int u=unlink(FIFO_FIFE);//成功返回0,失败返回-1,并设置错误码
    if(u==-1)
    {
        cout<<"unlink error"<<endl;
    }
    else if(u==0)
    {
        cout<<"unlink sucess"<<endl;

    }
    return 0;
}

commen.hpp:

复制代码
#define FIFO_FIFE "fifo"

client.cc:

复制代码
#include <iostream>
#include <cstdio>
#include <string>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "commen.hpp"
using namespace std;
int main()
{
    // 1.打开管道
    int fd = open(FIFO_FIFE, O_WRONLY);
    if (fd < 0)
    {
        cout << "open error" << endl;
    }
    else
    {
        cout << "open sucess" << endl;
    }
    // 2.向管道写
    while (1)
    {
        string s;
        getline(cin, s);
        write(fd,s.c_str(),s.size());
    }
    close(fd);
}

makefile:

复制代码
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client server
#all 是约定俗成的总入口:执行 make 等价于 make all,
#一次性编译出 client 和 server 两个可执行程序。

这里要说一下就是在读端打开了即执行./server后,而写端打开即没有./client时,读端就在open函数内部阻塞(只会打印mkfifio success),等到执行./client后写端也把文件打开了,open才会返回,读端才成功打开(打印open success)。

阻塞本质:内核要保证管道通信有收发双方,避免单方面打开导致无意义读写

封装成类

通过一个bool值很妙,首先不管真假都会构建fifoname,只有bool为真即为server端才会创建管道然后open读端,为假的话就直接open写端。

  • 特点:对象一诞生,管道 + 文件就准备好
  • 销毁:对象一消亡,自动 close + unlink

如果没有这个bool值的话构造函数就不好写,如果构造函数写了创建管道那定义一个写端的对象时,那岂不是也创建了一个管道了吗,要是单独把构建管道,写成成员函数,在读端就要先手动的调用创建管道函数,才能调用open函数(因为open要先创建才能open呀,所以open也不能写在构造函数内部)。

复制代码
int main()
{
    //Fifo server();
    
    server.create_fifo();  // 手动创建管道
    server.open_fifo(O_RDONLY); // 手动打开

    // 读写...

    return 0; 
   
}

下面来看一下具体实现:

common.hpp

复制代码
#pragma once
#include <iostream>
#include <stdio.h>
#include <string>
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
using namespace std;

#define PATH "."
#define FILENAME "fifo"

#define ERR_EXIT(m)         \
    do                      \
    {                       \
        perror(m);          \
        exit(EXIT_FAILURE); \
    } while (0)

class Fifo
{
public:
    Fifo(const string &path, const string &name, bool flag)
        : _name(name), _path(path), _fd(-1), _flag(flag)
    {
        _fifoname = _path + "/" + _name;
        // 只有flag为真即只有是server才会创建管道
        if (_flag)
        {
            // 1.创建管道
            int m = mkfifo(_fifoname.c_str(), 0666);
            if (m < 0)
            {
                ERR_EXIT("mkfifo");
            }
            else if (m == 0)
            {
                cout << "mkfifo sucess" << endl;
            }
            _fd = open(_fifoname.c_str(), O_RDONLY); // 打开读端
            if (_fd < 0)
            {
                cout << "open 读端 error" << endl;
            }
            cout << "open 读端 sucess" << endl;
        }
        else//else就是写端直接打开文件
        {
            _fd = open(_fifoname.c_str(), O_WRONLY);
            if (_fd < 0)
            {
                cout << "open 写端 error" << endl;
            }
           
            cout << "open 写端 sucess" << endl;
        }
    }

    void Write()
    {
        while (1)
        {
            string s;
            getline(cin, s);
            write(_fd, s.c_str(), s.size());
        }
    }

    void Read()
    {
        while (1)
        {
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            ssize_t n = read(_fd, buffer, sizeof(buffer) - 1);
            if (n > 0)
            {
                buffer[n] = 0;
                cout << buffer << endl;
            }
            else if (n == 0)
            {
                cout << "读到结尾了,break" << endl;
                break;
            }
            else
            {
                cout << "read error" << endl;
                break;
            }
            // break后就直接~Fifo析构了析构里面会关闭fd
        }
    }
    ~Fifo()
    {
        if (_fd > 0)
        {
            close(_fd);
            cout << "close _fd success" << endl;
        }
        if (_flag) // 只有serve才删除文件
        {
            int u = unlink(_fifoname.c_str()); // 成功返回0,失败返回-1,并设置错误码
            if (u == -1)
            {
                cout << "unlink error" << endl;
            }
            else if (u == 0)
            {
                cout << "unlink sucess" << endl;
            }
        }
    }

private:
    string _path;
    string _name;
    string _fifoname;
    int _fd;
    bool _flag;
};

server.cc

复制代码
#include"commen.hpp"
using namespace std;
int main()
{
    
    Fifo server(PATH,FILENAME,true);
    server.Read();
    return 0;
}

client.cc

复制代码
#include "commen.hpp"
using namespace std;
int main()
{
    Fifo client(PATH,FILENAME,false);
    client.Write();
    return 0;
  
}

重点说一下这个宏(省去了每次都写 perror + exit 的重复代码,代码更干净)

复制代码
#define ERR_EXIT(m) \
do { \
    perror(m); \
    exit(EXIT_FAILURE); \
} while(0)

运行结果:

exit(EXIT_FAILURE):以失败状态码(通常是 1)退出程序。

为什么要do while

坑 1:在 if 语句里直接报错

不写 do{}while(0) 时,宏会被展开成:

复制代码
if (n < 0)
    perror("mkfifo");
exit(EXIT_FAILURE);
else
    std::cout << "success" << std::endl;
  • if 只控制了 perror 这一行,exit 会被当成独立语句,永远执行。
  • 更严重的是,else 没有对应的 if,直接编译报错!

坑 2:宏后面加 ; 产生多余空语句

不写:

复制代码
if (n < 0) {
    perror("mkfifo");
    exit(EXIT_FAILURE);; // 这里多了一个分号
}

do{}while(0); 是标准写法,加了分号后语法依然正常,不会有多余的空语句问题。

相关推荐
AOwhisky10 小时前
Redis 学习笔记(第三期):持久化与主从复制
运维·数据库·redis·笔记·学习·云计算
c2385610 小时前
Linux C++ 进度条进阶美化与工程化封装
linux·运维·服务器
李小白6610 小时前
第四天-WEB服务器基本原理,IIS服务
运维·服务器·前端
2401_8346369910 小时前
Nginx 从入门到实战:静态 / 动态站点、PHP 部署与反向代理全解析
运维·nginx·php
爱喝水的鱼丶11 小时前
SAP-ABAP:SAP视图开发入门:四类标准视图的适用场景与创建步骤详解
服务器·数据库·性能优化·sap·abap
aosky11 小时前
一台电脑配置多个 SSH Key 对应不同的 GitHub 账号
运维·ssh·github
云登指纹浏览器12 小时前
WebDriver反检测技术详解:如何让自动化脚本看起来像真实浏览器
运维·自动化·跨境电商
xmtxz12 小时前
计算机网络基础课程学习心得:从理论抽象到硬核实战的进阶之路
运维·学习
凡人叶枫12 小时前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法