書き込みブロックプロセスを使用して共有ファイル記述子を閉じると、ブロックはどのように解除されますか?

書き込みブロックプロセスを使用して共有ファイル記述子を閉じると、ブロックはどのように解除されますか?

共有ファイル記述子(STDOUT / STDERRを示す)を持つ親プロセスから分岐された複数のプロセスがある場合、そのプロセスの1つがSTDOUTに書き込まれて〜64Kバッファーを超えると、期待どおりにブロックされます。他のプロセスのすべての共有ファイル記述子が閉じられると、プロセスはブロックを解除し、引き続き STDOUT に書き込みます。

共有ファイル記述子を閉じる行為により、書き込みブロックされたプロセスはどのようにブロック解除されますか? (バッファがフラッシュされていると仮定していますが、それについての証拠が見つかりません)

再現のために状態を設定する2つのスクリプトがあります。私の目標は、問題を解決するのではなく、これらの記述子を閉じることによってブロックされたプロセスがどのように続くかを理解することです。 (つまり意図はいいえPythonまたはサブプロセスのトラブルシューティング)

ファイル:A.py

#!/usr/bin/env python2.6

import subprocess

if __name__ == "__main__":
   subprocess.Popen("./B.sh 70000", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
   subprocess.Popen("./B.sh 100", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
   subprocess.Popen("./B.sh 100", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

ファイル:B.sh

#!/usr/bin/env bash

for i in `seq 1 $1`; do
   echo -n "#"
done

echo ""

while true; do
    echo > /dev/null
done

Python 2.6 の subprocess.Popen は、状態を設定する手段として使用されます。その下では、パイプとフォーク(おそらくその順序ではないかもしれません)は、現在のプロセスがフォークされている各プロセスのファイル記述子をコピーして、共有ファイル記述子を持つプロセスチェーンを作成します。

シェルスクリプトはB.sh単にデータをSTDOUTに出力してループします(htopのようなものから実行中の状態とスリープ状態を区別するために意図的にスリープモードをオフにします)。

両方のスクリプトを同じ作業ディレクトリに配置し、A.pyを実行して動作を複製します(CentOS 6.7ですが、CentOS 6.Xバージョンが複製されるかどうか疑問です)。

参考までに、以下は、共有状態を示すプロセスファイル記述子のディレクトリのリストです。

# Process 1: ./B.sh 70000
ls -la /proc/4144/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53061]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53061]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh

# Process 2: ./B.sh 100
ls -la /proc/4145/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53063]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53063]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
lr-x------ 1 root root 64 Sep 28 14:18 3 -> pipe:[53061]

# Process 3: ./B.sh 100
ls -la /proc/4146/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53065]
l-wx------ 1 root root 64 Sep 28 14:24 10 -> pipe:[53065]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53065]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
lr-x------ 1 root root 64 Sep 28 14:18 3 -> pipe:[53061]
lr-x------ 1 root root 64 Sep 28 14:18 4 -> pipe:[53063]

生成された最初のプロセス(プロセス1)は〜> 64KのデータをSTDOUTに出力し、書き込み中にブロックされるため、スリープモードになります(htopとpidにstraceを追加することで見ることができます)。

2番目と3番目のプロセス(それぞれプロセス2とプロセス3)は、引き続き実行され、プロセス1の一部として設定されたパイプを参照する冗長ファイル記述子があります。

プロセス2またはプロセス3を終了し、プロセス1はまだ眠っています。どちらも終了すると、プロセス1はロック解除され(なぜですか)実行状態になります。

テストを再開し、gdbプロセス2またはプロセス3への接続を使用して、p close(#)プロセス1がまだ待機している間にプロセス1と共有されているファイル記述子を閉じます。別のプロセスに接続して共有記述子を閉じると、プロセス1はブロックを解除して実行状態に入ります。

したがって、ブロックされたプロセスと共有されているすべての記述子を閉じる行為はブロックを解除します。この状況で以前に書き込みブロックされたプロセスが解放される原因は何ですか?

答え1

パイプの読み出し端が閉じると、書き込みの試行にエラーが発生します。 SIGPIPE を使用してプロセスを終了します。または信号が遮断されると、書き込みはすぐに返されますerrno == EPIPE。これはあなたの行動を説明する必要があります。これはUNIXパイプの元の機能の1つです。

これは、パイプの読み取り端への最後の残りの参照が閉じられたときに発生します。たとえば、他の参照があるかもしれませんdup()

あなたの場合は、fork()新しいプロセスを作成し、子プロセスはすべて同じファイル記述子で始まります。パイプを閉じるには、親および子ファイル記述子を閉じる必要があります。親close()ファイル記述子は子ファイル記述子に影響を与えず、その逆も同様です。

これは、参照カウントの一般的な概念の例です。カーネルは、パイプの読み出し端を参照するファイル記述子の数を追跡します。呼び出すたびに数が1ずつ減りますclose()。カウントがゼロになると、カーネルは適切なクリーンアップ機能を実行します。 Linuxカーネルではこれを関数ポインタと呼びます。.release関連するすべてのリソースを解放するためです。

参照計算システムは、UNIXファイル記述子にとって非常に重要です。たとえば、次のようになります。dup() とフォーク()研究にはUNIX V5を使用した。


python2.6で始まるサブプロセスでSIGPIPEがブロックされる理由を知りたい場合は、次を参照してください。https://bugs.python.org/issue1652

パイプFDがP2とP3で漏れていることに驚いた場合は、次を参照してください。https://bugs.python.org/issue7213。つまり、より多くの情報に基づいて動作を取得するには、Popen()パスを使用するだけですclose_fds=True

そうでなければ、考えるpass_fds特定の追加のFDをP2とP3に渡すには、パラメータを使用して明示的に作成したいと思います。

私はあなたが実際にこれをしたいと仮定しています。それ以外の場合、このサンプルプログラムが何をすべきかは本当に理解されません。子プロセスオブジェクトを削除して終了します。したがって、親プロセスは少なくとも終了するとパイプFDを閉じます。

特定のバージョンのPythonに依存するような詳細に頼ることなく、それをシェルで再現できます。

$ strace -f sh -c 'cat </dev/zero | { sleep 1& sleep 2& }'
...
[pid 26477] read(0, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072) = 131072
[pid 26477] write(1, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 131072 <unfinished ...>
...
[pid 26480] nanosleep({tv_sec=2, tv_nsec=0},  <unfinished ...>
...
[pid 26479] nanosleep({tv_sec=1, tv_nsec=0}, NULL) = 0
...
[pid 26479] +++ exited with 0 +++
[pid 26480] <... nanosleep resumed> NULL) = 0
...
[pid 26480] +++ exited with 0 +++

[pid 26477] <... write resumed> )       = 65536
[pid 26477] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=26477, si_uid=1001} ---
[pid 26477] +++ killed by SIGPIPE +++

私はここで以前考えなかった詳細を見つけました。write()パイプバッファに64Kを書き込むことに成功したという結果のみが返されます。呼び出し側がSIGPIPEのデフォルト出口を無効にした場合はどうなりますか? 「短い書き込み」は、再試行によってパイプやソケットで許可する必要があることがよくあります。たとえば、プロセスが無関係な信号を受信し、その信号に対してハンドラ機能が設定されている場合、これが発生する可能性があります。したがって、発信者はwrite()残りのデータを再試行する必要があります。それ write()呼び出しはすぐに返されますerrno == EPIPE

関連情報