C语言fork创建子进程后如何用pipe实现进程间通信?

99ANYc3cd6
预计阅读时长 18 分钟
位置: 首页 C语言 正文

管道

pipe()fork() 通常一起使用,目的是实现 父子进程间的单向通信,这种通信方式是通过一个叫做 管道 的特殊文件来实现的。

pipe c语言 fork
(图片来源网络,侵删)

管道 的核心特点:

  1. 单向数据流:数据只能从一个方向流向另一个方向,就像一根真正的管道,水只能从一头流进,从另一头流出。
  2. 匿名管道:我们通常说的 pipe() 创建的是匿名管道,它没有在文件系统中对应的文件名,存在于内存中,它的生命周期随进程的结束而结束。
  3. 基于文件描述符:在 Linux/Unix 中,一切皆文件,管道在程序中表现为两个文件描述符:
    • 一个用于 读取 (fd[0])
    • 一个用于 写入 (fd[1])
  4. 同步与阻塞:当管道为空时,读操作会阻塞,直到有数据写入,当管道已满时,写操作会阻塞,直到有数据被读出,这天然地起到了同步作用。

pipe() 系统调用

pipe() 函数用于创建一个管道。

函数原型

#include <unistd.h>
int pipe(int pipefd[2]);

参数

  • pipefd: 一个包含两个整数的数组,函数执行成功后,pipefd[0] 将代表管道的 读端pipefd[1] 将代表管道的 写端

返回值

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

关键规则

  • 数据从 pipefd[1] 写入,从 pipefd[0] 读出。
  • 进程必须关闭它不使用的端点! 这是避免死锁的关键,如果一个进程只写不读,它就必须关闭读端;如果只读不写,就必须关闭写端。

fork() 系统调用

fork() 函数用于创建一个与当前进程(父进程)几乎完全相同的副本(子进程)。

函数原型

#include <unistd.h>
pid_t fork(void);

返回值

这是 fork() 最独特的地方:

pipe c语言 fork
(图片来源网络,侵删)
  • 在父进程中fork() 返回 子进程的 PID (Process ID),这是一个大于 0 的整数。
  • 在子进程中fork() 返回 0
  • 失败:返回 -1

关键点

  • 写时复制fork() 后,父进程和子进程共享相同的物理内存空间,但只有在其中一个进程试图修改内存时,系统才会为该进程复制一份副本,这提高了效率。
  • 文件描述符的继承:子进程会继承父进程的所有打开的文件描述符,包括 pipe() 创建的管道的两个端点,这是父子进程能够通过管道通信的基础。

pipe()fork() 协同工作流程

让我们把这两者结合起来,看看如何实现父子进程通信,最常见的场景是:父进程写入数据,子进程读取数据

标准步骤:

  1. 创建管道:在 fork() 之前,父进程先调用 pipe() 创建一个管道,父进程同时拥有了管道的读端 (fd[0]) 和写端 (fd[1])。

  2. 创建子进程:父进程调用 fork(),会发生两件事:

    • 子进程被创建。
    • 子进程继承了父进程的文件描述符表,所以子进程也同时拥有了管道的读端 (fd[0]) 和写端 (fd[1])。
  3. 关闭不必要的端点:这是最关键的一步,必须严格遵守。

    pipe c语言 fork
    (图片来源网络,侵删)
    • 父进程:如果父进程只负责写入,它必须调用 close(fd[0]) 来关闭读端,如果它不关闭,它自己可能会意外地从管道中读取数据,或者更严重,可能导致子进程的 read() 永远阻塞(如果父进程一直不写)。
    • 子进程:如果子进程只负责读取,它必须调用 close(fd[1]) 来关闭写端,如果它不关闭,它自己可能会意外地向管道中写入数据,或者更严重,可能导致父进程的 write() 永远阻塞(如果管道已满,而子进程一直不读)。
  4. 进行通信

    • 父进程使用 write(fd[1], ...) 向管道中写入数据。
    • 子进程使用 read(fd[0], ...) 从管道中读取数据。
  5. 关闭所有端点:通信结束后,父进程和子进程都应该关闭它们各自使用的端点。


代码示例:父进程写,子进程读

