【问题标题】:Chaos when fork() meets fopen()fork() 遇到 fopen() 时的混乱
【发布时间】:2019-03-04 21:08:59
【问题描述】:

我发现如果我们在关闭它之前执行 fork(),打开的文件流会变得混乱。众所周知,当父进程和子进程想要修改文件流时,可能会发生并发,即竞争条件。但是,即使子进程从未接触过文件流,它仍然具有未定义的行为。我想知道是否有人可以从子进程被分叉和退出的阶段内核如何处理文件流来解释这一点。

下面是一个奇怪行为的快速sn-p:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> 
#include <sys/wait.h> 

int main() {
    // Open file
    FILE* fp = fopen("test.txt", "r");

    int count = 0;
    char* buffer = NULL;
    size_t capacity = 0;
    ssize_t line = 0;
    while ( (line = getline(&buffer, &capacity, fp)) != -1 ) {
        if (line > 0 && buffer[line - 1] == '\n') // remove the end '\n'
            buffer[line - 1] = 0;

        pid_t pid = fork();
        if (pid == 0) {
            // fclose(fp); // Magic line here: when you add this, everything is fine
            if (*buffer == '2')
                execlp("xyz", "xyz", NULL);
            else
                execlp("pwd", "pwd", NULL);
            exit(1);
        } else {
            waitpid(pid, NULL, 0);
        }
        count++;
    }
    printf("Loops: %d\n", count);
    return 0;
}

只需将代码复制到一个新文件中(例如 test.c)。并用简单的内容创建一个 .txt 文件 test.txt

1
2
3
4

然后运行

$ gcc test.c && ./a.out

文件中有 4 行。预计循环将读取每一行并准确执行 4 次 (1 2 3 4)。我选择让它在第二个循环中执行无效命令“xyz”。然后,你会发现循环实际上执行了 6 次(1 2 3 4 3 4)!事实是,当执行的所有四个命令都有效时,什么都不会出错。但是如果执行了一个无效的命令,它之后的每个命令都会被执行两次。 (请注意,这种奇怪的行为只发生在 Linux 机器上,我的 Mac OS 运行良好,不确定 Windows。所以问题与平台有关?)

看起来每当我 fork() 时,父级中的文件流不再被承诺为旧 fp(非确定性行为),即使我的子进程没有触及它。

我找到的一个临时解决方案是:子进程中的 fclose(fp)。这将使上述奇怪的行为沉默,但在更复杂的情况下,仍然可以观察到其他事情。如果有人能给我一些关于这个问题的见解,我们将不胜感激。谢谢

【问题讨论】:

  • 在调用execlp之前需要关闭打开的文件描述符,见here。所以你的临时解决方案不是临时的,而是真正的。 “其他事情”可能有不同的原因。
  • 如果您同时进行以下操作,您的所有问题都会得到解决: (1) 在致电fork 之前立即致电fflush(0); (2) 当exec 失败时调用_exit 而不是exit。在子进程中调用fclose(fp)其实是错误
  • 附录:在调用 exec 之前在子进程中调用 close(fileno(fp)) 可能是合适的,和/或在 fopen 之后立即在该文件描述符上设置 close-on-exec 位.但是,这是否真的正确取决于您未向我们展示的完整程序的详细信息。
  • 非常感谢大家,我会尝试这些解决方案
  • 查看我对问题 Why does forking my process cause the file to be read infinitely? 的回答,尤其是标题为 POSIX 和 Exegesis 的部分。我相当肯定这将解释你所看到的。这是非常微妙的。真正看到这种事情发生也花了 30 年的时间——Linux 最终选择利用这种草率(至少,这就是“从这里”的感觉)。

标签: c fork system filestream


【解决方案1】:

正如 cmets 中所说,您需要在调用 exec 之前关闭打开的文件描述符。

this blogpost(第 4 节)中有一个简洁的代码示例,您可以使用它来确保关闭所有 fd,即使在您并不总是知道当前打开哪些文件的复杂应用程序中也是如此:

for ( i=getdtablesize(); i>2; --i) 
close(i); /* close all descriptors */

(稍作修改以保持标准输入、标准输出、标准错误打开)

这有点骇人听闻,但它确实有效。如果您想避免这种情况,您还可以在您打开的每个文件描述符上设置O_CLOEXEC 标志。因为在使用 fopen 时,您不直接调用 open(),您可以通过向其添加 'e' 标志来完成此操作(使用 glibc >= 2.7 时):

FILE* fp = fopen("test.txt", "er");

当调用exec*()时,所有带有该标志的文件描述符都会自动关闭。

【讨论】:

  • 为了完整起见:“可以通过添加 'e' 标志来完成此操作”这是一个仅随 glibc 2.7 提供的 GNU 扩展(请参阅注释部分 here) .
  • 对。我将其添加到答案中。
猜你喜欢
  • 2012-01-12
  • 2011-07-24
  • 1970-01-01
  • 1970-01-01
  • 2017-08-08
  • 1970-01-01
  • 1970-01-01
  • 2017-09-04
相关资源
最近更新 更多