当前位置: 首页 > news >正文

深入解析命名管道:原理、实现与进程间通信应用

目录

一、命名管道(Named Pipe)的原理与应用

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

命名管道的严格技术定义:

内核视角的二元性:

2、命名管道的工作原理

3、技术优势

4、命名管道的特性

5、与普通文件的区别

6、补充说明

示例场景

7、应用限制

二、使用命令创建命名管道

1、创建命名管道

2、命名管道的通信机制

实际操作示例

3、管道读端关闭的影响

4、补充说明

三、 创建命名管道详解

1、mkfifo 函数

2、参数说明

3、权限计算

4、返回值

5、完整创建示例

6、注意事项

7、扩展知识

四、命名管道的打开规则详解

1、读取端打开规则

2、写入端打开规则

3、错误处理建议

4、实际应用注意事项

5、典型应用场景

五、命名管道实现服务端与客户端通信详解

1、系统架构设计

通信流程

共享头文件(comm.h)

2、服务端实现(server.c)

3、客户端实现(client.c)

4、关键特性说明

进程关系

异常处理

5、进程关系和异常处理的验证

六、命名管道通信的内存机制详解

1、管道缓冲区本质上就是内核缓冲区!!!但与普通内核缓冲区有关键区别:(了解)

2、管道缓冲区状态机

3、内存通信的本质

文件系统视角

实际通信过程

4、实验现象分析

5、技术原理深入

内核缓冲区机制

6、与匿名管道的对比

7、实际应用启示

8、扩展知识:管道满的情况

七、使用命名管道实现计算任务派发

1、实现原理

2、服务端实现代码

3、客户端实现代码

4、公共头文件 comm.h

5、功能说明

6、编译与运行结果

八、使用命名管道实现进程间控制

1、概念与实现原理

2、服务端核心实现代码

3、客户端代码实现

1. read(0, buf, sizeof(buf)-1) 的减1

2. buf[s-1] = '\0' 的减1

4、实现效果

1. 基础进程间通信(IPC)

2. 远程命令执行

3. 安全隔离机制

4. 会话管理

5. 关键注意事项

九、使用命名管道实现文件拷贝

1、概述

2、实现原理

服务端功能

客户端功能

3、代码实现

公共头文件 (comm.h)

服务端代码 (server.c)

客户端代码 (client.c)

1. msg[0] = '\0' 的作用

(1)逻辑上清空缓冲区

(2)防止残留数据干扰

(3)和 read() 的关系

2. sizeof(msg)-1 的作用

(1)预留空间给 \0

(2)防止缓冲区溢出

(3)和 msg[0] = '\0' 的关系

4、使用说明

5、实际应用意义

十、命名管道与匿名管道的区别及命令行管道解析

1、管道的基本概念

2、创建与打开方式

3、核心区别

十一、命令行管道的本质分析

1、实例观察

2、管道类型判断(判断命令行中的管道("|")是匿名管道还是命名管道)

进程关系角度验证

总结

结论

3、补充说明


一、命名管道(Named Pipe)的原理与应用

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

        匿名管道(Anonymous Pipe)仅适用于具有亲缘关系的进程(如父子进程或兄弟进程)之间的通信。通常,一个进程创建管道后调用 fork(),其子进程继承管道的文件描述符,从而实现进程间通信。

        而命名管道(Named Pipe,又称 FIFO)通过文件系统中的路径名标识,允许任意两个进程(无论是否相关)进行通信。命名管道在文件系统中表现为一个特殊类型的文件,进程通过打开该文件进行读写操作,从而实现数据交互。

命名管道的严格技术定义:

不是普通文件,但是特殊类型的文件。更准确地说:

  • 不是存储文件:不占用磁盘数据块,没有实际数据存储

  • 是文件系统节点:在VFS(虚拟文件系统)中有完整的文件属性和操作接口

内核视角的二元性:

  • 文件表象:拥有inode、权限、时间戳等所有文件属性

  • 内核实质:数据流通完全在内核缓冲区完成(pipe_inode_info结构体)

2、命名管道的工作原理

命名管道是一种特殊的文件类型,其通信机制包含以下关键点:

  • 文件系统可见性:在文件系统中以特殊文件形式存在(如Linux系统中的FIFO文件)

  • 共享访问机制:不同进程通过打开同一个命名管道文件来访问共享资源

  • 内存驻留特性:与匿名管道类似,所有通信数据都驻留在内存中

  • 磁盘映像特性:在磁盘上仅保留一个空文件映像(大小为0),不实际存储通信数据

3、技术优势

相比普通文件,命名管道具有以下优势:

  • 实时性:数据直接在内核缓冲区中传递,无需磁盘I/O

  • 安全性:提供进程间同步机制,避免竞态条件

  • 原子性:保证读写操作的完整性

4、命名管道的特性

  • 文件系统可见性:命名管道在磁盘上有一个路径节点(如 /tmp/myfifo),但其数据始终驻留在内存中,磁盘上的文件大小恒为 0。

  • 跨进程通信:只要进程具有足够的权限,即可通过路径名打开同一个命名管道进行通信。

  • 半双工通信:默认情况下,命名管道是单向通信的(类似匿名管道),但可通过双向管道或两个管道实现全双工通信。

5、与普通文件的区别

