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

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

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

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

#include  #include  #include  #include  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(High Sierra),我不希望它在Unix的其他变种上也是一个问题。

正如我在评论中指出的那样:

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

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

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

neof97.c

我使用了以下代码 – 原始版本的温和版本,可以使用严格的编译选项进行干净编译:

 #include "posixver.h" #include  #include  #include  #include  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## ,其中哈希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  #include  #include  #include  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之外,这也表现出相同的行为。 所以,我的两个假设被certificate是错误的 - 它与freopen()stdin ; 第二个测试代码都显示错误。

初步诊断

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


GLIBC错误23151

GLIBC 错误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文件描述符和标准I / O流的交互

可以通过文件描述符来访问打开的文件描述,文件描述符是使用诸如open()pipe()函数创建的,或者是通过使用诸如fopen()popen()函数创建的流来创建的。 文件描述符或流在其引用的打开文件描述上称为“句柄”; 打开的文件描述可能有几个句柄。

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

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

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

当一个带有非full (1)文件名的fclose()freopen()在其上执行时(对于具有空文件名的freopen() ,它被认为是一个句柄被关闭freopen()它是实现定义的是创建新句柄还是重新使用现有句柄,或者拥有该流的进程是以exit()abort()还是由于信号abort() 。 当在该文件描述符上设置FD_CLOEXEC时, close() ,_ exit exec()exec()函数关闭文件描述符。

(1) [原文如此]使用'non-full'可能是'非null'的拼写错误。

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

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

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

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

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

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

  • 如果它是无缓冲的流,则不需要采取任何操作。

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

     putc('\n') 

    是该流的最新操作),不需要采取任何行动。

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

  • 如果流打开以供读取并且它位于文件的末尾( feof()为true),则不需要采取任何操作。

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

对于第二个句柄:

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

如果在满足上面第一个句柄的要求之前,活动句柄不再可访问,则打开文件描述的状态将变为未定义。 这可能发生在诸如fork()_exit()函数期间。

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

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

在流上操作的每个函数被称为具有零个或多个“底层函数”。 这意味着流函数与底层函数共享某些特征,但不要求流函数的实现与其底层函数之间存在任何关系。

注释

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

在这个问题中的代码的上下文中(以及在文件读取时创建的不需要的子进程 ),我们有一个文件流句柄只能读取尚未遇到EOF(因此feof()不会返回true,甚至虽然读取位置在文件的末尾)。

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

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

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

如果你看一下fflush()的定义,你会发现:

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

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

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

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

因此,对于这个问题中的代码,似乎在fork()之前必须使用fflush(stdin) fork()来确保一致性。 不这样做会导致未定义的行为 ('如果没有这样做,结果是未定义') - 例如无限循环。

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

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

通过检查程序是否处于终端模式,我能够解决它

 !isatty(fileno(stdin)) 

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