Linux管道通信

一、Linux IPC 通信方式总览

常见的 Linux IPC 通信方式可分为三大类,覆盖从单机到跨主机的不同场景:

  1. 传统基础类:无名管道、有名管道、信号 ------ 特点是实现简单、开销小,适用于单机进程通信
  2. IPC 对象类:共享内存、信号量集、消息队列 ------ 专为高并发、大数据量的单机进程通信设计
  3. 网络跨主机类:Socket 通信 ------ 突破单机限制,支持跨主机甚至跨网络的进程通信

本文聚焦管道通信 ,详细拆解无名管道与有名管道的原理、用法,并补充 fileno()/fdopen() 函数的实战应用,最后通过一个字典查询实战案例,完整演示管道在父子进程协同工作中的落地场景。

二、核心工具:fileno () 与 fdopen () 函数

管道默认通过 read()/write()系统 IO 接口 操作,而实际开发中常需结合 fprintf()/fgets()标准 IO 接口 (带缓冲区、使用更便捷),fileno()fdopen() 正是连接两者的桥梁:

1. fdopen ():文件描述符转标准 IO 流

复制代码
#include <stdio.h>
FILE *fdopen(int fd, const char *mode);
  • 功能 :将已打开的文件描述符(如管道的读 / 写端 fd)转换为标准 IO 流(FILE* 指针)
  • 参数
    • fd:待转换的文件描述符(管道的 pipefd[0]/pipefd[1]、FIFO 的 open 返参等)
    • mode:IO 流模式,与 fopen() 一致(如 "r" 读、"w" 写、"r+" 读写)
  • 返回值 :成功返回 FILE* 流指针;失败返回 NULL,并设置 errno

2. fileno ():标准 IO 流转文件描述符

复制代码
#include <stdio.h>
int fileno(FILE *stream);
  • 功能 :将标准 IO 流(FILE*)转换回文件描述符(fd)
  • 参数stream:已打开的标准 IO 流指针(如 stdin/stdoutfdopen() 生成的流)
  • 返回值 :成功返回文件描述符;失败返回 -1,并设置 errno

核心使用场景

  • 管道默认返回文件描述符,若想使用 fprintf()/fgets() 等便捷接口,需通过 fdopen() 转换;
  • 若需对标准 IO 流执行 fcntl()/dup() 等系统调用(仅支持 fd),需通过 fileno() 转换。

三、无名管道(匿名管道):亲缘进程的专属通信通道

无名管道(对应系统调用 pipe)是 Linux 内核提供的轻量级通信机制,仅支持父子、兄弟等有亲缘关系的进程间通信,是管道家族的基础形态。

1. 核心特性与读写规则

  • 半双工通信:数据只能单向流动,实际开发中通常固定为 "一端写、一端读" 的单工模式
  • 伪文件属性 :管道是内核中的内存缓冲区,并非真实磁盘文件,不支持 lseek() 定位操作,只能顺序读写
  • IO 操作兼容 :可直接用 read()/write(),也可通过 fdopen() 转标准 IO 流操作
  • 缓冲区限制:默认 64KB 内核缓冲区,超过则写端阻塞;管道为空时读端阻塞
  • 关键读写规则
    • 读端存在时,写端写入超 64KB → 写进程阻塞,直到读端读取数据;
    • 写端存在时,读端读取过快 → 读进程阻塞,直到写端写入新数据;
    • 读端关闭后写端继续写 → 触发 SIGPIPE 信号,写进程终止;
    • 写端关闭且管道无数据 → 读端 read() 返回 0,标志通信结束。

2. 核心编程接口

