Linux管道(PIPE/FIFO)

进程间通信(IPC)概览

在Linux/Unix系统中,进程间通信(Inter-Process Communication, IPC)方式多样,各有适用场景,具体分类如下:

  • 管道
    • 匿名管道(pipe):适用于亲缘关系进程间的、一对一的通信
    • 具名管道(fifo):适用于任何进程间一对一、多对一的通信
  • 套接字(socket):适用于跨网络的进程间通信
  • 信号:异步通信方式
  • system-V IPC对象
    • 共享内存:效率最高的通信方式
    • 消息队列:相当于带标签的增强版管道
    • 信号量组(信号灯):用来协调进程间或线程间的执行进度
  • POSIX信号量
    • POSIX匿名信号量:适用于多线程,参数简单,接口明晰
    • POSIX具名信号量:适用于多进程,参数简单,接口明晰

这些通信机制统称IPC,需根据实际场景选择合适的方式。


匿名管道(PIPE)

基本逻辑

    不管是匿名管道还是具名管道,在Linux系统下都属于文件的范畴,区别是匿名管道没有名称,因此无法使用open创建或打开,事实上匿名管道有自己独特的创建接口,但其读写方式与普通的文件一样,支持read()/write()操作。

    管道文件事实上还包括网络编程中的核心概念套接字,所谓的管道指的是这些文件不能进行“定位”,只能顺序对其读写数据,就像一根水管,拧开水龙头不断读取,就可以源源不断读到水管中的数据,但如果没有水出来那只能继续等待,不能试图“跳过”部分文件去读写水管的中间地带,这是管道的最基本的特性。

示意图

函数接口

创建匿名管道的函数接口:

#include <unistd.h>
int pipe( int fd[2] );  // 返回值:成功返回0,失败返回-1

关键注意事项

  1. 文件描述符:数组fd需至少包含2个整型元素,分别对应:
    • fd[0]:专用于操作
    • fd[1]:专用于操作
  2. 通信范围:仅支持亲缘进程间通信(如父子、兄弟进程),因无文件名,非亲缘进程无法获取其描述符(描述符需通过继承传递)
  3. 写操作限制:不可多个进程同时对匿名管道写操作,否则会导致数据覆盖

使用示例(父子进程通信)

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

// 父进程:写数据到管道
void ParentProcess(int fd_write) {
    char SendMsg[32];
    while (1) {
        bzero(SendMsg, sizeof(SendMsg));  // 清空缓冲区
        fgets(SendMsg, sizeof(SendMsg), stdin);  // 从标准输入获取数据
        int ret_val = write(fd_write, SendMsg, strlen(SendMsg));  // 写入管道
        printf("【%d】发送了%d字节数据..\n", getpid(), ret_val);
    }
}

// 子进程:从管道读数据
void ChildProcess(int fd_read) {
    char RecvMsg[32];
    while (1) {
        bzero(RecvMsg, sizeof(RecvMsg));  // 清空缓冲区
        int ret_val = read(fd_read, RecvMsg, sizeof(RecvMsg));  // 读取管道
        printf("【%d】收到%d字节数据:%s\n", getpid(), ret_val, RecvMsg);
    }
}

int main(int argc, char const *argv[]) {
    int pipefd[2];  // 0:读端,1:写端

    // 创建匿名管道
    if (pipe(pipefd)) {
        perror("pipe error");
        return -1;
    }

    // 创建子进程(子进程继承管道描述符)
    pid_t tid = fork();
    if (tid > 0) {
        /* 父进程:使用写端 */
        ParentProcess(pipefd[1]);
    } else if (tid == 0) {
        /* 子进程:使用读端 */
        ChildProcess(pipefd[0]);
    }

    return 0;
}
示意图

具名管道(FIFO)

概述

具名管道(FIFO,First In First Out)是匿名管道的增强版,核心特性:

  • 外在形态接近普通文件:有文件名、可通过open()打开、支持read()/write()操作
  • 本质仍为管道:不可定位(不支持lseek())、遵循先进先出(FIFO)逻辑
  • 关键优势:支持任意进程间通信(无需亲缘关系)、支持多路同时写入(写入原子性,数据不丢失)
示意图