普通文件虽然可以通过读写操作共享数据,但存在以下问题:

  • 实时性差:读写文件涉及磁盘 I/O,效率远低于内存操作。

  • 同步困难:需要额外机制(如文件锁)避免竞争条件。

  • 安全性风险:文件权限管理不当可能导致未授权访问。

命名管道通过内存缓冲区和内核调度机制,避免了上述问题,确保了高效、安全的进程间通信。

6、补充说明

  • 持久性:命名管道的文件节点会一直存在,直到被显式删除(如 unlink())。

  • 阻塞与非阻塞模式:默认情况下,打开命名管道会阻塞直到另一端也被打开,可通过 O_NONBLOCK 标志设置为非阻塞模式。

示例场景

        进程 A 创建命名管道 /tmp/data_pipe 并写入数据,进程 B 从同一管道读取数据。即使两者由不同用户启动,只要权限允许,通信即可完成。 

7、应用限制

需要注意的是:

  • 命名管道不支持随机访问(遵循先进先出原则)

  • 通信数据不持久化(进程退出后数据丢失)

  • 需要正确处理打开/关闭时序以避免阻塞

        这种设计使命名管道成为本地进程间通信(IPC)的高效解决方案,特别适用于需要松耦合通信的场景。


二、使用命令创建命名管道

1、创建命名管道

        在Linux系统中,我们可以使用mkfifo命令创建一个命名管道。命名管道(Named Pipe)是一种特殊的文件类型,也称为FIFO(First In First Out),它允许不相关的进程通过文件系统进行通信。

mkfifo fifo

        执行上述命令后,使用ls -l查看文件属性时,可以看到文件类型标记为p,这表示该文件是一个命名管道文件:

2、命名管道的通信机制

命名管道可以实现两个独立进程之间的通信。下面是一个典型的应用场景:

  1. 进程A(写入端):使用shell脚本每秒向命名管道写入一个字符串

  2. 进程B(读取端):使用cat命令从命名管道中读取数据

实际操作示例

  1. 首先在终端1中启动读取进程:

    cat < fifo
  2. 然后在终端2中启动写入进程:

while :; do echo "hello fifo"; sleep 1; done > fifo

        此时可以观察到,终端1会每秒显示一个"hello fifo"字符串,这证明两个独立进程通过命名管道成功实现了通信:

3、管道读端关闭的影响

        正如匿名管道的行为特性,命名管道也具有类似的特性:当管道的读端进程退出后,写端进程继续写入数据将失去意义。在这种情况下:

  1. 如果终止读取端(终端1中的cat进程)

  2. 写入端(终端2中的循环脚本)会收到SIGPIPE信号

  3. 因为该持续输入的脚本是由bash解释执行的,bash进程会被操作系统终止

  4. 最终导致当前会话结束,写端的云服务器的登录也就退出了。

        这一现象验证了操作系统对管道通信的管理机制:当通信的另一端不存在时,系统会终止无意义的写入操作,避免资源浪费。

4、补充说明

命名管道与匿名管道的主要区别在于:

  1. 命名管道在文件系统中有实体文件,可用于无关进程间通信

  2. 匿名管道只能用于有亲缘关系的进程间通信

  3. 命名管道使用文件系统路径名作为标识,而匿名管道使用文件描述符

命名管道在实际系统中有广泛应用,如日志收集、进程间数据交换等场景。


三、 创建命名管道详解

        命名管道(Named Pipe)是一种特殊的文件类型,允许不相关的进程通过文件系统进行通信。下面详细介绍如何在程序中创建和使用命名管道。

1、mkfifo 函数

mkfifo 函数用于创建命名管道,其原型如下:

#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);

2、参数说明

  1. pathname:指定要创建的命名管道文件路径

    • 如果包含路径(如"/tmp/myfifo"),则管道将在指定目录创建

    • 如果只有文件名(如"myfifo"),则管道将在当前工作目录创建

  2. mode:指定管道的访问权限(八进制表示)

    • 例如 0666 表示所有者、组和其他用户都有读写权限

    • 实际权限会受到 umask 值的影响

3、权限计算

umask

实际创建的管道权限计算公式为:mode & (~umask)

  • 典型 umask 值为 0002(屏蔽其他用户的写权限)

  • 设置 mode=0666 时,实际权限为 0664rw-rw-r--

要确保精确的权限设置,可以在创建前修改 umask

umask(0); // 临时清除所有权限掩码

4、返回值

  • 成功时返回 0

  • 失败时返回 -1,并设置 errno

5、完整创建示例

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>#define FIFO_NAME "myfifo"int main() 
{// 清除权限掩码以确保精确权限设置umask(0);// 尝试创建命名管道if (mkfifo(FIFO_NAME, 0666) < 0) {perror("mkfifo创建失败");return 1;}printf("命名管道 %s 创建成功\n", FIFO_NAME);// 程序退出后管道文件仍会保留在文件系统中// 实际应用中需要记得在不再需要时删除return 0;
}

编译并运行代码后,命名管道myfifo就在当前路径下被创建了:

6、注意事项

  1. 命名管道创建后会持久存在于文件系统中,直到被显式删除

  2. 多个进程可以同时打开管道进行读写,但通常用于单向通信

  3. 打开管道的进程会阻塞,直到另一端也被打开

  4. 使用完毕后应删除管道文件:unlink(FIFO_NAME)

  5. 如果在命令行中操作的话,也可以使用命令:

    unlink 命名管道

