为什么分叉我的进程导致文件无限读取

时间:2018-05-01 03:27:02

标签: c fork

我已将整个程序归结为一个复制问题的简短主题,所以原谅我没有任何意义。

input.txt是一个文本文件,里面有几行文字。这个简化的程序应该打印这些行。但是,如果调用fork,程序将进入无限循环,在此循环中反复打印文件的内容。

据我所知,我在这个代码片段中使用它的方式本质上是一个无操作。它分叉,父母在继续之前等待孩子,孩子立即被杀死。

#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);
    }
}

编辑:进一步的调查只会使我的问题变得更加奇怪。如果文件包含&lt; 4空行或&lt; 3行文本,则不会中断。但是,如果有更多,它会无限循环。

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

4 个答案:

答案 0 :(得分:7)

我很惊讶有一个问题,但它似乎确实是Linux上的一个问题(我在Mac上的VMWare Fusion VM中运行的Ubuntu 16.04 LTS上测试过) - 但是我的Mac运行时这不是问题macOS 10.13.4(High Sierra),我不希望它在Unix的其他变体上出现问题。

正如我在comment中所说:

  

每个流后面都有一个打开的文件描述和一个打开的文件描述符。当进程分叉时,子进程有自己的一组打开文件描述符(和文件流),但子进程中的每个文件描述符与父进程共享打开的文件描述。 IF (以及&#39;大&#39;如果&#39;)关闭文件描述符的子进程首先执行相当于lseek(fd, 0, SEEK_SET)的操作,然后,这也将定位父进程的文件描述符,这可能导致无限循环。但是,我从来没有听说过那个寻求的图书馆。没有理由这样做。

有关打开文件描述符和打开文件描述的详细信息,请参阅POSIX open()fork()

打开的文件描述符对进程是私有的;打开的文件描述由初始打开的文件创建的文件描述符的所有副本共享&#39;操作。打开文件描述的关键属性之一是当前搜索位置。这意味着子进程可以更改父进程的当前搜索位置 - 因为它位于共享打开文件描述中。

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 +++
$

事实恰恰相反,前4个孩子各自表现出四种行为中的一种 - 每一组4个孩子表现出相同的模式。

这表明,在退出之前,四分之三的孩子确实在标准输入上做了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 Bug 23151

GLIBC Bug 23151 - 带有未关闭文件的分叉进程在退出之前会执行lseek,并且会导致父I / O中出现无限循环。

该错误创建于2019-05-08美国/太平洋地区,并于2018-05-09关闭为无效。给出的理由是:

  

请阅读   http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01,   尤其是这一段:

     

请注意,在fork()之后,存在两个句柄,其中之前存在一个句柄。 [...]

POSIX

POSIX的完整部分(除了注意到C标准未涵盖的措辞之外)是:

  

2.5.1 Interaction of File Descriptors and Standard I/O Streams

     

可以通过文件描述符访问打开的文件描述,文件描述符是使用open()pipe()等函数创建的,或者是通过使用{{3}等函数创建的流创建的。 }或fopen()。文件描述符或流被称为&#34;句柄&#34;在它所指的开放文件描述中;打开的文件描述可能有几个句柄。

     

可以通过显式用户操作创建或销毁句柄,而不会影响基础打开文件描述。创建它们的一些方法包括popen()fcntl()dup()fdopen()fileno()。至少fork()fclose()close()函数可以销毁它们。

     

在可能影响文件偏移的操作中从不使用的文件描述符(例如,execread()write())不被视为此讨论的句柄,但可能会产生一个(例如,lseek()fdopen()dup()的结果。此异常不包括流的基础文件描述符,无论是使用fork()还是fopen()创建的,只要应用程序不直接使用它来影响文件偏移量。 fdopen()read()函数隐式影响文件偏移量; write()明确影响了它。

     

涉及任何一个句柄的函数调用的结果(&#34;活动句柄&#34;)在本卷POSIX.1-2017的其他地方定义,但如果使用了两个或更多句柄,并且任何一个句柄它们是一个流,应用程序应确保其行为如下所述进行协调。如果不这样做,结果是不确定的。

     

当在{(3}}或lseek()上使用非完整(1)文件名执行时,被视为流的句柄被关闭(对于具有空文件名的fclose(),无论是创建新句柄还是重用现有句柄,都是实现定义的,或者当拥有该流的进程终止于freopen()freopen()时,或者由于信号。当在该文件描述符上设置FD_CLOEXEC时,文件描述符由exit()abort()close()函数关闭。

(1) [sic]使用&#39; non-full&#39;可能是非空的错误&#39;

  

要使句柄成为活动句柄,应用程序应确保在最后一次使用句柄(当前活动句柄)和第一次使用第二个句柄(未来活动句柄)之间执行以下操作。然后第二个句柄成为活动句柄。应用程序影响第一个句柄上的文件偏移量的所有活动都应暂停,直到它再次成为活动文件句柄。 (如果流函数具有影响文件偏移量的基础函数,则应认为流函数会影响文件偏移量。)

     

处理这些规则的手柄不需要在同一个过程中。

     

请注意,在_exit()之后,存在两个句柄,其中之前存在一个句柄。应用程序应确保,如果可以访问这两个句柄,它们都处于另一个可以成为活动句柄的状态。应用程序应准备exec(),就像更改活动句柄一样。 (如果其中一个进程执行的唯一操作是fork()函数之一或fork()(而不是exec()),则在该进程中永远不会访问句柄。)

     

对于第一个句柄,适用以下第一个适用条件。在执行下面所需的操作后,如果句柄仍处于打开状态,则应用程序可以将其关闭。

     
      
  • 如果是文件描述符,则无需任何操作。

  •   
  • 如果要对此打开文件描述符的任何句柄执行的唯一进一步操作是关闭它,则不需要采取任何操作。

  •   
  • 如果是无缓冲的流,则无需采取任何措施。

  •   
  • 如果它是一个行缓冲的流,并且写入流的最后一个字节是<newline>(就像a:

    putc('\n')
    
         

    是该流的最新操作),无需采取任何措施。

  •   
  • 如果它是一个可以写入或附加的流(但也不能打开阅读),则应用程序应执行_exit(),否则流将被关闭。

  •   
  • 如果流已打开以供阅读且位于文件末尾(exit()为真),则无需采取任何措施。

  •   
  • 如果使用允许读取的模式打开流,并且底层打开文件描述指的是能够搜索的设备,则应用程序应执行fflush(),或者流应为闭合。

  •   
     

对于第二个句柄:

     
      
  • 如果显式更改文件偏移量的函数使用了任何先前的活动句柄,除非上面第一个句柄需要,应用程序应执行feof()fflush()(根据需要)把手的类型)到适当的位置。
  •   
     

如果在满足上面第一个句柄的要求之前,活动句柄仍然无法访问,则打开文件描述的状态将变为未定义。这可能发生在lseek()fseek()等功能中。

     

fork()函数使得在调用它们时打开的所有流都无法访问,而与新过程映像可用的流或文件描述符无关。

     

当遵循这些规则时,无论使用的句柄顺序如何,实现都应确保应用程序(即使是由多个进程组成的应用程序)应产生正确的结果:写入时不会丢失或重复数据,并且所有数据都应除非被要求提出要求,否则应按顺序书写。它是实现定义的,无论是在什么条件下,所有输入都只被看到一次。

     

在流上运行的每个函数都被认为具有零个或多个基础函数&#34;。这意味着stream函数与底层函数共享某些特征,但不要求stream函数的实现与其底层函数之间存在任何关系。

诠释

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

在这个问题的代码(以及file descriptor)的上下文中,我们有一个文件流句柄只能读取尚未遇到的EOF(所以feof()不会返回true ,即使读取位置位于文件的末尾。)

规范的一个关键部分是:应用程序应准备fork(),就像更改活动句柄一样。

这意味着针对&#39;首先处理文件的步骤&#39;是相关的,并通过它们,第一个适用的条件是最后一个:

  
      
  • 如果使用允许读取的模式打开流,并且底层打开文件描述指的是能够搜索的设备,则应用程序应执行fflush(),否则应关闭流。< / LI>   

如果你看一下open file description的定义,你会发现:

  

如果 stream 指向未输入最新操作的输出流或更新流,fflush()将导致该流的任何未写入数据写入文件,[CX]⌦以及基础文件的最后一次数据修改和上次文件状态更改时间戳应标记为更新。

     

对于打开以使用基础文件描述进行读取的流,如果文件尚未处于EOF,并且该文件是能够搜索的文件,则基础打开文件描述的文件偏移应设置为文件位置流,以及随后未从流中读取的Unwanted child processes being created while file readingfflush()推回到流上的任何字符都将被丢弃(不再进一步更改文件偏移量)。 ⌫

如果您将fflush()应用于与不可搜索文件相关联的输入流,则不清楚会发生什么,但这不是我们直接关注的问题。但是,如果您正在编写通用库代码,那么在对流执行fflush()之前,您可能需要知道底层文件描述符是否可搜索。或者,使用fflush(NULL)让系统执行所有I / O流所需的操作,并注意这将丢失任何推回的字符(通过ungetc()等)。

lseek()输出中显示的strace操作似乎正在实现将打开文件描述的文件偏移量与流的文件位置相关联的fflush()语义。

因此,对于此问题中的代码,fflush(stdin)之前似乎需要fork()以确保一致性。不这样做会导致未定义的行为(&#39;如果没有这样做,结果是未定义的&#39;) - 例如无限循环。

答案 1 :(得分:2)

exit()调用将关闭所有打开的文件句柄。在fork之后,子级和父级具有相同的执行堆栈副本,包括FileHandle指针。当子进入时,它会关闭文件并重置指针。

  int main(){
        freopen("input.txt", "r", stdin);
        char s[MAX];
        prompt(s);
        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();   // At this point both processes has a copy of the filehandle
            if (pid == 0) {
                exit(0);          // At this point the child closes the filehandle
            } else {
                waitpid(pid, &status, 0);
            }
            //End region
            printf("%s", s);
            ret = fgets(s, MAX, stdin);
        }
    }

答案 2 :(得分:1)

正如/ u / visibleman指出的那样,子线程正在关闭文件并在main中弄乱了。

通过检查程序是否处于终端模式

,我能够解决这个问题
!isatty(fileno(stdin))

如果stdin已被重定向,那么在进行任何处理或分叉之前,它会将所有内容读入链接列表。

答案 3 :(得分:0)

用_exit(0)替换exit(0),一切都很好。这是一个古老的Unix传统,如果您使用的是stdio,则分叉的图像必须使用_exit(),而不是exit()。