复制代码
#include <unistd.h>
int pipe(int pipefd[2]);
  • 功能:在内核中创建无名管道,返回读写端文件描述符
  • 参数pipefd[0](读端)、pipefd[1](写端)
  • 返回值 :成功返回 0;失败返回 -1(设置 errno

3. 基础实战:父子进程简单通信

功能:父进程通过 fprintf() 写管道,子进程通过 fgets() 读管道,演示 fdopen()/fileno() 用法。

复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int pipefd[2];
    pid_t pid;
    char buf[128] = {0};
    const char *msg = "Hello from Parent (Standard IO)!";
    FILE *stream;  // 标准IO流指针

    // 1. 创建无名管道
    if (pipe(pipefd) == -1) {
        perror("pipe create failed");
        return -1;
    }

    // 2. fork创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork failed");
        close(pipefd[0]);
        close(pipefd[1]);
        return -1;
    }

    // 子进程:读管道(标准IO方式)
    if (pid == 0) {
        close(pipefd[1]);  // 关闭写端
        // 将管道读端fd转换为标准IO读流("r"模式)
        stream = fdopen(pipefd[0], "r");
        if (stream == NULL) {
            perror("fdopen failed");
            close(pipefd[0]);
            return -1;
        }

        // 用标准IO接口fgets读数据(替代read())
        fgets(buf, sizeof(buf), stream);
        printf("Child Received: %s\n", buf);

        // 可选:将流转回fd(演示fileno用法)
        int fd = fileno(stream);
        printf("Child: Stream converted back to fd = %d\n", fd);

        // 关闭流(会自动关闭底层fd)
        fclose(stream);
        return 0;
    }

    // 父进程:写管道(标准IO方式)
    else {
        close(pipefd[0]);  // 关闭读端
        // 将管道写端fd转换为标准IO写流("w"模式)
        stream = fdopen(pipefd[1], "w");
        if (stream == NULL) {
            perror("fdopen failed");
            close(pipefd[1]);
            wait(NULL);
            return -1;
        }

        // 用标准IO接口fprintf写数据(替代write())
        fprintf(stream, "%s", msg);
        fflush(stream);  // 标准IO有缓冲区,需手动刷新

        // 关闭流(触发写端关闭)
        fclose(stream);
        wait(NULL);
        printf("Parent Finished (Standard IO)\n");
        return 0;
    }
}
输出结果
复制代码
Child Received: Hello from Parent (Standard IO)!
Child: Stream converted back to fd = 3
Parent Finished (Standard IO)

4. 进阶实战:父子进程协同实现字典查询

下面通过一个完整的实战案例,演示无名管道在父子进程分工协作中的应用:父进程负责读取字典文件并写入管道,子进程负责接收用户输入的单词,从管道中匹配单词对应的释义。

4.1 需求分析
  • 父进程:循环读取字典文件 /home/linux/dict.txt 的内容,写入无名管道
  • 子进程:将管道读端转为标准 IO 流,接收用户输入的单词,匹配后输出释义
  • 退出条件:用户输入 #quit 时,子进程终止,父进程回收资源后退出
4.2 完整代码实现
复制代码
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAXLINE 19661  // 字典最大行数限制
#define BUF_SIZE 1024  // 读写缓冲区大小

int main(int argc, char **argv)
{
    int fd[2] = {0};
    int ret = pipe(fd);
    if (-1 == ret)
    {
        perror("pipe error");
        return 1;
    }

    pid_t pid = fork();
    if (pid > 0)
    {
        // ========== 父进程:读取字典文件并写入管道 ==========
        close(fd[0]);  // 关闭读端,只写管道
        int fd_dict = open("/home/linux/dict.txt", O_RDONLY);
        if (-1 == fd_dict)
        {
            perror("open dict failed");
            close(fd[1]);
            return 1;
        }

        char buf[BUF_SIZE] = {0};
        // 读取字典文件并写入管道(仅需一次写入,避免管道缓冲区溢出)
        ssize_t rd_ret;
        while ((rd_ret = read(fd_dict, buf, sizeof(buf))) > 0)
        {
            write(fd[1], buf, rd_ret);
            memset(buf, 0, sizeof(buf));
        }

        // 关键:关闭写端,触发子进程读管道的EOF
        close(fd_dict);
        close(fd[1]);
        wait(NULL);  // 等待子进程退出,避免僵尸进程
        printf("父进程退出\n");
    }
    else if (0 == pid)
    {
        // ========== 子进程:接收用户输入,匹配字典释义 ==========
        close(fd[1]);  // 关闭写端,只读管道

        // 将管道读端fd转为标准IO读流
        FILE *fp = fdopen(fd[0], "r");
        if (NULL == fp)
        {
            perror("fdopen failed");
            close(fd[0]);
            return 1;
        }

        while (1)
        {
            char want_word[100] = {0};
            printf("\n请输入要查询的单词(输入#quit退出):");
            fgets(want_word, sizeof(want_word), stdin);
            // 去除换行符
            want_word[strcspn(want_word, "\n")] = '\0';

            // 退出条件判断
            if (0 == strcmp(want_word, "#quit"))
            {
                printf("退出查询程序\n");
                break;
            }

            // 空输入跳过
            if (strlen(want_word) == 0)
            {
                continue;
            }

            // 逐行读取管道内容,匹配单词
            int num = 0;
            int find_flag = 0;
            char line_buf[BUF_SIZE] = {0};
            // 重置文件流指针到开头,支持重复查询
            fseek(fp, 0, SEEK_SET);

            while (fgets(line_buf, sizeof(line_buf), fp) != NULL)
            {
                // 分割单词和释义(假设字典格式:单词 释义)
                char *word = strtok(line_buf, " ");
                char *mean = strtok(NULL, "\r\n");  // 兼容Windows/Linux换行符
                if (word == NULL || mean == NULL)
                {
                    continue;
                }

                // 匹配成功则输出释义
                if (0 == strcmp(word, want_word))
                {
                    printf("【%s】的释义:%s\n", word, mean);
                    find_flag = 1;
                    break;
                }

                num++;
                // 超过最大行数则判定无此单词
                if (num > MAXLINE)
                {
                    break;
                }
            }

            if (!find_flag)
            {
                printf("未找到单词:%s\n", want_word);
            }
        }

        // 关闭流和fd
        fclose(fp);
        close(fd[0]);
    }
    else
    {
        perror("fork failed");
        close(fd[0]);
        close(fd[1]);
        return 1;
    }

    return 0;
}
4.3 代码优化说明