函数接口

创建具名管道的函数接口:

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);  // 返回值:成功返回0,失败返回-1

关键注意事项

  1. pathname:具名管道的路径+文件名(需创建在Linux系统目录下,不可在Windows共享文件夹创建,因Windows不支持管道文件)
  2. mode:文件权限模式(与open()一致,如0666),实际权限受系统umask影响(最终权限 = mode & ~umask)
  3. 打开方式:支持O_RDONLY(读)、O_WRONLY(写)、O_RDWR(读写),其中O_RDWR可避免“等待对方打开”的阻塞

使用示例

发送端示例(写FIFO)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char const *argv[]) {
    // 打开具名管道(O_RDWR模式可立即打开,无需等待读端)
    int fd = open("/home/even/myfifo", O_RDWR);
    if (fd < 0) {
        perror("open fifo error");
        return -1;
    }
    perror("open fifo ");

    char SendMsg[32] = {0};
    while (1) {
        bzero(SendMsg, sizeof(SendMsg));  // 清空缓冲区
        fgets(SendMsg, sizeof(SendMsg), stdin);  // 从标准输入获取数据
        int ret_val = write(fd, SendMsg, strlen(SendMsg));  // 写入FIFO
        printf("写入%d字节数据..\n", ret_val);
    }

    close(fd);  // 关闭文件描述符,释放资源
    return 0;
}

接收端示例(读FIFO)

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char const *argv[]) {
    // 打开具名管道
    int fd = open("/home/even/myfifo", O_RDWR);
    if (fd < 0) {
        perror("open fifo error");
        return -1;
    }
    perror("open fifo ");

    char RecvMsg[32] = {0};
    while (1) {
        bzero(RecvMsg, sizeof(RecvMsg));  // 清空缓冲区
        int ret_val = read(fd, RecvMsg, sizeof(RecvMsg));  // 读取FIFO
        printf("读取%d字节数据:%s\n", ret_val, RecvMsg);
    }

    close(fd);  // 关闭文件描述符,释放资源
    return 0;
}

【拓展】FIFO实现日志服务程序

核心思路

利用FIFO的写入原子性,解决多个无关联进程同时写日志导致的数据混乱问题:

  • 多个进程(P1~P5)无需协调,直接向FIFO写入日志数据
  • 一个专门的日志服务器进程,从FIFO读取数据并写入日志文件
  • FIFO保证每个进程的写入操作原子性,数据不丢失、不覆盖
示意图

关键优势

  • 简化开发:业务进程无需关注日志同步(无需互斥锁、信号量)
  • 数据安全:FIFO写入原子性保证日志完整
  • 解耦:业务进程与日志写入进程分离,便于维护

管道的读写特性

    当我们对一个管道文件(包括匿名管道、具名管道和网络socket)进行读写操作时,我们需要知道将会发生什么,比如读一个空管道会怎么样?对一个缓冲区已满的管道执行写入操作会怎么样等等,可以对这些读写操作做一个统一的整理。

术语约定

  • 读者:对管道拥有读权限的进程(非“正在读”的进程)
  • 写者:对管道拥有写权限的进程(非“正在写”的进程)
  • 核心规则:所谓的读者、写者不是只正在读或者正在写的进程,而是只要拥有读写权限就称为管道的读者写者,当只有读者或者只有写者时,管道文件不支持单方向打开(只有同时出现读者和写者双方时才能被打开)!!

读写特性对照表

场景 操作结果 备注
读空管道,且存在写者 阻塞(等待写者写入数据) 默认阻塞模式
读空管道,且无写者 返回0(类似读普通文件到末尾) -
写满管道,且存在读者 阻塞(等待读者读取数据) 默认阻塞模式
写管道,且无读者 触发SIGPIPE信号(默认终止进程) 管道破裂
多个写者同时写FIFO 写入原子性(数据不覆盖) 匿名管道不支持多写者
关闭读端,写者继续写 触发SIGPIPE信号 匿名管道、FIFO均适用
关闭写端,读者继续读 读完剩余数据后返回0 -
示意图

示例说明

  1. 仅写者(关闭读端):
