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

目录

一、命名管道

什么是命名管道?

命名管道的原理

命令管道文件

创建命名管道

在命令行中创建

在程序中创建

二、命名管道编码实现

Makefile

Pipe.hpp

数据传送核心分析:

Server.cc

Client.cc

程序效果演示

[问题 :](#问题 :)

三、匿名管带和命名管道对比

四、总结


在之前我们学习了匿名管道,我们知道匿名管道只能用于亲缘进程间的通信,但现实中很多进程没有亲缘关系,不能继承文件描述符表。那如果我们希望是两个不相关,没有血缘关系的进程之间进行通信,该怎么办?

下面就涉及到另一种管道的通信方式 --- 命名管道

一、命名管道

什么是命名管道?

我们已经知道,匿名管道可以实现亲缘进程间的通信,但它有一个非常明显的缺点:没有文件名、无法被无关进程找到。因为匿名管道依赖 fork 继承文件描述符,只能在父子、兄弟进程之间使用。那如果我们想让两个毫无关系的进程通信,该怎么办?

答案就是:给管道一个名字,让它变成文件系统中可见的文件。于是,Linux 就设计出了命名管道(FIFO)。命名管道本质上就是一个特殊的管道文件。它在磁盘上拥有一个真实的文件节点,我们可以通过 ls 看到它、通过路径找到它。但它并不存储真实数据,只是作为一个 "通信入口",让任意进程都能通过打开这个文件,找到内核中的管道缓冲区,从而完成进程间通信。

命名管道的原理

要理解命名管道,先从一个核心问题入手:两个不同进程打开同一个文件时,内核中会打开几个文件?

答案是内核层面只打开一个文件,进程层面各自持有独立的打开对象。当两个无关进程打开同一个文件时,内核会为该文件维护唯一的 inode 结构体、唯一的文件操作集合 (file_operations) 和唯一的内核缓冲区,所有进程共享这份内核资源;而每个进程会在自己的文件描述符表中新增一个独立的文件描述符 fd,并分配一个独立的 struct file 对象(即文件打开对象),用于记录各自的打开方式(读 / 写)等信息。正是这种设计,让两个毫无亲缘关系的进程,通过 "打开同一个文件" 达成了共享内核缓冲区的目的:进程一以只读方式打开,得到记录读状态的 struct file r;进程二以只写方式打开,得到记录写状态的 struct file w;两个 struct file 都指向同一个内核缓冲区,从而实现了跨进程通信。

这就是命名管道的本质 : 命名管道其实就是这个文件,是一个存在于文件系统中的管道文件,任何进程都可以通过路径找到并打开它,进而共享内核中的管道缓冲区,完成数据传输。

命令管道文件

管道文件和我们平时读写的普通文件有着本质的区别:

  1. 普通文件是用来持久化存储数据的,数据会真正写入磁盘,即使进程退出,文件内容依然存在,下次打开还能读取;而命名管道文件只是一个通信入口,不存储任何数据,数据只会在内核缓冲区中临时传输,不会写入磁盘中,也不会长期保存。
  2. 普通文件打开后不会阻塞;而管道文件默认阻塞打开------ 只读打开必须等有人写,只写打开必须等有人读。

创建命名管道

创建命名管道的方式有两种 : 一种是在命令行中使用命令直接创建,另一种是再程序中通过系统调用创建。我们先来看第一种方法:

在命令行中创建

下面我们来认识一下命名管道的接口,在命令行中中,匿名管道是 | ,而命名管道的指令是mkfifo,mkfifo可以在当前路径下创建一个命名管道,同时我们还可以看到FIFOs叫做命名管道named pipes,即命名管道又叫做FIFOs,什么意思呢?

FIFO其实是first in first out,先进先出,即这是队列的特性,换句话来讲管道也就是队列,管道是单向通信的,管道是基于字节流的,管道是以字节为单位的,即管道是一个基于字节的队列。

  • 文件类型 p:最开头的 p 明确标识这是一个管道文件(pipe),区别于普通文件 -、目录 d 等。
  • 文件大小 0:大小为 0 是因为命名管道不存储任何实际数据 ,它只是一个通信入口,数据仅在内核缓冲区中临时传输,不会在磁盘上。
  • 文件名 myfifo:这是我们创建的命名管道文件名称,任意进程都可通过该路径打开并使用这个管道文件。

首先命名管道一定是要有两个进程进行通信的,在左侧,所以此时我们先使用一个进程作为读端,即当我们在命令行中输出指令 echo "hello linux"的时候,此时这条指令就会以进程的形式去执行,那么我们就采用 echo 作为写端向命名管道中进行写入,那么如何写入,重定向写入即可,但是当我们重定向之后,此时左侧就卡住了,即陷入阻塞等待的状态,等待什么?等待读端进行读取命名管道中的数据。

此时我们复制ssh渠道在右侧,我们在右侧的相同路径下,使用cat查看管道文件的内容,当cat运行起来同样也是一个进程,此时右侧的echo和左侧的cat就是两个不相关的进程,此时使用cat < myfifo,将命名管道的数据重定向给cat,那么cat的作用就是得到数据,将数据向显示器上输出,所以此时在右侧的屏幕上就会出现 hello linux,那么此时echo本该打印到左侧屏幕上的数据就通过命名管道进行进程间通信,将数据通信给了cat,进而打印到了右侧显示器上**。**并且此时我们再次查看这个命名管道,发现命名管道的文件属性中的文件大小依旧是0,这很容易理解,因为管道文件并不会向磁盘中进行刷新,即管道文件仅仅是一个内存级文件,所以文件大小是0。

并且我们还可以使用追加重定向,结合shell脚本命令持续不断的进行通信。

我们使用脚本命令进行进程间通信,echo进行追加重定向的向 myfifo 进行写入,那么此时由于读端还没有进行读取,所以此时写端就会阻塞等待读端进行读取。

那么我们接下来在右侧使用 cat < myfifo 从管道文件 myfifo 中进行读取即可,此时echo和cat这两个进程,就会源源不断的进行进程间通信了。

如果想要在当前路径下删除命名管道文件,那么使用 unlink 命名管道文件名,即可删除命名管道文件。


在程序中创建

在代码程序中,创建命名管道同样有对应的函数接口,这个函数接口就是 mkfifo 。

mkfifo 不是系统调用,是一个库函数,底层封装了 mknod () 系统调用作用:创建一个命名管道文件(FIFO)。第一个参数pathname的意思是要创建的命名管道文件名 /路径比如 "myfifo" 或 /tmp/fifo 第二个参数mode是指定管道文件的权限比如 0664、0666。

返回值成功返回 0,失败返回 -1,并设置 errno。

同样的如果在代码中想要删除命名管道文件,那么同样使用 unlink 函数即可删除命名管道文件即可。

二、命名管道编码实现

命名管道需要有两个毫不相关的进程,看到同一份命名管道文件进行通信,因此,我们设置两个可执行程序,Server.cc作为服务端,Client.cc作为客户端。

服务端用于提供命名管道文件,并且以读的方式打开管道文件,读取管道文件的数据,客户端以写的方式打开管道文件,并且向管道中进行写入,那么两个进程要进行通信,首先就要有管道文件才可以进行通信,换句话来说要让服务端先运行创建管道文件才能为进程间通信创建管道文件,进而客户端后运行,才可以进行通信。

并且我们提供一个头文件Pipe.hh,这个同文件中提供服务端和客户端所需要的错误码,创建命名管道文件的方法,以及清理命名管道文件的方法。这是我们第一次使用 .hpp 文件,**.hpp就是声明 + 实现都写在同一个头文件里,**因为 C++ 模板(template) 必须声明和实现写在一起,不能分开放到 .cpp 里,否则会链接报错。所以大家就约定俗成用 .hpp 表示这是 C++ 头文件,实现已经写在里面了。

我们还需要一个自动化构建工具Makefile,将两个源文件进行编译形成各自的可执行程序,并且还可以清理可执行文件。

Makefile

Pipe.hpp

在这个命名管道项目中, .hpp 文件是 C++ 的头文件,它的核心作用是存放类的定义和接口声明,并为其他 .cpp 源文件提供"使用说明书"。你把 Fifo 类的完整结构,包括成员变量、构造函数、创建、打开、发送、接收等成员函数,都写在这个 .hpp 文件里。当服务端 server.cc 和客户端client.cc 需要使用这个类时,只要通过 #include 引入这个头文件,就能直接调用对应的接口,而无需重复编写类的代码。这样不仅实现了代码的封装与复用,也让多文件项目的结构更加清晰、易于维护。

数据传送核心分析:

服务端发送数据时,先定义好要发送的字符串 msg ,并将其作为实参传给 Send 函数的 msgin 形参;函数内部通过 write 系统调用,把 msgin 中的数据写入内核缓冲区,完成数据发送。客户端接收数据时,定义用于存储数据的 msg 并传入 Recv 函数,内核先通过 read 将管道数据读到临时缓冲区 buf,再通过 assign 把数据复制给形参 msgout ;由于 msgout 是引用类型,它与外部的 msg 指向同一块内存,数据会直接存入 msg 中,最终直接打印 msg 就能拿到服务端发来的内容。

Server.cc

这是命名管道的服务端程序,核心职责是创建管道并持续接收数据。它先通过 Fifo 对象调用 Built() 创建管道文件,再以读模式 ForRead 打开管道并阻塞等待客户端连接;之后进入循环,反复调用 Recv() 读取客户端消息,若读到数据则打印,若返回 0 则说明客户端已断开连接,循环结束;最后调用 Delete() 删除管道文件,完成资源清理。

Client.cc

这是命名管道的客户端程序,核心职责是连接管道并发送数据。它创建 Fifo 对象后,以写模式 ForWrite 打开管道(阻塞等待服务端就绪);之后进入循环,提示用户输入消息,若输入 exit 则退出循环,否则将消息通过 Send() 发送给服务端;循环结束后程序退出,自动关闭文件描述符,完成通信。

程序效果演示

我们先复制ssh渠道分别作为客户端与服务端,此时我们就有了两个ssh渠道,并且这两个ssh渠道处于同一个路径下。

那么我们接下来编译,运行即可,注意这里一定要先运行服务端,观察下面左侧,小编已经将服务端运行起来了,此时服务端创建命名管道文件,可是奇怪的是服务端没有打开命名管道文件,因为我没有看到显示器上打印服务端打开命名管道文件结束的回馈信息,那么此时左侧的服务端在阻塞等待什么?

左侧的服务端在等待客户端启动,进而才会打开管道文件,这也不稀奇,因为管道文件本身就是要有一个读端,一个写端,当读端和写端同时存在的时候才可以建立信道进行进行间通信,这里仅仅有一个读端,自然无法建立信道,所以这里的阻塞等待是很正常的现象。

那么接下来我们运行客户端,此时一瞬间服务端与客户端就打开了对应的命名管道文件,此时两个相关的进程处于同一个路径并且可以看到同一份资源,建立起了信道,同时我们写的回馈信息也被打印了。

那么接下来客户端与服务端就可以进行进程间通信了。

ctrl + c 退出客户端,此时写端关闭,自然而然的,服务端中的read就会返回0,表示已经读取到了文件结尾,写端已经关闭了,所以此时读端也会进行退出,关闭对应的命名管道文件,即关闭读端,此时进程间通信结束。

问题 :

1. 整个数据传输的过程中命名管道文件的文件描述符_fd起了什么作用?1. 1

整个数据传输,真正走的是内核里的管道缓冲区,而不是那个 p 类型的管道文件本身;

服务端先用 mkfifo 创建管道文件(./fifo),它只是文件系统里一个入口标记,不存数据。服务端和客户端都通过这个路径 open 打开这个管道文件,内核会为每个进程返回一个文件描述符 fd。从这一刻起,双方通信再也不碰管道文件,只通过 fd 和内核打交道:

  • 服务端调用 write(fd, ...) :内核通过 fd 找到对应的管道缓冲区,把数据写进去。
  • 客户端调用 read(fd, ...) :内核通过 fd 找到同一个缓冲区,把数据读出来。

所以管道文件 = 让双方找到同一条管道的"入口地址"。fd = 打开入口后,进程用来持续读写这条管道的唯一凭证。

2. Server.cc和Client.cc中最先定义的Fifo对象fifo是一个东西吗?

不是,真正传输数据的,是"文件标识符"fd (File Descriptor)。当我们创建了一个命名管道(比如 fifo),它是一个真实存在的管道文件。当你打开它,就会获得一个 fd(比如3 、4 、5 )。数据传输,是靠这个 fd 来定位和操作的。定义对象的名字,只是给我们自己看的。代码里叫它 fifo、my_fifo、server_fifo,这只是变量名。这个名字只在你的代码文件里有效,对操作系统、对管道文件本身没有任何影响。操作系统根本不管你代码里给这个对象起了什么名字,它只看你打开的是哪个文件路径。

3. 管道文件( ./fifo )里真的存数据吗?如果我用 cat 直接看它,能看到之前传输的内容吗?如果我把管道文件删掉了,正在通信的进程会立刻断开吗?为什么?

管道文件 ./fifo 本身并不存储数据,它只是文件系统里一个用于进程间"约定见面"的接口标识,真正存放传输数据的是内核维护的管道缓冲区。直接用 cat 等命令查看这个管道文件,看不到历史传输内容,因为数据不会落地到文件里,只会在内核缓冲区中临时传递,被读取后就会清空。即便中途删掉管道文件,已经建立连接并打开文件描述符的进程依然可以正常通信,因为内核缓冲区和文件描述符已经关联完成,文件系统层面的节点删除不影响已建立的内核通信通道。

4. 服务端先运行 Open(ForRead),客户端还没启动,服务端会卡在哪个函数里?为什么?客户端先运行 Open(ForWrite),服务端还没创建管道,客户端会报错吗?还是会阻塞?

服务端先以 ForRead 模式调用 Open 打开管道,若客户端未启动、没有进程以写方式打开该管道,服务端会阻塞在 open 函数处,直到有写端进程连接才会继续执行。客户端先以 ForWrite 模式运行 Open ,若服务端还未创建管道文件,客户端不会直接报错,而是会阻塞等待管道文件被创建并被读端进程打开,这是命名管道默认的阻塞打开机制,保证通信双方能顺利建立连接。

5. 服务端先退出,客户端还在 Send() ,会发生什么?内核会怎么处理?客户端先退出,服务端还在 Recv() ,为什么会返回 0?这个 0 代表什么含义?

服务端先退出,其对应的管道读端会被关闭,此时客户端继续调用 Send 执行 write 操作,内核会触发 SIGPIPE 信号,默认会直接终止客户端进程。客户端先退出,对应的写端关闭后,服务端阻塞的 Recv 函数中的 read 调用会返回0,这个0代表管道的写端全部关闭,没有数据可再读取,并非读取失败,服务端可据此判断客户端已断开连接,从而结束接收循环。
6. 如果有多个客户端同时打开同一个管道文件写数据,服务端能收到所有消息吗?数据会混乱吗?服务端能不能同时打开管道的读端和写端,自己给自己发消息?这样做有意义吗?

多个客户端同时打开同一个命名管道写入数据,服务端可以收到所有消息,但数据可能会出现混乱拼接的情况,因为内核会将多个进程的写入数据按顺序追加到缓冲区,没有进程间同步机制时容易出现消息交错。服务端可以同时以读、写模式打开管道自己给自己发数据,这种操作语法上可行,但没有实际的进程间通信意义,命名管道的设计初衷是用于不同进程间的数据传递,单进程内的自发自收完全可以用普通变量实现。

7. 匿名管道和命名管道阻塞特性的异同

匿名管道和命名管道的阻塞特性底层逻辑完全相同,无论是匿名管道还是命名管道,在读写数据时的阻塞规则是一致的:

  • 读阻塞:管道为空时,read 会阻塞等待数据写入。
  • 写阻塞:管道缓冲区满时,write 会阻塞等待数据读出。
  • 断连处理:当所有读端关闭后,写端继续写入会触发 SIGPIPE 信号,导致进程终止。

关键区别在于打开阶段 (open):

  1. 匿名管道通过 pipe() 系统调用一次性创建读、写两端,程序直接获得两个文件描述符,不存在 open 操作,打开阶段不会阻塞。
  2. -命名管道需要先通过 mkfifo() 创建管道文件,再由不同进程分别 open。 open 操作本身会阻塞:以只读 ( O_RDONLY ) 打开,会阻塞直到有进程以只写 ( O_WRONLY ) 打开。以只写 ( O_WRONLY ) 打开,会阻塞直到有进程以只读 ( O_RDONLY ) 打开。

一句话总结 : 匿名管道打开不阻塞,只在读写时卡;命名管道打开要等伴,读写也会卡。

三、匿名管带和命名管道对比

匿名管道和命名管道本质都是内核提供的进程间通信机制,底层数据传输、读写阻塞规则、断开处理逻辑完全相同,都遵循数据读完即删除、单向字节流通信的特点;二者最核心的区别在于使用场景和创建方式------ 匿名管道由 pipe() 一次性创建读写两端,只能用于有血缘关系(父子 / 兄弟)的进程间通信,且不生成磁盘文件、随进程结束自动释放,而命名管道通过 mkfifo() 创建磁盘管道文件,支持任意无关进程间通信,且在打开管道时会阻塞等待通信另一端连接。

四、总结

命名管道(FIFO)是一种解决非亲缘进程间通信的机制。与匿名管道不同,命名管道在文件系统中创建可见的管道文件(类型为p),任何进程都能通过路径访问。其核心原理是:多个进程打开同一个管道文件时共享内核缓冲区,通过各自的文件描述符进行读写操作。创建方式包括命令行mkfifo命令和程序中的mkfifo()函数。使用时需注意:默认阻塞打开(读端等待写端,反之亦然);数据传输仅在内核缓冲区进行,不写入磁盘;已建立的通信不受管道文件删除影响。命名管道实现了无关进程间的单向字节流通信,弥补了匿名管道只能用于亲缘进程的局限性。

谢谢大家的观看!

相关推荐
还是做不到嘛\.1 小时前
DVWA靶场-Brute Force
运维·服务器·数据库·学习
克莱因3582 小时前
linux主机名与Hosts映射 (顺带个DNS简介
linux·运维·服务器
kongba0073 小时前
OpenClaw v2026.3.23 安全配置复盘:从多处明文到集中受控存储《OpenClaw 安全部署 SOP(v2026.3.23)V2》
服务器·网络·安全
意疏3 小时前
【Linux 篇】Docker 容器星河与镜像灯塔:Linux 系统下解锁应用部署奇幻征程
linux·docker
朱包林3 小时前
k8s-Pod基础管理,标签管理,rc控制器及重启策略实战
linux·运维·云原生·容器·kubernetes·云计算
勇闯逆流河3 小时前
【Linux】linux进程概念(环境变量详解)
linux·运维·服务器
郭涤生4 小时前
CANopen 基础复习
服务器·网络·c++
normanhere4 小时前
H3C无线调优案例
网络
东芝、铠侠总代136100683934 小时前
从混合存储架构看SSD与HDD的互补性:技术特性决定应用场景
服务器·架构·ssd·hdd