You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用父进程与两个子进程写入文件?循环fork引发重复写入问题求助

问题分析与修复方案

你猜的没错,核心问题就是循环内部重复调用fork()——每次循环都创建新的child_a和child_b,n=2时就会生成4个子进程,每个子进程都在执行写文件相关的操作,最终导致文件行数变成2ⁿ。而且原代码里子进程执行一次就exit(0),完全没有复用两个子进程来处理多组数据,这和需求里“仅使用两个子进程”的要求完全不符。

另外,原代码的同步逻辑(信号、管道读写)也存在很多时序问题,比如子进程发送信号后直接读写管道,没有确保对方已经准备好,很容易出现读写阻塞或数据错乱的情况。

关键修复点

1. 把fork()移到循环外

只创建一次child_a和child_b,让这两个子进程通过循环和父进程配合,完成N次数据处理,而不是每次处理都新建子进程。

2. 重构同步逻辑

用信号+管道的组合,严格按照需求的步骤来实现进程间的同步:

  • 父进程启动后先等待child_b的SIGUSR1信号
  • child_b发送信号后等待父进程写入数据到管道
  • child_b读取数据后,通知child_a生成随机数
  • child_a生成随机数写入管道,child_b读取后发给父进程
  • 父进程完成文件写入后,触发下一轮循环

3. 正确管理管道生命周期

确保管道的读写端在进程间正确共享,不要在单次操作后就关闭,直到所有数据处理完成后再关闭。

修复后的示例代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <time.h>
#include <string.h>

// 全局原子变量用于信号同步(简单场景下可用,复杂场景建议用信号量)
volatile sig_atomic_t parent_ready = 0;
volatile sig_atomic_t child_b_ready = 0;
volatile sig_atomic_t child_a_triggered = 0;

void handler(int signum) {
    if (signum == SIGUSR1) {
        child_b_ready = 1;
    } else if (signum == SIGUSR2) {
        if (getpid() == getppid()) { // 父进程收到child_b的完成信号
            parent_ready = 1;
        } else { // child_a收到触发信号
            child_a_triggered = 1;
        }
    }
}

typedef struct {
    char address[100];
} Data;

