进程间通信(三):命名管道

  • 管道的应用的一个限制就是只能在具有共同组先【具有亲缘关系】的进程间通信
  • 如果我们想在不相关的进程之间交互数据 ,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种【特殊类型】的文件

一、创建一个命名管道

  • 进程间通信的本质,就是让不同的进程,看到同一份资源 !
  • 如果两个进程看不到同一份资源,那么就没有通信的条件;
  • A/B两个进程看到的同一个资源,其实就是文件内核缓冲区~ **每个进程有自己的struct file和文件描述符,但最终指向的是同一个 inode 和内核缓冲区 ,**路径具有唯一性 → 路径解析后得到唯一的 inode 编号 → 内核自然就知道它们要访问同一个 inode

管道文件本质:不存储数据,只是内核缓冲区的入口

  • 普通磁盘文件(如c.txt:数据最终要落盘,所以需要fsync等操作把内存缓冲区的数据刷新到磁盘持久化。
  • 管道文件(FIFO):它是一个伪文件 ,文件大小永远是 0,不占用磁盘存储空间 ,所有数据都只存在于内核的管道缓冲区 中,不会写入磁盘**。数据直接在内核中传递,不落盘,效率比普通文件高得多。**
  • 既然数据根本不会写到磁盘,自然就不存在 "刷新到磁盘" 这个操作,只需要通过open建立进程和内核缓冲区的连接即可。
特性 普通磁盘文件(如c.txt 命名管道文件(FIFO)
数据存储位置 磁盘 + 内核缓冲区 仅内核缓冲区,不落地盘
核心目的 持久化存储数据 进程间临时数据传输
是否需要刷新到磁盘 需要(fsync/fflush 不需要,数据永远不写盘
文件大小 随内容变化 永远为 0(只是个 "入口标记")
生命周期 随文件系统,手动删除才消失 随内核,所有进程关闭后释放缓冲区
  • 命名管道可以从命令行上创建,命令行方法是使用下面的这个命令:

    mkfifo filename

  • 命令管道也可以从程序里创建,相关函数有

    int mkfifo(const char*filename,mode_t mode);

复制代码
int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}

二、匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与 pipr(匿名管道)之间唯一的区别就在于它们创建与打开的方式不同,一旦这些工作完成之后,他们就有相同的语义

两者的通信语义完全一致 (比如半双工、流式服务、读写规则),唯一的区别在于创建和打开的方式

特性 匿名管道 命名管道
创建方式 pipe()函数 mkfifo()函数 /mkfifo命令
打开方式 pipe()函数自动打开 open()函数手动打开
标识形式 内核中匿名缓冲区,无文件名 存在于文件系统,有文件名
通信进程 仅限有亲缘关系进程 无亲缘关系进程也可通信
生命周期 随进程(进程退出则释放) 随内核(需手动删除)

三、命名管道的打开规则

命名管道的打开行为是其核心特性 ,也是初学者最容易踩坑的地方。FIFO 的打开规则分读打开(O_RDONLY)写打开(O_WRONLY) ,且受非阻塞标志(O_NONBLOCK) 影响,核心规则如下表:

打开方式 阻塞模式(默认,无 O_NONBLOCK) 非阻塞模式(有 O_NONBLOCK)
读打开(O_RDONLY) 阻塞,直到有进程以写方式打开同一个 FIFO 立刻返回成功,无需等待写进程
写打开(O_WRONLY) 阻塞,直到有进程以读方式打开同一个 FIFO 立刻返回失败,错误码为ENXIO

核心结论默认情况下,FIFO 的读端和写端是 "相互等待" 的------ 单独打开读端或写端都会阻塞,只有当两者都被打开后,阻塞才会解除,通信才能开始。

这一规则是内核为了保证进程间通信的有效性,避免出现 "读进程打开后无数据可读" 或 "写进程打开后无进程读" 的空等情况。

四、案例1:用命名管道实现文件拷贝

我们来实现一个简单的文件拷贝功能:将一个普通文件 abc 的内容通过FIFO复制到 abc.bak

需要两个独立程序:

  • write_fifo.c:读取源文件,写入FIFO。

  • read_fifo.c:读取FIFO,写入目标文件。

write_fifo.c(写入端)

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

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main(int argc, char *argv[])
{
    // 1. 创建命名管道文件 "tp",权限 0644
    if (mkfifo("tp", 0644) == -1) {
        // 如果文件已存在,mkfifo 会返回 EEXIST,这里简单处理
        perror("mkfifo");
    }

    // 2. 打开普通文件 "abc" 用于读取
    int infd = open("abc", O_RDONLY);
    if (infd == -1)
        ERR_EXIT("open abc");

    // 3. 打开命名管道 "tp" 用于写入(阻塞等待读端)
    int outfd = open("tp", O_WRONLY);
    if (outfd == -1)
        ERR_EXIT("open tp");

    char buf[1024];
    int n;
    // 4. 从源文件读取数据,写入管道
    while ((n = read(infd, buf, 1024)) > 0) {
        if (write(outfd, buf, n) != n)
            ERR_EXIT("write to fifo");
    }

    // 5. 关闭文件
    close(infd);
    close(outfd);
    return 0;
}

read_fifo.c(读取端)

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

#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while(0)

int main(int argc, char *argv[])
{
    // 1. 打开目标文件 "abc.bak" 用于写入(若不存在则创建,存在则截断)
    int outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (outfd == -1)
        ERR_EXIT("open abc.bak");

    // 2. 打开命名管道 "tp" 用于读取(阻塞等待写端)
    int infd = open("tp", O_RDONLY);
    if (infd == -1)
        ERR_EXIT("open tp");

    char buf[1024];
    int n;
    // 3. 从管道读取数据,写入目标文件
    while ((n = read(infd, buf, 1024)) > 0) {
        if (write(outfd, buf, n) != n)
            ERR_EXIT("write to abc.bak");
    }

    // 4. 关闭文件并删除管道文件
    close(infd);
    close(outfd);
    unlink("tp");   // 删除管道文件
    return 0;
}

五、案例2:用命名管道实现server&client通信

  • 用命名管道实现经典的 C/S(服务器 - 客户端)通信模型,这是命名管道最典型的应用场景
  • Server 端:创建 FIFO,以读方式打开,持续阻塞等待 Client 端的消息,接收并打印;
  • Client 端:以写方式打开 Server 创建的 FIFO,从键盘读取输入,发送给 Server 端;
  • 实现单向通信(Client→Server),若要实现双向通信,只需再创建一个 FIFO(Server→Client)。

5.1 相关参数/函数解析

  • umask :
  • unlink:
  • 如果write 方没有执行open , read的open会阻塞
  • 客户端退的时候,文件的返回值就是0了

我们可以进行完善代码

复制代码
    // 正常的read
    while (true)
    {
        char buffer[1024];
        int number = read(fd, buffer, sizeof(buffer) - 1);
        if (number > 0)
        {
            buffer[number] = 0;
            std::cout << "Client Say#" << buffer << std::endl;
        }
        else if(number == 0)
        {
            std::cout << "client quit!me too" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }

5.2 面向过程式的写

sever.cc

复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    umask(0);
    // 1. 新建管道
    int n = mkfifo(FIFO_FILE, 0666);
    if (n != 0)
    {
        std::cerr << "mkfifo error" << std::endl;
        return 1;
    }
    std::cout << "mkfifo success" << std::endl;


    // read
    int fd = open(FIFO_FILE, O_RDONLY);
    if (fd < 0)
    {
        std::cerr << "open fifo error" << std::endl;
        return 2;
    }
    std::cout << "open fifo success" << std::endl;


    // 正常的read
    while (true)
    {
        char buffer[1024];
        int number = read(fd, buffer, sizeof(buffer) - 1);
        if (number > 0)
        {
            buffer[number] = 0;
            std::cout << "Client Say#" << buffer << std::endl;
        }
        else if(number == 0)
        {
            std::cout << "client quit!me too" << std::endl;
            break;
        }
        else
        {
            std::cerr << "read error" << std::endl;
            break;
        }
    }

    close(fd);


    // 删除管道文件
    n = unlink(FIFO_FILE);
    if (n == 0)
    {
        std::cout << "remove fifo success" << std::endl;
    }
    else
    {
        std::cout << "remove fifo failed" << std::endl;
    }

    return 0;
}

client.cc

复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{

    int fd = open(FIFO_FILE, O_WRONLY);
    if (fd < 0)
    {
        std::cerr << "open fifo error" << std::endl;
        return 1;
    }
    std::cout << "open fifo success" << std::endl;

    //写入操作
    std::string message;
    int cnt = 1;
    pid_t id = getpid();
    while(true)
    {
        std::cout << "Please Enter# ";
        std::getline(std::cin,message);
        message += (",message number: " + std::to_string(cnt++) + ",[" + std::to_string(id) + "]");
        write(fd,message.c_str(),message.size());
    }


    close(fd);
    return 0;
}

Makefile

复制代码
.PHONY:all
all:client server
client:client.cc
	g++ -o $@ $^ -std=c++11
server:server.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f client server

comm.hpp

复制代码
#pragma once

#define FIFO_FILE "fifo"

5.3 面向对象式的写

comm.hpp

复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

class NamedFifo
{
public:
    NamedFifo(const std::string &path, const std::string &name)
        : _path(path), _name(name)
    {
        _fifoname = _path + "/" + _name;
        umask(0);
        // 1. 新建管道
        int n = mkfifo(_fifoname.c_str(), 0666);
        if (n != 0)
        {
            std::cerr << "mkfifo error" << std::endl;
        }
        std::cout << "mkfifo success" << std::endl;
    }

    ~NamedFifo()
    {
        // 删除管道文件
        int n = unlink(_fifoname.c_str());
        if (n == 0)
        {
            std::cout << "remove fifo success" << std::endl;
        }
        else
        {
            std::cout << "remove fifo failed" << std::endl;
        }
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
};

class FileOper
{
public:
    FileOper(const std::string &path, const std::string &name)
        : _path(path), _name(name), _fd(-1)
    {
        _fifoname = _path + "/" + _name;
    }

    void OpenForRead()
    {
        // read
        _fd = open(_fifoname.c_str(), O_RDONLY);
        if (_fd < 0)
        {
            std::cerr << "open fifo error" << std::endl;
        }
        std::cout << "open fifo success" << std::endl;
    }

    void OpenForWrite()
    {
        _fd = open(_fifoname.c_str(), O_WRONLY);
        if (_fd < 0)
        {
            std::cerr << "open fifo error" << std::endl;
        }
        std::cout << "open fifo success" << std::endl;
    }

    void Write()
    {
        // 写入操作
        std::string message;
        int cnt = 1;
        pid_t id = getpid();
        while (true)
        {
            std::cout << "Please Enter# ";
            std::getline(std::cin, message);
            message += (",message number: " + std::to_string(cnt++) + ",[" + std::to_string(id) + "]");
            write(_fd, message.c_str(), message.size());
        }
    }

    void Read()
    {
        // 正常的read
        while (true)
        {
            char buffer[1024];
            int number = read(_fd, buffer, sizeof(buffer) - 1);
            if (number > 0)
            {
                buffer[number] = 0;
                std::cout << "Client Say#" << buffer << std::endl;
            }
            else if (number == 0)
            {
                std::cout << "client quit!me too" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error" << std::endl;
                break;
            }
        }
    }

    void Close()
    {
        if (_fd > 0)
            close(_fd);
    }

    ~FileOper()
    {
    }

private:
    std::string _path;
    std::string _name;
    std::string _fifoname;
    int _fd;
};

server.cc

复制代码
#include "comm.hpp"

int main()
{
    //创建管道到文件
    NamedFifo fifo(".","fifo");

    //文件操作
    FileOper readerfile(".","fifo");
    readerfile.OpenForRead();
    readerfile.Read();
    readerfile.Close();



    return 0;
}

client.cc

复制代码
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include "comm.hpp"

int main()
{
    FileOper writefile(".", "fifo");
    writefile.OpenForWrite();
    writefile.Write();
    writefile.Close();
    return 0;
}
相关推荐
满天星83035772 小时前
【MySQL】表的操作
linux·服务器·数据库·mysql
幸福从心动开始2 小时前
脱单不是拖,爱要主动说——写给还在“git commit -m ‘等缘分’”的程序员
git
F1FJJ2 小时前
VS Code 里管理 PostgreSQL,有哪些选择?主流扩展横向对比
网络·数据库·postgresql·容器
普马萨特2 小时前
A-GNSS 和 CORS 简介
网络
凉、介2 小时前
SylixOS 多核启动
服务器·笔记·学习·嵌入式·sylixos
17(无规则自律)3 小时前
深度剖析Linux Input子系统(2):驱动开发流程与现代 Multi-touch 协议
linux·驱动开发·嵌入式硬件
kainx3 小时前
Linux编译eeprom
linux·运维·c语言·eeprom
cooldream20093 小时前
Windows11中 WSL2全方位安装与实战指南
linux·部署·wsl
张人玉3 小时前
C#类常用知识总结Pro
服务器·c#