跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux 进程间通信之命名管道(FIFO)实战与面向对象封装

Linux 进程间通信通过命名管道(FIFO)实现文件级数据交换。介绍 mkfifo 命令创建管道,演示服务端读取、客户端写入的代码逻辑,涵盖 open、read、write、close 等系统调用细节及阻塞特性。进一步通过 C++ 类封装命名管道操作,解耦创建、打开、读写与关闭流程,提供可复用的面向对象实现方案,适用于无亲缘关系进程间的通信场景。

日志猎手发布于 2026/3/16更新于 2026/10/21107 浏览
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,这样就能一次创建两个可执行程序了。

all: client server

client: client.cpp common.hpp
	g++ -o client client.cpp

server: server.cpp common.hpp NamedPipe.hpp
	g++ -o server server.cpp

clean:
	rm -f client server
创建命名管道

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

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

#ifndef __COMMON_HPP__
#define __COMMON_HPP__
#include <iostream>
#include <string>
#include <sys/types.h>
# 


std::string fifoname = ;
 mode = ;

include
<sys/stat.h>
#include <unistd.h>
"fifo"
mode_t
0666
#endif

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

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

#include "common.hpp"
int main(){
    int n = mkfifo(fifoname.c_str(), mode);
    if(n == 0){
        std::cout << "mkfifo successful" << 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 的返回值进行特殊处理。

//server.cpp
#include "common.hpp"
int main(){
    // 1、创建管道文件
    int n = mkfifo(fifoname.c_str(), mode);
    if(n == 0){
        std::cout << "mkfifo successful" << std::endl;
    }else{
        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 << "client 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 可以读入空格。

//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+&

输出参数:*

输入输出参数:&

源码
//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
//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 successful" << std::endl;
        }else{
            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; // 管道文件描述符
};
//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;
}
//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;
}

总结

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

目录

  1. 命名管道
  2. 命名管道的操作
  3. 指令操作
  4. 代码操作
  5. Makefile
  6. 创建命名管道
  7. 实现通信
  8. 以面向对象封装命名管道
  9. 源码
  10. 总结

更多推荐文章

查看全部
  • ClawdBot 安装指南:从零搭建本地 AI 助手
  • Cursor、Windsurf、Kiro、Zed、VS Code 等 AI 编程工具定价对比
  • 网络安全十大热门岗位详解与职业发展路径分析
  • 2025 年 12 月 GESP CCF 编程能力等级认证 C++ 六级真题
  • 大语言模型 LoRA 微调实战指南
  • 无线联邦学习:隐私保护下的分布式 AI 协同训练
  • 递归算法实战:汉诺塔与合并有序链表详解
  • Java核心语法:从变量到流程控制
  • Android 使用 ViewPager 实现自动无限轮播图
  • 傅里叶变换:FFT 与 DFT 原理及算法详解
  • Web 安全 Shell 注入与成绩查询漏洞解析
  • 为什么要做大语言模型:技术原理与行业价值深度解析
  • Spring AOP 核心代理组件 MethodProxy 深度解析
  • RTX 4090 本地部署腾讯混元与阿里通义万相视频模型
  • C++ 分布式系统通信效率低下的原因及协议优化细节
  • Java 面向对象入门:类与对象及封装核心
  • OpenClaw 插件新增 QQ 与飞书机器人支持
  • 微信 4.1.5.16 UI 树隐藏原理与 UIAutomation 修复及 AI-RPA 方案
  • AI 运维机器人:从自动化脚本到协作型数字员工
  • JavaScript 进度事件详解:从 loadstart 到 loadend

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online