int main() {
    FILE *fp2 = fopen("bill.dat", "w"); // 用w清空文件,避免追加旧内容
    if (!fp2) {
        perror("fopen failed");
        exit(EXIT_FAILURE);
    }

    pid_t child_a, child_b;
    int pipefd_parent_to_b[2]; // 父进程->child_b的管道
    int pipefd_a_to_b[2];      // child_a->child_b的管道
    int pipefd_b_to_parent[2]; // child_b->父进程的管道

    // 初始化信号处理
    signal(SIGUSR1, handler);
    signal(SIGUSR2, handler);

    // 创建三个管道分别对应不同的数据流
    if (pipe(pipefd_parent_to_b) == -1 || pipe(pipefd_a_to_b) == -1 || pipe(pipefd_b_to_parent) == -1) {
        perror("pipe failed");
        exit(EXIT_FAILURE);
    }

    // 先创建child_a
    child_a = fork();
    if (child_a < 0) {
        perror("fork child_a failed");
        exit(EXIT_FAILURE);
    }

    if (child_a == 0) {
        // child_a逻辑:等待触发信号,生成随机数,写入管道
        srand(time(NULL) ^ getpid()); // 用pid避免和父进程随机种子重复
        close(pipefd_parent_to_b[0]);
        close(pipefd_parent_to_b[1]);
        close(pipefd_b_to_parent[0]);
        close(pipefd_b_to_parent[1]);

        while (1) {
            // 等待child_b的触发信号
            while (!child_a_triggered) {
                pause();
            }
            child_a_triggered = 0;

            // 生成1-5的随机数
            int r = rand() % 5 + 1;
            char rand_str[10];
            snprintf(rand_str, sizeof(rand_str), "%d", r);
            printf("Child_a generated random number: %s\n", rand_str);

            // 写入到child_b的管道
            write(pipefd_a_to_b[1], rand_str, strlen(rand_str) + 1);
        }
        exit(EXIT_SUCCESS);
    }

    // 创建child_b
    child_b = fork();
    if (child_b < 0) {
        perror("fork child_b failed");
        exit(EXIT_FAILURE);
    }

    if (child_b == 0) {
        // child_b逻辑:按步骤和父进程、child_a交互
        char data_buf[100];
        char rand_buf[10];
        close(pipefd_parent_to_b[1]);
        close(pipefd_a_to_b[0]);
        close(pipefd_b_to_parent[0]);

        while (1) {
            // 步骤2:向父进程发送信号,等待数据
            kill(getppid(), SIGUSR1);
            printf("Child_b sent signal to parent, waiting for data...\n");

            // 读取父进程发来的data[i]
            read(pipefd_parent_to_b[0], data_buf, sizeof(data_buf));
            printf("Child_b read data from parent: %s\n", data_buf);

            // 步骤4:触发child_a生成随机数
            kill(child_a, SIGUSR2);
            printf("Child_b triggered child_a...\n");

            // 读取child_a的随机数
            read(pipefd_a_to_b[0], rand_buf, sizeof(rand_buf));
            printf("Child_b read random number from child_a: %s\n", rand_buf);

            // 步骤6:发送随机数给父进程
            write(pipefd_b_to_parent[1], rand_buf, strlen(rand_buf) + 1);
            kill(getppid(), SIGUSR2); // 通知父进程可以读取了
        }
        exit(EXIT_SUCCESS);
    }

    // 父进程逻辑:循环处理N组数据
    Data data[] = {{"data1"}, {"data2"}}; // 示例数据,n=2
    int n = sizeof(data) / sizeof(data[0]);
    close(pipefd_parent_to_b[0]);
    close(pipefd_a_to_b[0]);
    close(pipefd_a_to_b[1]);
    close(pipefd_b_to_parent[1]);

    for (int i = 0; i < n; i++) {
        parent_ready = 0;
        child_b_ready = 0;

        printf("\n============== Processing data %d ==============\n", i);
        // 步骤1:等待child_b的信号
        printf("Parent waiting for child_b's signal...\n");
        while (!child_b_ready) {
            pause();
        }
        printf("Parent received signal from child_b\n");

        // 步骤3:写入data[i]到管道
        write(pipefd_parent_to_b[1], data[i].address, strlen(data[i].address) + 1);
        printf("Parent wrote data to pipe: %s\n", data[i].address);

        // 等待child_b发送随机数完成的信号
        while (!parent_ready) {
            pause();
        }

        // 读取child_b发来的随机数
        char rand_buf[10];
        read(pipefd_b_to_parent[0], rand_buf, sizeof(rand_buf));

        // 步骤7:写入文件
        fprintf(fp2, "%s - %s\n", data[i].address, rand_buf);
        printf("Parent wrote to file: %s - %s\n", data[i].address, rand_buf);
    }

    // 处理完成后终止子进程
    kill(child_a, SIGKILL);
    kill(child_b, SIGKILL);
    waitpid(child_a, NULL, 0);
    waitpid(child_b, NULL, 0);

    fclose(fp2);
    return 0;
}

代码说明

  1. 子进程复用:child_a和child_b在循环外创建,内部用while(1)循环等待触发,直到父进程发送终止信号。
  2. 信号同步:用全局的原子变量跟踪信号状态,避免信号丢失或时序混乱。
  3. 管道分工:三个管道分别负责不同的数据流,避免数据交叉:
    • pipefd_parent_to_b:父进程向child_b发送数据
    • pipefd_a_to_b:child_a向child_b发送随机数
    • pipefd_b_to_parent:child_b向父进程发送随机数
  4. 资源清理:处理完成后父进程主动杀死子进程并等待回收,避免僵尸进程。

这样修改后,n=2时文件只会生成2行内容,完全符合需求。

内容的提问来源于stack exchange,提问作者TheBASS

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 10:03:39