Linux进程通信---2---命名管道

 命名管道(FIFO / 有名管道)

命名管道(Named Pipe),也称为FIFO(First In First Out),是 Linux 系统中一种特殊的进程间通信(IPC)机制。它的本质是一个存在于文件系统中的特殊文件,通过文件路径名标识,允许任意进程(无论是否有亲缘关系)通过打开该文件进行数据传输,数据遵循 "先进先出" 的原则。

1.1 与匿名管道的核心区别

特性命名管道(FIFO)匿名管道(Pipe)
存在形式存在于文件系统中,有具体路径名仅存在于内核中,无文件系统实体
适用进程任意进程(无亲缘关系也可通信)仅适用于有亲缘关系的进程(父子 / 兄弟)
创建方式mkfifo()系统调用或mkfifo命令pipe()系统调用
生命周期手动删除(rm)或文件系统卸载随进程退出自动销毁
打开方式通过open()函数按路径打开直接使用pipe()返回的文件描述符
可复用性可被多个进程反复打开和使用仅能被创建它的进程及其子进程使用

1.2 核心特性

  1. 先进先出:数据的读取顺序与写入顺序完全一致,不支持随机访问。
  2. 半双工通信:同一时刻只能单向传输数据,双向通信需要两个命名管道。
  3. 文件系统可见:通过ls -l命令查看时,文件类型标识为p(pipe)。
  4. 内核缓冲:数据存储在内核缓冲区中,不落地到磁盘,性能接近匿名管道。
  5. 原子操作保障:当写入数据量小于等于PIPE_BUF时,系统保证写入的原子性(不会被其他进程的写入打断)。

2.1 内核数据结构

命名管道的底层实现依赖于内核中的两个核心结构:

  1. struct inode:文件系统中的索引节点,标识命名管道的存在,其i_pipe指针指向管道的核心数据结构。
  2. struct pipe_inode_info:管道的核心控制结构,包含:
    • 环形缓冲区:存储传输的数据(默认大小通常为 64KB,可通过fcntl调整)。
    • 读写等待队列:阻塞的读 / 写进程队列,用于实现同步机制。
    • 引用计数:记录当前打开该管道的进程数。
    • 互斥锁与自旋锁:保障并发访问的线程安全。

2.2 数据传输流程

命名管道的数据传输本质是用户态 - 内核态 - 用户态的拷贝过程:

  1. 写进程:通过write()将用户空间数据拷贝到内核的管道缓冲区。
  2. 内核调度:当缓冲区有数据时,唤醒阻塞的读进程。
  3. 读进程:通过read()将内核缓冲区的数据拷贝到用户空间。

注意:命名管道的数据不会写入磁盘,仅存在于内核内存中,因此性能远高于普通文件传输。

3.1 创建命名管道

3.1.1 命令行创建

使用mkfifo命令直接在文件系统中创建命名管道:

# 创建名为myfifo的命名管道,权限为0666(读写权限)
mkfifo -m 0666 myfifo
# 查看管道文件
ls -l myfifo
# 输出:prw-rw-rw- 1 user user 0  6月 10 10:00 myfifo

3.1.2 系统调用创建

mkfifo()函数是创建命名管道的核心系统调用,原型如下:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
  • 参数说明
    • pathname:管道文件的路径名(绝对路径或相对路径)。
    • mode:管道文件的权限(如0666),实际权限会受umask影响。
  • 返回值:成功返回 0,失败返回 - 1 并设置errno
  • 常见错误码
    • EEXIST:指定路径的文件已存在。
    • ENOENT:路径中的目录不存在。
    • EACCES:没有权限创建文件。

3.2 打开命名管道

命名管道通过open()函数打开,但其行为与普通文件有显著区别:

#include <fcntl.h>

int open(const char *pathname, int flags);

3.2.1 阻塞模式(默认)

  • 只读打开(O_RDONLY):如果没有进程以只写模式打开该管道,调用会阻塞,直到有写进程打开。
  • 只写打开(O_WRONLY):如果没有进程以只读模式打开该管道,调用会阻塞,直到有读进程打开。
  • 读写打开(O_RDWR):不会阻塞,但这种用法不符合管道的设计初衷,一般不推荐。

3.2.2 非阻塞模式(O_NONBLOCK)

flags包含O_NONBLOCK时,open()不会阻塞:

  • 只读打开:如果没有写进程,立即返回成功(后续读操作可能返回 0)。
  • 只写打开:如果没有读进程,立即返回失败,errno设为ENXIO

3.3 读写操作

命名管道的读写操作与普通文件一致,使用read()write()函数,但有特殊的行为规则:

