c - 为什么 fork 我的进程导致文件被无限读取

标签 c linux fork glibc stdio

我将整个程序简化为可重复该问题的简短主程序,因此请原谅我没有任何意义。

input.txt是一个文本文件,其中包含几行文本。这个煮好的程序应该打印那些行。但是,如果调用fork,程序将进入无限循环,在此循环反复打印文件的内容。

据我了解,在此片段中使用fork的方式本质上是禁止操作的。它 fork , parent 在继续之前等待 child , child 立即被杀死。

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

enum { MAX = 100 };

int main(){
    freopen("input.txt", "r", stdin);
    char s[MAX];

    int i = 0;
    char* ret = fgets(s, MAX, stdin);
    while (ret != NULL) {
        //Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0) {
            exit(0);
        } else {
            waitpid(pid, &status, 0);
        }
        //End region
        printf("%s", s);
        ret = fgets(s, MAX, stdin);
    }
}

编辑:进一步的调查只使我的问题变得陌生。如果文件包含<4空行或<3行文本,则文件不会中断。但是,如果超过此数目,它将无限循环。

Edit2:如果文件包含3行数字,它将无限循环,但是如果包含3行单词,则不会循环。

最佳答案

我对此感到惊讶,但在Linux上似乎确实是个问题(我在Mac上的VMWare Fusion VM上运行的Ubuntu 16.04 LTS上进行了测试),但在运行macOS 10.13的Mac上却不是问题。 4(高Sierra),我也不希望它在Unix的其他变体上成为问题。

正如我在comment中指出的:

There's an open file description and an open file descriptor behind each stream. When the process forks, the child has its own set of open file descriptors (and file streams), but each file descriptor in the child shares the open file description with the parent. IF (and that's a big 'if') the child process closing the file descriptors first did the equivalent of lseek(fd, 0, SEEK_SET), then that would also position the file descriptor for the parent process, and that could lead to an infinite loop. However, I've never heard of a library that does that seek; there's no reason to do it.



有关打开文件描述符和打开文件描述的更多信息,请参见POSIX open() fork()

打开文件描述符是进程专用的。打开的文件描述由初始“打开文件”操作创建的文件描述符的所有副本共享。打开文件描述的关键属性之一是当前查找位置。这意味着子进程可以更改父进程的当前查找位置,因为它位于共享的打开文件描述中。
neof97.c
我使用了以下代码-原始代码的适度修改版本,可以使用严格的编译选项进行干净地编译:
#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX = 100 };

int main(void)
{
    if (freopen("input.txt", "r", stdin) == 0)
        return 1;
    char s[MAX];
    for (int i = 0; i < 30 && fgets(s, MAX, stdin) != NULL; i++)
    {
        // Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0)
        {
            exit(0);
        }
        else
        {
            waitpid(pid, &status, 0);
        }
        // End region
        printf("%s", s);
    }
    return 0;
}

修改之一将周期数(子级)限制为仅30个。
我使用了一个数据文件,其中包含4行20个随机字母加一个换行符(总共84个字节):
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe

我在Ubuntu的strace下运行了命令:
$ strace -ff -o st-out -- neof97
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
…
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
$

有31个文件名为st-out.808##格式,其中哈希是2位数字。主过程文件很大。其他的则很小,尺寸为66、110、111或137:
$ cat st-out.80833
lseek(0, -63, SEEK_CUR)                 = 21
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80834
lseek(0, -42, SEEK_CUR)                 = -1 EINVAL (Invalid argument)
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80835
lseek(0, -21, SEEK_CUR)                 = 0
exit_group(0)                           = ?
+++ exited with 0 +++
$ cat st-out.80836
exit_group(0)                           = ?
+++ exited with 0 +++
$

碰巧的是,前四个 child 每个都表现出四种行为中的一种-并且每四个 child 中的每一个表现出相同的模式。

这表明四分之三的 child 确实在退出之前在标准输入上做了lseek()。显然,我现在已经看到了一个图书馆。我不知道为什么认为这是一个好主意,但从经验上讲,这就是正在发生的事情。
neof67.c
使用单独的文件流(和文件描述符)和fopen()而不是freopen()的此版本的代码也遇到了问题。
#include "posixver.h"
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

enum { MAX = 100 };

