BLOGTIMES
2009/09/09

printf() のバッファリングにはまる

  c 
このエントリーをはてなブックマークに追加

昨日に引き続き後輩がプログラムが想定通りに動かないと相談に来たので、一緒にデバッグ。

開発しているプログラムはJavaのプログラムからRuntime.exec()経由でC言語のプログラムを起動し、開いたInput / Output Streamを使って起動したプログラムと対話するというというもの。どうも対話がうまくいかなくて、途中で詰まってしまうとのこと。

双方向のパイプを使ってプログラムを操作しようとして、バッファリングのせいでデッドロックになるとか、プログラムが終了するまでログファイルになにも出力されないというようなハマり方はこれまで個人的に散々やらかしていたので、すぐにこれがバッファリングの問題であろうという見当はつきました。ちなみにブログ内検索をしたら、去年もPythonでハマった記録がありますね。

C言語は普段あまり使わないので、printfも標準でバッファリングが行われているというのは恥ずかしながら認識していませんでした。

検証用のプログラムを書いてみる

このあたりの問題は、例えば下記のような単純なプログラムでも確認することができます。

#include <stdio.h> int main(){ printf("line1\n"); fprintf(stderr, "line2\n"); printf("line3\n"); fprintf(stderr, "line4\n"); }

このプログラムをコマンドラインから二通りの方法で、起動させてみると不思議なことが起こります。

$ ./a.out line1 line2 line3 line4 $ ./a.out | cat line2 line4 line1 line3

前者は標準出力、標準エラーがターミナルにつながっているもので、後者はパイプを通して他のプログラムにつながっているものですが、パイプを通したものはバッファリングによって、出力順序が入れ替わっていることが確認出来ます。これは、後述しますがバッファリングについては出力先がターミナルかそうでないかによって挙動が変わるような仕様になっているためです。

バッファリングを無効化する

ストリームのバッファを無効にする方法については、Perlの「$| = 1;」とか、Rubyの「$stdout.sync = TRUE」というのはすぐに思い浮かんだのですが、さすがにCは普段使っていないせいか良くわかりませんでした。

ちなみにこれはprintf()ではなくて、stdoutの性質に依存する問題なのでprintf()にはバッファリングの解説はありません。解決の糸口がないので「printf バッファ 無効」というキーワードでウェブを検索してみるとsetvbuf()を使えば出来るという話が沢山出てきたので、setvbufをmanであたってみるたところ、詳しい使い方が出てきました。

これによると、前述の標準エラーはバッファリングがデフォルトで無効であることや、出力先がターミナルの場合にはブロックバッファでなく、ラインバッファになっていることが確認できます。

Manpage of SETBUF

通常、ファイルはすべて block buffered である。ファイルに対して 初めて入出力処理を行うと malloc(3) が呼び出されバッファが獲得される。もし ストリームが (通常、 stdout がそうであるように) ターミナルを参照する場合には、ファイルは line buffered と なる。標準エラー出力 stderr はデフォルトでは常に unbuffered である。

結論としては、printfのバッファを無効にするためには、バッファを無効にする_IONBFを指定した下記の一行をプログラムに足せばいいようです。バッファは無効にしているので、バッファの領域にはNULL、長さは0を指定しています。

setvbuf(stdout, (char *)NULL, _IONBF, 0);

ということで、修正後の下記のようなプログラムに対して、同様の実験をしてみます。

#include <stdio.h> int main(){ setvbuf(stdout, (char *)NULL, _IONBF, 0); printf("line1\n"); fprintf(stderr, "line2\n"); printf("line3\n"); fprintf(stderr, "line4\n"); }

こうすることで、バッファが無効になり、ターミナルでもそうでなくても挙動の差をなくす事ができました。

$ ./a.out line1 line2 line3 line4 $ ./a.out | cat line1 line2 line3 line4

普段、バッファがフラッシュされるタイミングはあまり意識することは少ないのですが、他のプログラムと双方向のパイプでデータをやりとりするときには思わぬハマりポイントになりやすいので注意したいところです。


    トラックバックについて
    Trackback URL:
    お気軽にどうぞ。トラックバック前にポリシーをお読みください。[policy]
    このエントリへのTrackbackにはこのURLが必要です→https://blog.cles.jp/item/3194
    Trackbacks
    このエントリにトラックバックはありません
    Comments
    愛のあるツッコミをお気軽にどうぞ。[policy]
    古いエントリについてはコメント制御しているため、即時に反映されないことがあります。
    Kimitake [OpenID] (2009/09/10 05:17) http://kimitakeblog.net/ <%HatenaAuth()%>

    自前の C++ のログクラスでは vfprintf とか使ってますが、その後に fflush(fp) で強制的に吐き出すようにしてます。

    hsur (2009/09/10 05:29) <%HatenaAuth()%>

    なるほど。考えてみたらflushするのが順当なんですよね。

    今回はデバッガのフロントエンドを開発してるんで、元のプログラムには手を加えてはいけないという制約があっていろいろと面倒な事になっています。

    Comments Form

    コメントは承認後の表示となります。
    OpenIDでログインすると、即時に公開されます。

    OpenID を使ってログインすることができます。

    Identity URL: Yahoo! JAPAN IDでログイン