命名管道(Named Pipe / FIFO)

命名管道的概念

匿名管道具有如下特点:

  • 用于 进程间通信(IPC, Inter-Process Communication)
  • 只能在 具有血缘关系的进程之间使用
    (例如:父进程与子进程)
    产生这一限制的原因是:

匿名管道的文件描述符需要通过 进程继承(fork)机制 才能被多个进程共享。

因此,匿名管道的使用场景通常是:

Parent Process
      │
     fork()
      │
Child Process

父子进程继承同一组文件描述符,从而访问同一个管道。
然而,在实际系统开发中,我们经常需要解决一个问题:

如何让两个互不相关的进程进行通信?

也就是说:

  • 两个进程之间 *不存在父子关系
  • 甚至可能由 两个不同的程序启动

为了解决这一问题,Linux 提供了另一种 IPC 机制:

命名管道(Named Pipe / FIFO)

命名管道本质上仍然是 管道机制的一种实现形式,但其特点是:

  • 管道具有 *文件系统中的路径名
  • 任何进程只要能够访问该路径,都可以通过该管道通信
    因此,命名管道可以实现:
Process A  <---->  Named Pipe  <---->  Process B

即:

允许两个完全独立的进程进行通信。


命名管道的实现原理:基于文件对象共享机制

为了理解 命名管道(Named Pipe / FIFO) 的实现机制,需要先回顾 Linux 中文件打开时的内部结构。
当一个进程打开文件时,操作系统会在内核中建立一系列数据结构,用于管理该文件的访问。
典型结构如下:

用户进程  
   │  
   ▼  
文件描述符表 (File Descriptor Table)  
   │  
   ▼  
struct file (文件对象)  
   │  
   ▼  
inode (磁盘文件元数据)  
   │  
   ▼  
磁盘文件

其中:

  • 文件描述符表(File Descriptor Table) 位于进程控制块中,用于记录当前进程打开的文件。
  • struct file 表示一个内核中的 文件对象(Open File Object),包含:
    • 文件操作方法
    • 文件偏移量
    • 文件状态标志
    • 缓冲区信息
  • inode 表示磁盘文件的元数据。

单进程打开文件的过程

假设当前存在一个普通文件:
named_pipe
该文件存在于磁盘文件系统中,并具有:

  • inode
  • 文件路径
  • 文件权限
    当进程调用系统调用:
    open(“named_pipe”, O_RDONLY);
    操作系统会执行以下步骤:
  1. 在文件系统中查找该路径对应的 inode
  2. 在内核中创建一个 struct file 对象
  3. 初始化该文件对象的属性
  4. 在进程的 文件描述符表 中分配一个条目
  5. 返回对应的文件描述符
    例如:
    进程A 文件描述符表
    fd0 → stdin
    fd1 → stdout
    fd2 → stderr
    fd3 → struct file (named_pipe)

两个进程打开同一文件的情况

现在考虑一个新的问题:
假设存在两个 彼此没有任何关系的进程
Process A
Process B
两者都执行:
open(“named_pipe”, O_RDWR);
问题是:

操作系统是否需要为第二个进程重新创建一个新的 struct file 对象?


操作系统的处理方式

在这种情况下,操作系统通常 不会重复创建完全相同的文件对象
原因包括:
资源管理原则
操作系统在设计上遵循:

尽可能减少重复资源的创建与维护。

如果两个进程打开同一个文件:

  • 文件属性相同
  • inode 相同
  • 文件操作方式一致
    则可以共享底层资源。

文件对象的共享机制
因此,在很多情况下:

Process A fd3 ──┐  
                │  
                ▼  
            struct file  
                │  
                ▼  
              inode  
                │  
                ▼  
             磁盘文件  
                ▲  
                │  
Process B fd4 ──┘

即:

  • 两个进程的 文件描述符不同
  • 但它们可以指向 同一个 struct file 或同一底层文件对象
    这样可以避免:
  • 重复维护文件状态
  • 重复分配缓冲区
  • 额外内存开销

文件共享带来的效果

由于两个进程访问的是 同一份底层文件资源,因此会出现如下现象:
如果一个进程对文件进行写入:
write(fd, data, size);
另一个进程就可以读取到这些数据:
read(fd, buffer, size);
例如:
Process A → 写入文件
Process B → 读取文件
在用户层表现为:
echo “data” >> file
cat file
即可读取到刚写入的内容。
需要注意:
在某些情况下,可能暂时无法看到更新的数据,其原因可能是:

  • 数据仍然位于 用户缓冲区
  • 尚未 刷新到磁盘(flush)
    但从逻辑上讲:

