Linux IPC全揭秘(三):命名管道(FIFO):任意进程通信
·
目录
1. 命名管道解决了什么问题?
匿名管道的局限性
// 匿名管道只能用于具有亲缘关系的进程
int pipefd[2];
pipe(pipefd); // 父进程创建管道
fork(); // 子进程继承文件描述符
// 只能父子进程或兄弟进程通信
命名管道的优势
命名管道(FIFO,First In First Out) 允许不相关的进程通过文件系统中的路径名进行通信。
# 进程A(PID: 1000)
$ ./process_a
# 进程B(PID: 2000,无亲缘关系)
$ ./process_b
# 两个进程可以通过命名管道通信
2. 命名管道的基本概念
2.1 什么是命名管道?
命名管道是一种特殊类型的文件,它在文件系统中有一个路径名,但不存储实际数据,只作为进程间通信的通道。
# 查看命名管道
$ mkfifo myfifo
$ ls -l myfifo
prw-r--r-- 1 user group 0 Jan 1 12:00 myfifo
# ↑ 'p' 表示这是一个管道文件
2.2 命名管道 vs 匿名管道
| 特性 | 匿名管道 | 命名管道 |
|---|---|---|
| 文件系统中可见 | 否 | 是(有路径名) |
| 进程关系要求 | 必须有亲缘关系 | 任意进程 |
| 创建方式 | pipe()系统调用 |
mkfifo()函数或mkfifo命令 |
| 打开方式 | 继承文件描述符 | open()函数 |
| 生命周期 | 随进程结束 | 手动删除或进程结束 |
3. 创建命名管道
3.1 命令行创建
# 创建命名管道
$ mkfifo mypipe
$ mkfifo /tmp/myfifo
# 设置权限
$ mkfifo -m 0666 shared_pipe # 所有人可读写
3.2 编程创建
#include <sys/types.h>
#include <sys/stat.h>
// 函数原型
int mkfifo(const char *pathname, mode_t mode);
// 参数
// pathname: 管道文件路径
// mode: 权限模式(如0644)
// 返回值
// 成功: 返回0
// 失败: 返回-1,设置errno
3.3 创建示例
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 创建命名管道,权限为rw-r--r--
if (mkfifo("myfifo", 0644) == -1) {
perror("mkfifo");
exit(EXIT_FAILURE);
}
printf("命名管道创建成功: myfifo\n");
return 0;
}
4. 命名管道的打开规则
4.1 阻塞模式(默认)
// 读端打开规则
int read_fd = open("myfifo", O_RDONLY); // 阻塞直到有写端打开
// 写端打开规则
int write_fd = open("myfifo", O_WRONLY); // 阻塞直到有读端打开
4.2 非阻塞模式
#include <fcntl.h>
// 非阻塞读打开
int read_fd = open("myfifo", O_RDONLY | O_NONBLOCK);
// 立即返回,无论是否有写端
// 非阻塞写打开
int write_fd = open("myfifo", O_WRONLY | O_NONBLOCK);
// 如果没有读端,立即返回-1,errno=ENXIO
4.3 打开规则总结
| 打开方式 | 阻塞模式(默认) | 非阻塞模式(O_NONBLOCK) |
|---|---|---|
| 为读而打开 | 阻塞直到有进程为写而打开 | 立即成功返回 |
| 为写而打开 | 阻塞直到有进程为读而打开 | 立即返回(如果没有读端则失败) |
5. 命名管道通信示例
5.1 示例1:文件拷贝
写入端(生产者)
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
// 创建命名管道(如果不存在)
if (access("tp", F_OK) == -1) {
mkfifo("tp", 0644);
}
// 打开源文件
int infd = open("abc", O_RDONLY);
if (infd == -1)
ERR_EXIT("open source file");
// 打开命名管道(写端)
int outfd = open("tp", O_WRONLY);
if (outfd == -1)
ERR_EXIT("open fifo for writing");
char buf[1024];
int n;
// 读取源文件,写入管道
while ((n = read(infd, buf, 1024)) > 0) {
if (write(outfd, buf, n) != n) {
ERR_EXIT("write to fifo");
}
}
close(infd);
close(outfd);
return 0;
}
读取端(消费者)
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
// 打开目标文件
int outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (outfd == -1)
ERR_EXIT("open target file");
// 打开命名管道(读端)
int infd = open("tp", O_RDONLY);
if (infd == -1)
ERR_EXIT("open fifo for reading");
char buf[1024];
int n;
// 从管道读取,写入目标文件
while ((n = read(infd, buf, 1024)) > 0) {
if (write(outfd, buf, n) != n) {
ERR_EXIT("write to file");
}
}
close(infd);
close(outfd);
// 删除命名管道
unlink("tp");
return 0;
}
5.2 示例2:Client-Server通信
Server端
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
// 设置umask,确保创建的文件有正确权限
umask(0);
// 创建命名管道(如果不存在)
if (mkfifo("mypipe", 0644) < 0) {
if (errno != EEXIST) { // 如果已经存在,忽略错误
ERR_EXIT("mkfifo");
}
}
// 以只读方式打开命名管道(阻塞模式)
int rfd = open("mypipe", O_RDONLY);
if (rfd < 0) {
ERR_EXIT("open");
}
printf("Server started. Waiting for client messages...\n");
char buf[1024];
while (1) {
memset(buf, 0, sizeof(buf));
printf("Waiting for message...\n");
// 从管道读取数据
ssize_t s = read(rfd, buf, sizeof(buf) - 1);
if (s > 0) {
// 去掉换行符
if (buf[s - 1] == '\n')
buf[s - 1] = 0;
printf("Client says: %s\n", buf);
// 检查退出命令
if (strcmp(buf, "quit") == 0 || strcmp(buf, "exit") == 0) {
printf("Client requested to quit. Server exiting.\n");
break;
}
} else if (s == 0) {
printf("Client disconnected. Server exiting.\n");
break;
} else {
ERR_EXIT("read");
}
}
close(rfd);
// 清理命名管道
unlink("mypipe");
return 0;
}
Client端
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do { \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main()
{
// 以只写方式打开命名管道(阻塞模式)
int wfd = open("mypipe", O_WRONLY);
if (wfd < 0) {
ERR_EXIT("open");
}
printf("Connected to server. Type 'quit' or 'exit' to disconnect.\n");
char buf[1024];
while (1) {
printf("Enter message: ");
fflush(stdout);
// 从标准输入读取
if (fgets(buf, sizeof(buf), stdin) == NULL) {
break; // EOF (Ctrl+D)
}
// 写入命名管道
ssize_t s = write(wfd, buf, strlen(buf));
if (s <= 0) {
perror("write");
break;
}
// 检查退出命令
if (strncmp(buf, "quit", 4) == 0 || strncmp(buf, "exit", 4) == 0) {
printf("Disconnecting...\n");
break;
}
}
close(wfd);
return 0;
}
6. 命名管道的读写特性
6.1 与匿名管道相同的特性
// 命名管道继承匿名管道的读写特性
// 1. 数据是字节流,无消息边界
write(fd, "Hello", 5);
write(fd, "World", 5);
read(fd, buf, 10); // 可能读取到 "HelloWorld"
// 2. 原子性保证(≤PIPE_BUF)
#define PIPE_BUF 4096 // Linux典型值
// 3. 读端/写端关闭时的行为相同
// - 所有写端关闭:读端读到EOF
// - 所有读端关闭:写端收到SIGPIPE信号
6.2 信号处理
#include <signal.h>
// 处理SIGPIPE信号
void handle_sigpipe(int sig) {
printf("Caught SIGPIPE: Reader has closed the pipe\n");
// 清理资源
exit(EXIT_FAILURE);
}
int main() {
signal(SIGPIPE, handle_sigpipe);
// ... 使用命名管道 ...
return 0;
}
7. 高级用法:双向通信
// 双向通信需要两个命名管道
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define FIFO1 "fifo1" // 进程A → 进程B
#define FIFO2 "fifo2" // 进程B → 进程A
// 进程A
void process_a() {
mkfifo(FIFO1, 0666);
mkfifo(FIFO2, 0666);
int write_fd = open(FIFO1, O_WRONLY);
int read_fd = open(FIFO2, O_RDONLY);
char buf[100];
while (1) {
// 发送消息
printf("A: ");
fgets(buf, sizeof(buf), stdin);
write(write_fd, buf, strlen(buf) + 1);
// 接收回复
read(read_fd, buf, sizeof(buf));
printf("B says: %s\n", buf);
}
}
// 进程B
void process_b() {
int read_fd = open(FIFO1, O_RDONLY);
int write_fd = open(FIFO2, O_WRONLY);
char buf[100];
while (1) {
// 接收消息
read(read_fd, buf, sizeof(buf));
printf("A says: %s\n", buf);
// 发送回复
printf("B: ");
fgets(buf, sizeof(buf), stdin);
write(write_fd, buf, strlen(buf) + 1);
}
}
8. 命名管道的最佳实践
8.1 错误处理
int open_fifo_safely(const char *fifo_path, int flags) {
int fd;
int attempts = 0;
const int max_attempts = 5;
while (attempts < max_attempts) {
fd = open(fifo_path, flags);
if (fd >= 0) {
return fd; // 成功打开
}
if (errno == ENXIO) {
// 对于写打开,没有读端
if (flags & O_WRONLY) {
printf("Waiting for reader...\n");
sleep(1); // 等待1秒再试
attempts++;
continue;
}
}
// 其他错误
perror("open");
return -1;
}
fprintf(stderr, "Failed to open FIFO after %d attempts\n", max_attempts);
return -1;
}
8.2 清理资源
void cleanup_fifo(const char *fifo_path) {
// 检查文件是否存在且为FIFO
struct stat st;
if (stat(fifo_path, &st) == 0) {
if (S_ISFIFO(st.st_mode)) {
if (unlink(fifo_path) == 0) {
printf("Removed FIFO: %s\n", fifo_path);
} else {
perror("unlink");
}
}
}
}
8.3 使用select/poll进行多路复用
#include <sys/select.h>
int monitor_multiple_fifos(int fds[], int nfds) {
fd_set readfds;
int maxfd = 0;
FD_ZERO(&readfds);
for (int i = 0; i < nfds; i++) {
FD_SET(fds[i], &readfds);
if (fds[i] > maxfd) maxfd = fds[i];
}
// 设置超时
struct timeval timeout = {5, 0}; // 5秒
int ret = select(maxfd + 1, &readfds, NULL, NULL, &timeout);
if (ret == -1) {
perror("select");
return -1;
} else if (ret == 0) {
printf("Timeout\n");
return 0;
}
// 检查哪个FIFO有数据
for (int i = 0; i < nfds; i++) {
if (FD_ISSET(fds[i], &readfds)) {
return fds[i]; // 返回有数据的文件描述符
}
}
return -1;
}
9. 命名管道的实际应用场景
9.1 Shell管道命令
# Shell中的管道实际上创建了匿名管道
$ ls | grep ".txt"
# 命名管道可以在命令行中使用
$ mkfifo mypipe
$ ls -l > mypipe & # 后台写入
$ grep ".txt" < mypipe # 前台读取
9.2 进程间日志收集
// 多个进程写入同一个日志管道
// 日志收集进程读取并写入文件
void log_to_fifo(const char *message) {
static int log_fd = -1;
if (log_fd == -1) {
log_fd = open("/var/log/app_fifo", O_WRONLY | O_APPEND);
}
if (log_fd >= 0) {
write(log_fd, message, strlen(message));
}
}
9.3 简单的进程间消息队列
// 使用命名管道实现简单的消息队列
struct message {
long mtype;
char mtext[100];
};
int send_message(const char *fifo_path, struct message *msg) {
int fd = open(fifo_path, O_WRONLY);
if (fd < 0) return -1;
write(fd, msg, sizeof(struct message));
close(fd);
return 0;
}
总结
命名管道(FIFO)是Unix/Linux系统中重要的进程间通信机制,它克服了匿名管道只能用于亲缘进程的限制。关键要点:
- 创建方式:
mkfifo()函数或mkfifo命令 - 打开规则:读写端需要协调打开,有阻塞和非阻塞模式
- 通信特性:与匿名管道具有相同的读写语义
- 文件系统:在文件系统中可见,有路径名
- 进程关系:可用于任意不相关进程
- 生命周期:需要显式删除(
unlink())
命名管道适合以下场景:
- 不相关进程间的简单通信
- 命令行工具间的数据传递
- 简单的客户端-服务器模型
- 临时性的进程间数据交换
对于更复杂的进程间通信需求,可能需要考虑其他IPC机制如Unix域套接字、System V消息队列或共享内存。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)