UNIX/Linuxの部屋 stdbufコマンドの使い方

TOP UNIX/Linuxの部屋 UNIX/Linuxコマンド一覧 用語集 新版 由来/読み方辞書 環境変数マニュアル Cシェル変数 システム設定ファイル システムコール・ライブラリ ネットワークプログラミングの基礎知識 クラウドサービス徹底比較・徹底解説




コマンド stdbuf コマンドのバッファリングのモードを変更して実行する このエントリーをはてなブックマークに追加

最終更新


UNIX/Linux の stdbuf コマンドは、任意のコマンドのバッファリングのモードを変更して実行するコマンドである。一般的には、任意のコマンドのバッファリングを無効にして即座に画面に結果を出力させるために使用される。


stdbuf コマンドで何ができるか
下記のように tail -f に対して grep や sed を多段に使うと、ログファイルにどんどん追記されてもリアルタイムで結果が出力されない。
% tail -f log | grep xxx | sed 's/aaa/bbb'
% tail -f log | awk '{print $1,$2}' | sed 's/aaa/bbb'
そのような場合、下記のように stdbuf でバッファリングのモードを変えることで、リアルタイムに出力されるようになる。
% tail -f log | stdbuf -oL grep xxx | sed 's/aaa/bbb'

stdbuf コマンドの使い方
stdbuf コマンドの書式は以下のとおりである。
% stdbuf [オプション] [任意のコマンド]
オプションは下記のとおり。
▷ -o0
標準出力をバッファリングなしとする。
▷ -oL
標準出力を行バッファリングとする。
▷ -e0
標準エラー出力をバッファリングなしとする。
▷ -eL
標準エラー出力を行バッファリングとする。
▷ -i0
標準入力をバッファリングなしとする。
▷ -iL
標準入力を行バッファリングとする (GNU coreutils の stdbuf では不可)
▷ --output=[mode]
-i[mode] と同じ。GNU coreutils の stdbuf のみ使用可。
▷ --error=[mode]
-e[mode] と同じ。GNU coreutils の stdbuf のみ使用可。
▷ --input=[mode]
-i[mode] と同じ。GNU coreutils の stdbuf のみ使用可。

基礎知識: バッファリングとは何か
stdbuf コマンドの説明の前に、バッファリングとはどういうことかを説明する。
  • バッファとは、データの一時的な保存場所のこと。
  • バッファリングとは、バッファにデータを一時的に保存しておくことである。その目的は、速度の向上である。

UNIX/Linux のプロセスが "a" の 1文字を出力する際、内部ではシステムコール write(2) が実行され、カーネルが文字を出力する。たった一文字であってもその処理は必要である。なぜならシステムコールに頼らないと、プロセス自身が出力することすらできないからである。

システムコールの実行というのは OS としてはかなり重い処理になる。よって、"abc" を出力する際、
  • "a" を出力するためにシステムコール write(2) を呼ぶ
  • "b" を出力するためにシステムコール write(2) を呼ぶ
  • "c" を出力するためにシステムコール write(2) を呼ぶ
ではなく、
  • "abc" を出力するためにシステムコール write(2) を一度だけ呼ぶ
とするのが望ましい。

そこで OS の標準入出力ライブラリ (stdio と呼ばれるもの) においては、自動的にバッファを持っておき、一定量を溜め込んで、そのバッファが一杯になったらはじめて write(2) を呼ぶようになっている。

C 言語のコードで言うと、下記は stdio によるバッファリングが行われる。
main(){
printf("abc\n");
}
printf(3) 以外でも、putchar(3)・fgetc(3)・fgets(3)・fputs(3) などはすべてバッファリングが行われる。

C言語以外でも、下記はすべて内部的にバッファリングを行う書き方である。
Perl:
print "abc\n";
Python:
print("abc\n")
Java:
System.out.println("abc");

基礎知識: 行単位バッファリングとフルバッファリング
バッファリングには、下記の 3種類のモードがある。
▷ バッファリングなし
文字通りバッファリングしない
▷ 行単位バッファリング
改行コードが現れるか、バッファが一杯になったら出力する
▷ フルバッファリング
バッファが一杯になったときのみ出力する

コマンドを単体で、リダイレクトなしで実行した場合のデフォルトは下記のとおり。
  • 標準出力は、行単位バッファリング
  • 標準エラー出力は、バッファリングなし

コマンドの出力先が端末以外 (ファイルやパイプなど) の場合は下記のとおり。
  • 標準出力は、フルバッファリング
  • 標準エラー出力は、バッファリングなし

と言ってもすぐには理解しづらい。実験をしてみよう。

実験: 標準出力編
下記は標準出力に文字列を 5回出力するプログラムである。出力のたびに 1秒 sleep する。2つのソースの違いは、改行コードを出力するか否かだけである。