3.3.1 读操作(read ())

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
参数名类型核心作用
fdint文件描述符,即通过open()打开 FIFO 后返回的整数(必须是O_RDONLY/O_RDWR模式)
bufvoid *指向用户空间缓冲区的指针,用于存储从 FIFO 读取的数据(需提前分配内存)
countsize_t期望读取的最大字节数(不能超过buf的内存大小,否则会导致缓冲区溢出)
返回值
  • 正数:成功读取的字节数(可能小于count,比如 FIFO 中剩余数据不足);
  • 0:所有写端已关闭(FIFO 到达 EOF,这是命名管道的关键特征);
  • -1:读取失败,需检查errno(结合 FIFO 场景的关键错误码见下文)。
FIFO 场景专属注意事项(对应之前的 3.3.1)
  • 阻塞模式(默认):若 FIFO 无数据但写端未关闭,read()会阻塞,直到有数据写入或写端全部关闭;
  • 非阻塞模式(O_NONBLOCK):若 FIFO 无数据,read()立即返回 - 1,且errno = EAGAIN(需重试,非真正错误);
  • 常见错误码:
    • EAGAIN:非阻塞模式下无数据;
    • EBADFfd不是有效的读文件描述符;
    • EINTR:读取过程中被信号中断(可重试)。

3.3.2 写操作(write ())

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
参数名类型核心作用
fdint文件描述符,即通过open()打开 FIFO 后返回的整数(必须是O_WRONLY/O_RDWR模式)
bufconst void *指向用户空间缓冲区的指针,存储要写入 FIFO 的数据(数据需提前准备)
countsize_t期望写入的字节数(注意PIPE_BUF阈值,保证原子性)
返回值
  • 正数:成功写入的字节数(可能小于count,比如 FIFO 缓冲区空间不足);
  • -1:写入失败,需检查errno(结合 FIFO 场景的关键错误码见下文);
  • 注意:write()不会返回 0(这是与read()的核心区别)。
FIFO 场景专属注意事项(对应之前的 3.3.2)
  • 阻塞模式(默认):若 FIFO 缓冲区空间不足,write()会阻塞,直到有读进程取走数据、缓冲区有空间;
  • 非阻塞模式(O_NONBLOCK):若缓冲区不足,write()立即返回 - 1,且errno = EAGAIN(需重试);
  • 关键错误 / 信号:
    • 若所有读端已关闭,write()会触发SIGPIPE信号(默认终止进程),若捕获该信号,write()返回 - 1 且errno = EPIPE
    • 常见错误码:EAGAIN(非阻塞无空间)、EPIPE(读端全关)、EBADF(无效写描述符)、EINTR(被信号中断);
  • 原子性:当count ≤ PIPE_BUF(通常 4096 字节)时,系统保证写入原子性,多进程并发写入不会错乱;超过则无原子性保证。

3.3.3 原子操作与 PIPE_BUF

PIPE_BUF是 Linux 内核定义的宏(在<limits.h>中),表示管道的原子写入阈值,通常为 4096 字节(4KB)。

  • 当写入数据量 ≤ PIPE_BUF时,系统保证写入的原子性,不会被其他进程的写入打断。
  • 当写入数据量 > PIPE_BUF时,写入可能被拆分,不保证原子性,多进程并发写入可能导致数据错乱。

可以通过sysconf()函数获取系统的PIPE_BUF值:

#include <unistd.h>
long pipe_buf = sysconf(_SC_PIPE_BUF);

3.4 关闭与销毁

  • 关闭:通过close()函数关闭管道的文件描述符,当最后一个进程关闭管道时,内核会释放管道的内核缓冲区资源。
  • 销毁:命名管道作为文件系统中的实体,需要手动通过unlink()系统调用或rm命令删除,否则会一直存在于文件系统中。

4.1 基础读写通信示例

写进程(fifo_writer.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_PATH "./myfifo"
#define BUFFER_SIZE 1024

int main() {
    // 1. 创建命名管道,权限0666(读写权限)
    int ret = mkfifo(FIFO_PATH, 0666);
    if (ret == -1) {
        perror("mkfifo failed");
        // 若管道已存在,忽略错误(允许复用)
        if (errno != EEXIST) {
            exit(EXIT_FAILURE);
        }
    }

    // 2. 以只写模式打开管道(阻塞模式)
    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) {
        perror("open fifo failed");
        exit(EXIT_FAILURE);
    }
    printf("Writer: 成功打开管道,等待读进程连接...\n");

    // 3. 写入数据
    const char *messages[] = {
        "Hello from writer!",
        "This is a named pipe example.",
        "End of message."
    };
    int msg_count = sizeof(messages) / sizeof(messages[0]);

    for (int i = 0; i < msg_count; i++) {
        ssize_t bytes_written = write(fd, messages[i], strlen(messages[i]) + 1);
        if (bytes_written == -1) {
            perror("write failed");
            close(fd);
            exit(EXIT_FAILURE);
        }
        printf("Writer: 已写入数据: %s\n", messages[i]);
        sleep(1); // 模拟间隔写入
    }

    // 4. 关闭管道
    close(fd);
    printf("Writer: 数据写入完成,关闭管道\n");

    return 0;
}
读进程(fifo_reader.c)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_PATH "./myfifo"
#define BUFFER_SIZE 1024

