当前位置: 首页 > news >正文

【Linux系统】进程间通信:命名管道

1. 匿名管道的限制

匿名管道存在以下核心限制:

  • 仅限亲缘关系进程:只能用于父子进程等有血缘关系的进程间通信(如通过 fork() 创建的子进程)。
  • 单向通信:数据只能单向流动(一端写,另一端读),双向通信需创建两个管道。
  • 临时性:存在于内存中,进程结束后自动销毁。
  • 缓冲区有限:大小固定(通常为一个内存页,如4KB),易写满阻塞。

引入命名管道的原因
为解决匿名管道的局限性,命名管道允许任意进程(无论是否有亲缘关系)通过文件系统路径访问,实现跨进程通信。


2. 什么是命名管道

命名管道(Named Pipe/FIFO)是一种特殊的文件类型,特点包括:

  • 文件系统可见:通过路径名(如 /tmp/myfifo)标识,任何进程可访问。
  • 遵循FIFO原则:数据按写入顺序读取,严格保持先进先出。
  • 突破亲缘限制:不相关进程可通过路径名打开同一管道通信。
  • 双向通信支持:部分场景下支持读写双向操作(需显式设计)。

示例:命名管道在文件系统中显示为特殊文件(权限位带 p,如 prw-r--r--)。


3. 如何创建命名管道

方法一:命令行创建

mkfifo <路径名>   # 例如:mkfifo /tmp/my_pipe

生成一个具名管道文件,权限默认受 umask 影响。

示例:

方法二:程序内创建

使用 mkfifo() 函数:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);  // 成功返回0,失败返回-1
  • 参数
    • pathname:管道路径(如 /tmp/my_pipe)。
    • mode:权限标志(如 0666 表示所有用户可读写)。
  • 后续操作
    • 需用 open() 打开管道(读模式 O_RDONLY 或写模式 O_WRONLY)。
    • 默认阻塞行为:读端打开时写端阻塞,反之亦然;可通过 O_NONBLOCK 设为非阻塞。

删除管道

  • 命令行:rm <路径名> 或 unlink <路径名>
  • 程序内:unlink(pathname)

4. 匿名管道和命名管道的区别

特性匿名管道命名管道证据来源
创建方式pipe(fd) 一步创建并打开mkfifo() 创建 + open() 打开
进程关系要求必须具有亲缘关系(如父子进程)任意进程均可访问
持久性随进程结束销毁文件系统持久,需手动删除
通信方向仅单向可支持双向通信
性能略快(无文件系统操作)稍慢(涉及磁盘索引节点)
使用场景短期亲缘进程通信长期/跨进程通信(如C/S架构)

关键补充

  • 语义一致性:打开后两者操作方式相同(如 read()/write())。
  • 网络支持:命名管道可跨机器通信,匿名管道仅限本地。
  • 阻塞行为:两者均受缓冲区影响,但命名管道可通过 O_NONBLOCK 灵活控制阻塞。

5. 命名管道的打开规则

