032 进程间通信 —— 有名管道篇

进程间通信 —— 有名管道篇

1. 什么是有名管道

1. 基本定义

有名管道是 Linux 中的一种进程间通信方式,其本质也是一个特殊类型的文件,存在于文件系统中,支持 无亲缘关系的进程 之间的数据通信。

2. 与匿名管道的对比

特性 匿名管道(pipe) 有名管道(FIFO)
是否有文件路径 ❌ 没有 ✅ 有(存在于文件系统中)
是否只能父子进程通信 ✅ 是 ❌ 可以无亲缘关系通信
创建方式 pipe() mkfifo() / mknod()
常见用途 父子进程、线程内通信 shell 脚本、后台服务通信

2. 有名管道的创建与使用

1. mkfifo 函数原型 —— 创建有名管道

1
2
3
4
#include <sys/types.h>
#include <sys/stat.h>

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

参数详解

  1. const char *pathname 表示希望创建的 有名管道的路径(通常是绝对路径或当前目录下的文件名)。它创建出来的是一个 文件系统中的特殊设备文件,使用 ls -l 可以看到文件类型为 p(pipe):
1
prw-r--r-- 1 user user 0 Jun 21 14:00 myfifo
  1. 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。

1
2
#include <unistd.h>					   // 头文件
int unlink(const char *pathname); // 删除一个名字与文件系统中 inode 的链接

参数: 要删除的文件(或管道、socket 文件等)的路径名,支持相对路径或绝对路径。

返回值:

  • 成功:返回 0
  • 失败:返回 -1,并设置全局变量 errno

重点理解

  • unlink() 不会管里面有没有读写端打开,只是把文件名从文件系统中移除。
  • 如果此时还有进程打开这个管道文件,管道内内核资源还在用,直到所有引用关闭后,内核才真正释放资源。(和普通文件 inode 行为一样)

  • unlink() 是 Linux 内核提供的低级系统调用,直接作用于文件系统的 inode 链接数。
  • rm 是用户空间程序,底层就是调用 unlink() 来完成删除。

在实际生产场景中:

  • 有名管道通常是 临时文件,由程序创建出来给进程通信用。
  • 程序结束时,最好自己负责清理掉。
  • 如果不主动 unlink(),就要依赖人去 rm,这就是“脏文件”了 —— 下次 mkfifo 会报 EEXIST
操作 作用 场景
rm 命令行层面的文件删除 人工手动操作
unlink() 系统调用层面的文件删除 程序自动管理资源,退出时清理

本质是一样的,只是执行主体和使用时机不同。

思考:你知道 rm 删除文件背后的原理吗?如果文件还被打开,rm 会发生什么?

答案:

  • rm 会把文件名目录项删除,但是只要还有进程打开这个文件,inode 不会被释放
  • 所以有名管道如果还有进程在读写,rm(或 unlink)后文件看不到了,但内核仍然维护着,直到最后一个 fd 关闭,内核才真正释放。

3. 基于有名管道通信的服务端-客户端模型 Demo

1. 头文件 comm.hpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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;
}

运行展示:有名管道通信的服务端-客户端模型 Demo 运行示例 | B 站