进程间通信 —— 有名管道篇
1. 什么是有名管道
1. 基本定义
有名管道是 Linux 中的一种进程间通信方式,其本质也是一个特殊类型的文件,存在于文件系统中,支持 无亲缘关系的进程 之间的数据通信。
2. 与匿名管道的对比
| 特性 | 匿名管道(pipe) | 有名管道(FIFO) |
|---|---|---|
| 是否有文件路径 | ❌ 没有 | ✅ 有(存在于文件系统中) |
| 是否只能父子进程通信 | ✅ 是 | ❌ 可以无亲缘关系通信 |
| 创建方式 | pipe() | mkfifo() / mknod() |
| 常见用途 | 父子进程、线程内通信 | shell 脚本、后台服务通信 |
2. 有名管道的创建与使用
1. mkfifo 函数原型 —— 创建有名管道
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
参数详解
const char *pathname: 表示希望创建的 有名管道的路径(通常是绝对路径或当前目录下的文件名)。它创建出来的是一个 文件系统中的特殊设备文件,使用ls -l可以看到文件类型为p(pipe):
prw-r--r-- 1 user user 0 Jun 21 14:00 myfifo
-
mode_t mode: 表示新创建的 FIFO 文件的权限,常用类似 shell 中的权限位(与chmod相同),如:-
0666:所有用户可读写(不加执行权限)。 -
0644:仅拥有者可写,其它用户只读。 -
0600:仅拥有者可读写。
-
注意:这个权限会受到 umask(用户掩码) 的影响!
返回值
- 成功时:返回 0。
- 失败时:返回 -1,并设置 errno 表示错误原因。
通信 Demo:
创建一个有名管道文件 myfifo,使用 while :; do echo "Hello Linux" ; sleep 1; done >> myfifo 循环写入内容,发现管道文件大小始终是 0。实验演示 | B 站。说明:FIFO 是一种特殊文件,用于进程通信,其内容不保存在磁盘上,而是临时存放在内存缓冲区中,并且只有当有读端存在时,写入操作才能成功。即使有数据写入,文件大小也始终显示为 0。
2. unlink 函数原型 —— 删除有名管道(本质就是从文件系统里删掉一个文件)
#include <unistd.h> // 头文件
int unlink(const char *pathname); // 删除一个名字与文件系统中 inode 的链接
参数: 要删除的文件(或管道、socket 文件等)的路径名,支持相对路径或绝对路径。
返回值:
- 成功:返回 0
- 失败:返回 -1,并设置全局变量 errno
重点理解
unlink()不会管里面有没有读写端打开,只是把文件名从文件系统中移除。- 如果此时还有进程打开这个管道文件,管道内内核资源还在用,直到所有引用关闭后,内核才真正释放资源。(和普通文件 inode 行为一样)
3. unlink 和 rm 的关系
unlink()是 Linux 内核提供的低级系统调用,直接作用于文件系统的 inode 链接数。rm是用户空间程序,底层就是调用unlink()来完成删除。
在实际生产场景中:
- 有名管道通常是 临时文件,由程序创建出来给进程通信用。
- 程序结束时,最好自己负责清理掉。
- 如果不主动
unlink(),就要依赖人去rm,这就是“脏文件”了 —— 下次mkfifo会报EEXIST。
| 操作 | 作用 | 场景 |
|---|---|---|
rm | 命令行层面的文件删除 | 人工手动操作 |
unlink() | 系统调用层面的文件删除 | 程序自动管理资源,退出时清理 |
本质是一样的,只是执行主体和使用时机不同。
思考:你知道
rm删除文件背后的原理吗?如果文件还被打开,rm会发生什么?答案:
rm会把文件名目录项删除,但是只要还有进程打开这个文件,inode 不会被释放。- 所以有名管道如果还有进程在读写,
rm(或unlink)后文件看不到了,但内核仍然维护着,直到最后一个 fd 关闭,内核才真正释放。
3. 基于有名管道通信的服务端-客户端模型 Demo
1. 头文件 comm.hpp
#pragma once
#include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
using namespace std;
#define FIFO_FILE "./myfifo"
#define MODE 0664
enum
{
FIFO_CREATE_ERR = 1, // 这是创建管道文件失败的错误码
FIFO_DELETE_ERR = 2, // 这是删除管道文件失败的错误码
FIFO_OPEN_ERR // 这是打开管道文件失败的错误码(枚举会自动赋值为 3)
};
class Init
{
public:
Init()
{
int n = mkfifo(FIFO_FILE, MODE); // 创建管道文件
if (n == -1)
{
perror("mkfifo");
exit(FIFO_CREATE_ERR);
}
}
~Init()
{
int m = unlink(FIFO_FILE); // 删除管道文件
if (m == -1)
{
perror("unlink");
exit(FIFO_DELETE_ERR);
}
}
};
2. 客户端 Client.cc
#include "comm.hpp"
int main()
{
int fd = open(FIFO_FILE, O_WRONLY); // 以只写方式打开管道文件
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
string str; // 定义消息字符串
while(true)
{
cout << "请输入要发送的消息:";
getline(cin, str); // 读取用户输入的消息(一整行)
write(fd, str.c_str(), str.size()); // 向管道文件写入消息,str.c_str()是 string 转换为 C 风格字符串 的方法
}
close(fd); // 关闭管道文件
return 0;
}
3. 服务端 Server.cc
#include "comm.hpp"
int main()
{
Init fifo; // 初始化管道
int fd = open(FIFO_FILE, O_RDONLY); // 以只读方式打开管道
if(fd < 0)
{
perror("open");
exit(FIFO_OPEN_ERR);
}
while(true)
{
char buf[1024] = {0};
int x = read(fd, buf, sizeof(buf)); // 读取管道数据,x 为读取的字节数
if(x > 0)
{
buf[x] = 0; // 将完整的字节流转换为字符串,并添加结束符‘\0’
cout << "客户端说:" << buf << endl;
}
}
close(fd); // 关闭管道
return 0;
}