34、 Linux IPC进程间通信:无名管道(Pipe) 和有名管道(FIFO)

Linux IPC进程间通信:无名管道(Pipe) 和有名管道(FIFO)

进程是操作系统资源分配的基本单位,每个进程拥有独立的地址空间,默认无法直接共享数据。但实际开发中,进程间常需交换数据(如父子进程协同处理任务、不同程序共享文件内容),这就需要IPC(InterProcess Communication,进程间通信) 机制。管道(Pipe)是Linux最古老、最基础的IPC方式,分为无名管道(Pipe)有名管道(FIFO)

一、IPC基础认知

1. 为什么需要IPC?

进程的地址空间是隔离的,内核会为每个进程分配独立的内存区域,进程无法直接访问其他进程的内存。但实际场景中:

  • 父子进程需协同完成任务(如父进程读取文件,子进程处理数据);
  • 无亲缘关系的进程需共享数据(如两个独立程序交换配置信息);
  • 多进程需竞争共享资源(如多个进程访问同一个硬件设备)。
    IPC的核心作用就是打破进程隔离,实现数据交换、同步或资源共享。

2. Linux IPC的主要类型

IPC类型 分类 特点
无名管道(Pipe) 古老IPC 仅支持亲缘进程(父子/兄弟)通信,基于内核队列,文件系统不可见
有名管道(FIFO) 古老IPC 支持任意单机进程通信,文件系统可见(有文件名),底层仍是内核队列
信号 古老IPC 用于进程间异步通知(如终止进程、处理异常),仅传递简单信号量,无数据
共享内存 System V IPC 最快的IPC方式,多个进程映射同一块物理内存,直接读写(需同步机制)
信号量集 System V IPC 用于进程间同步,控制共享资源的排他性访问(避免死锁)
消息队列 System V IPC 按类型/优先级传递数据块,使用频率低(已被Socket替代)
Socket 网络IPC 支持跨主机通信,也可用于本机进程通信,通用性最强

3. 管道的核心共性

无论无名管道还是有名管道,都具备以下核心特性:

  1. 半双工通信:数据只能单向流动(如读端→写端),实际编程中通常当作"单工"使用(固定读写方向);
  2. 特殊文件:管道是内核维护的伪文件(无实际磁盘存储),文件系统中不可见(无名管道)或仅显示为特殊文件(有名管道);
  3. 不支持定位操作 :无法使用lseek()(文件IO)或fseek()(标准IO)调整读写位置,只能顺序读写;
  4. 基于队列实现:管道的底层是内核队列,读写遵循"先进先出(FIFO)"规则;
  5. 阻塞特性
    • 读端存在时,写管道超过64KB(默认上限)会阻塞;
    • 写端存在时,读空管道会阻塞;
    • 读端关闭后写管道,会导致写进程退出(管道破裂);
    • 写端关闭后读管道,读操作返回0(表示EOF,通信结束)。

二、无名管道(Pipe):亲缘进程的专属通信方式

1. 无名管道核心特性

  • 适用场景 :仅支持亲缘进程(父子、兄弟)通信,由fork()创建子进程后继承文件描述符实现;
  • 创建方式 :通过pipe()函数创建,返回两个文件描述符(读端fd[0]、写端fd[1]);
  • 生命周期:随进程退出而销毁,无持久化;
  • 读写方式 :优先使用文件IO(read()/write()/close()),也可使用标准IO(fread()/fwrite(),但有缓冲区风险)。

2. 无名管道核心函数

c 复制代码
#include <unistd.h>
int pipe(int pipefd[2]);
  • 功能:创建并打开一个无名管道;
  • 参数pipefd[0]为固定读端,pipefd[1]为固定写端;
  • 返回值 :成功返回0,失败返回-1(需通过perror()查看错误原因)。

3. 无名管道编程步骤

  1. 调用pipe()创建管道,获取读/写文件描述符;
  2. 调用fork()创建子进程(子进程继承管道描述符);
  3. 父/子进程关闭不需要的描述符(如父进程关闭读端,子进程关闭写端);
  4. 进程间通过read()/write()读写管道;
  5. 通信结束后,关闭管道描述符。

4. 无名管道实战代码解析

示例1:读阻塞场景(01pipereadblock.c)