原代码存在几个核心问题,优化后解决如下:

  1. 父进程死循环问题 :原代码外层 while(1) 会无限写入字典内容,导致管道缓冲区溢出。优化后仅写入一次字典内容,关闭写端触发子进程读 EOF。
  2. 重复查询支持 :子进程每次查询前通过 fseek(fp, 0, SEEK_SET) 重置流指针,实现多次查询。
  3. 换行符兼容 :使用 strcspn 去除换行符,兼容不同系统的换行格式。
  4. 资源泄漏修复 :补充 wait(NULL) 回收子进程,避免僵尸进程;所有 fd 和流都正确关闭。
  5. 用户体验优化:增加提示信息,空输入自动跳过,交互更友好。
4.4 编译运行步骤
  1. 编译代码:

    复制代码
    gcc dict_query.c -o dict_query
  2. 运行程序:

    复制代码
    ./dict_query
4.5 输出效果
复制代码
请输入要查询的单词(输入#quit退出):linux
【linux】的释义:一套免费使用和自由传播的类UNIX操作系统

请输入要查询的单词(输入#quit退出):apple
【apple】的释义:苹果

请输入要查询的单词(输入#quit退出):#quit
退出查询程序
父进程退出

四、有名管道(FIFO):任意单机进程的通信桥梁

无名管道仅支持亲缘进程,而有名管道(FIFO)通过文件系统中的管道文件,实现任意单机进程通信,同样可结合 fileno()/fdopen() 简化操作。

1. 核心特性

  • 继承无名管道所有特性(半双工、64KB 缓冲区等);
  • 文件系统可见(ls -l 显示类型为 p),进程通过文件名访问;
  • 默认阻塞式打开(无对应读写端时,open() 阻塞);
  • 支持 fdopen()/fileno() 转换,兼容标准 IO 操作。

2. 核心编程接口

复制代码
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
  • 功能:创建有名管道文件
  • 参数pathname(管道路径,如 /tmp/my_fifo)、mode(权限,如 0664
  • 返回值 :成功返回 0;失败返回 -1(已存在则 errno=EEXIST

3. 实战代码示例(结合标准 IO)

(1)写进程(fifo_write_stdio.c)
复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define FIFO_PATH "/tmp/my_fifo"
#define MSG "Hello from FIFO Write (Standard IO)!"

int main() {
    int fd;
    FILE *write_stream;

    // 1. 创建有名管道(已存在则忽略)
    if (mkfifo(FIFO_PATH, 0664) == -1 && errno != EEXIST) {
        perror("mkfifo failed");
        return -1;
    }

    // 2. 以写模式打开管道,阻塞等待读进程连接
    fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    // 3. 转换为标准IO写流
    write_stream = fdopen(fd, "w");
    if (write_stream == NULL) {
        perror("fdopen failed");
        close(fd);
        return -1;
    }

    // 4. 标准IO写数据
    fprintf(write_stream, "%s", MSG);
    fflush(write_stream);  // 刷新缓冲区
    printf("Write Process: Sent via stdio: %s\n", MSG);

    // 5. 关闭流(自动关闭fd)
    fclose(write_stream);
    unlink(FIFO_PATH);  // 删除管道文件
    return 0;
}
(2)读进程(fifo_read_stdio.c)
复制代码
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>

#define FIFO_PATH "/tmp/my_fifo"

int main() {
    int fd;
    char buf[128] = {0};
    FILE *read_stream;

    // 1. 打开管道读端
    fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open failed");
        return -1;
    }

    // 2. 转换为标准IO读流
    read_stream = fdopen(fd, "r");
    if (read_stream == NULL) {
        perror("fdopen failed");
        close(fd);
        return -1;
    }

    // 3. 标准IO读数据
    fgets(buf, sizeof(buf), read_stream);
    printf("Read Process: Received via stdio: %s\n", buf);

    // 4. 演示fileno:流转回fd
    int fd_convert = fileno(read_stream);
    printf("Read Process: Stream -> fd = %d\n", fd_convert);

    // 5. 关闭流
    fclose(read_stream);
    return 0;
}
编译运行
复制代码
# 编译
gcc fifo_write_stdio.c -o fifo_w_stdio
gcc fifo_read_stdio.c -o fifo_r_stdio

# 终端1运行写进程
./fifo_w_stdio
# 终端2运行读进程
./fifo_r_stdio

五、核心知识点总结

1. fileno ()/fdopen () 核心价值

函数 作用 典型场景
fdopen() fd → FILE*(系统 IO 转标准 IO) 管道操作中使用 fprintf()/fgets() 等便捷接口
fileno() FILE* → fd(标准 IO 转系统 IO) 对标准流执行 fcntl()/dup() 等系统调用

2. 管道核心对比(补充 IO 类型)

特性维度 无名管道 有名管道
核心接口 pipe() mkfifo() + open()
通信范围 亲缘进程 任意单机进程
IO 操作方式 系统 IO / 标准 IO(fdopen) 系统 IO / 标准 IO(fdopen)
文件系统可见性 不可见 可见(管道文件)
资源释放 进程退出自动释放 unlink() 删除文件

3. 开发注意事项

  • 标准 IO 缓冲区 :使用 fdopen() 后,标准 IO 有默认缓冲区,需通过 fflush() 手动刷新(写管道时),避免数据滞留;
  • 流与 fd 的关闭fclose() 会自动关闭底层文件描述符,无需重复 close(fd),否则会导致双重关闭错误;
  • 错误处理fdopen() 失败时,需先关闭原 fd,避免资源泄漏;
  • 非阻塞模式 :若需非阻塞通信,打开 FIFO 时加 O_NONBLOCK 标志,转换为标准流后仍生效;
  • 管道破裂处理 :写进程需捕获 SIGPIPE 信号,避免读端关闭后写进程被意外终止。
相关推荐
Sunsets_Red3 小时前
2025 FZYZ夏令营游记
java·c语言·c++·python·算法·c#
k***92163 小时前
【Linux】进程概念(五):详解环境变量的本质
linux·运维·服务器
世转神风-3 小时前
VMware-挂载报错:no mountpoint specified
linux
KakiNakajima4 小时前
CentOS 7 x86系统安装EMQX 【kaki备忘录】
linux·运维·centos
少年、潜行5 小时前
F1C100/200S学习笔记(1)-- 核心板和验证板硬件设计
linux·驱动开发·f1c200s
东木君_5 小时前
Linux 驱动框架中 Class 机制完整讲解(以 ov13855 摄像头为例)
linux
红豆诗人5 小时前
数据结构初阶知识--单链表
c语言·数据结构
yiSty5 小时前
linux命令行下使用百度云网盘【自用】
linux·运维·百度云
超绝振刀怪5 小时前
【Linux工具】环境基石:软件包管理器 yum 与 Vim 编辑器详解
linux·编辑器·vim