int main() {
    // 1. 以只读模式打开管道(阻塞模式)
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) {
        perror("open fifo failed");
        exit(EXIT_FAILURE);
    }
    printf("Reader: 成功打开管道,开始读取数据...\n");

    // 2. 读取数据
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;
    while ((bytes_read = read(fd, buffer, BUFFER_SIZE)) > 0) {
        printf("Reader: 读取到数据: %s\n", buffer);
        memset(buffer, 0, BUFFER_SIZE); // 清空缓冲区
    }

    // 3. 处理读取结果
    if (bytes_read == 0) {
        printf("Reader: 写端已关闭,读取完成\n");
    } else if (bytes_read == -1) {
        perror("read failed");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 4. 关闭管道并删除文件
    close(fd);
    unlink(FIFO_PATH); // 读取完成后删除管道
    printf("Reader: 关闭管道并删除文件\n");

    return 0;
}
编译与运行
# 编译
gcc fifo_writer.c -o writer
gcc fifo_reader.c -o reader

# 运行(两个终端分别执行)
# 终端1:运行读进程
./reader
# 终端2:运行写进程
./writer

4.2 非阻塞模式通信示例

修改读进程的打开方式,使用非阻塞模式:

// 以只读+非阻塞模式打开管道
int fd = open(FIFO_PATH, O_RDONLY | O_NONBLOCK);
if (fd == -1) {
    perror("open fifo failed");
    exit(EXIT_FAILURE);
}

// 非阻塞读取逻辑
while (1) {
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read > 0) {
        printf("Reader: 读取到数据: %s\n", buffer);
        memset(buffer, 0, BUFFER_SIZE);
    } else if (bytes_read == 0) {
        printf("Reader: 写端已关闭\n");
        break;
    } else {
        if (errno == EAGAIN) {
            // 无数据,等待1秒后重试
            printf("Reader: 暂无数据,等待...\n");
            sleep(1);
        } else {
            perror("read failed");
            break;
        }
    }
}

5.1 阻塞与非阻塞机制的底层逻辑

命名管道的阻塞机制依赖于内核的等待队列

  • 当读进程打开管道但无数据时,读进程会被加入到管道的读等待队列,进程状态变为TASK_INTERRUPTIBLE,让出 CPU。
  • 当写进程写入数据后,内核会唤醒读等待队列中的进程,使其恢复运行并读取数据。
  • 非阻塞模式下,进程不会被加入等待队列,而是立即返回错误或 0,由用户态程序自行处理重试逻辑。

5.2 SIGPIPE 信号处理

当写进程向已关闭读端的管道写入数据时,内核会向写进程发送SIGPIPE信号,默认行为是终止进程。为了避免进程意外退出,可以通过signal()sigaction()函数捕获该信号:

#include <signal.h>

void sigpipe_handler(int signum) {
    printf("Received SIGPIPE signal, read end closed\n");
}

int main() {
    // 注册SIGPIPE信号处理函数
    signal(SIGPIPE, sigpipe_handler);

    // ... 后续代码 ...
}

6.1 无亲缘关系进程通信

这是命名管道最核心的应用场景,例如:

  • 后台服务进程与前台命令行工具的通信。
  • 不同用户的进程之间的数据传输(需保证管道文件的权限正确)。

6.2 日志收集与处理

通过命名管道实现日志的解耦:

  • 业务进程将日志写入命名管道。
  • 专门的日志处理进程(如日志过滤、归档、上传)从管道读取日志并处理。
  • 优势:业务进程无需关心日志的存储和上传,提高性能和可维护性。

6.3 命令行工具协作

通过命名管道连接多个命令行工具,实现数据流式处理:

# 将ls的输出通过命名管道传递给grep过滤
mkfifo pipe
ls -l > pipe &
grep ".c" < pipe
rm pipe

6.4 服务端 - 客户端模型

服务端创建命名管道,客户端通过该管道向服务端发送请求,服务端处理后返回结果(需另一个管道用于响应):

  • 优点:实现简单,无需复杂的网络编程。
  • 缺点:仅适用于本地进程通信,不支持跨机器。
IPC 机制优点缺点适用场景
命名管道实现简单,文件系统可见,跨进程半双工,无数据边界,速度一般本地任意进程间的简单数据传输
匿名管道轻量,性能高仅适用于亲缘进程,无文件实体父子进程间的临时通信
消息队列有数据边界,支持多消息类型数据量有限,内核资源消耗较大结构化数据的进程间通信
共享内存速度最快(无数据拷贝)需同步机制(信号量),实现复杂大数据量、高性能需求的通信
套接字(UDS)支持全双工,可跨网络实现复杂,性能开销较大本地 / 跨网络的进程通信
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值