功能:父进程休眠3秒后才向管道写数据,子进程读空管道会阻塞,直到父进程写入数据。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    int fd[2]={0};
    int ret = pipe(fd); // 1. 创建管道
    if(-1 == ret)
    {
        perror("pipe error\n");
        return 1;
    }
    pid_t pid = fork(); // 2. 创建子进程
    if(pid>0) // 父进程
    {
        close(fd[0]); // 3. 父进程关闭读端(只写)
        int i =  3;
        while(i--) // 休眠3秒,模拟数据准备
        {
            printf("father 准备数据\n");
            sleep(1);
        }
        char buf[1024]="hello ,son";
        write(fd[1],buf,strlen(buf)); // 4. 向管道写数据
        close(fd[1]); // 5. 关闭写端
    }
    else if(0== pid) // 子进程
    {
        close(fd[1]); // 3. 子进程关闭写端(只读)
        char buf[1024]={0};
        // 读阻塞:管道为空,子进程阻塞直到父进程写入数据
        read(fd[0],buf,sizeof(buf)); 
        printf("father say:%s\n",buf); // 输出:father say:hello ,son
        close(fd[0]); // 5. 关闭读端
    }
    else  
    {
        perror("fork");
        return 1;
    }
    return 0;
}

关键解析 :子进程执行read(fd[0], buf, sizeof(buf))时,管道为空且写端(父进程fd[1])未关闭,因此子进程阻塞3秒,直到父进程写入数据后才继续执行。

示例2:写阻塞场景(02pipewriteblock.c)

功能:父进程持续向管道写数据,子进程不读数据,管道写满64KB后父进程阻塞。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char **argv)
{
    int fd[2]={0};
    int ret = pipe(fd);
    if(-1 == ret)
    {
        perror("pipe error\n");
        return 1;
    }
    pid_t pid = fork();
    if(pid>0) // 父进程(写数据)
    {
        close(fd[0]);
        char buf[1024]={0};
        memset(buf,'a',sizeof(buf)); // 填充1024个'a'
        int i = 0 ;
        // 写阻塞:管道默认上限64KB(64*1024=65536字节)
        // 写入65次*1024=66560字节,第65次写入时阻塞
        for(i=0;i<65;i++)
        {
             write(fd[1],buf,sizeof(buf));
             printf("已写入第%d次\n",i);
        }
        close(fd[1]);
    }
    else if(0== pid) // 子进程(不读数据)
    {
        close(fd[1]);
        while(1) { sleep(1); } // 子进程休眠,不读取管道数据
        close(fd[0]);
    }
    else  
    {
        perror("fork");
        return 1;
    }
    return 0;
}

关键解析 :无名管道的默认缓冲区大小为64KB,父进程写入64次(64*1024=65536字节)后,管道已满,第65次write()会阻塞,直到子进程读取数据释放缓冲区。

示例3:管道破裂场景(03pipebroken.c)

功能:若读端关闭后仍向管道写数据,写进程会被内核终止(管道破裂)。

注:原代码未完全体现"管道破裂",此处补充修正版逻辑(子进程提前关闭读端):

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>

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

    pid_t pid = fork();
    if (pid > 0) { // 父进程(写)
        close(fd[0]);
        sleep(1); // 等待子进程关闭读端
        char buf[1024] = "hello world";
        // 子进程已关闭读端,此处写管道会导致父进程被终止(管道破裂)
        int w_ret = write(fd[1], buf, strlen(buf));
        if (w_ret == -1) { perror("write error"); }
        close(fd[1]);
    }
    else if (pid == 0) { // 子进程(读)
        close(fd[1]);
        close(fd[0]); // 子进程提前关闭读端
        sleep(3); // 休眠,观察父进程行为
    }
    else { perror("fork error"); exit(1); }
    return 0;
}

关键解析 :子进程关闭读端(fd[0])后,管道的读端不存在,父进程再执行write()会触发SIGPIPE信号,默认导致父进程终止(即"管道破裂")。

示例4:读EOF场景(04readeof.c)

功能:写端关闭后,读管道返回0(EOF),表示通信结束。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

    pid_t pid = fork();
    if (pid == -1) { perror("fork error"); exit(1); }
    else if (pid > 0) { // 父进程(写)
        close(fd[0]);
        char *buf = "hello world\n";
        write(fd[1], buf, strlen(buf));
        close(fd[1]); // 关闭写端
        exit(0);
    }
    else if (0 == pid) { // 子进程(读)
        close(fd[1]);
        sleep(3); // 等待父进程关闭写端
        while (1) {
            char buf[1024] = {0};
            int ret = read(fd[0], buf, sizeof(buf));
            if (ret == 0) { // 写端关闭,read返回0(EOF)
                printf("通信结束,写端已关闭\n");
                break;
            }
            printf("buf = %s\n", buf); // 输出:buf = hello world
        }
        close(fd[0]);
    }
    return 0;
}

关键解析 :父进程关闭写端后,子进程read()返回0,这是判断"管道通信结束"的核心依据。

示例5:管道实现文件拷贝(05pipecp.c)