7、扩展知识

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

  • 匿名管道只能用于有亲缘关系的进程间通信

  • 命名管道可用于任意进程间的通信

  • 命名管道在文件系统中有可见的入口点

在实际应用中,命名管道常用于客户端-服务器模式的进程通信场景。


四、命名管道的打开规则详解

        命名管道(FIFO)的打开行为与其他文件类型有所不同,其操作会受到打开模式和非阻塞标志的影响。以下是命名管道的详细打开规则:

1、读取端打开规则

  1. 阻塞模式(O_NONBLOCK未设置)

    • 打开操作将阻塞,直到至少有一个进程以写入方式打开同一个FIFO

    • 这种设计确保了读取端不会在没有写入端的情况下无限等待数据

    • 典型应用场景:确保通信双方都已就绪才开始数据传输

  2. 非阻塞模式(O_NONBLOCK设置)

    • 无论是否有写入端打开FIFO,读取端都会立即成功返回

    • 后续的读取操作将遵循非阻塞I/O的语义

    • 典型应用场景:需要轮询或非阻塞检查FIFO状态的程序

2、写入端打开规则

  1. 阻塞模式(O_NONBLOCK未设置)

    • 打开操作将阻塞,直到至少有一个进程以读取方式打开同一个FIFO

    • 这种设计避免了写入端向没有读取端的FIFO写入数据

    • 典型应用场景:确保通信对端已准备好接收数据

  2. 非阻塞模式(O_NONBLOCK设置)

    • 如果没有读取端打开FIFO,写入端打开操作将立即失败

    • 返回错误码ENXIO(表示"没有这样的设备或地址")

    • 典型应用场景:需要快速失败检查的应用程序

3、错误处理建议

#include <fcntl.h>
#include <errno.h>
#include <stdio.h>int main()
{int fd;// 非阻塞方式打开写入端fd = open("myfifo", O_WRONLY | O_NONBLOCK);if (fd == -1) {if (errno == ENXIO) {printf("错误:没有读取端打开FIFO\n");} else {perror("打开FIFO失败");}return -1;}else{printf("打开FIFO成功");}
}

编译运行结果: 

4、实际应用注意事项

  1. 双向通信:虽然FIFO可以同时以读写方式打开,但通常建议使用两个FIFO实现双向通信

  2. 进程同步:打开操作的阻塞特性天然提供了进程同步机制

  3. 超时处理:在阻塞模式下,可考虑结合信号或定时器实现超时控制

  4. 资源清理:确保进程退出前正确关闭FIFO文件描述符

5、典型应用场景

// 进程A(读取端)
int fd = open("myfifo", O_RDONLY);  // 阻塞等待写入端
// ...读取操作...// 进程B(写入端)
int fd = open("myfifo", O_WRONLY);  // 阻塞等待读取端
// ...写入操作...

        理解这些打开规则对于开发可靠的进程间通信程序至关重要,特别是在需要精确控制进程启动顺序的系统中。


五、命名管道实现服务端与客户端通信详解

        命名管道(FIFO)是一种进程间通信(IPC)机制,允许不相关的进程通过文件系统进行数据交换。下面详细介绍如何使用命名管道实现服务端(server)和客户端(client)之间的通信。

1、系统架构设计

通信流程

  1. 服务端创建并打开命名管道(读取端)

  2. 客户端打开已存在的命名管道(写入端)

  3. 客户端写入数据,服务端读取数据

  4. 通信完成后双方关闭管道

共享头文件(comm.h)

#ifndef _COMM_H_
#define _COMM_H_#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>#define FILE_NAME "myfifo"  // 共享的命名管道文件名
#define BUFFER_SIZE 128     // 通信缓冲区大小#endif

2、服务端实现(server.c)

#include "comm.h"int main() 
{// 设置文件创建权限掩码umask(0);// 创建命名管道if (mkfifo(FILE_NAME, 0666) < 0) {if (errno != EEXIST) {  // 忽略已存在的错误perror("mkfifo");return 1;}}// 以只读方式打开命名管道(阻塞模式)int fd = open(FILE_NAME, O_RDONLY);if (fd < 0) {perror("open");return 2;}char buffer[BUFFER_SIZE];while (1) {memset(buffer, 0, sizeof(buffer));  // 清空缓冲区// 从管道读取数据ssize_t s = read(fd, buffer, sizeof(buffer)-1);if (s > 0) {// 成功读取数据printf("[Client]# %s\n", buffer);} else if (s == 0) {// 客户端关闭printf("Client disconnected!\n");break;} else {// 读取错误perror("read");break;}}close(fd);  // 关闭管道unlink(FILE_NAME);  // 删除管道文件return 0;
}

3、客户端实现(client.c)

#include "comm.h"int main() 
{// 以只写方式打开命名管道(阻塞模式)int fd = open(FILE_NAME, O_WRONLY);if (fd < 0) {perror("open");return 1;}char buffer[BUFFER_SIZE];while (1) {printf("Please Enter> ");fflush(stdout);// 从标准输入读取数据ssize_t s = read(0, buffer, sizeof(buffer)-1);if (s > 0) {buffer[s-1] = '\0';  // 去掉换行符// 写入命名管道if (write(fd, buffer, strlen(buffer)) < 0) {perror("write");break;}} else {perror("read");break;}}close(fd);  // 关闭管道return 0;
}

