子プロセス起動したら標準入力待ち状態になって困った話

お仕事でLinux上のある常駐プログラムを起動するスクリプトを、JavaのProcessBuilder#startを使って実行し、起動スクリプトの出力とexit codeをJava側で受け取る必要があった。

さくっと書いて実行してみたところ、標準出力されるしプログラムも起動するが、なぜかプロンプトが帰ってこない。

シェルスクリプトが処理に時間かかってたりするのかな?と思い、直接叩いてみると、すぐにプロンプトも帰ってくる。

停止し、Javaのスレッドのスタックトレースを見てみると、JavaはProcess#getInputStreamからのreadで止まってた。

そこでスクリプトの末端に「done.」と出すようにして実行してみたが、「done.」と出るが、プロンプトは帰ってこない。

多分、起動スクリプトの処理としては終了しているが、JavaはInputStreamから何かしら読み出すのを待っている状態になっている?

簡単な再現

常駐プログラムを起動するスクリプトに見立てた test.sh を用意

#!/bin/bash
# 常駐プログラムをバックグラウンド実行している見立て (":" で何もしない。)
# 5秒間隔でプロセスの状態をツリーで表示する
sh -c 'while sleep 5; do ps f -f ; done' &

# 直後のプロセスの状態をツリーで表示
ps f -f

これを実行してみる。

sh test.sh

プロンプトも帰ってきた。このときの出力はこんな感じ。

UID        PID  PPID  C STIME TTY      STAT   TIME CMD
nimono    5167  5166  0 Dec25 pts/0    Ss     0:00 -bash # Javaプログラム
nimono    7677  5167  0 00:47 pts/0    S+     0:00  \_ sh test.sh # 起動スクリプト
nimono    7678  7677  0 00:47 pts/0    S+     0:00      \_ sh -c while sleep 5; do ps f -f ; done # 常駐プログラム
nimono    7679  7677  0 00:47 pts/0    R+     0:00      \_ ps f -f

プロンプトが帰ってきて、期待通り動いてくれているようです。

という流れになってます。

5秒後に再度ps f -fが行われるのでこれを確認してみます

UID        PID  PPID  C STIME TTY      STAT   TIME CMD
nimono    5167  5166  0 Dec25 pts/0    Ss+    0:00 -bash
nimono    7678     1  0 00:47 pts/0    S      0:00 sh -c while sleep 5; do ps f -f ; done # 常駐プログラム
nimono    7681  7678  0 00:47 pts/0    R      0:00  \_ ps f -f

起動スクリプトのプロセスは表示されず、常駐プログラムのプロセスだけになっていますが、PPIDが"1"に変化しています。なんだろう。


バックグラウンドでループし続けるので、killする必要があります。

ps -f | grep "sh -c while" | grep -v "grep" | awk '{print $2}' | xargs kill

次に標準出力を待った状態にしてみます。

sh test.sh | cat - && echo done!!

標準出力をcatに流し込みそのまま標準出力させ、終わったら分かるようにdone!!と表示します。
すると、先ほどと同じ出力がされますが、done!!と表示されませんし、プロンプトが帰ってきません。
どうやら、子プロセスの出力(常駐プログラムの出力)が終わるまで、待つようです。
Ctrl+CでSIGINTを出して抜けると、出力は止まりますが、バックグラウンド実行はそのまま続きます。


この操作をそのまま適用するとなると、実際はJavaのプログラムにSIGINTを送ることになるのですが、
そうすると、Javaの処理の途中で終了してしまいます。
後始末処理もちゃんとさせたいのでそういうわけにもいきません(Hookとか出来るらしいけども・・・)

どうするのか

調べるとdaemon化すれば良いらしいですが、よくわかりませんでした。

標準出力とか標準入力とか扱うのだから /proc/[PID]/fd に何かあるかな、と見てみました。

$ ps f
  PID TTY      STAT   TIME COMMAND
 5167 pts/0    Ss     0:00 -bash
 8602 pts/0    S      0:00  \_ -bash
 8604 pts/0    S      0:00  |   \_ cat -
 8610 pts/0    R+     0:00  \_ ps f
 8605 pts/0    S      0:00 sh -c while sleep 5; do : ; done
 8609 pts/0    S      0:00  \_ sleep 5
$ ls -l /proc/8604/fd
合計 0
lr-x------. 1 nimono nimono 64 12月 26 01:51 2013 0 -> pipe:[8510482]
lrwx------. 1 nimono nimono 64 12月 26 01:51 2013 1 -> /dev/pts/0
lrwx------. 1 nimono nimono 64 12月 26 01:51 2013 2 -> /dev/pts/0
$ ls -l /proc/8605/fd
合計 0
lr-x------. 1 nimono nimono 64 12月 26 01:52 2013 0 -> /dev/null
l-wx------. 1 nimono nimono 64 12月 26 01:52 2013 1 -> pipe:[8510482]
lrwx------. 1 nimono nimono 64 12月 26 01:51 2013 2 -> /dev/pts/0

catの標準入力と常駐プログラムの標準出力が同じパイプを見ている事がわかります。

詳しい仕組みはわかりませんが、なんとなく原因はわかりました。

今回は起動スクリプトの標準出力だけ得られれば良かったので、常駐プログラムの標準出力とエラー出力を/dev/nullに向けてやることで解決できそうです。

#!/bin/bash
#sh -c 'while sleep 5; do ps f -f ; done' &
sh -c 'while sleep 5; do ps f -f ; done' 1>/dev/null 2>/dev/null &

Javaで同じように動くかは試していないけど...

あと実際は常駐プログラムを実行するスクリプトは別にあるからうまく行くかどうかはやってみないとわからんなぁ。

daemon化しろ、という話なのだけど、/etc/init.d/functionのdaemonを使ってもうまくできなかったし。お手上げ!