問題があることに驚いていますが、Linux では問題のようです (Mac の VMWare Fusion VM で実行されている Ubuntu 16.04 LTS でテストしました) — しかし、macOS 10.13 を実行している Mac では問題ではありませんでした。 4 (High Sierra) であり、Unix の他のバリアントでも問題になるとは思いません。
コメントで指摘したとおり:
<ブロック引用>
各ストリームの背後には、開いているファイルの説明と開いているファイルの記述子があります。プロセスが fork すると、子は独自のオープン ファイル記述子 (およびファイル ストリーム) のセットを持ちますが、子の各ファイル記述子は、開いているファイルの説明を親と共有します。 IF (そしてそれは大きな「if」です) ファイル記述子を閉じる子プロセスは、最初に lseek(fd, 0, SEEK_SET)
と同等のことを行いました 、その場合、親プロセスのファイル記述子も配置され、無限ループにつながる可能性があります。ただし、そのシークを行うライブラリについては聞いたことがありません。そうする理由はありません。
POSIX open()
を参照してください と fork()
開いているファイル記述子と開いているファイルの説明の詳細については、
開いているファイル記述子はプロセス専用です。開いているファイルの説明は、最初の「ファイルを開く」操作によって作成されたファイル記述子のすべてのコピーによって共有されます。開いているファイルの説明の重要なプロパティの 1 つは、現在のシーク位置です。これは、子プロセスが親の現在のシーク位置を変更できることを意味します — これは共有オープン ファイルの説明にあるためです。
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;
}
変更の 1 つは、サイクル (子) の数をわずか 30 に制限します。20 個のランダムな文字と改行 (合計 84 バイト) の 4 行を含むデータ ファイルを使用しました:
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
strace
でコマンドを実行しました Ubuntu の場合:
$ strace -ff -o st-out -- neof97
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
…
uGnxGhSluywhlAEBIXNP
plRXLszVvPgZhAdTLlYe
ywYaGKiRtAwzaBbuzvNb
eRsjPoBaIdxZZtJWfSty
$
st-out.808##
という形式の名前のファイルが 31 個ありました ハッシュは 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 つの行動のいずれかを示し、その後の 4 人の子供の各セットが同じパターンを示しました。
これは、4 人中 3 人の子供が実際に 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
の代わりに .つまり、私の仮説のうち 2 つが反証されました — freopen()
に関連しています と stdin
; 2 番目のテスト コードでは、どちらも正しくないことが示されています。
予備診断
IMO、これはバグです。この問題に遭遇することはありません。カーネルではなく、Linux (GNU C) ライブラリのバグである可能性が最も高いです。 lseek()
が原因です 子プロセスで。 (ソース コードを見に行っていないため) ライブラリが何をしているのか、またはその理由が明確ではありません。
GLIBC バグ 23151
GLIBC バグ 23151 - ファイルが閉じられていないフォークされたプロセスは、終了前に lseek を実行し、親 I/O で無限ループを引き起こす可能性があります。
このバグは 2018 年 5 月 8 日米国/太平洋で作成され、2018 年 5 月 9 日までに INVALID としてクローズされました。与えられた理由:
<ブロック引用>http://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_05_01、特にこの段落をお読みください:
<ブロック引用>
fork()
の後に注意してください 、前に 1 つ存在した場所に 2 つのハンドルが存在します。 […]
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()
)。この例外には、fopen()
で作成されたかどうかにかかわらず、ストリームの基になるファイル記述子は含まれません。 または fdopen()
、ファイル オフセットに影響を与えるためにアプリケーションによって直接使用されない限り。 read()
と write()
関数は暗黙的にファイル オフセットに影響します。 lseek()
明示的に影響します。
任意の 1 つのハンドル (「アクティブなハンドル」) を含む関数呼び出しの結果は、POSIX.1-2017 のこのボリュームの別の場所で定義されていますが、2 つ以上のハンドルが使用され、それらのいずれかがストリームである場合、アプリケーションは以下に説明するように、彼らの行動が調整されていることを確認してください。これが行われない場合、結果は未定義です。
fclose()
のいずれかの場合、ストリームであるハンドルは閉じていると見なされます。 、または freopen()
完全ではないファイル名で、その上で実行されます (freopen()
の場合) ファイル名が null の場合、新しいハンドルが作成されるか、既存のハンドルが再利用されるかは実装定義です)、またはそのストリームを所有するプロセスが exit()
で終了する場合 、 abort()
、または信号による。ファイル記述子は close()
によって閉じられます 、 _exit()
、または exec()
そのファイル記述子に FD_CLOEXEC が設定されている場合に機能します。
[sic] 'non-full' の使用は、おそらく 'non-null' のタイプミスです。
<ブロック引用>ハンドルがアクティブ ハンドルになるには、アプリケーションは、ハンドルの最後の使用 (現在のアクティブ ハンドル) と 2 番目のハンドル (将来のアクティブ ハンドル) の最初の使用の間に以下のアクションが実行されることを保証する必要があります。 2 番目のハンドルがアクティブなハンドルになります。最初のハンドルのファイル オフセットに影響を与えるアプリケーションによるすべてのアクティビティは、それが再びアクティブなファイル ハンドルになるまで中断されます。 (ストリーム関数が、ファイル オフセットに影響を与える関数を基になる関数として持つ場合、ストリーム関数はファイル オフセットに影響を与えると見なされます。)
これらのルールを適用するために、ハンドルが同じプロセスにある必要はありません。
fork()
の後に注意してください 、前に 1 つ存在した場所に 2 つのハンドルが存在します。アプリケーションは、両方のハンドルにアクセスできる場合、両方とも、もう一方が最初にアクティブなハンドルになる可能性がある状態にあることを確認する必要があります。アプリケーションは fork()
に備える必要があります あたかもアクティブなハンドルの変更であるかのように。 (プロセスの 1 つによって実行されるアクションが exec()
の 1 つだけの場合 関数または _exit()
(exit()
ではありません )、そのプロセスでハンドルにアクセスすることはありません。)
最初のハンドルについては、以下の最初の適用条件が適用されます。以下で必要なアクションが実行された後、ハンドルがまだ開いている場合、アプリケーションはハンドルを閉じることができます。
-
ファイル記述子の場合、アクションは必要ありません。
-
この開いているファイル記述子へのハンドルに対して実行する必要がある追加のアクションが、それを閉じることだけである場合は、アクションを実行する必要はありません。
-
バッファリングされていないストリームの場合、アクションを実行する必要はありません。
-
ライン バッファリングされたストリームで、ストリームに書き込まれた最後のバイトが
<newline>
の場合 (つまり、あたかもputc('\n')
がそのストリームでの最新の操作である場合)、アクションを実行する必要はありません。 -
書き込みまたは追加用に開かれている (読み取り用には開かれていない) ストリームである場合、アプリケーションは
fflush()
を実行する必要があります。 、またはストリームを閉じる必要があります。 -
ストリームが読み取り用に開いていて、ファイルの末尾にある場合 (
feof()
が true)、アクションを実行する必要はありません。 -
ストリームが読み取りを許可するモードで開かれており、基になる開いているファイルの記述がシーク可能なデバイスを参照している場合、アプリケーションは
fflush()
を実行する必要があります。 、またはストリームを閉じる必要があります。
2 番目のハンドルの場合:
- ファイル オフセットを明示的に変更した関数によって以前のアクティブなハンドルが使用された場合、上記の最初のハンドルで必要な場合を除き、アプリケーションは
lseek()
を実行する必要があります。 またはfseek()
(ハンドルの種類に応じて) 適切な場所に移動します。
上記の最初のハンドルの要件が満たされる前に、アクティブなハンドルにアクセスできなくなった場合、開いているファイル記述の状態は未定義になります。これは、fork()
などの機能中に発生する可能性があります または _exit()
.
exec()
関数は、呼び出された時点で開かれているすべてのストリームにアクセスできないようにします。これは、どのストリームまたはファイル記述子が新しいプロセス イメージで使用可能であるかに関係なく行われます。
これらの規則に従う場合、使用されるハンドルの順序に関係なく、実装は、アプリケーションが複数のプロセスで構成されている場合でも、正しい結果が得られることを保証する必要があります。書き込み時にデータが失われたり複製されたりすることはなく、すべてのデータはただし、シークによって要求された場合を除きます。
ストリームで動作する各関数は、0 個以上の「基になる関数」を持つと言われています。これは、ストリーム関数が特定の特性を基になる関数と共有することを意味しますが、ストリーム関数の実装とその基になる関数の間に関係がある必要はありません。
解釈
それは読みにくいです!オープン ファイル記述子とオープン ファイル記述の違いがよくわからない場合は、open()
の仕様を読んでください。 と fork()
(そして dup()
または dup2()
)。簡潔であれば、ファイル記述子とオープンファイルの説明の定義も関連しています。
この質問のコードのコンテキスト (およびファイルの読み取り中に作成される不要な子プロセス) では、まだ EOF に遭遇していない読み取り専用のファイル ストリーム ハンドルが開かれています (したがって feof()
読み取り位置がファイルの末尾であっても true を返しません)。
仕様の重要な部分の 1 つは次のとおりです。アプリケーションは fork()
に備える必要があります あたかもアクティブなハンドルの変更であるかのように。
これは、「最初のファイル ハンドル」で概説されている手順が関連していることを意味し、それらをステップ実行すると、最初に適用される条件が最後になります:
- ストリームが読み取りを許可するモードで開かれており、基になる開いているファイルの記述がシーク可能なデバイスを参照している場合、アプリケーションは
fflush()
を実行する必要があります。 、またはストリームを閉じる必要があります。
fflush()
の定義を見ると 、次を見つけます:
ストリームの場合 最新の操作が入力されていない出力ストリームまたは更新ストリームを指す fflush()
そのストリームの書き込まれていないデータがファイルに書き込まれるようにし、[CX] ⌦ と、基礎となるファイルの最後のデータ変更と最後のファイル ステータス変更のタイムスタンプが更新用にマークされます。
基礎となるファイル記述を使用して読み取り用に開かれたストリームの場合、ファイルがまだ EOF になっておらず、ファイルがシーク可能なファイルである場合、基礎となる開いているファイル記述のファイル オフセットは、ストリームのファイル位置に設定されます。 ungetc()
によってストリームにプッシュバックされたすべての文字 または ungetwc()
その後ストリームから読み取られなかったものは破棄されます (ファイル オフセットをさらに変更することなく)。 ⌫
fflush()
を適用するとどうなるかは明確ではありません シークできないファイルに関連付けられた入力ストリームに送信されますが、それは当面の関心事ではありません。ただし、一般的なライブラリ コードを記述している場合は、fflush()
を実行する前に、基になるファイル記述子がシーク可能かどうかを知る必要がある場合があります。 ストリームで。または、fflush(NULL)
を使用します すべての I/O ストリームに必要なことをシステムに実行させるため、プッシュバックされた文字が失われることに注意してください (ungetc()
経由)。 など)
lseek()
strace
で示される操作 出力は fflush()
を実装しているようです 開いているファイル記述のファイル オフセットをストリームのファイル位置に関連付けるセマンティクス。
したがって、この質問のコードでは、 fflush(stdin)
のようです fork()
の前に必要です 一貫性を確保します。そうしないと、未定義の動作につながります (「これが行われない場合、結果は未定義です」) — 無限ループなど。
exit() 呼び出しは、開いているすべてのファイル ハンドルを閉じます。フォークの後、子と親は、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);
}
}