▷ stdout-with-crlf.c (標準出力に、改行コードありの文字列を出力する)
#include <stdio.h>
#include <unistd.h>
int main(){
int i;
for ( i=0 ; i<5 ; i++){
printf("stdout with crlf\n");
sleep(1);
}
return 0;
}
▷ stdout-no-crlf.c (標準出力に、改行コードなしの文字列を出力する)
#include <stdio.h>
#include <unistd.h>
int main(){
int i;
for ( i=0 ; i<5 ; i++){
printf("stdout no crlf");
sleep(1);
}
return 0;
}

上記 2つのソースを、下記のようにコンパイルする。すると stdout-no-crlf と stdout-with-crlf という 2つのバイナリファイルが生成される。
% cc -o stdout-with-crlf stdout-with-crlf.c
% cc -o stdout-no-crlf stdout-no-crlf.c

まずは stdout-with-crlf を実行してみる。1秒おきに文字列が出力されたと思う
% ./stdout-with-crlf
stdout with crlf
stdout with crlf
stdout with crlf
stdout with crlf
stdout with crlf

次に改行なしの stdout-no-crlf を実行する。すると、5秒間は何も表示されず、5秒後に全文字列が出力される。
% ./stdout-with-crlf
stdout no crlfstdout no crlfstdout no crlfstdout no crlfstdout no crlf

これはなぜか。デフォルトでは標準出力は「行バッファリング」であるため、改行コードが来るまでバッファ内にデータが溜まっていたためである (実際はどこにも改行コードが出てこないので、終了時に exit(3) が自動的に呼ばれ、バッファにたまっていたデータが吐き出されている)。

もうひとつ実験をしよう。実行ファイル単体はなく、下記のようにパイプでつないでみよう。ここでは cat -n としているので行番号が表示されるはずだ。
% ./stdout-with-crlf | cat -n
% ./stdout-no-crlf | cat -n

実行すると、改行コードあり・なしに関わらず、5秒経過するまで何も表示されない。さきほどは改行コードありの場合はリアルタイムで表示されたのに、今回は 5秒待たされてしまう。

これはなぜか。stdio のデフォルトが下記のようになっているからである。
  • コマンドを単体で実行した場合、標準出力は、行単位バッファリング
  • コマンドの出力先が端末以外 (ファイルやパイプなど) の場合、標準出力は、フルバッファリング

