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系统中重要的进程间通信机制,它克服了匿名管道只能用于亲缘进程的限制。关键要点:

  1. 创建方式mkfifo()函数或mkfifo命令
  2. 打开规则:读写端需要协调打开,有阻塞和非阻塞模式
  3. 通信特性:与匿名管道具有相同的读写语义
  4. 文件系统:在文件系统中可见,有路径名
  5. 进程关系:可用于任意不相关进程
  6. 生命周期:需要显式删除(unlink()

命名管道适合以下场景:

  • 不相关进程间的简单通信
  • 命令行工具间的数据传递
  • 简单的客户端-服务器模型
  • 临时性的进程间数据交换

对于更复杂的进程间通信需求,可能需要考虑其他IPC机制如Unix域套接字、System V消息队列或共享内存。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