目录
一、命名管道是什么
命名管道就像一个特殊的“信封”。我们平时用的文件,就像是存储在硬盘上的“宝盒”,里面可以装各种各样的数据。而命名管道呢,它有点不一样。它更像是住在内存里的一个“小精灵”。虽然它在硬盘上也有一个小小的“家”,但这个“家”其实只是个标记,大小永远是0。因为这个“小精灵”真正工作的时候,数据都是在内存里快速地跑来跑去,不会存到硬盘里。
这就意味着,命名管道能让两个进程像朋友一样聊天。普通的文件虽然也能让进程之间交流,但不太安全。比如,一个进程写的信息,另一个进程可以随意修改,这样就可能出问题。而命名管道和匿名管道不一样,它们是内存文件,数据都存在电脑的内存里,就像把信息放在一个快速传递的信封里,不会把数据真正写到硬盘上。
所以,命名管道就像是一个能让两个进程快速、安全交流的神奇工具。它让那些没有亲缘关系的进程也能轻松地分享信息,完成各种任务。
二、创建命名管道
要创建命名管道,最简单的方法就是使用 mkfifo
命令。只需要在终端里输入 mkfifo fifo
,瞬间就能创建一个叫 fifo
的命名管道文件。你会看到这个文件的类型是 p
,这表示它是一个命名管道文件。
这个命名管道文件就像一个特殊的“信箱”,两个没有亲缘关系的进程可以通过它进行通信。比如,一个进程(进程A)可以用shell脚本每秒向命名管道写入一个字符串,另一个进程(进程B)可以用 cat
命令从命名管道中读取数据。当进程A启动后,进程B就会每秒从命名管道中读取一个字符串并打印到显示器上,这就证明了两个毫不相关的进程可以通过命名管道进行数据传输。
说明:
while :; do ... done
:这是一个无限循环,:
是一个空命令,条件永远为真。
echo "Hello,命名管道!"
:向命名管道中写入字符串 "Hello,命名管道!"。
sleep 1
:暂停 1 秒。
> fifo
:将echo
的输出重定向到命名管道fifo
。使用方法:
确保命名管道
fifo
已经存在。如果不存在,先创建它:mkfifo fifo
在一个终端中运行上述命令,开始每秒向
fifo
写入字符串。在另一个终端中,可以用以下命令读取命名管道中的内容:
cat fifo
这样,就能看到每秒从命名管道中读取到的字符串了。
还有一点需要注意,当管道的读端进程退出后,写端进程再向管道写入数据就没有意义了,此时写端进程会被操作系统杀掉。比如,当你终止掉读端进程后,因为写端执行的循环脚本是由命令行解释器bash执行的,所以bash会被操作系统杀掉,云服务器也会退出。
如果想在程序中创建命名管道,可以使用 mkfifo
函数。这个函数的原型是:
int mkfifo(const char *pathname, mode_t mode);
mkfifo
函数的第一个参数是 pathname
,表示要创建的命名管道文件的路径。如果是以路径的方式给出,命名管道文件就会创建在这个路径下;如果是以文件名的方式给出,命名管道文件就会默认创建在当前路径下。
第二个参数是 mode
,表示创建命名管道文件的默认权限。比如,把 mode
设置为 0666
,命名管道文件的权限就会是 -rw-rw-r--
。不过,实际创建出来的文件权限还会受到 umask
(文件默认掩码)的影响。实际权限是 mode & (~umask)
。如果想让权限不受 umask
影响,可以在创建文件前用 umask(0)
把文件默认掩码设置为0。
mkfifo
函数的返回值很简单:创建成功返回0,创建失败返回-1。
下面是一个创建命名管道的示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
umask(0); // 将文件默认掩码设置为0
if (mkfifo(“myfifo”, 0666) < 0) { // 使用mkfifo创建命名管道文件
perror("mkfifo");
return 1;
}
// 创建成功...
return 0;
}
运行这段代码后,当前路径下就会创建一个名为 myfifo
的命名管道文件。
三、打开命名管道的规则
1. 为读而打开
-
O_NONBLOCK禁用:系统会阻塞读操作,直到有相应进程为写而打开该FIFO。这意味着读进程会一直等待,直到有一个写进程出现并打开同一个命名管道。
-
O_NONBLOCK启用:系统不会阻塞读操作,而是立即返回成功。读进程可以继续执行其他任务,而不需要等待写进程出现。
2. 为写而打开
-
O_NONBLOCK禁用:系统会阻塞写操作,直到有相应进程为读而打开该FIFO。写进程会一直等待,直到有一个读进程出现并打开同一个命名管道。
-
O_NONBLOCK启用:系统不会阻塞写操作,而是立即返回失败,错误码为ENXIO。这表示当前没有可用的读进程,写进程需要自行处理这种情况。
四、用命名管道实现服务端和客户端通信
1. 共用头文件
为了让客户端和服务端用同一个命名管道文件,可以写个头文件,把文件名放进去。代码如下:
#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" // 让客户端和服务端使用同一个命名管道
2. 服务端代码
服务端先运行,创建命名管道文件,然后以读的方式打开,等着客户端发信息。代码如下:
#include "comm.h"
int main()
{
// 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
umask(0);
// 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,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数组清空,避免残留数据干扰
msg[0] = '\0';
// 从命名管道中读取数据,sizeof(msg)-1确保不会超出数组范围
ssize_t s = read(fd, msg, sizeof(msg)-1);
// 如果读取到有效数据(字节数大于0)
if (s > 0){
// 在消息末尾添加字符串结束符,确保输出正确
msg[s] = '\0';
// 打印客户端发送的消息
printf("client: %s\n", msg);
}
// 如果读取到0字节,表示客户端已退出
else if (s == 0){
printf("client quit!\n");
break; // 退出循环
}
// 如果读取失败,打印错误信息并退出循环
else{
printf("read error!\n");
break;
}
}
// 关闭命名管道文件描述符,释放资源
close(fd);
return 0;
}
注释说明:
umask(0):
umask
用于设置文件的默认权限掩码。这里将其设置为 0,确保后续创建的命名管道文件权限不受系统默认掩码的影响,能够以最大权限(0666)创建。mkfifo(FILE_NAME, 0666):
mkfifo
函数用于创建命名管道文件。FILE_NAME
是管道文件的名称,0666
表示权限设置为所有用户可读可写(rw-rw-rw-)。如果创建失败(例如文件已存在或权限不足),
mkfifo
返回 -1,程序通过perror
打印错误信息并退出。open(FILE_NAME, O_RDONLY):
以只读模式打开命名管道文件。
O_RDONLY
表示只读。如果打开失败(例如文件不存在或权限不足),
open
返回 -1,程序通过perror
打印错误信息并退出。read(fd, msg, sizeof(msg)-1):
从命名管道中读取数据,
fd
是文件描述符,msg
是存储数据的缓冲区,sizeof(msg)-1
确保不会超出数组范围。返回值
s
表示读取到的字节数。如果s > 0
,表示读取到有效数据;如果s == 0
,表示管道另一端已关闭;如果s < 0
,表示读取失败。msg[s] = '\0':
在读取到的数据末尾添加字符串结束符,确保输出正确。
close(fd):
关闭文件描述符,释放资源。
3. 客户端代码
客户端看到服务端运行后,命名管道文件已经存在,就以写的方式打开,把信息写进去。代码如下:
#include "comm.h"
int main()
{
// 以只写方式打开命名管道文件
// O_WRONLY 表示只写模式
int fd = open(FILE_NAME, O_WRONLY);
// 如果打开失败,返回错误信息并退出程序
if (fd < 0){
perror("open");
return 1;
}
// 定义一个字符数组用于存储用户输入的消息
char msg[128];
// 无限循环,持续从标准输入读取数据并写入命名管道
while (1){
// 每次读取前将msg数组清空,避免残留数据干扰
msg[0] = '\0';
// 提示用户输入信息
printf("Please Enter:");
fflush(stdout); // 刷新输出缓冲区,确保提示信息立即显示
// 从标准输入读取用户输入的信息,sizeof(msg)-1确保不会超出数组范围
ssize_t s = read(0, msg, sizeof(msg)-1);
// 如果读取到有效数据(字节数大于0)
if (s > 0){
// 去掉输入的换行符
msg[s - 1] = '\0';
// 将用户输入的信息写入命名管道
write(fd, msg, strlen(msg));
}
}
// 关闭命名管道文件描述符,释放资源
close(fd);
return 0;
}
注释说明:
open(FILE_NAME, O_WRONLY):
open
函数用于打开命名管道文件。O_WRONLY
表示以只写模式打开。如果打开失败(例如文件不存在或权限不足),
open
返回 -1,程序通过perror
打印错误信息并退出。read(0, msg, sizeof(msg)-1):
read
函数从标准输入(文件描述符 0)读取用户输入的数据。
sizeof(msg)-1
确保不会超出数组范围,避免缓冲区溢出。返回值
s
表示读取到的字节数。如果s > 0
,表示读取到有效数据;如果s == 0
,表示输入流结束;如果s < 0
,表示读取失败。msg[s - 1] = '\0':
去掉输入的换行符,确保写入管道的是干净的字符串。
write(fd, msg, strlen(msg)):
write
函数将用户输入的数据写入命名管道。fd
是命名管道的文件描述符,msg
是要写入的数据,strlen(msg)
是数据的长度。close(fd):
关闭文件描述符,释放资源。
4. 服务端与客户端运行流程
4.1 服务端启动 :
首先运行服务端进程,此时服务端会创建一个命名管道文件。这一操作相当于搭建起了一个供后续通信使用的 “数据通道”。
4.2 客户端启动 :
接着运行客户端进程,客户端会连接到服务端先前创建的命名管道文件。此时,客户端与服务端通过命名管道建立了联系,尽管它们是两个毫无亲缘关系的进程。
4.3 通信实现 :
客户端将信息写入命名管道,服务端从命名管道中读取这些信息并打印在服务端的显示器上。这一过程证明了两个不相关的进程可以通过命名管道进行数据交互。
5. 进程关系验证
通过 ps
命令查看服务端和客户端的进程信息,可以发现它们的 PID(进程标识符)和 PPID(父进程标识符)都不相同。这表明两个进程是相互独立的,进一步验证了命名管道能够实现不相关进程间的通信。
6. 服务端和客户端的退出关系
6.1 客户端退出 :
当客户端退出后,服务端会将管道中剩余的数据读取完毕。由于之后服务端再也读取不到新的数据,会执行其他代码逻辑(在示例代码中是直接退出)。
6.2 服务端退出 :
服务端退出后,客户端若再次向管道写入数据,会收到操作系统发来的 13 号信号(SIGPIPE),客户端会被操作系统强制杀掉。这是因为此时已没有进程去读取客户端写入的数据,客户端的写入操作变得没有意义。
7. 通信在内存中进行的验证
在服务端不读取管道数据的情况下,客户端依然向管道写入数据。通过 ll
命令查看命名管道文件的大小,发现其大小依旧为 0。这表明即使服务端不读取数据,数据也并未被刷新到磁盘,通信过程始终在内存中进行,与匿名管道通信的机制一致。
#include "com.h"
int main()
{
// 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
umask(0);
// 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,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)
{
//服务端不读取管道信息
}
// 关闭命名管道文件描述符,释放资源
close(fd);
return 0;
}
五、用命名管道实现派发计算任务
客户端可以给服务端派发计算任务,比如双操作数的加减乘除。服务端收到信息后,进行计算并打印结果。代码如下:
#include "com.h"
int main()
{
// 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
umask(0);
// 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,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数组清空,避免残留数据干扰
msg[0] = '\0';
// 从命名管道中读取数据,sizeof(msg)-1确保不会超出数组范围
ssize_t s = read(fd, msg, sizeof(msg) - 1);
// 如果读取到有效数据(字节数大于0)
if (s > 0)
{
// 在消息末尾添加字符串结束符,确保输出正确
msg[s] = '\0';
// 打印客户端发送的消息
printf("client: %s\n", msg);
// 定义支持的运算符字符串
char *lable = "+-*/%";
// 指向消息字符串的指针
char *p = msg;
// 用于存储运算符类型的标志变量
int flag = 0;
// 遍历消息字符串,查找运算符
while (*p)
{
switch (*p)
{
case '+':
flag = 0; // 加法运算符
break;
case '-':
flag = 1; // 减法运算符
break;
case '*':
flag = 2; // 乘法运算符
break;
case '/':
flag = 3; // 除法运算符
break;
case '%':
flag = 4; // 取模运算符
break;
}
p++;
}
// 使用strtok函数按运算符分割字符串,获取第一个操作数
char *data1 = strtok(msg, "+-*/%");
// 获取第二个操作数
char *data2 = strtok(NULL, "+-*/%");
// 将字符串转换为整数
int num1 = atoi(data1);
int num2 = atoi(data2);
// 定义结果变量
int ret = 0;
// 根据运算符标志执行相应的计算
switch (flag)
{
case 0:
ret = num1 + num2; // 加法
break;
case 1:
ret = num1 - num2; // 减法
break;
case 2:
ret = num1 * num2; // 乘法
break;
case 3:
ret = num1 / num2; // 除法
break;
case 4:
ret = num1 % num2; // 取模
break;
}
// 打印计算结果
printf("%d %c %d = %d\n", num1, lable[flag], num2, ret);
}
// 如果读取到0字节,表示客户端已退出
else if (s == 0)
{
printf("client quit!\n");
break; // 退出循环
}
// 如果读取失败,打印错误信息并退出循环
else
{
printf("read error!\n");
break;
}
}
// 关闭命名管道文件描述符,释放资源
close(fd);
return 0;
}
六、用命名管道实现进程遥控
服务端可以执行客户端发来的命令。比如,客户端输入ls
,服务端就列出文件。代码如下:
#include "com.h"
int main()
{
// 将文件默认掩码设置为0,确保创建的命名管道文件权限不受umask影响
umask(0);
// 使用mkfifo创建命名管道文件,FILE_NAME是管道文件名,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数组清空,避免残留数据干扰
msg[0] = '\0';
// 从命名管道中读取数据,sizeof(msg)-1确保不会超出数组范围
ssize_t s = read(fd, msg, sizeof(msg) - 1);
// 如果读取到有效数据(字节数大于0)
if (s > 0)
{
// 在消息末尾添加字符串结束符,确保输出正确
msg[s] = '\0';
// 打印客户端发送的消息
printf("client: %s\n", msg);
// 创建子进程
if (fork() == 0)
{
// 子进程执行客户端发送的命令
execlp(msg, msg, NULL);
exit(1);
}
// 父进程等待子进程完成
waitpid(-1, NULL, 0);
}
// 如果读取到0字节,表示客户端已退出
else if (s == 0)
{
printf("client quit!\n");
break; // 退出循环
}
// 如果读取失败,打印错误信息并退出循环
else
{
printf("read error!\n");
break;
}
}
// 关闭命名管道文件描述符,释放资源
close(fd);
return 0;
}
七、命名管道和匿名管道的区别
1. 创建方式
-
匿名管道:通过
pipe()
系统调用创建。pipe()
会创建两个文件描述符,一个用于读(pipefd[0]),一个用于写(pipefd[1])。 -
命名管道:通过
mkfifo()
函数或mknod()
系统调用创建。命名管道需要指定一个路径名,创建后会在文件系统中出现一个特殊文件。
2. 使用方式
-
匿名管道:只能在具有亲缘关系的进程之间通信(如父子进程)。通常在父进程中创建管道,然后通过
fork()
创建子进程,父子进程通过管道的读写端进行通信。 -
命名管道:可以在任意两个进程之间通信,即使这两个进程没有亲缘关系。进程通过打开命名管道文件进行通信。
3. 打开方式
-
匿名管道:不需要显式打开,创建后直接使用
read()
和write()
进行操作。 -
命名管道:需要使用
open()
函数打开,可以指定只读(O_RDONLY
)或只写(O_WRONLY
)模式。
4. 通信范围
-
匿名管道:仅限于父子进程之间通信。
-
命名管道:可以用于任意两个进程之间通信。
5. 生命周期
-
匿名管道:随进程生命周期结束而消亡。当所有使用管道的进程都退出后,管道也被自动销毁。
-
命名管道:在文件系统中存在,即使创建它的进程已经退出,其他进程仍然可以打开和使用它。需要显式删除(
unlink()
)才能从文件系统中移除。
6. 性能
-
匿名管道:通常比命名管道稍微快一点,因为它们不需要文件系统支持。
-
命名管道:由于涉及文件系统操作,可能会稍微慢一些。
7. 使用场景
-
匿名管道:适合父子进程之间的简单通信,尤其是当通信是单向且短期的。
-
命名管道:适合需要在多个不相关进程之间进行通信的场景,尤其是当通信是长期的或需要多个进程同时访问时。
特性 | 匿名管道 | 命名管道 |
---|---|---|
创建方式 | 通过 pipe() 系统调用创建。 | 通过 mkfifo() 函数或 mknod() 系统调用创建。 |
使用方式 | 仅限父子进程间通信,通常由父进程创建,子进程继承。 | 可在任意两个进程间通信,进程通过打开命名管道文件进行通信。 |
打开方式 | 不需显式打开,创建后直接使用。 | 需使用 open() 函数打开,可指定只读或只写模式。 |
通信范围 | 仅限具有亲缘关系的进程间通信。 | 可用于任意两个进程间通信,无论是否有亲缘关系。 |
生命周期 | 随进程生命周期结束而消亡。 | 在文件系统中存在,需显式删除才能移除。 |
性能 | 通常比命名管道快,无需文件系统支持。 | 涉及文件系统操作,可能稍慢。 |
使用场景 | 适合父子进程间简单、短期的单向通信。 | 适合需要长期通信或多进程同时访问的场景。 |