这是一个经典的例子,父进程向管道写入一条消息,子进程读取并打印。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#define BUFFER_SIZE 100
int main() {
    int pipefd[2]; // 用于存储管道的文件描述符
    pid_t pid;
    char write_buf[] = "Hello from parent process!";
    char read_buf[BUFFER_SIZE];
    // 1. 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    // 2. 创建子进程
    pid = fork();
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        // --- 父进程代码 ---
        printf("Parent process (PID: %d) started.\n", getpid());
        // 3. 关闭父进程的读端,因为父进程只写
        close(pipefd[0]);
        // 4. 向管道写入数据
        write(pipefd[1], write_buf, strlen(write_buf));
        printf("Parent: Wrote message to pipe.\n");
        // 关闭写端,表示写入完成
        close(pipefd[1]);
        // 等待子进程结束,避免子进程成为僵尸进程
        wait(NULL);
        printf("Parent process finished.\n");
    } else if (pid == 0) {
        // --- 子进程代码 ---
        printf("Child process (PID: %d) started.\n", getpid());
        // 3. 关闭子进程的写端,因为子进程只读
        close(pipefd[1]);
        // 4. 从管道读取数据
        // read 会阻塞,直到有数据可读
        int nbytes = read(pipefd[0], read_buf, BUFFER_SIZE - 1);
        if (nbytes > 0) {
            read_buf[nbytes] = '\0'; // 确保字符串正确终止
            printf("Child: Read message from pipe: %s\n", read_buf);
        }
        // 关闭读端
        close(pipefd[0]);
        printf("Child process finished.\n");
    }
    return 0;
}

代码解析:

  1. pipe(pipefd):创建管道,pipefd[0] 是读端,pipefd[1] 是写端。
  2. fork():创建子进程,父子进程现在都拥有 pipefd[0]pipefd[1]
  3. 父进程 (pid > 0)
    • close(pipefd[0]):父进程关闭读端,因为它只负责写。
    • write(pipefd[1], ...):向管道写入字符串。
    • close(pipefd[1]):写入完成后关闭写端。
    • wait(NULL):等待子进程执行完毕。
  4. 子进程 (pid == 0)
    • close(pipefd[1]):子进程关闭写端,因为它只负责读。
    • read(pipefd[0], ...):从管道读取数据,这个调用会阻塞,直到父进程写入数据。
    • close(pipefd[0]):读取完成后关闭读端。

输出结果:

Parent process (PID: 1234) started.
Child process (PID: 1235) started.
Parent: Wrote message to pipe.
Child: Read message from pipe: Hello from parent process!
Child process finished.
Parent process finished.

(注意:PID 1234 和 1235 是示例,每次运行都会不同)


进阶应用:双向通信

上面的例子是单向通信,要实现双向通信(父进程和子进程互相发送消息),需要创建两个管道

  • 管道1:用于父进程 -> 子进程通信。
  • 管道2:用于子进程 -> 父进程通信。

基本思路:

  1. 创建两个管道:pipe1pipe2
  2. fork() 创建子进程。
  3. 父进程
    • 关闭 pipe1 的读端 (pipe1[0]) 和 pipe2 的写端 (pipe2[1])。
    • 使用 pipe1[1] 写入,用 pipe2[0] 读取。
  4. 子进程
    • 关闭 pipe1 的写端 (pipe1[1]) 和 pipe2 的读端 (pipe2[0])。
    • 使用 pipe1[0] 读取,用 pipe2[1] 写入。
特性 描述
pipe() 创建一个匿名管道,返回两个文件描述符,一个用于读,一个用于写。
fork() 创建一个子进程,子进程继承父进程的文件描述符。
协同作用 fork() 让子进程继承父进程的管道端点,然后通过关闭不用的端点,建立起单向或双向的通信通道。
核心原则 “谁用谁开,不用谁关”,明确每个进程的角色(写者或读者),并立即关闭不用的端,以避免死锁和意外行为。

理解 pipe()fork() 的组合是迈向高级 Unix/Linux C 编程的重要一步,它们是构建复杂系统、实现进程同步和通信的基础。

-- 展开阅读全文 --
头像
dede如何实现多行多列显示文章标题和缩略图?
« 上一篇 12-07
dede cms网上商城二次开发视频怎么学?
下一篇 » 12-07

相关文章

取消
微信二维码
支付宝二维码

目录[+]