一、为读而打开 FIFO(O_RDONLY

  1. O_NONBLOCK 未设置(默认阻塞)
    • 行为:调用 open() 会阻塞当前进程,直到有另一个进程为写而打开同一 FIFO
    • 原理:内核需确保存在数据生产者,否则读操作无意义。

"open以只读方式打开FIFO时,要阻塞到某个进程为写而打开此FIFO" 。
"若没有指定O_NONBLOCK,只读 open 要阻塞到某个其他进程为写而打开此 FIFO" 。

  1. O_NONBLOCK 设置(非阻塞)
    • 行为open() 立即成功返回(返回文件描述符),无论是否有写端打开
    • 后续注意:此时若管道无数据,read() 可能返回 0(EOF)或 EAGAIN 错误(见下文读写规则)。

"先以只读方式打开,如果没有进程已经为写而打开一个FIFO,只读 open() 成功,并且 open() 不阻塞" 。
"若指定了O_NONBLOCK,则只读 open 立即返回" 。


二、为写而打开 FIFO(O_WRONLY

  1. O_NONBLOCK 未设置(默认阻塞)
    • 行为:调用 open() 会阻塞当前进程,直到有另一个进程为读而打开同一 FIFO
    • 原理:内核需确保存在数据消费者,否则写操作可能无限等待。

"open以只写方式打开FIFO时,要阻塞到某个进程为读而打开此FIFO" 。
"只写 open 要阻塞到某个其他进程为读而打开它" 。

  1. O_NONBLOCK 设置(非阻塞)
    • 行为:若无读端已打开open() 立即失败,返回 -1 并设置错误码 ENXIO(表示设备不存在)。
    • 行为:若已有读端打开,则 open() 成功。

"先以只写方式打开,如果没有进程已经为读而打开一个FIFO,只写 open() 将出错返回 -1" 。
"若指定了O_NONBLOCK,则只写 open 将出错返回 -1 如果没有进程已经为读而打开该 FIFO,其errno置ENXIO" 。


三、关键补充与深度解析

  1. O_RDWR(读写模式)的特殊性

    • 行为:以 O_RDWR 模式打开时 永不阻塞,因进程自身已同时打开读写端 。
    • 风险:可能导致自我死锁(如写满后读阻塞),实践中极少使用。
  2. 读写操作的阻塞行为(与 open 独立)

    操作O_NONBLOCK 未设置O_NONBLOCK 设置
    read() 空管道阻塞直到有数据写入立即返回 EAGAIN(或空数据)
    write() 满管道阻塞直到有空间部分写入或返回 EAGAIN
  3. 管道断裂与信号处理

    • 写端关闭:读端 read() 返回 0(EOF),不阻塞 。
    • 读端关闭:写端 write() 触发 SIGPIPE 信号(默认终止进程),错误码 EPIPE 。
  4. 原子性与 PIPE_BUF

    • 规则:写入 ≤ PIPE_BUF 字节的数据保证原子性(不与其他进程交织)。
    • 典型值:Linux 中 PIPE_BUF 为 4096 字节(一页大小)。

 四、内核实现原理(选读)

  1. 阻塞的本质
    • 进程休眠在 FIFO inode 的等待队列中,由另一端打开或数据变动时唤醒 。
    • 示例:
      // Linux 内核片段(读打开阻塞逻辑)
      if (PIPE_READERS(*inode)++ == 0) wait_for_partner(inode, &PIPE_WCOUNTER(*inode)); // 等待写端
      
  1. 非阻塞的冲突处理
    • 写打开时若无读端,内核直接返回 ENXIO 而非加入等待队列 :

"若命名管道读端尚未打开,而 O_NONBLOCK=1,写端打开失败并释放资源" 。


总结与建议

场景打开模式O_NONBLOCK结果
读打开,无写端存在O_RDONLY未设置阻塞
读打开,无写端存在O_RDONLY设置立即成功
写打开,无读端存在O_WRONLY未设置阻塞
写打开,无读端存在O_WRONLY设置立即失败(ENXIO)
读写打开O_RDWR任意立即成功(不依赖外部进程)

工程建议

  1. 生产-消费模型:推荐读端阻塞打开(确保写端就绪),写端非阻塞打开(快速失败+重试逻辑)。
  2. 超时控制:若需阻塞但避免无限等待,结合 select()/poll() 设置超时。
  3. 错误处理:始终检查 open() 返回值和 errno,尤其非阻塞模式。

6. 代码示例

下面为了更好理解命名管道,我们直接来一段代码,使用命名管道让两个无血缘关系的进程进行通信——一个进程写一个进程读。

这里client.cc和server.cc代表两个没有血缘关系的进程,在前面学习进程时我们知道,.cc文件跑起来就是一个进程,所以这里不多赘述。而我们命名管道的创建,以及打开管道文件进行操作的代码则封装在comm.hpp中。Makefile则是我们配置的自动化工具。

下面我们就来在comm.hpp中将代码封装起来

首先需要将命名管道创建,最后结束通信后还需要将管道回收,因为命名管道不会随进程的生命周期,所以需要我们手动回收

代码如下:

class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_filename = _path + "/" + _name;// 创建命名管道int n = mkfifo(_filename.c_str(), 0666);if(n < 0){std::cerr << "mkfifo failed" << std::endl;}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 回收命名管道int n = unlink(_filename.c_str());if(n < 0){std::cerr << "remove fifo failed" << std::endl;}else{std::cout << "remove fifo success" << std::endl;}}private:std::string _path;std::string _name;std::string _filename;
};

由于我们要实现一个进程写,一个进程读的单向通信,所以我们先规定,让客户端client.cc进程来写,服务端server.cc进程来读,那么读写操作我们还需要再封装一个类,因为我们只要创建一个管道就行了。

如果都封装在一个类中,那么客户端和服务端都需要实例化出一个对象,才能对管道读写通信,但这样就会创建两个命名管道了,因为只要构造函数就会创建命名管道,而我们不需要两个命名管道,我们只需要创建一个命名管道,然后服务端和客户端分别以读写的方式打开这个管道文件就可以进行通信了,所以我们可以再封装一个类来实现对打开的命名管道进行操作。

代码如下:

class Fileoper
{
public:Fileoper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_filename = _path + "/" + _name;}void OpenForRead(){_fd = open(_filename.c_str(), O_RDONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void OpenForWrite(){_fd = open(_filename.c_str(), O_WRONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}~Fileoper() {}private:std::string _path;std::string _name;std::string _filename;int _fd;
};

由于我们需要打开指定路径的管道文件,所以成员变量仍然需要和NamedFifo类一样,但是我们打开管道文件后,需要通过返回的文件描述符后续管理规管道文件,所以我们还需要一个成员变量_fd,来接收open返回的文件描述符。客户端需要从管道写入,服务端需要从管道读取,所以客户端以只写的方式打开管道文件,而服务端以只读的方式打开管道文件。但是打开之后我们客户端和服务端还需要对管道进行读写操作,所以我们还需要分别实现一个写函数和一个读函数

代码如下:

    void Write(){std::string message;while(true){std::cout << "Please Enter#";std::getline(std::cin, message);write(_fd, message.c_str(), message.size());}}void Read(){while(true){char buffer[1024];ssize_t n = read(_fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;std::cout << "Client say#" << buffer << std::endl;}else if(n == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}

当然,通信结束之后我们需要关闭文件描述符

    void Close(){close(_fd);}

测试

我们先定义两个宏

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

我们想要在当前路径下创建一个fifo的管道文件

服务端:

#include "comm.hpp"int main()
{// 创建管道NamedFifo f(PATH, FILENAME);// 文件操作Fileoper reader(PATH, FILENAME);reader.OpenForRead();reader.Read();reader.Close();return 0;
}

客户端:

#include "comm.hpp"int main()
{Fileoper Writer(PATH, FILENAME);Writer.OpenForWrite();Writer.Write();Writer.Close();   return 0;
}

运行测试:

可以看到成功实现了两个没有血缘关系的进程的单向通信

源码:

comm.hpp:

#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define PATH "."
#define FILENAME "fifo"class NamedFifo
{
public:NamedFifo(const std::string &path, const std::string &name): _path(path), _name(name){_filename = _path + "/" + _name;// 创建命名管道int n = mkfifo(_filename.c_str(), 0666);if (n < 0){std::cerr << "mkfifo failed" << std::endl;}else{std::cout << "mkfifo success" << std::endl;}}~NamedFifo(){// 回收命名管道int n = unlink(_filename.c_str());if (n < 0){std::cerr << "remove fifo failed" << std::endl;}else{std::cout << "remove fifo success" << std::endl;}}private:std::string _path;std::string _name;std::string _filename;
};class Fileoper
{
public:Fileoper(const std::string &path, const std::string &name): _path(path), _name(name), _fd(-1){_filename = _path + "/" + _name;}void OpenForRead(){_fd = open(_filename.c_str(), O_RDONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void OpenForWrite(){_fd = open(_filename.c_str(), O_WRONLY);if(_fd < 0){std::cerr << "open fifo failed" << std::endl;}else{std::cout << "open fifo success" << std::endl;}}void Write(){std::string message;while(true){std::cout << "Please Enter#";std::getline(std::cin, message);write(_fd, message.c_str(), message.size());}}void Read(){while(true){char buffer[1024];ssize_t n = read(_fd, buffer, sizeof(buffer)-1);if(n > 0){buffer[n] = 0;std::cout << "Client say#" << buffer << std::endl;}else if(n == 0){std::cout << "Client quit! me too!" << std::endl;break;}else{std::cerr << "read error" << std::endl;break;}}}void Close(){close(_fd);}~Fileoper() {}private:std::string _path;std::string _name;std::string _filename;int _fd;
};

http://www.lryc.cn/news/611817.html

相关文章:

  • 嵌入式处理器指令系统:精简指令集RISC与复杂指令集CISC的简介,及区别
  • Chrontel 昆泰【CH7107B-BF】CH7107B ——HDMI to CVBS Converter
  • 【arcmap中shp图层数据导入到postgresql数据库中,中文出现乱码,怎么办?】
  • 使用 Maxwell 和 RabbitMQ 监控 Mysql Flowable 表变更
  • 医学影像PACS系统的设计与实现,PACS系统源码
  • LMS/NLMS最小均值算法:双麦克风降噪
  • python中的推导式
  • YOLOv5 上使用 **labelImg** 标注并训练自己的数据集
  • PyTorch生成式人工智能——Hugging Face环境配置与应用详解
  • 【32】C++实战篇—— m行n列的坐标点,求每行相邻点X差值dX,每列相邻点y差值dY,并以矩阵形式左端对齐
  • 远程连接----ubuntu ,rocky 等Linux系统,WindTerm_2.7.0
  • Spring选择哪种方式代理?
  • 阿里云DMS Data Copilot——高效智能的数据助手,助力企业实现数据驱动的未来
  • 深入理解 Maven POM 文件:核心配置详解
  • Jenkinsfile各指令详解
  • Java学习第一百零九部分——Jenkins(一)
  • 基于通用优化软件GAMS的数学建模和优化分析
  • AlphaEarth模型架构梳理及借鉴哪些深度学习领域方面的思想
  • React:受控组件和非受控组件
  • WebStorm转VSCode:高效迁移指南
  • 前端开发_怎么禁止用户复制内容
  • vue3 el-dialog自定义实现拖拽、限制视口范围增加了拖拽位置持久化的功能
  • 【前端开发】三. JS运算符
  • 2.6 sync
  • vue3 find 数组查找方法
  • JSON巴巴 - 专业JSON格式化工具:让任何JSON都能完美格式化
  • Excel将整列值转换为字符串
  • Git 乱码文件处理全流程指南
  • 通过最严时序标准,再登产业图谱榜首,TDengine 时序数据库在可信数据库大会荣获双荣誉
  • Apache Flink 的详细介绍