功能 :父进程读取图片文件1.png,通过管道传递给子进程,子进程将数据写入2.png,实现文件拷贝。

c 复制代码
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

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

    pid_t pid = fork();
    if (pid > 0) { // 父进程:读1.png,写管道
        close(fd[0]);
        // 打开源文件(1.png)
        int src_fd = open("/home/linux/1.png", O_RDONLY);
        if (-1 == src_fd) { perror("open src"); return 1; }
        
        while (1) {
            char buf[1024] = {0};
            int rd_ret = read(src_fd, buf, sizeof(buf));
            if(rd_ret<=0) break; // 源文件读取完毕
            write(fd[1], buf, rd_ret); // 写入管道
        }
        close(fd[1]);
        close(src_fd);
    }
    else if (0 == pid) { // 子进程:读管道,写2.png
        close(fd[1]);
        // 创建目标文件(2.png)
        int dst_fd = open("2.png",O_WRONLY|O_CREAT|O_TRUNC,0666);
        if (-1 == dst_fd) { perror("open dst"); return 1; }
        
        while(1) {
            char buf[1024]={0};
            int rd_ret = read(fd[0],buf,sizeof(buf));
            if(rd_ret<=0) break; // 管道读取完毕(写端关闭)
            write(dst_fd,buf,rd_ret); // 写入目标文件
        }
        close(fd[0]);
        close(dst_fd);
    }
    else { perror("fork"); return 1; }
    return 0;
}

关键解析:利用管道实现父子进程的"数据接力",父进程读取文件数据写入管道,子进程从管道读取数据写入新文件,完成拷贝。该逻辑是多进程文件处理的典型场景。

三、有名管道(FIFO):任意进程的单机通信方式

无名管道仅支持亲缘进程通信,而有名管道(FIFO) 解决了这一限制------它在文件系统中存在一个可见的文件名,任意单机进程只要知道该文件名,就能通过FIFO通信。

1. 有名管道核心特性

  • 适用场景 :支持无亲缘关系的进程通信,文件系统中可见(ls -l显示为p类型文件);
  • 创建方式 :通过mkfifo()函数创建,或命令行mkfifo 文件名
  • 生命周期:文件系统中持久化(需手动删除),但数据仍存储在内核缓冲区,随进程退出/关闭管道销毁;
  • 阻塞特性 :打开FIFO时,若读端未打开则写端open()阻塞,反之亦然(可通过O_NONBLOCK设置非阻塞)。

2. 有名管道核心函数

c 复制代码
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
  • 功能:创建有名管道文件;
  • 参数
    • pathname:管道文件的路径+名称(如./myfifo);
    • mode:文件权限(8进制,如0666表示所有用户可读可写);
  • 返回值 :成功返回0,失败返回-1(若文件已存在,错误码为EEXIST)。

3. 有名管道编程步骤

  1. 调用mkfifo()创建有名管道文件;
  2. 进程A以读/写模式open()管道文件;
  3. 进程B以对应模式open()管道文件(如A读则B写);
  4. 进程间通过read()/write()读写管道;
  5. 通信结束后,关闭管道描述符,手动删除管道文件(unlink()rm)。

4. 有名管道实战代码解析

示例6:FIFO读端(06fifor.c)

功能 :创建有名管道myfifo,以只读模式打开,阻塞等待写端写入数据后读取。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    // 1. 创建有名管道(已存在则忽略)
    int ret = mkfifo("myfifo", 0666);
    if (-1 == ret) {
        if (EEXIST == errno) {} // 管道已存在,正常继续
        else { perror("mkfifo"); return 1; }
    }
    // 2. 以只读模式打开(阻塞,直到写端打开)
    int fd = open("myfifo", O_RDONLY);  
    if (-1 == fd) { perror("open"); return 1; }
    
    // 3. 读取管道数据
    char buf[1024] = {0};
    read(fd, buf, sizeof(buf));
    printf("从FIFO读取:%s\n", buf); // 输出:从FIFO读取:Hello from fifo
    
    // 4. 关闭管道
    close(fd);
    // 5. (可选)删除管道文件
    unlink("myfifo");
    return 0;
}
示例7:FIFO写端(06fifow.c)

功能 :打开已创建的myfifo,以只写模式写入数据。

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>

int main(int argc, char *argv[])
{
    // 1. 创建有名管道
    int ret = mkfifo("myfifo", 0666);
    if (ret == -1) { perror("mkfifo"); exit(EXIT_FAILURE); }
    
    // 2. 以只写模式打开(阻塞,直到读端打开)
    int fd = open("myfifo", O_WRONLY);
    if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
    
    // 3. 写入管道数据
    char *buf = "Hello from fifo";
    write(fd, buf, strlen(buf) + 1); // +1 包含字符串结束符'\0'
    
    // 4. 关闭管道
    close(fd);
    // 5. (可选)删除管道文件
    unlink("myfifo");
    return 0;
}