int fd[2];
pipe(fd);
close(fd[0]);  // 关闭读端,仅保留写端(进程为写者)
  1. 既是读者也是写者(读写权限打开):
int fd = open("fifo", O_RDWR);  // 同时拥有读写权限

管道的阻塞特性

基本概念

    仔细看管道读写特性的表会发现,当试图读取一个空管道,或者试图写入一个缓冲区已满的管道时,读写操作默认会进入所谓“阻塞(se)”的状态。所谓的阻塞实际上就是系统将该进程挂起,等待资源就绪再继续调度的一种状态,这种阻塞的状态有利于系统中别的进程可高效地使用闲置CPU资源,提高系统的吞吐量。

  • 阻塞:当读写操作无法立即完成时(如读空管道、写满管道),系统将进程挂起,等待资源就绪后再恢复执行
  • 优势:提高CPU利用率,避免进程空循环浪费资源
  • 默认行为:
    • 普通文件:默认非阻塞(不可修改)
    • 管道文件(PIPE/FIFO/socket):默认阻塞(可修改为非阻塞)

设置非阻塞模式

通过fcntl()函数修改文件描述符的属性,实现非阻塞模式:

#include <fcntl.h>

// 1. 获取当前文件描述符的属性
long flag = fcntl(fd[0], F_GETFL);  // fd[0]为管道读端

// 2. 添加非阻塞属性(O_NONBLOCK)
flag |= O_NONBLOCK;

// 3. 重新设置文件描述符属性
fcntl(fd[0], F_SETFL, flag);

非阻塞模式的读写行为

场景 非阻塞模式结果 错误码
读空管道,且存在写者 返回-1(读取失败) EAGAIN(资源暂时不可用)
写满管道,且存在读者 返回-1(写入失败) EAGAIN(资源暂时不可用)
读空管道,且无写者 返回0(类似读文件到末尾) -
写管道,且无读者 触发SIGPIPE信号 -

管道关键补充知识点

管道缓冲区大小

  • Linux系统中,管道默认缓冲区大小为64KB(可通过fcntl()获取实际大小):
    #include <fcntl.h>
    int buf_size = fcntl(fd[0], F_GETPIPE_SZ);  // 获取管道缓冲区大小
    
  • 当写入数据量≤缓冲区大小时,写入操作可立即完成;超过则阻塞(默认模式)或返回失败(非阻塞模式)

管道破裂(SIGPIPE信号)

  • 触发条件:写者向“无读者”的管道写入数据
  • 默认行为:进程被终止
  • 自定义处理:可通过signal()函数捕获SIGPIPE信号,避免进程终止:
    #include <signal.h>
    void sig_handler(int sig) {
        printf("捕获到SIGPIPE信号(%d),管道破裂!\n", sig);
    }
    // 注册信号处理函数
    signal(SIGPIPE, sig_handler);
    

匿名管道与具名管道对比表

特性 匿名管道(PIPE) 具名管道(FIFO)
适用进程 仅亲缘进程 任意进程
创建接口 pipe() mkfifo()
文件名 有(文件系统中存在节点)
打开方式 继承描述符,无需open() open()打开
多写者支持 不支持(数据覆盖) 支持(写入原子性)
通信方向 半双工(需双向通信需两个管道) 半双工(同上)
生命周期 随进程(进程退出后释放) 随文件系统(需手动删除)

管道的应用场景

  1. 父子进程间的数据传递(如命令行管道cmd1 | cmd2,本质是匿名管道)
  2. 无关联进程间的简单通信(具名管道)
  3. 日志收集系统(具名管道+日志服务器)
  4. 进程间的同步与协作(结合阻塞特性)

常见问题与解决方案

问题 原因 解决方案
打开FIFO时阻塞 仅以O_RDONLY/O_WRONLY打开,等待对方 用O_RDWR模式打开,或确保读写端同时打开
写入数据丢失 多个进程同时写匿名管道 改用FIFO(支持多写者原子性)
读管道返回-1 非阻塞模式下资源未就绪 检查错误码EAGAIN,重试或延时
进程意外终止 触发SIGPIPE信号 捕获SIGPIPE信号,自定义处理逻辑
posted @ 2025-12-01 19:33  Jaklin  阅读(37)  评论(0)    收藏  举报