この記事は本当に自分用のメモなので得るものはあまりないと思います。私が最初にリンクを貼っているブログを読まれると最高なので、みなさんはそちらを参照してください。
- 技術/UNIX/なぜnohupをバックグランドジョブとして起動するのが定番なのか?(擬似端末, Pseudo Terminal, SIGHUP他)
- bash: 標準出力、標準エラー出力をファイル、画面それぞれに出力する方法
師匠のかずきさんのブログエントリに触発されて、私も簡単なことを馬鹿にせず、自分で調べて試してから物事を進めようとおもっています。時間が何倍もかかるのですが、これは最初は仕方ないと思うことにしました。ただ、コントロール出来ている感があるので、やっていくとスピードアップできそうです。
調査の背景
VMをプロビジョンするときに、スクリプトで様々な初期処理をしますが、定番のバックグラウンドジョブをプロビジョニングスクリプトから実行する場合、次のような構成にします。これは前からふわっと知っていますが、正直ちゃんと理解していないので、今回調べてみました。
$ nohup /foo/var.sh > some.log 1<2&
nohup コマンド
nohup コマンドは、SIGHUP
がプロセスに送信されても無視するコマンドです。こちらのブログに詳細の解説があるのですが、例えば、あなたがターミナルからコマンドを実行していて、バックグラウンドプロセスとして実行するために、&
をつけていたとしても、使っていたターミナルを落とすと大体その実行されていたプロセスは死んでしまいます。死ぬかどうかは、シェルや、ターミナルの実装によるのですが、死ぬ可能性が高いです。実際にBashなどは終了時に、バックグラウンドプロセス含めSIGHUP
を発行してしまいますので、せっかくバックグラウンドで実行していても、死んでしまうことが発生します。ですので、バックグラウンドにするだけではなく、nohupをつけて、SIGHUP
を無視すると落ちることは無いということです。
実行例
hello.sh
#!/bin/bash
while true; do
echo "hello still alive"
sleep 5
done
nohup なし
$ ./hello.sh > hello.log &
$ cat ./hello.log
hello still alive
hello still alive
ターミナルを殺す(私はVSCodeのインテグレーションターミナルでWSLのターミナルを殺しました。)
$ ps -ef | grep hello
死んでいます。
nohup あり
$ nohup ./hello.sh > hello.log &
ターミナル殺す
$ ps -ef | grep hello
s -ef | grep hello
ushio 8111 1 0 12:28 ? 00:00:00 /bin/bash ./hello.sh
ushio 8588 2058 0 12:30 pts/1 00:00:00 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svnhello
生きてます。
プロセスが停止中
ブログによるとバックグラウンドプロセスが停止中の場合は、nohupをしてもプロセスが死ぬ場合があります。これは、SIGCONT
やSIGTERM
はSIGHUP
ではないので受け付けてしまうからです。Runningのプロセスの場合は、これらのシグナルを受信することはありません。
さて、プロセス停止中とはどういう状態でしょうか?
Linux のプロセスのステータス
Linux のプロセスのステータスを調べてみましょう。
5つのステータスがあるようです。
- R: 実行中もしくは、実行可能。CPUからの割り当てを待っている。
- S: インタラクティブスリープ、イベントを待っている。例えばターミナルからの入力待ちなど
- D: 非インタラクティブなスリープ。プロセスが殺せなかったり、シグナルで中断されている。大抵はOSのリブートで解消する
- Z: ゾンビ、殺されたプロセスで、ステータスが収集されるのを待っている
- T: 停止中、プロセスは、停止されていたり、止められている。
これらは、例えば、ps aux
のSTAT
のカラムで確認できる。
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 8936 312 ? Ssl Dec13 0:00 /init ro
root 8 0.0 0.0 8944 228 tty1 Ss Dec13 0:00 /init ro
ushio 9 0.0 0.0 10660 664 tty1 S Dec13 0:00 sh -c "$VSCODE_WSL_EXT_LOCATION/scrip
ushio 10 0.0 0.0 10660 712 tty1 S Dec13 0:00 sh /mnt/c/Users/tsushi/.vscode/extens
ushio 15 0.0 0.0 10660 696 tty1 S Dec13 0:00 sh /home/ushio/.vscode-server/bin/879
ushio 17 0.0 0.1 971068 30456 tty1 Sl Dec13 0:35 /home/ushio/.vscode-server/bin/8795a9
ushio 32 0.0 0.0 865420 14532 tty1 Sl Dec13 0:14 /home/ushio/.vscode-server/bin/8795a9
ushio 157 0.0 0.3 1216168 56644 tty1 Rl Dec13 1:27 /home/ushio/.vscode-server/bin/8795a9
ushio 170 0.0 0.0 20616 4020 pts/0 Ss Dec13 0:09 /usr/bin/zsh
ushio 307 0.0 0.0 566720 1148 tty1 Sl Dec13 0:00 /home/ushio/.vscode-server/bin/8795a9
ushio 2058 0.0 0.0 20088 4416 pts/1 Ss Dec14 0:03 /usr/bin/zsh
ushio 9777 0.0 0.0 17380 1920 pts/1 R 13:35 0:00 ps aux
シグナルの種類
先ほどの2つのシグナルは
-
SIGTERM
: 終了シグナル (termination) -
SIGCONT
: 一時停止からの再開 -
SIGHUP
: 制御端末のハングアップ検出、もしくは、制御しているプロセスの死
nohupまとめ
ただし、私が意図しているプロセスは、サーバープロセスなので、停止中という状態は何かおかしいことが発生しない限りならないので、基本nohup ... &
で、確実に実行可能だと思われます。(すっきり)
リダイレクト
最初の定番のコードの残りはリダイレクトです。 > filename 1<2&
をすると、標準出力と、エラー出力を両方ファイルに出力することは知っていますが、文法的にどうなっているのでしょうか?
$ nohup /foo/var.sh > some.log 1<2&
std_in_out.sh
#!/bin/bash
echo "stdout" >&1
echo "stderr" >&2
こんな感じで、標準出力と標準エラー出力を単純にアウトプットするシェルを書いてみます。早速リダイレクトがでてきていますが 、まずは、そういう機能であることだけ理解しましょう。
>
これは、1>
が省略された形です。標準出力をファイルに出力します。このリダイレクトの本質は、ファイルディスクリプタの代入です。1のファイルディスクリプタをファイルに変更しているわけです。
$ ./std_in_out.sh
stdout
stdout
$ ./std_in_out.sh > stdout.log
stderr
# cat stdout.log
stdout
ですので、結果として、標準出力のみがファイルに出力されて、エラー出力に関してはディスクリプタを変更していないので、そのまま画面に表示されています。
数字>
ファイルディスクリプタを指定してリダイレクトします。
cat stdout.log 1> stdout.log
$ ./std_in_out.sh 1> stdout.log
stderr
$ cat stdout.log
stdout
$ ./std_in_out.sh 2> stderr.log
stdout
$ cat stderr.log
stderr
&>
標準出力と、標準エラー出力を両方ファイルに出力します。
$ ./std_in_out.sh &> stdout_error.log
$ cat stdout_error.log
stdout
stderr
/dev/null
出力を消去します。次の例では、標準エラー出力だけ消去しています。
``bash
$ ./std_in_out.sh 2> /dev/null
stdout
## 複数リダイレクト実行
リダイレクトは順番に実行される。`num1>&num2` のコマンドは、ファイルディスクリプタのnum2のコピーをnum1に代入するというコマンド。つまり、このリダイレクトは、未定義のディスクリプタ3に標準入力(のコピー、つまり、その後、1が変更されても影響を受けない)が代入される。次に、1に2が代入されて、最終的に3が2に代入される。つまり、標準出力と、標準エラー出力が入れ替わるというものだ。
```bash
$ { bash ./std_in_out.sh 3>&1 1>&2 2>&3; } 1> stdout.log 2>stderr.log
$ cat stdout.log
stderr
$ cat stderr.log
stdout
ちなみに、ここで出てきている{ }
の記述は複合コマンドという文法で、複数のコマンド実行を一つの標準出力、エラー出力にまとめるというものだ。
num1>&num2
のコマンドの動作を確認しよう。ここで、
-
3>&1
(1: 標準出力 2: 標準エラー出力 3: 標準出力) -
1> stdout.log
(1: stdout.log 2: 標準エラー出力 3: 標準出力) -
2>&3
(1: stdout.log 2: 標準出力 3: 標準出力)
つまり、シェル内の標準出力は、stdout.log
に書かれるが、エラー出力はそのまま標準出力に出力される。3に代入した、標準出力はコピーであり、1自体もしくは、ポインタではないので、1がのちに変更されても、その値は影響を受けない。
$ ./std_in_out.sh 3>&1 1> stdout.log 2>&3
stderr
$ cat stdout.log
stdout