关键解析

  • 先运行读端程序,读端open("myfifo", O_RDONLY)会阻塞,直到写端打开管道;
  • 写端打开管道后,读端解除阻塞,写端写入数据,读端读取并输出;
  • 通信结束后,需手动unlink("myfifo")删除管道文件(否则文件会残留)。

四、拓展:文件描述符与标准IO的转换

管道的读写优先使用文件IO(read()/write()),但有时需结合标准IO(fgets()/fputs()),Linux提供fileno()fdopen()实现两者转换。

1. fileno():将FILE*转为文件描述符(07fileeno.c)

c 复制代码
#include <stdio.h> 
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
    // 1. 标准IO打开文件
    FILE* fp = fopen("/etc/passwd","r");
    if(NULL== fp) { perror("fopen"); return 1; }

    // 2. FILE* 转 文件描述符
    int fd = fileno(fp);
    char buf[100]={0};
    read(fd,buf,sizeof(buf)-1); // 文件IO读取

    printf("%s",buf); // 输出/etc/passwd的前99个字符
    fclose(fp);
    return 0;
}

2. fdopen():将文件描述符转为FILE*(08fdopen.c)

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    // 1. 文件IO打开文件
    int fd = open("/etc/passwd",O_RDONLY);
    if (-1 ==fd) { perror("open"); return 1; }

    // 2. 文件描述符 转 FILE*
    FILE* fp = fdopen(fd,"r") ;
    if(NULL == fp) { perror("fdopen"); return 1; }
    
    char buf[100]={0};
    fgets(buf,sizeof(buf),fp); // 标准IO读取
    printf("buf :%s",buf); // 输出/etc/passwd的第一行
    fclose(fp);
    return 0;
}

五、管道使用的核心注意事项

  1. 避免死锁:多进程竞争管道时,需通过信号量/互斥锁同步(死锁的四个必要条件:互斥、请求与保持、不剥夺、循环等待);
  2. 阻塞与非阻塞 :默认打开管道为阻塞模式,可通过open()O_NONBLOCK参数设置非阻塞(需处理EAGAIN错误);
  3. 管道大小限制 :无名管道默认缓冲区64KB,可通过fcntl(fd, F_SETPIPE_SZ, size)调整;
  4. 管道破裂处理 :可捕获SIGPIPE信号,避免写进程被终止;
  5. 资源释放:通信结束后必须关闭管道描述符,有名管道需手动删除文件,避免资源泄漏。

六、总结

管道是Linux最基础的IPC方式,无名管道适用于亲缘进程的简单数据交换,有名管道支持任意单机进程通信,两者底层均基于内核队列,遵循"先进先出"规则。管道的核心是"阻塞特性"和"半双工通信",掌握其读写规则(如读空阻塞、写满阻塞、EOF判断)是编程的关键。

在实际开发中:

  • 简单的父子进程协同任务,优先使用无名管道;
  • 无亲缘关系的进程通信,使用有名管道;
  • 需跨主机通信或复杂数据交互,使用Socket;
  • 需高性能共享数据,使用共享内存(配合信号量同步)。
相关推荐
秦苒&2 小时前
【C语言】详解数据类型和变量(一):数据类型介绍、 signed和unsigned、数据类型的取值范围、变量、强制类型转换
c语言·开发语言·c++·c#
叽里咕噜怪2 小时前
Ansible Playbook 从入门到精通:零基础玩转自动化部署与配置管理
网络·自动化·ansible
云老大TG:@yunlaoda3602 小时前
如何使用华为云国际站代理商的FunctionGraph进行事件驱动的应用开发?
大数据·数据库·华为云·云计算
清水白石0082 小时前
《用 Python 单例模式打造稳定高效的数据库连接管理器》
数据库·python·单例模式
小虾米vivian2 小时前
dmetl5 web管理平台 监控-流程监控 看不到运行信息
linux·服务器·网络·数据库·达梦数据库
知码者2 小时前
对于Thinkphp5可能遇到的保存问题
服务器·php·apache·小程序开发·跨平台小程序
老蒋新思维2 小时前
创客匠人:从个人IP到知识变现,如何构建可持续的内容生态?
大数据·网络·人工智能·网络协议·tcp/ip·创客匠人·知识变现
TG:@yunlaoda360 云老大2 小时前
如何将外部镜像文件导入华为云国际站代理商的IMS服务?
linux·运维·华为云
怀旧,2 小时前
【Linux系统编程】13. Ext系列⽂件系统
android·linux·缓存