int main(void)
{
    FILE *fp = fopen("input.txt", "r");
    if (fp == 0)
        return 1;
    char s[MAX];
    for (int i = 0; i < 30 && fgets(s, MAX, fp) != NULL; i++)
    {
        // Commenting out this region fixes the issue
        int status;
        pid_t pid = fork();
        if (pid == 0)
        {
            exit(0);
        }
        else
        {
            waitpid(pid, &status, 0);
        }
        // End region
        printf("%s", s);
    }
    return 0;
}

除了显示查找的文件描述符是3而不是0之外,这也表现出相同的行为。因此,我的两个假设被驳斥了-与freopen()stdin有关;两者在第二个测试代码中显示不正确。

初步诊断

IMO,这是一个错误。您应该不会遇到这个问题。
这很可能是Linux(GNU C)库而不是内核中的错误。这是由子进程中的lseek()引起的。目前尚不清楚(因为我没有去看源代码)库在做什么或为什么这样做。

GLIBC错误23151

GLIBC Bug 23151-包含未关闭文件的 fork 进程在退出之前不会查找,并可能导致父I/O无限循环。

该错误创建于2019-05-08美国/太平洋地区,并于2018-05-09被关闭为INVALID。给出的原因是:

Please read http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01, especially this paragraph:

Note that after a fork(), two handles exist where one existed before. […]



POSIX

所提到的POSIX的完整部分(除了注意到该标准未涵盖的文字外)是:

2.5.1 Interaction of File Descriptors and Standard I/O Streams

An open file description may be accessed through a file descriptor, which is created using functions such as open() or pipe(), or through a stream, which is created using functions such as fopen() or popen(). Either a file descriptor or a stream is called a "handle" on the open file description to which it refers; an open file description may have several handles.

Handles can be created or destroyed by explicit user action, without affecting the underlying open file description. Some of the ways to create them include fcntl(), dup(), fdopen(), fileno(), and fork(). They can be destroyed by at least fclose(), close(), and the exec functions.

A file descriptor that is never used in an operation that could affect the file offset (for example, read(), write(), or lseek()) is not considered a handle for this discussion, but could give rise to one (for example, as a consequence of fdopen(), dup(), or fork()). This exception does not include the file descriptor underlying a stream, whether created with fopen() or fdopen(), so long as it is not used directly by the application to affect the file offset. The read() and write() functions implicitly affect the file offset; lseek() explicitly affects it.

The result of function calls involving any one handle (the "active handle") is defined elsewhere in this volume of POSIX.1-2017, but if two or more handles are used, and any one of them is a stream, the application shall ensure that their actions are coordinated as described below. If this is not done, the result is undefined.

A handle which is a stream is considered to be closed when either an fclose(), or freopen() with non-full(1) filename, is executed on it (for freopen() with a null filename, it is implementation-defined whether a new handle is created or the existing one reused), or when the process owning that stream terminates with exit(), abort(), or due to a signal. A file descriptor is closed by close(), _exit(), or the exec() functions when FD_CLOEXEC is set on that file descriptor.



(1)[sic]使用“non-full”可能是“non-null”的错字。

For a handle to become the active handle, the application shall ensure that the actions below are performed between the last use of the handle (the current active handle) and the first use of the second handle (the future active handle). The second handle then becomes the active handle. All activity by the application affecting the file offset on the first handle shall be suspended until it again becomes the active file handle. (If a stream function has as an underlying function one that affects the file offset, the stream function shall be considered to affect the file offset.)

The handles need not be in the same process for these rules to apply.

Note that after a fork(), two handles exist where one existed before. The application shall ensure that, if both handles can ever be accessed, they are both in a state where the other could become the active handle first. The application shall prepare for a fork() exactly as if it were a change of active handle. (If the only action performed by one of the processes is one of the exec() functions or _exit() (not exit()), the handle is never accessed in that process.)

