我很惊讶有一个问题,但它在 Linux 上似乎确实是一个问题(我在 Mac 上的 VMWare Fusion VM 中运行的 Ubuntu 16.04 LTS 上进行了测试),但在运行 macOS 10.13 的 Mac 上这不是问题。 4 (High Sierra),我也不认为这在其他 Unix 变体上会成为问题。
正如我在一篇文章中指出的comment https://stackoverflow.com/questions/50110992/why-does-forking-my-process-cause-the-file-to-be-read-infinitely#comment87240933_50111085:
每个流后面都有一个打开的文件描述和一个打开的文件描述符。当进程分叉时,子进程拥有自己的一组打开文件描述符(和文件流),但子进程中的每个文件描述符与父进程共享打开文件描述。IF(这是一个很大的“如果”)子进程首先关闭文件描述符所做的相当于lseek(fd, 0, SEEK_SET)
,那么这也会定位父进程的文件描述符,这可能会导致无限循环。然而,我从未听说过有哪个图书馆会这样做;没有理由这样做。
参见 POSIXopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html and fork() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html有关打开文件描述符和打开文件描述的更多信息。
打开的文件描述符是进程私有的;打开文件描述由初始“打开文件”操作创建的文件描述符的所有副本共享。打开文件描述的关键属性之一是当前查找位置。这意味着子进程可以更改父进程的当前查找位置 - 因为它位于共享的打开文件描述中。
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
我在下面运行了命令strace
在Ubuntu上:
$ strace -ff -o st-out -- neof97
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
…
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
$
有 31 个文件,其名称格式为st-out.808##
其中哈希值是两位数。主进程文件相当大;其他尺寸较小,尺寸为 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()
and stdin
;第二个测试代码显示两者都不正确。
初步诊断
IMO,这是一个错误。您不应该遇到这个问题。
这很可能是 Linux (GNU C) 库而不是内核中的错误。这是由lseek()
在子进程中。目前尚不清楚(因为我没有去查看源代码)该库正在做什么或为什么。
GLIBC 错误 23151
GLIBC 错误 23151 https://sourceware.org/bugzilla/show_bug.cgi?id=23151- 具有未关闭文件的分叉进程在退出前执行 lseek,并可能导致父 I/O 中的无限循环。
该错误于 2018 年 5 月 8 日美国/太平洋地区创建,并于 2018 年 5 月 9 日以无效状态关闭。给出的理由是:
请阅读http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01 http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01,
尤其是这一段:
请注意,经过fork()
,之前存在一个句柄的地方存在两个句柄。 […]
POSIX
所引用的 POSIX 的完整部分(除了指出 C 标准未涵盖的措辞之外)是这样的:
2.5.1 文件描述符和标准I/O流的交互 http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01
可以通过文件描述符访问打开的文件描述,该文件描述符是使用以下函数创建的open() http://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html or pipe() http://pubs.opengroup.org/onlinepubs/9699919799/functions/pipe.html,或通过流,该流是使用诸如fopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fopen.html or popen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/popen.html。文件描述符或流都被称为它所引用的打开文件描述的“句柄”;一个打开的文件描述可能有多个句柄。
可以通过显式用户操作创建或销毁句柄,而不会影响底层的打开文件描述。创建它们的一些方法包括fcntl() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fcntl.html, dup() http://pubs.opengroup.org/onlinepubs/9699919799/functions/dup.html, fdopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fdopen.html, fileno() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fileno.html, and fork() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html。它们至少可以被摧毁fclose() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fclose.html, close() http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html,以及exec http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html功能。
从未在可能影响文件偏移量的操作中使用的文件描述符(例如,read() http://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html, write() http://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html, or lseek() http://pubs.opengroup.org/onlinepubs/9699919799/functions/lseek.html)不被视为此讨论的句柄,但可能会引起一个句柄(例如,由于fdopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fdopen.html, dup() http://pubs.opengroup.org/onlinepubs/9699919799/functions/dup.html, or fork() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html)。此异常不包括流底层的文件描述符,无论是使用fopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fopen.html or fdopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fdopen.html,只要应用程序不直接使用它来影响文件偏移量。这read() http://pubs.opengroup.org/onlinepubs/9699919799/functions/read.html and write() http://pubs.opengroup.org/onlinepubs/9699919799/functions/write.html函数隐式影响文件偏移量;lseek() http://pubs.opengroup.org/onlinepubs/9699919799/functions/lseek.html明确地影响它。
涉及任何一个句柄(“活动句柄”)的函数调用的结果在 POSIX.1-2017 本卷的其他地方定义,但如果使用两个或多个句柄,并且其中任何一个是流,则应用程序应确保他们的行动如下所述协调一致。如果不这样做,结果是不确定的。
A handle which is a stream is considered to be closed when either an fclose() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fclose.html, or freopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/freopen.html with non-full(1) filename, is executed on it (for freopen() http://pubs.opengroup.org/onlinepubs/9699919799/functions/freopen.html 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() http://pubs.opengroup.org/onlinepubs/9699919799/functions/exit.html, abort() http://pubs.opengroup.org/onlinepubs/9699919799/functions/abort.html, or due to a signal. A file descriptor is closed by close() http://pubs.opengroup.org/onlinepubs/9699919799/functions/close.html, _exit() http://pubs.opengroup.org/onlinepubs/9699919799/functions/_exit.html, or the exec() http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html functions when FD_CLOEXEC is set on that file descriptor.
(1) [sic] Using 'non-full' is probably a typo for 'non-null'.
对于要成为活动句柄的句柄,应用程序应确保在最后一次使用句柄(当前活动句柄)和第一次使用第二个句柄(未来活动句柄)之间执行以下操作。然后第二个手柄将成为活动手柄。应用程序影响第一个句柄上的文件偏移量的所有活动都应暂停,直到它再次成为活动文件句柄。 (如果流函数具有影响文件偏移量的底层函数,则该流函数应被视为影响文件偏移量。)
句柄不必位于同一进程中即可应用这些规则。
请注意,经过fork() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html,之前存在一个句柄的地方存在两个句柄。应用程序应确保,如果两个句柄都可以访问,则它们都处于另一个可以首先成为活动句柄的状态。申请应准备fork() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html就像更改活动手柄一样。 (如果进程之一执行的唯一操作是exec() http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html函数或_exit() http://pubs.opengroup.org/onlinepubs/9699919799/functions/_exit.html (not exit() http://pubs.opengroup.org/onlinepubs/9699919799/functions/exit.html),在该进程中永远不会访问该句柄。)
对于第一个句柄,适用以下第一个适用条件。执行以下所需操作后,如果句柄仍然打开,应用程序可以将其关闭。
-
如果它是文件描述符,则无需执行任何操作。
-
如果对此打开的文件描述符的任何句柄执行的唯一进一步操作是关闭它,则无需执行任何操作。
-
如果它是未缓冲的流,则无需采取任何操作。
-
如果它是行缓冲的流,并且写入流的最后一个字节是<newline>
(也就是说,就好像一个putc('\n')
是该流上的最新操作),无需采取任何操作。
-
如果它是一个开放用于写入或附加(但不开放用于读取)的流,则应用程序应执行fflush() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fflush.html,否则流将被关闭。
-
如果流打开以供读取并且位于文件末尾(feof() http://pubs.opengroup.org/onlinepubs/9699919799/functions/feof.html是 true),无需采取任何操作。
-
如果流以允许读取的模式打开,并且底层打开文件描述引用了能够查找的设备,则应用程序应执行fflush() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fflush.html,否则流将被关闭。
对于第二个手柄:
- 如果任何先前的活动句柄已被显式更改文件偏移量的函数使用,除了上面第一个句柄的要求之外,应用程序应执行lseek() http://pubs.opengroup.org/onlinepubs/9699919799/functions/lseek.html or fseek() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fseek.html(根据手柄的类型)到适当的位置。
如果在满足上述第一个句柄的要求之前活动句柄不再可访问,则打开文件描述的状态将变为未定义。这可能会发生在诸如fork() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fork.html or _exit() http://pubs.opengroup.org/onlinepubs/9699919799/functions/_exit.html.
The exec() http://pubs.opengroup.org/onlinepubs/9699919799/functions/exec.html函数使得在调用它们时打开的所有流都不可访问,而与新进程映像可用的流或文件描述符无关。
当遵循这些规则时,无论使用的句柄顺序如何,实现都应确保应用程序(即使是由多个进程组成的应用程序)应产生正确的结果:写入时不会丢失或重复数据,并且所有数据都应写入顺序,除非搜索请求。是否以及在什么条件下所有输入仅被看到一次是由实现定义的。
每个在流上运行的函数都具有零个或多个“底层函数”。这意味着流函数与底层函数共享某些特征,但不要求流函数的实现与其底层函数之间存在任何关系。
Exegesis
那是很难读的!如果您不清楚打开文件描述符和打开文件描述之间的区别,请阅读以下规范open()
and fork()
(and dup()
or dup2() http://pubs.opengroup.org/onlinepubs/9699919799/functions/dup2.html)。的定义文件描述符 http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_166 and 打开文件描述 http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_258如果简洁的话,也是相关的。
在这个问题的代码的上下文中(也适用于读取文件时创建不需要的子进程 https://stackoverflow.com/questions/50244579/),我们有一个打开的文件流句柄仅供读取,尚未遇到 EOF(因此feof()
即使读取位置位于文件末尾,也不会返回 true)。
该规范的关键部分之一是:申请应准备fork()
就像更改活动手柄一样。
这意味着“第一个文件句柄”概述的步骤是相关的,并且逐步执行这些步骤,第一个适用的条件是最后一个:
- 如果流以允许读取的模式打开,并且底层打开文件描述引用了能够查找的设备,则应用程序应执行
fflush()
,否则流将被关闭。
如果你看一下定义fflush() http://pubs.opengroup.org/onlinepubs/9699919799/functions/fflush.html, 你发现:
If stream指向未输入最近操作的输出流或更新流,fflush()
应导致该流的任何未写入数据写入文件,[CX] ⌦ 并且底层文件的最后一次数据修改和最后一次文件状态更改时间戳应标记为更新。
对于打开用于读取底层文件描述的流,如果文件尚未位于 EOF,并且该文件能够查找,则底层打开文件描述的文件偏移量应设置为流的文件位置,以及通过以下方式推回流中的任何字符ungetc() http://pubs.opengroup.org/onlinepubs/9699919799/functions/ungetc.html or ungetwc() http://pubs.opengroup.org/onlinepubs/9699919799/functions/ungetwc.html随后未从流中读取的内容将被丢弃(不进一步更改文件偏移量)。 ⌫
目前尚不清楚如果您申请会发生什么fflush()
到与不可查找文件关联的输入流,但这不是我们当前关心的问题。但是,如果您正在编写通用库代码,那么您可能需要在执行操作之前知道底层文件描述符是否可查找。fflush()
在流上。或者,使用fflush(NULL)
让系统执行所有 I/O 流所需的操作,注意这将丢失任何推回的字符(通过ungetc()
etc).
The lseek()
中显示的操作strace
输出似乎正在实施fflush()
将打开文件描述的文件偏移量与流的文件位置相关联的语义。
所以,对于这个问题中的代码,似乎fflush(stdin)
之前是必要的fork()
以确保一致性。不这样做会导致未定义的行为(“如果不这样做,结果是未定义的”)——例如无限循环。