4、关键特性说明

进程关系

  • 服务端和客户端是完全独立的进程(PID和PPID不同)

  • 命名管道实现了无亲缘关系进程间的通信

异常处理

  1. 客户端退出:服务端read返回0,检测到客户端断开

  2. 服务端退出

    • 客户端继续写入会触发SIGPIPE信号(错误号EPIPE)

    • 典型处理方式:捕获信号或检查write返回值

5、进程关系和异常处理的验证

        代码编写完毕后,先将服务端进程运行起来,之后我们就能在客户端看到这个已经被创建的命名管道文件:

        接着再将客户端也运行起来,此时我们从客户端写入的信息被客户端写入到命名管道当中,服务端再从命名管道当中将信息读取出来打印在服务端的显示器上,该现象说明服务端是能够通过命名管道获取到客户端发来的信息的,换句话说,此时这两个进程之间是能够通信的:

        当客户端和服务端运行起来时,我们还可以通过ps命令查看这两个进程的信息,可以发现这两个进程确实是两个毫不相关的进程,因为它们的PID和PPID都不相同。也就证明了,命名管道是可以实现两个毫不相关进程之间的通信的:

        当客户端退出后,服务端将管道当中的数据读完后就再也读不到数据了,那么此时服务端也就会去执行它的其他代码了(在当前代码中是直接退出了):

        当服务端退出后,客户端写入管道的数据就不会被读取了,也就没有意义了,那么当客户端下一次再向管道写入数据时,就会收到操作系统发来的13号信号(SIGPIPE)(信号后面会讲到),此时客户端就被操作系统强制杀掉了。

首先结束服务端:

客户端再一次输入haha,先不按回车: 

按回车,然后发现客户端退出: 


六、命名管道通信的内存机制详解

        命名管道(FIFO)的通信过程完全发生在内存中,这一特性是理解其工作原理的关键。下面我们将深入分析这一机制。

1、管道缓冲区本质上就是内核缓冲区!!!但与普通内核缓冲区有关键区别:(了解)

虽然同属内核内存,但管道缓冲区有独特设计:

特性管道缓冲区普通文件缓冲区
存储位置独立环形队列页缓存(page cache)
生命周期随管道销毁释放受LRU算法管理
同步机制等待队列+自旋锁地址空间操作(address_space)
最大容量严格受限(1MB默认)可占用大部分内存
数据一致性实时同步可能延迟回写

2、管道缓冲区状态机

状态读取行为写入行为
阻塞立即执行
部分满立即返回立即执行
全满立即返回阻塞
无写入端返回EOF触发SIGPIPE

3、内存通信的本质

文件系统视角

  • 表象:命名管道在文件系统中有一个可见的节点(创建命名管道后,可通过ll命令查看)

  • 实质:这个"文件"只是一个通信端点,不实际存储数据

实际通信过程

  1. 数据流向

    • 写入进程 → 内核缓冲区 → 读取进程

    • 数据始终在内核管理的缓冲区中流动

  2. 存储介质

    • 完全不涉及磁盘I/O操作

    • 即使系统重启,未读取的管道数据也会丢失

4、实验现象分析

修改后的服务端代码