两个进程访问的是同一文件资源,因此数据是共享的。


与命名管道的关系

理解上述机制后,就可以理解 命名管道的设计思想
命名管道本质上也是一种 文件对象
p (FIFO 文件)
其特点包括:

  • 在文件系统中具有 路径名
  • 具有 inode
  • 可以被多个进程打开
    例如:
mkfifo named_pipe

创建后:
ls -l
会看到:
prw-r–r-- 1 user user 0 named_pipe
其中:
p
表示 FIFO 文件类型


命名管道通信原理

Linux 中,命名管道(Named Pipe / FIFO) 是一种重要的进程间通信(IPC)机制
与普通文件不同,命名管道在逻辑上表现为文件,但其数据传输过程不经过磁盘 IO,而是在内核缓冲区中完成数据交换
其通信过程如下:

  1. 一个进程向管道写入数据
  2. 数据被写入内核缓冲区
  3. 另一个进程从管道读取数据
    因此,本质上它仍然是一种管道通信机制

当两个进程分别打开该 FIFO 文件时:
Process A open(named_pipe) → 写端
Process B open(named_pipe) → 读端
操作系统在内核中维护:

  • 管道缓冲区
  • 读写指针
  • 同步机制
    通信过程如下:
Process A (write)  
        │  
        ▼  
   内核管道缓冲区  
        │  
        ▼  
Process B (read)

与普通文件不同的是:

FIFO 的数据 不会写入磁盘,而是直接存储在 内核缓冲区 中。

因此:

  • 文件大小始终为 0
  • 数据仅在进程之间流动


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

Linux 中,管道主要分为两种:

类型 特点 适用场景
匿名管道(Anonymous Pipe) 没有文件名 仅限父子进程
命名管道(Named Pipe / FIFO) 有文件路径和名称 任意进程

匿名管道

匿名管道通过 文件描述符继承实现通信:

  • 父进程创建 pipe
  • fork 创建子进程
  • 子进程继承父进程的文件描述符
  • 两个进程共享同一个 内核对象
    该内核对象通常表示为:
struct file

该结构体在内核中具有唯一内存地址,因此可以作为资源标识。
但是:

  • 匿名管道没有文件名
  • 只能依赖继承关系共享资源

命名管道

命名管道如何做到让不同的进程先看到了同一份资源:
让不同的进程打开指定名称(路径+文件名)的同一份文件
命名管道在文件系统中以特殊文件形式存在,例如:

/tmp/my_fifo

由于 Linux 文件系统中路径 + 文件名具有唯一性,因此:
多个进程只要:

open("/tmp/my_fifo")

就可以访问同一个管道对象
因此命名管道的资源共享方式是:

通过文件路径定位同一个内核对象

而不是依赖继承关系。


命名管道的创建

Linux 中,可以使用系统调用:

mkfifo()

其函数原型:

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

参数说明:

参数 含义
pathname 管道文件路径
mode 文件权限

例如:

mkfifo("/tmp/my_fifo", 0666);

表示创建一个权限为:

rw-rw-rw-

的 FIFO 文件。
如果创建成功:

返回 0

失败:

返回 -1
并设置 errno

命名管道文件的删除

管道文件在通信结束后通常需要删除,可以使用系统调用:

unlink()

函数原型:

int unlink(const char *pathname);

示例:

unlink("/tmp/my_fifo");

其作用是:

  • 删除文件系统中的管道文件
  • 不影响已经打开的文件描述符

命名管道通信流程

一个典型的 Client–Server 模型如下:
Server 端
职责:

  1. 创建 FIFO
  2. 打开 FIFO
  3. 从 FIFO 读取数据
  4. 删除 FIFO
    示例:
mkfifo("/tmp/my_fifo", 0666)
fd = open("/tmp/my_fifo", O_RDONLY)
read(fd, ...)
close(fd)
unlink("/tmp/my_fifo")

Client 端
职责:

  1. 打开 FIFO
  2. 向 FIFO 写入数据
    示例:
fd = open("/tmp/my_fifo", O_WRONLY)

write(fd, ...)

close(fd)

进程间通信的基本前提
Linux 中,进程间通信(IPC) 的基本前提是:

不同进程必须能够访问同一份系统资源。

在命名管道(FIFO)通信模型中,实现该前提的方法是:

  1. 在文件系统中创建一个 FIFO 文件
  2. 不同进程通过 相同的路径名 打开该文件
  3. 进程通过读写该文件实现通信
    因此,本质上是:
不同进程 → 打开同一管道文件 → 共享同一内核对象

通信过程表现为文件读写操作


Server 端实现逻辑
Server 端主要负责接收数据,其实现流程如下:

  1. 打开命名管道(只读方式)
  2. 循环读取数据
  3. 处理读取结果
  4. 关闭文件描述符
    伪代码结构如下:
while(true)
{
    ssize_t s = read(fd, buffer, sizeof(buffer)-1);

    if(s > 0)
    {
        buffer[s] = '\0';
        cout << "client say: " << buffer << endl;
    }
    else if(s == 0)
    {
        // 写端关闭
        break;
    }
    else
    {
        // 读取错误
        perror("read error");
        break;
    }
}

读取返回值语义
read() 的返回值具有如下含义:

返回值 含义
>0 成功读取字节数
=0 写端关闭(EOF)
<0 读取错误

因此,当返回值为 0 时,说明客户端已经关闭写端,服务器可以安全退出。


Client 端实现逻辑
Client 端主要负责发送数据
基本流程:

  1. 打开 FIFO 文件(写模式)
  2. 从标准输入读取数据
  3. 将数据写入管道
  4. 循环发送消息
    伪代码:
while(true)
{
    printf("please say: ");

    fgets(buffer, sizeof(buffer), stdin);

    write(fd, buffer, strlen(buffer));
}

通信流程:

Client → write → FIFO → read → Server

FIFO 打开时的阻塞行为

命名管道具有一个重要特性:

FIFO 在默认情况下具有 阻塞打开(blocking open)行为

具体规则:

操作 行为
只读打开 FIFO 若没有写端打开,则阻塞
只写打开 FIFO 若没有读端打开,则阻塞

因此在实验中会出现:

  1. Server 打开 FIFO
  2. 程序停在 open()
  3. 直到 Client 打开 FIFO
  4. 两端同时继续执行
    流程如下:
Server: open(O_RDONLY) → 阻塞
Client: open(O_WRONLY) → 成功
Server: open 返回

这就是命名管道的同步机制


通信终止机制
当 Client 进程退出时:

  1. 写端文件描述符关闭
  2. FIFO 写端消失
  3. Server 再次 read() 时返回 0
    因此 Server 可以检测:
read() == 0

表示:

客户端已退出

服务器即可安全结束通信。


命名管道通信完整流程

整体流程如下:
创建 FIFO

mkfifo("/tmp/my_fifo", 0666)

Server 启动

open("/tmp/my_fifo", O_RDONLY)

Client 启动

open("/tmp/my_fifo", O_WRONLY)

数据通信

Client write → FIFO → Server read

客户端退出

close(write_fd)

服务器检测 EOF

read() == 0

服务器退出并删除 FIFO

unlink("/tmp/my_fifo")

命名管道通信特点

特性 匿名管道 命名管道
是否有文件名
是否依赖父子关系
是否存在于文件系统
使用范围 父子进程 任意进程
因此命名管道的优势是:
可以在无亲缘关系的进程之间通信

命名管道本质

从内核实现角度来看:
命名管道可以理解为:

一个通过文件系统路径暴露出来的内核管道对象。

即:

文件系统路径  
      │  
      ▼  
FIFO inode  
      │  
      ▼  
内核管道缓冲区  
      │  
      ▼  
进程间通信

因此命名管道既具有:

  • 文件的外部形式
  • 管道的通信语义

通过上述分析,可以得到以下结论:

  1. Linux 进程通过 文件描述符表 管理已打开文件。
  2. 文件打开后,内核会创建 struct file 文件对象
  3. 多个进程可以 共享同一底层文件资源
  4. 命名管道(FIFO)利用这一机制,实现:
    无血缘关系进程之间的通信
  5. FIFO 的数据存储在:
    内核管道缓冲区
    而不是磁盘文件中。

通过命名管道通信,俩个进程通信本质上变成了:

Client ----write----> FIFO ----read----> Server

即:

写文件 → 读文件

但数据不会落盘,而是在 内核缓冲区中流动。
因此它仍属于:

基于内核缓冲区的 IPC 机制

命名管道通信的核心思想:

  1. 在文件系统中创建 FIFO 文件
  2. 不同进程通过 路径名 打开同一个 FIFO
  3. 一端写入数据
  4. 另一端读取数据
    其本质仍然是:
管道通信 + 文件系统命名机制

但实际上数据仍然在 内核缓冲区中传输,不会落盘。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