web-dev-qa-db-ja.com

「fork()」の後のprintfの異常

OS:Linux、言語:pure C

私は、一般的なCプログラミングの学習と、特殊なケースでのUNIXでのCプログラミングの学習を進めています。

printf()呼び出しを使用した後、fork()関数の奇妙な(私にとって)動作を検出しました。

コード

#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d", getpid() );

    pid = fork();
    if( pid == 0 )
    {
            printf( "\nI was forked! :D" );
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf( "\n%d was forked!", pid );
    }
    return 0;
}

出力

Hello, my pid is 1111
I was forked! :DHello, my pid is 1111
2222 was forked!

子の出力に2番目の「Hello」文字列が発生したのはなぜですか?

はい、親のpidを使用して、開始時に親が印刷したものとまったく同じです。

だが! \n文字を各文字列の最後に追加すると、予想される出力が得られます。

#include <stdio.h>
#include <system.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d\n", getpid() ); // SIC!!

    pid = fork();
    if( pid == 0 )
    {
            printf( "I was forked! :D" ); // removed the '\n', no matter
            sleep( 3 );
    }
    else
    {
            waitpid( pid, NULL, 0 );
            printf( "\n%d was forked!", pid );
    }
    return 0;
}

出力

Hello, my pid is 1111
I was forked! :D
2222 was forked!

なぜ起こるのですか?それは正しい動作ですか、それともバグですか?

64
pechenie

_<system.h>_は非標準のヘッダーです。それを_<unistd.h>_と置き換え、コードをきれいにコンパイルしました。

プログラムの出力が端末(画面)に送られるとき、行バッファリングされます。プログラムの出力がパイプに送られると、完全にバッファリングされます。バッファリングモードは、標準C関数setvbuf()および__IOFBF_(フルバッファリング)、__IOLBF_(ラインバッファリング)および__IONBF_(バッファリングなし)モードで制御できます。 。

プログラムの出力をたとえばcatにパイプすることで、修正したプログラムでこれを実証できます。 printf()文字列の末尾に改行があっても、二重の情報が表示されます。端末に直接送信すると、1つの情報だけが表示されます。

話の教訓は、フォークする前にすべてのI/Oバッファーを空にするためにfflush(0);を呼び出すように注意することです。


要求された行ごとの分析(ブレースなどが削除され、先頭のスペースがマークアップエディターによって削除されました):

  1. printf( "Hello, my pid is %d", getpid() );
  2. pid = fork();
  3. if( pid == 0 )
  4. printf( "\nI was forked! :D" );
  5. sleep( 3 );
  6. else
  7. waitpid( pid, NULL, 0 );
  8. printf( "\n%d was forked!", pid );

解析:

  1. 「こんにちは、私のpidは1234です」を標準出力用のバッファーにコピーします。最後に改行はなく、出力はラインバッファモード(またはフルバッファモード)で実行されているため、端末には何も表示されません。
  2. Stdoutバッファーにまったく同じ素材を使用して、2つの別個のプロセスを提供します。
  3. 子は_pid == 0_を持ち、4行目と5行目を実行します。親にはpidのゼロ以外の値があります(2つのプロセスのわずかな違いの1つ-getpid()getppid()からの戻り値はさらに2つです)。
  4. 改行と「I was forked!:D」を子の出力バッファーに追加します。出力の最初の行が端末に表示されます。出力は行バッファされるため、残りはバッファに保持されます。
  5. すべてが3秒間停止します。この後、子は通常、mainの終わりに戻るまで終了します。その時点で、stdoutバッファー内の残留データがフラッシュされます。これにより、改行がないため、出力位置が行末になります。
  6. 親がここに来ます。
  7. 親は、子が死ぬのを待ちます。
  8. 親は改行を追加し、「1345 was fork!」出力バッファへ。改行は、子によって不完全な行が生成された後、「Hello」メッセージを出力にフラッシュします。

これで、親はmainの最後のリターンを介して正常に終了し、残りのデータがフラッシュされます。末尾にはまだ改行がないため、カーソル位置は感嘆符の後にあり、シェルプロンプトは同じ行に表示されます。

私が見るものは:

_Osiris-2 JL: ./xx
Hello, my pid is 37290
I was forked! :DHello, my pid is 37290
37291 was forked!Osiris-2 JL: 
Osiris-2 JL: 
_

PID番号は異なりますが、全体的な外観は明らかです。 printf()ステートメントの最後に改行を追加すると(これがすぐに標準的な方法になります)、出力が大きく変わります。

_#include <stdio.h>
#include <unistd.h>

int main()
{
    int pid;
    printf( "Hello, my pid is %d\n", getpid() );

    pid = fork();
    if( pid == 0 )
        printf( "I was forked! :D %d\n", getpid() );
    else
    {
        waitpid( pid, NULL, 0 );
        printf( "%d was forked!\n", pid );
    }
    return 0;
}
_

私は今得ます:

_Osiris-2 JL: ./xx
Hello, my pid is 37589
I was forked! :D 37590
37590 was forked!
Osiris-2 JL: ./xx | cat
Hello, my pid is 37594
I was forked! :D 37596
Hello, my pid is 37594
37596 was forked!
Osiris-2 JL:
_

出力が端末に送信されると、行バッファリングされるため、fork()の前に 'Hello'行が表示され、コピーが1つしかなかったことに注意してください。出力がcatにパイプされると、完全にバッファリングされるため、fork()の前には何も表示されず、両方のプロセスには、バッファに「Hello」行がフラッシュされます。

82

理由は、書式文字列の最後に\nがないと、値がすぐに画面に出力されないためです。代わりに、プロセス内でバッファリングされます。これは、フォーク操作の後まで実際に印刷されないため、2回印刷されることを意味します。

ただし、\nを追加すると、バッファが強制的にフラッシュされ、画面に出力されます。これはフォークの前に発生するため、一度だけ印刷されます。

fflushメソッドを使用して、これを強制的に発生させることができます。例えば

printf( "Hello, my pid is %d", getpid() );
fflush(stdout);
25
JaredPar

fork()は、プロセスのコピーを効果的に作成します。 fork()を呼び出す前に、バッファリングされたデータがある場合、親と子の両方が同じバッファリングされたデータを持ちます。次に、それぞれがそのバッファーをフラッシュするために何かを実行すると(端末出力の場合に改行を印刷するなど)、そのプロセスによって生成される新しい出力に加えて、そのバッファー出力が表示されます。したがって、親と子の両方でstdioを使用する場合は、フォークする前にfflushを使用して、バッファリングされたデータがないことを確認する必要があります。

多くの場合、子は_exec*_関数を呼び出すためにのみ使用されます。それは完全な子プロセスイメージ(バッファを含む)を置き換えるので、実際にそれが子で行うすべてである場合、fflushは技術的に必要ありません。ただし、バッファリングされたデータがある可能性がある場合は、execエラーの処理方法に注意する必要があります。特に、stdio関数(writeは大丈夫です)を使用してエラーをstdoutまたはstderrに出力することを避け、exitを呼び出すか(バッファーされたものをフラッシュする)代わりに__exit_(または__Exit_)を呼び出します出力)。または、フォークする前にフラッシュして問題を完全に回避します。

5
mark4o