// server.c (不读取数据的版本)
#include "comm.h"int main() 
{umask(0);if (mkfifo(FILE_NAME, 0666) < 0) {perror("mkfifo");return 1;}int fd = open(FILE_NAME, O_RDONLY);if (fd < 0) {perror("open");return 2;}while (1) { // 故意不读取任何数据sleep(1); // 避免CPU空转}close(fd);return 0;
}

观察到的现象

  1. 客户端可以持续写入数据

  2. ll命令显示管道文件大小始终为0

  3. 系统内存使用量无明显变化(直到达到管道缓冲区上限)

5、技术原理深入

内核缓冲区机制

  1. 默认缓冲区大小

    • Linux系统中通常为64KB(可通过ulimit -p查看),单位为KB

    • 达到上限后写入操作会阻塞

  2. 缓冲管理

    # 查看系统管道缓冲区大小
    cat /proc/sys/fs/pipe-max-size

6、与匿名管道的对比

特性命名管道匿名管道
文件系统可见性
通信范围任意进程有亲缘关系进程
缓冲区管理相同的内核机制相同的内核机制
最大容量受系统限制相同受系统限制相同

7、实际应用启示

  1. 性能考量

    • 内存通信提供极高的吞吐量

    • 适合高频、小数据量的进程间通信

  2. 可靠性注意

    • 系统崩溃会导致未传递数据丢失

    • 不适合需要持久化的关键数据传输

  3. 容量监控

    // 获取管道缓冲区可用空间(Linux特有)
    int size;
    ioctl(fd, FIONREAD, &size);

8、扩展知识:管道满的情况

当管道缓冲区满时:

  • 写入进程会被阻塞(默认行为)

  • 或返回EAGAIN错误(非阻塞模式)

  • 可通过select/poll监控管道可写状态

        这种内存通信机制使得命名管道成为进程间通信的高效解决方案,特别适合需要低延迟的本地进程通信场景。


七、使用命名管道实现计算任务派发

        命名管道(FIFO)是一种进程间通信(IPC)机制,允许不相关的进程通过文件系统进行数据交换。下面我们将通过一个计算任务派发的例子,展示如何使用命名管道实现客户端-服务端模式的进程间通信。

1、实现原理

        在这个例子中,客户端进程通过命名管道向服务端发送包含两个操作数和运算符的计算请求,服务端接收请求后执行相应的计算并返回结果。这种模式体现了典型的请求-响应交互方式。

2、服务端实现代码

// server.c
#include "comm.h"int main()
{umask(0); // 将文件默认掩码设置为0,确保创建的管道具有正确的权限// 使用mkfifo创建命名管道文件,权限设置为0666(可读可写)if (mkfifo(FILE_NAME, 0666) < 0) {perror("mkfifo");return 1;}// 以只读方式打开命名管道文件int fd = open(FILE_NAME, O_RDONLY);if (fd < 0) {perror("open");return 2;}char msg[128];const char* operators = "+-*/%"; // 支持的运算符while (1) {msg[0] = '\0'; // 每次读取前清空消息缓冲区// 从命名管道中读取客户端发送的消息ssize_t s = read(fd, msg, sizeof(msg)-1);if (s > 0) {msg[s] = '\0'; // 手动添加字符串结束符printf("Received task: %s\n", msg); // 打印接收到的原始任务// 解析计算任务char* p = msg;int op_index = -1;// 识别运算符while (*p) {switch (*p) {case '+': op_index = 0; break;case '-': op_index = 1; break;case '*': op_index = 2; break;case '/': op_index = 3; break;case '%': op_index = 4; break;}if (op_index != -1) break; // 找到运算符后立即退出循环p++;}if (op_index == -1) {printf("Error: Invalid operator in task '%s'\n", msg);continue;}// 分割操作数char* operand1_str = strtok(msg, operators);char* operand2_str = strtok(NULL, operators);if (!operand1_str || !operand2_str) {printf("Error: Invalid operands in task '%s'\n", msg);continue;}int num1 = atoi(operand1_str);int num2 = atoi(operand2_str);//将字符串转换为整数,转换过程会:跳过前面的空白字符(空格、制表符等)/读取可选的正负号(+ 或 -)/读取连续的数字字符直到遇到非数字字符为止int result = 0;// 执行计算switch (op_index) {case 0: result = num1 + num2; break;case 1: result = num1 - num2; break;case 2: result = num1 * num2; break;case 3: if (num2 == 0) {printf("Error: Division by zero\n");continue;}result = num1 / num2; break;case 4: if (num2 == 0) {printf("Error: Modulo by zero\n");continue;}result = num1 % num2; break;}// 打印计算结果printf("Calculation result: %d %c %d = %d\n", num1, operators[op_index], num2, result);}else if (s == 0) {printf("Client disconnected.\n");break;}else {perror("read");break;}}close(fd); // 关闭命名管道文件unlink(FILE_NAME); // 删除命名管道文件return 0;
}

3、客户端实现代码

以下是配合服务端使用的客户端代码,负责向服务端发送计算任务请求:

// client.c
#include "comm.h"
#include <string.h>int main()
{// 以只写方式打开命名管道文件int fd = open(FILE_NAME, O_WRONLY);if (fd < 0) {perror("open");return 1;}char msg[128];printf("Enter calculation tasks (e.g., 3+5, 10*2). Enter 'quit' to exit.\n");while (1) {printf("> ");fflush(stdout);// 读取用户输入if (fgets(msg, sizeof(msg), stdin) == NULL) {break; // 读取失败或EOF}// 去除换行符msg[strcspn(msg, "\n")] = '\0';//用于计算字符串中 不包含 指定字符集中任何字符的 初始段的最大长度。// 检查退出命令if (strcmp(msg, "quit") == 0) {break;}// 验证输入格式int valid = 0;char* operators = "+-*/%";for (char* p = msg; *p; p++) {if (strchr(operators, *p) != NULL) {//strchr用于 查找字符串中第一次出现的指定字符。valid = 1;break;}}if (!valid) {printf("Error: Invalid format. Please include an operator (+, -, *, /, %%)\n");continue;}// 向服务端发送计算任务ssize_t s = write(fd, msg, strlen(msg));if (s < 0) {perror("write");break;}}close(fd); // 关闭命名管道文件return 0;
}

4、公共头文件 comm.h

// comm.h
#ifndef _COMM_H_
#define _COMM_H_#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>#define FILE_NAME "myfifo" // 命名管道文件名#endif

5、功能说明

  1. 用户交互

    • 客户端提供交互式界面,提示用户输入计算表达式

    • 支持输入形如"3+5"、"10*2"这样的简单算术表达式

    • 输入"quit"可退出程序

  2. 输入验证

    • 检查输入是否包含有效的运算符(+, -, *, /, %)

    • 提供友好的错误提示信息

  3. 通信机制

    • 以只写方式打开与服务端相同的命名管道

    • 将用户输入的计算表达式发送给服务端

  4. 退出处理

    • 正确处理用户退出命令

    • 关闭文件描述符,释放资源

6、编译与运行结果

        这种实现方式展示了如何使用命名管道进行简单的进程间通信和任务派发,可以根据需要进一步扩展功能,如支持更复杂的表达式或添加结果返回机制。


八、使用命名管道实现进程间控制

1、概念与实现原理

        命名管道(FIFO)是一种特殊的文件类型,它允许无亲缘关系的进程通过文件系统进行通信。我们可以利用这一特性实现一个有趣的进程控制机制:客户端进程通过向命名管道写入命令,服务端进程从管道读取并执行这些命令,从而实现远程控制的效果。

2、服务端核心实现代码

#include "comm.h"int main()
{umask(0); // 将文件默认掩码设置为0,确保创建的管道有正确的权限// 创建命名管道文件,权限设置为0666(所有用户可读可写)if (mkfifo(FILE_NAME, 0666) < 0) {perror("mkfifo");return 1;}// 以只读方式打开命名管道文件int fd = open(FILE_NAME, O_RDONLY);if (fd < 0) {perror("open");return 2;}char msg[128];while (1) {msg[0] = '\0'; // 清空消息缓冲区// 从命名管道读取命令ssize_t s = read(fd, msg, sizeof(msg)-1);if (s > 0) {msg[s] = '\0'; // 添加字符串终止符printf("Received command: %s\n", msg);// 创建子进程执行命令if (fork() == 0) {// 子进程执行命令execlp(msg, msg, NULL); // 使用PATH环境变量查找命令exit(1); // 如果exec失败则退出}waitpid(-1, NULL, 0); // 父进程等待子进程完成}else if (s == 0) {printf("Client disconnected.\n");break;}else {perror("read");break;}}close(fd); // 关闭管道文件return 0;
}

3、客户端代码实现

客户端负责向命名管道写入要执行的命令:

#include "comm.h"
#include <string.h>int main()
{// 以只写方式打开命名管道文件int fd = open(FILE_NAME, O_WRONLY);if (fd < 0) {perror("open");return 1;}char buf[128];while (1) {printf("Please enter command# ");fflush(stdout);// 从标准输入读取命令ssize_t s = read(0, buf, sizeof(buf)-1);if (s > 0) {buf[s-1] = '\0'; // 去掉换行符if (strlen(buf) == 0) {continue; // 忽略空命令}// 将命令写入命名管道write(fd, buf, strlen(buf));if (strcmp(buf, "quit") == 0) {printf("Client exit.\n");break;}}}close(fd); // 关闭管道文件return 0;
}

1. read(0, buf, sizeof(buf)-1) 的减1

作用:保留一个字节的空间给字符串终止符 \0

原因

  • C语言中字符串必须以 \0 结尾

  • buf 的大小是128字节,但实际最多只读127字节

  • 保留最后一个字节的空间,确保后续可以安全地添加 \0

  • 防止缓冲区溢出(防御性编程)

如果不减1
当输入正好填满128字节时,没有空间放 \0,会导致字符串操作(如strcmp)出现未定义行为(内存越界访问)。

2. buf[s-1] = '\0' 的减1

作用:去掉从终端输入时自带的换行符(\n

原因

  • 当用户在客户端输入命令后按回车,终端会添加 \n 字符

  • read() 会将这个换行符一起读入缓冲区

  • 例如输入 ls 实际得到的是 "ls\n"

4、实现效果

1. 基础进程间通信(IPC)

  • 通过命名管道实现了两个无亲缘关系进程间的单向通信

  • 客户端进程可以向服务端进程发送任意Linux命令

2. 远程命令执行

  • 服务端能接收并执行客户端发送的命令

  • 示例支持的命令包括:

    • 文件操作:lscatmkdir

    • 系统信息:datewhoamiuname

    • 文本处理:echogrep

    • 进程控制:killps

3. 安全隔离机制

  • 每个命令都在服务端的子进程中执行(通过fork()

  • 使用waitpid()确保命令执行完成后再接收下一条命令

  • 错误隔离:单个命令执行失败不会影响服务端主进程

4. 会话管理

  • 支持多命令连续执行(交互式会话)

  • 通过quit命令实现优雅退出

  • 客户端断开连接时服务端能自动检测(read返回0)

5. 关键注意事项

  1. 必须按顺序启动

    • 先启动服务端(创建管道)

    • 再启动客户端(连接管道)

  2. 管道文件残留问题

    如果异常退出,可能需要手动删除管道文件:

    rm myfifo
  3. 命令限制

    • 当前版本不支持带参数的命令(如 ls -l

    • 输入 quit 可退出客户端

  4. 执行结果

    • 命令输出显示在服务端的终端

    • 客户端只负责发送命令


九、使用命名管道实现文件拷贝

1、概述

        介绍如何使用命名管道(Named Pipe)在本地实现文件拷贝功能。命名管道是一种特殊的文件类型,允许不相关的进程进行通信。在这个示例中,我们将创建一个客户端-服务端模型,通过命名管道将file.txt的内容传输到file-bat.txt

2、实现原理

服务端功能

  1. 创建命名管道文件

  2. 以只读方式打开管道

  3. 创建目标文件file-bat.txt

  4. 从管道读取数据并写入目标文件

  5. 处理传输完成或错误情况

客户端功能

  1. 以只写方式打开已存在的命名管道

  2. 打开源文件file.txt进行读取

  3. 将源文件内容写入命名管道

  4. 处理文件结束或错误情况

3、代码实现

公共头文件 (comm.h)

#pragma once#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <fcntl.h>#define FILE_NAME "myfifo" // 客户端和服务端共用的命名管道名称

服务端代码 (server.c)

#include "comm.h"int main()
{umask(0); // 将文件默认掩码设置为0,确保创建的管道权限正确// 创建命名管道文件,权限设置为0666(可读可写)if (mkfifo(FILE_NAME, 0666) < 0) {perror("mkfifo");return 1;}// 以只读方式打开命名管道文件int fd = open(FILE_NAME, O_RDONLY);if (fd < 0) {perror("open");return 2;}// 创建目标文件file-bat.txt,并以只写方式打开int fdout = open("file-bat.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);if (fdout < 0) {perror("open");return 3;}char msg[128];while (1) {msg[0] = '\0'; // 每次读取前清空缓冲区// 从命名管道读取数据ssize_t s = read(fd, msg, sizeof(msg)-1);if (s > 0) {// 将读取到的数据写入目标文件write(fdout, msg, s);}else if (s == 0) {printf("客户端传输完成,连接关闭!\n");break;}else {perror("读取错误");break;}}close(fd);    // 关闭命名管道close(fdout); // 关闭目标文件return 0;
}

客户端代码 (client.c)

#include "comm.h"int main()
{// 以只写方式打开命名管道文件int fd = open(FILE_NAME, O_WRONLY);if (fd < 0) {perror("open");return 1;}// 以只读方式打开源文件file.txtint fdin = open("file.txt", O_RDONLY);if (fdin < 0) {perror("open");return 2;}char msg[128];while (1) {// 从源文件读取数据ssize_t s = read(fdin, msg, sizeof(msg));if (s > 0) {// 将读取到的数据写入命名管道write(fd, msg, s);}else if (s == 0) {printf("文件读取完成!\n");break;}else {perror("读取错误");break;}}close(fd);   // 关闭命名管道close(fdin); // 关闭源文件return 0;
}

1. msg[0] = '\0' 的作用

(1)逻辑上清空缓冲区
  • 执行 msg[0] = '\0' 后,msg 被视为空字符串(因为字符串以 \0 结尾)。

  • 但物理上,msg 的其他字节未被修改(可能是随机值)。

(2)防止残留数据干扰
  • 如果后续代码错误地将 msg 当作字符串处理(如 printf("%s", msg)),\0 能保证不会读取到垃圾数据。

(3)和 read() 的关系
  • read() 会从 msg[0] 开始覆盖数据,msg[0] = '\0' 会被覆盖

  • 它的作用主要是初始化缓冲区状态,而非直接影响 read()

2. sizeof(msg)-1 的作用

(1)预留空间给 \0
  • 让 read() 最多读取 sizeof(msg)-1 字节,保留 1 字节用于手动添加 \0(如 msg[s] = '\0')。

  • 示例

    char msg[5];
    read(fd, msg, sizeof(msg)-1);  // 最多读 4 字节
    msg[4] = '\0';                 // 手动补 \0(如果需字符串操作)
(2)防止缓冲区溢出
  • 如果 read() 尝试读取 sizeof(msg) 字节且数据正好填满缓冲区,msg 会没有空间存储 \0

  • 危险示例

    char msg[5];
    read(fd, msg, sizeof(msg));    // 读取 5 字节 "ABCDE"
    printf("%s", msg);             // 崩溃!因为 msg 没有 \0 结尾
(3)和 msg[0] = '\0' 的关系
  • sizeof(msg)-1 是读取时的保护msg[0] = '\0' 是初始化时的保护

  • 二者配合可确保:

    1. 即使 read() 未填满缓冲区,msg 也以 \0 结尾(因初始化时设置了 msg[0] = '\0')。

    2. 如果 read() 填满缓冲区(读取 sizeof(msg)-1 字节),仍有 1 字节空间手动补 \0

4、使用说明

  1. 首先编译两个程序:

    gcc server.c -o server
    gcc client.c -o client
  2. 准备源文件file.txt,内容任意

  3. 运行程序:

    • 先运行服务端:./server

    • 再运行客户端:./client

  4. 程序运行完成后,会生成file-bat.txt文件,内容与file.txt相同

5、实际应用意义

虽然在本地使用管道进行文件拷贝看似没有太大实用价值,但这个模型具有重要的教学意义:

  1. 网络传输模型:可以将其视为网络文件传输的简化模型

    • 命名管道相当于网络连接

    • 客户端相当于上传工具(如Xshell)

    • 服务端相当于远程服务器

  2. 理解进程间通信:展示了不相关进程如何通过文件系统进行通信

  3. 文件传输原理:演示了基本的文件传输机制,包括:

    • 分块读取

    • 数据传输

    • 分块写入

  4. 扩展性:此模型可以扩展为真正的网络文件传输程序,只需将管道替换为网络套接字

        通过这个简单的例子,我们可以更好地理解更复杂的文件传输协议(如FTP)和网络通信的基本原理。


十、命名管道与匿名管道的区别及命令行管道解析

1、管道的基本概念

        管道(Pipe)是Unix/Linux系统中进程间通信的一种基本机制,分为两种类型:匿名管道(无名管道)和命名管道(有名管道,FIFO)。

2、创建与打开方式

匿名管道

  • pipe()函数创建并直接打开

  • 存在于内核缓冲区,不占用磁盘空间

  • 没有实体文件与之对应

命名管道

  • mkfifo()函数创建

  • 通过open()函数显式打开

  • 在文件系统中有一个对应的节点(管道文件)

  • 具有实际的磁盘表示(虽然不存储实际数据)

3、核心区别

        FIFO(命名管道)与pipe(匿名管道)的主要区别仅在于它们的创建与打开方式不同。一旦完成这些初始操作后,它们在数据传输语义上是完全相同的。


十一、命令行管道的本质分析

1、实例观察

现有data.txt文件,内容如下:

我们可以利用管道(“|”)同时使用cat命令和grep命令,进而实现文本过滤:

cat data.txt | grep haha

2、管道类型判断(判断命令行中的管道("|")是匿名管道还是命名管道)

进程关系角度验证

  1. 创建管道连接的三个进程
    执行以下命令,通过管道(|)连接三个 sleep 进程:

    sleep 100 | sleep 200 | sleep 300
  2. 查看进程信息
    使用 ps axj 查看进程列表,结合 head 和 grep 筛选出目标进程信息:

    ps axj | head -1 && ps axj | grep sleep | grep -v grep

    输出结果

    • 三个 sleep 进程的父进程 ID(PPID)均为242937,说明它们由同一个父进程创建。

    • 进程组 ID(PGID)相同(246211),表示它们属于同一个进程组。

  3. 查看父进程信息
    通过 ps 命令查看父进程(PID 为 242937)的详细信息:

    ps 242937

    输出结果

  • 父进程是 bash(命令行解释器),说明这三个 sleep 进程是由当前 Shell 创建的。

总结

  • 管道命令sleep 100 | sleep 200 | sleep 300 创建了三个并行执行的 sleep 进程,通过管道连接。

  • 进程关系:三个子进程的父进程均为 bash,且属于同一进程组。

  • 验证方法:通过 ps 命令结合 grep 筛选进程信息,确认父子关系和进程组关系。

判断命令行中的管道("|")是匿名管道还是命名管道,可以从以下两个角度分析:

  1. 进程关系角度

    • 匿名管道只能用于有亲缘关系的进程间通信

    • 命名管道可用于任意无关进程间通信

    • 通过管道连接的多个进程(如catgrep等)具有相同的PPID(父进程ID)

    • 这些进程实际上是由shell(如bash)创建的兄弟进程

  2. 文件系统角度:

    • 命名管道需要在文件系统中存在对应的管道文件

    • 使用命令行管道时,并没有可见的管道文件创建

    • 管道数据仅存在于内存中,不涉及磁盘存储

结论

基于以上分析可以确定:命令行中使用的管道符号"|"实现的是匿名管道。这是因为:

  1. 连接的进程具有亲缘关系(同为shell的子进程)

  2. 没有对应的命名管道文件出现在文件系统中

  3. 完全符合匿名管道的使用场景和特征

3、补充说明

        虽然命令行管道是匿名管道,但shell在实现时可能会根据具体情况优化。例如,在某些复杂管道命令或多重管道情况下,shell可能会临时使用命名管道作为实现手段,但这属于实现细节,对用户透明。在日常使用中,我们完全可以将其视为匿名管道的应用。 

http://www.lryc.cn/news/600862.html

相关文章:

  • 大型微服务项目:听书——12 数据一致性自定义starter封装缓存操作
  • 2025年全国青少年信息素养大赛Scratch算法创意实践挑战赛 小低组 初赛 真题
  • Fast_Lio 修改激光雷达话题
  • C++核心编程学习--对象特性--对象模型和this指针
  • 在C#中判断两个列表数据是否相同
  • 服务器:数字世界的隐形引擎
  • C++ - 仿 RabbitMQ 实现消息队列--服务端核心模块实现(四)
  • 「iOS」————继承链与对象的结构
  • 数据结构 二叉树(3)---层序遍历二叉树
  • 系统性提升大模型回复准确率:从 RAG 到多层 Chunk 策略
  • 机器学习特征工程:特征选择及在医学影像领域的应用
  • 【AI】联网模式
  • odoo代码分析(二)
  • idea中无法删除模块,只能remove?
  • 建筑施工场景下漏检率↓76%!陌讯多模态融合算法在工程安全监控的落地实践
  • 三防平板搭载2D扫描头:工业数据采集的革新利器
  • python—————knn算法
  • 【图像分割】记录1:unet, yolov8_seg
  • 嵌入式分享#27:原来GT911有两个I2C地址(全志T527)
  • 深度学习损失函数的设计哲学:从交叉熵到Huber损失的深入探索
  • Spring AI Alibaba Video 示例
  • 阿里开源Qwen3-Coder,编程大模型进入高效时代
  • Go语言unsafe包深度解析
  • 机器学习入门:线性回归详解与实战
  • 高效无损压缩方案:轻松批量处理图片,节省存储空间
  • Python编程:初入Python魔法世界
  • 基于cooragent的旅游多智能体的MCP组件安装与其开发
  • 用Java实现rpc的逻辑和流程图和核心技术与难点分析
  • Android中ViewStub和View有什么区别?
  • 洛谷 P1226 【模板】快速幂-普及-