web-dev-qa-db-ja.com

stdoutへの出力は機能しますが、ファイルまたはFIFOにリダイレクトすると失敗します

Wifiアンテナを持っていて、その位置を最適化したい。その提案のために、現在のレイテンシーをリアルタイムで視覚化したいと思います。

Pingの待ち時間を出力する1行のスクリプトを作成しました。

lat.sh:

ping 8.8.8.8|gawk '/64 bytes/{ match($7,/[0-9.]+/,arr); print((i++),"  ",arr[0]);}'

Gnuplotでプロットしたい:

plot "<lat.sh"

それはうまくいきません。 lat.shは、次のように端末に何を表示するかを表示していることがわかりました。

$ ./lat.sh 
0    47.7
1    25.5
2    15.8
3    16.7

ただし、パイプに出力することはできません。 ./lat.sh > outfileまたは./lat.sh|tee outfileまたは./lat.sh &> outfile端末には何も出力しませんが、ファイル(またはパイプ)は空のままです。 gnuplotパイプについても同じです。私は本当に混乱しています。

3
fosp5

以下のmikeserv explains のように、これはバッファリングの問題です。これを解決する1つの方法は、fflush()を使用してawkにすぐに書き込むように強制することです(残りは、実行していたことの単純化されたバージョンです)。

_ping 8.8.8.8 | gawk -F'[= ]' '/^64/{print i++,$(NF-1); fflush()}'
_

fflush()呼び出しは、バッファがいっぱいになるのを待たずに、出力が利用可能になるとすぐにawkに出力を強制的に出力します。

または、pingを特定の回数だけ実行してみることもできます。例:10:

_ping  -c 10 8.8.8.8 | gawk ...
_
4
terdon

ここでの問題はawkのようです。プログラムは出力を省略します-それをトリミングします。これは非常に便利なことですが、これを行うと、awkのキューに入れられる出力が減ります。 stdio.h(ほとんどのUNIXプログラム)を使用するほとんどのUNIXプログラムは、出力デバイスのタイプに応じて出力をバッファリングします。出力デバイスが端末の場合、ほとんどのUNIXプログラムは出力をラインバッファリングし、1行あたりwrite()を実行します。の場合、ではありませんが、(たとえば、パイプの場合)上記のプログラムは、書き込みごとに定義されたバイト数にバッファリングします。

これは標準的な動作であり、通常は良いです。 write()のすべての呼び出しはシステムコールであり、システムカーネルのコアおよび最も基本的な機能へのフックです。 UNIXシステムはタイムシェアリング-カーネルは必要に応じてCPUの処理時間を各要求プロセスに共有します。したがって、効率的でI/Oを管理可能なチャンクにブロックすることは支払いです。これが、アプリケーションがバッファリングする理由です。

それがawkが行っていることです。そして、この問題は、入力のフィルタリングによって、それ自体の出力が毎秒約8バイトまたは10バイトに減少するという点で混乱しています。 awkが出力を4kbのチャンクにバッファリングする場合、(一般的に)であり、1秒あたり約10バイトの出力しか生成しない場合、6分に1回だけ書き込みが発生します。大陸移​​動の規模で周波数をチェックしない限り、待ち時間を測定するための非常に便利なアプリケーションにはなりません。