For the first handle, the first applicable condition below applies. After the actions required below are taken, if the handle is still open, the application can close it.

  • If it is a file descriptor, no action is required.

  • If the only further action to be performed on any handle to this open file descriptor is to close it, no action need be taken.

  • If it is a stream which is unbuffered, no action need be taken.

  • If it is a stream which is line buffered, and the last byte written to the stream was a <newline> (that is, as if a:

    putc('\n')
    

    was the most recent operation on that stream), no action need be taken.

  • If it is a stream which is open for writing or appending (but not also open for reading), the application shall either perform an fflush(), or the stream shall be closed.

  • If the stream is open for reading and it is at the end of the file (feof() is true), no action need be taken.

  • If the stream is open with a mode that allows reading and the underlying open file description refers to a device that is capable of seeking, the application shall either perform an fflush(), or the stream shall be closed.

For the second handle:

  • If any previous active handle has been used by a function that explicitly changed the file offset, except as required above for the first handle, the application shall perform an lseek() or fseek() (as appropriate to the type of handle) to an appropriate location.

If the active handle ceases to be accessible before the requirements on the first handle, above, have been met, the state of the open file description becomes undefined. This might occur during functions such as a fork() or _exit().

The exec() functions make inaccessible all streams that are open at the time they are called, independent of which streams or file descriptors may be available to the new process image.

When these rules are followed, regardless of the sequence of handles used, implementations shall ensure that an application, even one consisting of several processes, shall yield correct results: no data shall be lost or duplicated when writing, and all data shall be written in order, except as requested by seeks. It is implementation-defined whether, and under what conditions, all input is seen exactly once.

Each function that operates on a stream is said to have zero or more "underlying functions". This means that the stream function shares certain traits with the underlying functions, but does not require that there be any relation between the implementations of the stream function and its underlying functions.



注释

很难读!如果您不清楚打开文件描述符和打开文件描述之间的区别,请阅读open()fork()(以及dup() dup2() )的规范。如果简短,file descriptoropen file description的定义也相关。

在此问题代码的上下文中(以及Unwanted child processes being created while file reading),我们打开了一个文件流句柄,以仅读取尚未遇到EOF的文件(因此,即使读取位置位于EOF的末尾,feof()也不会返回true)文件)。

规范的关键部分之一是:应用程序应准确地准备fork(),就好像它是 Activity 句柄的更改一样。

这意味着为“第一个文件句柄”概述的步骤是相关的,并逐步执行,第一个适用条件是最后一个:

  • If the stream is open with a mode that allows reading and the underlying open file description refers to a device that is capable of seeking, the application shall either perform an fflush(), or the stream shall be closed.


如果查看 fflush() 的定义,则会发现:

If stream points to an output stream or an update stream in which the most recent operation was not input, fflush() shall cause any unwritten data for that stream to be written to the file, [CX] ⌦ and the last data modification and last file status change timestamps of the underlying file shall be marked for update.

For a stream open for reading with an underlying file description, if the file is not already at EOF, and the file is one capable of seeking, the file offset of the underlying open file description shall be set to the file position of the stream, and any characters pushed back onto the stream by ungetc() or ungetwc() that have not subsequently been read from the stream shall be discarded (without further changing the file offset). ⌫



目前尚不清楚如果将fflush()应用于与不可搜索文件关联的输入流会发生什么,但这不是我们的直接关注。但是,如果要编写通用库代码,则可能需要在对流执行fflush()之前,先了解基础文件描述符是否可搜索。或者,使用fflush(NULL)使系统执行所有I/O流所需的所有操作,请注意,这将丢失任何后推字符(通过ungetc()等)。
lseek()输出中显示的strace操作似乎正在实现fflush()语义,将打开的文件描述的文件偏移量与流的文件位置相关联。

因此,对于此问题中的代码,似乎fflush(stdin)fork()之前是必需的,以确保一致性。不这样做会导致不确定的行为(“如果不这样做,则结果是不确定的”),例如无限循环。

关于c - 为什么 fork 我的进程导致文件被无限读取,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50110992/

相关文章:

django - Gunicorn 同步 worker 生成进程

c - 在 C 中实现简单的高通和低通滤波器

linux - ctags 没有找到所有的 gtk 方法

c - 需要帮助处理 C 中的二维结构数组

c++ - 生成 C++ 代码的开源 UML 工具

linux - 如何将变量传递给命令行程序?

java - 使用 spring-batch 时 fork JVM

c - Fork() 导致打印语句重叠

c - linux 中的 gui 使用 gcc

c - 接口(interface) C 函数与 Fortran 中的结构