今回は標準出力の出力先がパイプであったため、フルバッファリングとなった。フルバッファリングはバッファが一杯になるまで出力がなされない。バッファサイズは歴史的に /usr/include/* あたりに BUFSIZ として定義されている。

BUFSIZ の値は、当ページ管理人の環境においては、FreeBSD では 1024バイト、Linux では 8192 バイトであった。今回出力した文字列は全体でも数十バイト程度であるため、バッファサイズには全く届いていないため、最後まで出力されなかったのである。

どうすればバッファリングさせないようにできるか
やり方は 2つある。一つは、printf した後に fflush(3) にて、バッファ内に溜まっているデータをすべて出力するよう stdio ライブラリに指示する方法。
printf("stdout no crlf");
fflush(stdout)

もうひとつは、最初に setvbuf(3) でバッファリングを無効にしておくことである。こうすると printf(3) するだけで常にシステムコール write(2) が呼ばれる。
setvbuf(stdout, NULL, _IONBF, 0);

しかしながらいずれも、プログラムの内部に手をいれて再コンパイルする必要があり、ハードルが高い。そのようなときこそ stdbuf コマンドの出番である。

どのコマンドに対して stdbuf コマンドを使うべきか
stdbuf コマンドをどこで使うか。下記であれば tail・grep・sed の 3つのコマンドを使っているが、どれに対して stdbuf コマンドを使えばよいか。
% tail -f log | grep xxx | sed 's/aaa/bbb'

上記の例であれば grep に対して、標準出力のバッファリングをなしとすればよい。
% tail -f log | stdbuf -o0 grep xxx | sed 's/aaa/bbb'

なぜかというと下記のとおり。
  • 今回の問題は「パイプやファイルに出力する場合、標準出力をフルバッファリングしてしまうこと」である。それに該当するのは grep のみであるため。
  • tail -f はバッファリングしないため。

ただ、tail -f がバッファリングしないかどうかは調べてみるしかないため、よくわからない場合は全部につけてみればよい。
% stdbuf -o0 tail -f log | stdbuf -o0 grep xxx | stdbuf -o0 sed 's/aaa/bbb'

上記の stdout-with-crlf であれば、標準出力が行バッファリングであるところに改行なしのデータを出力していたのが原因であるため、下記のように標準出力をバッファリングなしと設定すればよい。
% stdbuf -o0 ./stdout-with-crlf

標準入力に対するバッファリングを変更する場合
標準入力のバッファリングは下記のようになっている。
  • 端末の場合は行バッファリング
  • 端末以外 (ファイルやパイプなど) の場合はフルバッファリング

以下のコマンドを実行してほしい。
% (echo a;echo b;echo c) | (sed 1q; echo sed1 fin; sed 1q; echo sed2 fin; sed 1q; echo sed3 fin)

これは echo で a・b・c という 3行を出力している。それに対し sed で「1行目を受け取ったら表示して終了 (q) する」としている。その sed が 3つあるので、下記のような挙動を期待しているわけである。
  • a は 1つめの sed が取得して終了
  • b は 2つめの sed が取得して終了
  • c は 2つめの sed が取得して終了

しかしながら実行結果は下記のとおりで、1つめの sed が 1行目を表示し、2行目・3行目はどこにも出てこない。
a
sed1 fin
sed2 fin
sed3 fin

これはなぜか。1つめの sed が標準入力からデータを読み取る際、フルバッファリングになっているので、a・b・c のすべての行を read(2) で読み取ってしまい、1行目の a を表示した時点で b・c はバッファに入っているためである。すでにパイプから全データを読み取り済みなので、2つめと 3つめの sed が実行された時点では、標準入力にはデータは残っていない。

そこで下記のように 1つめと 2つめの sed に対して stdbuf -i0 とすることで、実際に処理する分のデータのみパイプから read(2) することになり、結果として a・b・c が出力される。
% (echo a;echo b;echo c) | \
(stdbuf -i0 sed 1q; echo sed1 fin; \
stdbuf -i0 sed 1q; echo sed2 fin; \
sed 1q; echo sed3 fin)
a
sed1 fin
b
sed2 fin
c
sed3 fin

ちなみに上記は sh・bash の read コマンドを使うほうがすっきりする。
% (echo a;echo b;echo c) | (read x; echo $x ; read x; echo $x ; read x; echo $x )

標準エラーに対するバッファリングを変更する場合
標準エラー出力は、デフォルトでバッファリングなしである。よって、stdbuf コマンドで下記のように標準エラー出力のバッファリングのモードを変えるケースは考えづらい。
% stdbuf -eL command ...
→ 標準エラー出力を行バッファリング
% stdbuf -e4096 command ...
→ 標準エラー出力を 4096バイトのバッファでフルバッファリング

強いていうなら、バッファリングさせて高速化させたいケースだろうか。

なぜ stdbuf コマンドは、コマンドの挙動を変えられるのか
通常 printf などの stdio ライブラリは、libc.so という動的ライブラリに記述されている。stdbuf コマンドは下記のようなしくみで動いてる。
  • libstdbuf.so というライブラリをあらかじめ準備する
  • コマンド実行時に LD_PRELOAD という環境変数をセットし、libc より先に libstdbuf を呼ぶようにする。
  • libstdbuf 内で setvbuf(3) を使ってバッファモードを変更する

stdbuf コマンドの限界
stdbuf コマンドは、stdio ライブラリを使っているプログラムにしか効果がない。つまり printf(3) などを使わず、直接システムコール write(2) を呼んでいる場合は影響しない。また、mmap(2) でメモリ経由で入出力を行っている場合も効果がない。

また、static リンクされている場合も効果がない (static リンクされたバイナリでも LD_PRELOAD でフックすることができるらしいが、stdbuf コマンドのソースを見る限りではそのような書き方をしていないように見える (未検証)。

バッファリングを無効化するオプション
行バッファリングにしたり、バッファリングを無効化するオプションを備えているコマンドは存在する。
  • grep なら --line-buffered で行単位バッファリング
  • sed なら -u オプションでバッファリングなし
  • python なら -u オプションでバッファリングなし

ただ、全てのコマンドがそのようなオプションを持っているわけではない。例えば ping コマンドや vmstat コマンドなどにはそのようなオプションがない。

さらに、grep・sed なども、SysV 系 UNIX など古めのコマンドにおいてはそのようなオプションはないことが多い。

おまけの実験: 標準エラー出力編
蛇足ではあるが、一応標準エラー出力の挙動も確かめよう。さきほどのソースは標準出力 (stdout) に出力するものであったが、今回は標準エラー出力 (stderr) に出力する。

▷ stderr-no-crlf.c (標準エラー出力に、改行コードなしの文字列を出力する)
#include <stdio.h>
#include <unistd.h>
int main(){
int i;
for ( i=0 ; i<5 ; i++){
fprintf(stderr, "stderr no crlf");
sleep(1);
}
return 0;
}

▷ stderr-with-crlf.c (標準エラー出力に、改行コードなしの文字列を出力する)
#include <stdio.h>
#include <unistd.h>
int main(){
int i;
for ( i=0 ; i<5 ; i++){
fprintf(stderr, "stderr with crlf\n");
sleep(1);
}
return 0;
}

さきほどと同様に下記でコンパイルしてもよいが、
% cc -o stderr-with-crlf stderr-with-crlf.c
% cc -o stderr-no-crlf stderr-no-crlf.c
下記のように make コマンドを使うと、make のデフォルトルールにより、よい感じにコンパイルしてくれる。
% make stderr-with-crlf
% make stderr-no-crlf
興味のある方は下記を参照してほしい。

実行してみると、いずれもリアルタイムで表示されている。
% ./stderr-with-crlf
stderr with crlf
stderr with crlf
stderr with crlf
stderr with crlf
stderr with crlf
% make stderr-no-crlf
stderr no crlfstderr no crlfstderr no crlfstderr no crlfstderr no crlf

これはなぜか。標準エラー出力は「バッファリングなし」であるためだ。