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

fork()后父子进程写入文件乱序/损坏的原因及解决方法

多进程文件写入乱序与数据损坏问题分析

问题代码

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

int main() {
    int fd = open("testfile.txt", O_WRONLY | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    if (fork() == 0) { 
        // 子进程
        write(fd, "Child\n", 6);
        close(fd);
    } else {
        // 父进程
        write(fd, "Parent\n", 7);
        close(fd);
    }

    return 0;
}

问题现象

  • 文件内容有时为Child\nParent\n,有时为Parent\nChild\n
  • 部分场景下出现内容混杂或损坏(例如ChildParent\n)
  • 预期两个进程独立写入,实际存在相互干扰

问题1:现象产生的原因

  1. 共享文件偏移量:fork()后,父子进程共享同一个文件描述符对应的内核文件表项,其中的文件偏移量是全局共享状态。一个进程执行write后偏移量会更新,另一个进程再写入时会从该位置开始,直接覆盖或混杂内容。
  2. 进程调度不确定性:内核进程调度是抢占式的,父子进程的执行顺序没有固定保证,谁先执行write完全由内核调度决定,导致写入顺序随机变化。
  3. 非原子写入风险:即便单条write(长度≤PIPE_BUF,通常为4096字节)是原子操作,但共享偏移量会让后续写入基于前一个进程的位置执行,依然会造成内容覆盖;若写入内容超过PIPE_BUF,单条write还可能被拆分,进一步加剧数据损坏。

问题2:实现正确独立写入的方案

方案1:使用O_APPEND标志打开文件

打开文件时添加O_APPEND标志,内核会自动保证每次write前将偏移量移到文件末尾,且单条write(长度≤PIPE_BUF)是原子操作,既能避免内容覆盖,也能保证每个进程的写入内容完整。

修改后的代码:

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

int main() {
    // 添加O_APPEND标志
    int fd = open("testfile.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    if (fork() == 0) { 
        write(fd, "Child\n", 6);
        close(fd);
    } else {
        write(fd, "Parent\n", 7);
        close(fd);
    }

    return 0;
}

注:此方案能保证内容不损坏,但写入顺序仍由进程调度决定,无法固定顺序。

方案2:使用文件锁实现互斥写入

通过fcntl给文件加互斥锁,确保同一时间只有一个进程执行写入操作,既避免内容损坏,也可按需控制写入顺序。

示例代码:

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

// 获取文件写锁(阻塞等待)
int lock_file(int fd) {
    struct flock fl;
    fl.l_type = F_WRLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0; // 锁定整个文件
    return fcntl(fd, F_SETLKW, &fl);
}

// 释放文件锁
int unlock_file(int fd) {
    struct flock fl;
    fl.l_type = F_UNLCK;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;
    return fcntl(fd, F_SETLK, &fl);
}

int main() {
    int fd = open("testfile.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    if (fork() == 0) { 
        lock_file(fd);
        write(fd, "Child\n", 6);
        unlock_file(fd);
        close(fd);
    } else {
        lock_file(fd);
        write(fd, "Parent\n", 7);
        unlock_file(fd);
        close(fd);
    }

    return 0;
}

方案3:父子进程各自独立打开文件

fork()后,父子进程分别调用open打开文件,每个进程拥有独立的文件表项和偏移量,避免共享偏移导致的覆盖。结合O_APPEND使用效果更佳:

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

int main() {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return 1;
    }

    int fd;
    if (pid == 0) { 
        // 子进程单独打开文件
        fd = open("testfile.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
        if (fd == -1) {
            perror("child open");
            return 1;
        }
        write(fd, "Child\n", 6);
        close(fd);
    } else {
        // 父进程单独打开文件
        fd = open("testfile.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);
        if (fd == -1) {
            perror("parent open");
            return 1;
        }
        write(fd, "Parent\n", 7);
        close(fd);
    }

    return 0;
}

方案4:使用进程间同步机制固定顺序

如果需要固定写入顺序,可通过管道、信号量等同步手段,让一个进程完成写入后通知另一个进程执行。例如用管道实现父进程先写、子进程后写:

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

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

    int fd = open("testfile.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }

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

    if (pid == 0) { 
        close(pipefd[1]); // 子进程关闭管道写端
        char buf;
        read(pipefd[0], &buf, 1); // 等待父进程通知
        write(fd, "Child\n", 6);
        close(pipefd[0]);
        close(fd);
    } else {
        close(pipefd[0]); // 父进程关闭管道读端
        write(fd, "Parent\n", 7);
        write(pipefd[1], "a", 1); // 通知子进程可以写入
        close(pipefd[1]);
        close(fd);
    }

    return 0;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 01:14:56