これを処理する方法があります。たとえば、パイプラインをptyでラップすることができます-screenは、あなたと他のいくつかのアプリケーションのためにこれを行うことができます。おそらくこれを処理するためのawk-ネイティブオプションがあります-しかし私はを知りません(たとえ terdonは間違いなくそうします。プログラムの実行時に出力バッファを設定するためのlibc呼び出しを挿入するGNUのstdbufツールがあります。これは通常非常に効果的ですが、実行するプログラムが後で実行中にバッファを調整する場合、あまり役に立ちません。

今私はawkのせいですが、pingも出力をバッファリングしている可能性があります。ただし、少なくとも私のLinuxシステムでは、次のように呼び出します。

_ping -O -n 8.8.8.8 | ...
_

...これはそうではありません。気になる方のために、標準のUNIXコマンドに対する非常に基本的なPOSIXオプションのみを使用して、出力行が利用可能になり次第、すべての出力行を一貫して書き込むようにしました。

最も簡単に:

_ping -On 8.8.8.8 |
sed -u 's/^64.*=\(.*\) ttl.*=/\1  /' |
more pipeline
_

... GNU、BSD、またはAST sedのいずれかで動作します。また、...

_ping -On 8.8.8.8 |
sed 's/^64.*=\(.*\) ttl.*=/\1  /w target_file' |
more pipeline
_

...可能な処理に続いてすべての入力行をstdoutにコピーします-これはおそらくブロックバッファリングされます。ただし、成功したすべての_s///_ ubstitutionの結果をtarget_fileまたすぐにwriteします。指定されたwriteファイルのいずれか/すべてに対する即時のwritesはPOSIX仕様のsed動作です-そして同じことが(ほぼ)readファイルにも当てはまります。そのテーマのわずかなバリエーションとして:

_ping -On 8.8.8.8 |
sed 's/^64.*=\(.*\) ttl.*=/\1  /w /dev/fd/1' |
more pipeline
_

_/dev_リンクを使用したファイル記述子のアドレス指定をサポートするシステムでは、少なくともsedに関する限り、上記の結果、標準のシェルパイプラインを介して行バッファー出力が生成されます。 _/dev/fd_リンクを直接サポートしないシステムの場合、とにかく/dev/std(in|out|err)を処理するのはまだ人気のあるsed機能です-古いminisedでさえこれを行います。 _-n_を使用して、デフォルトの標準出力を完全に無効にします。

これは、その構成に基づいた、より柔軟でより複雑なソリューションです。

_mkfifo  /tmp/ipipe    /tmp/opipe
exec 9<>/tmp/opipe 8<>/tmp/ipipe
trap '  printf \\nTTY:STOP\\n >&8' INT
sed -n 's/^64.*=\(.*\) ttl.*=/\1\t/
        /^TTY:START$/,/^TTY:STOP$/{
                /^TTY/d;w /dev/tty
};      /^PIP:START$/,/^PIP:STOP$/{
                /^PIP/d;w /tmp/opipe
}'      <&8     >/dev/null 2>&1 & SEDPID=$!
ping -On 8.8.8.8       >&8 2>&8 & PNGPID=$!
printf \\nPIP:START\\n >&8;rm /tmp/?pipe
_

これで、常に出力をラインバッファで書き込むバックグラウンドディスパッチャプロセスが設定されました。最初に呼び出されたときに保存した、親シェルの_<>&9_記述子に対応するファイル記述子に書き込むか、コマンドを送信するかどうかに応じてttyに書き込みます。

_PIP:START
TTY:START
_

あなたはこれらを次のように送ることができます:

_printf \\nPIP:START\\n >&8
printf \\nTTY:START\\n >&8
_

それはあなたが好きなように両方を同時に処理します。親シェルには、[〜#〜] int [〜#〜]シグナルを受信したときにコマンドTTY:STOPを送信するように構成されたtrapがあります-したがって、キーボードのCTRL + Cを押すだけで、いつでも端末への書き込みを停止できます。ただし、パイプへの書き込みを停止するように明示的に指示する必要があります。これは、必要に応じてtrapで処理することもできます。次のように停止するように指示できます。

_printf \\nPIP:STOP\\n >&8
_

sed will常にwrite on a line-buffer-標準をどのようにバッファリングするかに関係なく、したがって、これを使用すると、pingによって書き込まれた各行を書き込むとすぐに読み取ることができます。これを行うには、別のプロセスを呼び出してファイル記述子9を読み取ります。これは[〜#〜] pip [〜#〜]sedが指示されたときに書き込むものです-またはターミナルに直接印刷するだけです。[〜#〜] pip [〜#〜]から読み取る別のプロセスを呼び出すには、次のようにします。

_cat <&9
_

...または類似。また、rmは、パイプを必要とする可能性のあるすべてのプロセスがそれらのパイプでファイルハンドルを確実に確立するとすぐに、使用されるパイプのファイルシステムリンクを明示的にmkfifo作成したことにも注意してください。これは、ファイルシステムを介して関連するプロセスと対話できないことを意味します-すべてのIPCは、親シェルの記述子8および9を介して調整する必要があります。バックグラウンドのsedおよびpingプロセスのPIDシェル変数_$PNGPID_および_$SEDPID_に保存されます。どちらもkillでアドレス指定できます。

上記のスクリプト例を実行し、TTY:STARTコマンドを送信すると、結果は次のようになります。

_[mikeserv@localhost ~]$ printf \\nTTY:START\\n >&8

[mikeserv@localhost ~]$ 218 24.2 ms
219 21.2 ms
220 23.1 ms
221 21.3 ms
222 21.9 ms
^C

[mikeserv@localhost ~]$
_

...しかし、両方のプロセスはまだ実行中です-sedは出力をどこにも書き込んでいません。

_ps -Fp "$SEDPID" "$PNGPID"
UID        PID  PPID  C    SZ   RSS PSR STIME TTY      STAT   TIME CMD
mikeserv 31601 28945  0  2143  1712   4 14:22 pts/0    SN     0:00 sed -n s/^64.*q=\(.*\) ttl.*=/\1\
mikeserv 31602 28945  0  2106   740   3 14:22 pts/0    SN     0:00 ping -On 8.8.8.8
_
2
mikeserv

パイプに関しては、Unixは本当に制限されているようです。何か意味のあることをするために、最初にデータをファイルに書き込んだようです。全部書き直すとしたら、仮想端末が解決策かもしれません。しかし、派手なGUIの代わりに、私は単にターミナルに固執し、awkでプログレスバーをレンダリングします。

ping -i 0.5  8.8.8.8 | gawk -F'[= ]' '/^64/{ for(i=0;i<($(NF-1)/3);i++) printf(" "); printf("|");  for(i=120;i>$(NF-1);i--) printf(" "); printf("\r");}'

120を端末の幅に置き換えます。

0
fosp5