19. 管道.md 7.4 KB

19. 管道

一、概述

管道(Pipe)是Unix/Linux中最古老的进程间通信(IPC)方式,提供单向数据流。

二、管道概念

2.1 基本特性

  • 半双工:数据只能单向流动
  • 单向数据流:一端写入,另一端读出
  • 面向字节流:不保留消息边界
  • 内核缓冲区:Linux默认64KB(可调整)

2.2 两种类型

  1. 匿名管道(pipe):只能用于有亲缘关系的进程
  2. 命名管道(FIFO):可用于任意进程间通信

三、pipe()匿名管道

3.1 API详解

#include <unistd.h>
int pipe(int pipefd[2]);
  • pipefd[0]:读端文件描述符
  • pipefd[1]:写端文件描述符
  • 返回值:成功返回0,失败返回-1

3.2 代码示例:父子进程通信

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
    int pipefd[2];
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(1);
    }

    if (pid == 0) {
        // 子进程:写入数据
        close(pipefd[0]); // 关闭读端
        const char *msg = "Hello from child process!";
        write(pipefd[1], msg, strlen(msg) + 1);
        close(pipefd[1]);
        exit(0);
    } else {
        // 父进程:读取数据
        close(pipefd[1]); // 关闭写端
        char buf[1024];
        ssize_t n = read(pipefd[0], buf, sizeof(buf));
        if (n > 0) {
            printf("Parent received: %s\n", buf);
        }
        close(pipefd[0]);
        wait(NULL);
    }
    return 0;
}

编译命令:

gcc -o pipe_demo pipe_demo.c
./pipe_demo

3.3 代码示例:双向通信(两个管道)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
    int pipe1[2], pipe2[2]; // pipe1: 父->子, pipe2: 子->父
    pipe(pipe1);
    pipe(pipe2);

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程
        close(pipe1[1]); // 关闭pipe1写端
        close(pipe2[0]); // 关闭pipe2读端

        char buf[1024];
        read(pipe1[0], buf, sizeof(buf));
        printf("Child received: %s\n", buf);

        const char *reply = "Hello from child!";
        write(pipe2[1], reply, strlen(reply) + 1);

        close(pipe1[0]);
        close(pipe2[1]);
        exit(0);
    } else {
        // 父进程
        close(pipe1[0]); // 关闭pipe1读端
        close(pipe2[1]); // 关闭pipe2写端

        const char *msg = "Hello from parent!";
        write(pipe1[1], msg, strlen(msg) + 1);

        char buf[1024];
        read(pipe2[0], buf, sizeof(buf));
        printf("Parent received: %s\n", buf);

        close(pipe1[1]);
        close(pipe2[0]);
        wait(NULL);
    }
    return 0;
}

编译命令:

gcc -o pipe双向通信 pipe双向通信.c
./pipe双向通信

四、管道读写规则

4.1 原子写入

  • 管道写入原子性:当写入数据 <= PIPE_BUF(Linux默认4096字节)时,保证原子性
  • 超过PIPE_BUF可能被拆分

4.2 读写行为

场景 行为
管道空,读阻塞 read阻塞等待数据
管道满,写阻塞 write阻塞等待空间
读端关闭,写入 收到SIGPIPE信号,write返回EPIPE
写端关闭,读取 read返回0(EOF)
非阻塞读(O_NONBLOCK) 管道空返回-1,errno=EAGAIN
非阻塞写(O_NONBLOCK) 管道满返回-1,errno=EAGAIN

4.3 代码示例:非阻塞管道

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    // 设置非阻塞
    fcntl(pipefd[0], F_SETFL, O_NONBLOCK);
    fcntl(pipefd[1], F_SETFL, O_NONBLOCK);

    char buf[1024];
    ssize_t n = read(pipefd[0], buf, sizeof(buf));
    if (n == -1 && errno == EAGAIN) {
        printf("Pipe is empty (non-blocking read)\n");
    }

    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

编译命令:

gcc -o pipe_nonblock pipe_nonblock.c
./pipe_nonblock

五、管道与fork

5.1 共享文件描述符

fork后子进程继承父进程的文件描述符表,父子进程共享管道的读写端。

5.2 代码示例:管道与fork

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程关闭读端,只写
        close(pipefd[0]);
        write(pipefd[1], "child", 5);
        close(pipefd[1]);
        exit(0);
    } else {
        // 父进程关闭写端,只读
        close(pipefd[1]);
        char buf[1024];
        read(pipefd[0], buf, sizeof(buf));
        printf("Received: %s\n", buf);
        close(pipefd[0]);
        wait(NULL);
    }
    return 0;
}

六、命名管道mkfifo

6.1 API详解

#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
  • pathname:管道文件路径
  • mode:文件权限
  • 返回值:成功返回0,失败返回-1

6.2 代码示例:命名管道通信

// writer.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    mkfifo("/tmp/myfifo", 0666);
    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello FIFO!", 12);
    close(fd);
    return 0;
}

// reader.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[1024];
    read(fd, buf, sizeof(buf));
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}

编译命令:

gcc -o writer writer.c
gcc -o reader reader.c
# 终端1运行
./writer
# 终端2运行
./reader

6.3 特性

  • 命名管道存在于文件系统中,但不存储数据
  • 任意进程(有权限)都可以打开使用
  • 遵循FIFO原则

七、嵌入式场景

7.1 进程间数据流传递

// 传感器数据采集进程 -> 数据处理进程
// 通过管道传递采集数据

7.2 Shell管道命令

cat sensor_data.log | grep "temperature" | awk '{print $2}'
# 每个|创建一个管道,进程间通过管道通信

7.3 实时日志传输

// 嵌入式设备将日志通过管道传给串口打印程序
int pipefd[2];
pipe(pipefd);
pid_t pid = fork();
if (pid == 0) {
    // 子进程:读管道,写串口
    dup2(pipefd[0], STDIN_FILENO);
    execl("/bin/cat", "cat", "/dev/ttyS0", NULL);
}
// 父进程:写管道
write(pipefd[1], log_data, len);

八、注意事项

  1. 管道是单向的,双向通信需两个管道
  2. 匿名管道只能用于父子进程
  3. 命名管道在文件系统中创建文件,使用后应删除
  4. 管道缓冲区有限,注意阻塞问题
  5. 信号SIGPIPE的处理

九、面试要点

  1. 管道与消息队列的区别:管道面向字节流,消息队列保留消息边界
  2. PIPE_BUF的作用:保证原子写入的最大值
  3. 命名管道与匿名管道的区别:有无亲缘关系、文件系统存在性
  4. 管道的阻塞特性:读写端的阻塞行为
  5. 如何用管道实现进程池:父进程创建多个子进程,通过管道分发任务