#navi_header|C言語系| お題:nohup(1)コマンドが、端末からログアウトしても終了しないように子プロセスを起動する仕組みを調査せよ ※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。 #more|| #outline|| ---- * nohup(1)を使ってみる $ which nohup /usr/bin/nohup $ man 1 nohup nohup(1)から起動されたプログラムはSIGHUPを無視する、つまり端末からログアウトしてもバックグランドで動き続ける。 ** 簡単なプログラムでnohup(1)の効果を確認 標準出力と標準エラー出力に文字を出力するだけの簡単なプログラムを作成し、nohup(1)の効果を確認する。 nohuptest.c: #code|c|> #include #include int main(int argc, char *argv[]) { int i = 0; for (;;i++) { fprintf(stdout, "stdout, i = %d\n", i); fprintf(stderr, "stderr, i = %d\n", i); sleep(1); } return 0; } ||< コンパイルし、まずはnohup(1)を使わずに実行してみる: #pre||> $ gcc -o nohuptest nohuptest.c $ ./nohuptest stdout, i = 0 stderr, i = 0 stdout, i = 1 stderr, i = 1 stdout, i = 2 stderr, i = 2 ^C $ ||< 続いてnohup(1)を使ってみる。 #pre||> $ nohup ./nohuptest sending output to nohup.out ^C $ cat nohup.out stderr, i = 0 stderr, i = 1 stderr, i = 2 stderr, i = 3 stderr, i = 4 ||< "&"はつけなかったのでnohupがフォアグラウンドのまま動作している。nohuptestのstderrがnohup.outにリダイレクトされるのはmanpageに書いてあるとおり。しかし、stdoutの出力が保存されていない。 ** シグナル強制終了→printf()の内部バッファリングが消滅→fflush()で解決 stdoutの出力がnohup.outに出力されていないのが気になり、単独でリダイレクトさせてみる: $ ./nohuptest > std.out stderr, i = 0 stderr, i = 1 stderr, i = 2 ^C $ wc -c std.out 0 std.out 何も保存されていない。 いろいろ試してみたところ、たとえば次のようにプロセスが正常終了する場合は正常にリダイレクトされることが分かった。 #code|c|> int main(int argc, char *argv[]) { int i; for (i = 0; i < 10 ;i++) { fprintf(stdout, "Hello %d\n", i); sleep(1); } return 0; } ||< しかし、この場合であっても途中で"^C"でシグナルにより強制終了させた場合、リダイレクト先が空っぽのままであった。 ・・・もしかして、標準C関数のprintf()って内部でバッファリングしてて、シグナルにより強制終了の場合は内部バッファが書き出されずにプロセス終了→リダイレクト先は空っぽのまま、という流れか? ということで、fflush()を挟んでみたところ、ちゃんとシグナル強制終了の場合でもリダイレクト先にこれまでの出力内容が保存されていた。 この問題で1時間ほど時間をとられたが、再開する。結構凹んだ。 なお、あくまでもNetBSD 1.6で発生した現象であって、他のUNIX環境でも同様に発生するかは不明。libcの内部実装に依存するだろう。 fflush()付きのnohuptest.c: #code|c|> #include #include int main(int argc, char *argv[]) { int i = 0; for (;;i++) { fprintf(stdout, "stdout, i = %d\n", i); fflush(stdout); fprintf(stderr, "stderr, i = %d\n", i); fflush(stderr); sleep(1); } return 0; } ||< これでnohup.outにstdout/stderrの両方が出力されるようになった。 ** nohup(1)をバックグラウンドで使ってみる バックグランドで動作させてみる: #pre||> $ nohup ./nohuptest & [1] 280 $ sending output to nohup.out # コマンドライン入力ではなくて、nohupの出力 $ jobs [1]+ Running nohup ./nohuptest & $ ps PID TT STAT TIME COMMAND 206 p0 Ss+ 0:00.04 -bash 211 p1 Ss 0:00.01 -bash 280 p1 S 0:00.02 ./nohuptest 281 p1 R+ 0:00.00 ps 215 p2 Ss 0:00.00 -bash ||< この時点でnohupを起動した端末からlogoutしてみる。他の端末からログインしてみると、"./nohuptest"プロセスが動き続けていることが分かる。また、nohup.outへの出力も続いている。 $ ps PID TT STAT TIME COMMAND 206 p0 Ss+ 0:00.04 -bash 280 p1- S 0:00.02 ./nohuptest 215 p2 Ss 0:00.02 -bash 285 p2 R+ 0:00.00 ps SIGHUPには反応しないので、SIGINTで終了させる。 $ kill -INT 280 $ ps PID TT STAT TIME COMMAND 206 p0 Ss+ 0:00.04 -bash 215 p2 Ss 0:-1.99 -bash 288 p2 R+ 0:00.00 ps $ ** nohup(1)でバックグランド実行→端末終了→プログラムグループやセッションはどうなる? nohup(1)でバックグランド実行し、端末を終了させた場合、プログラムグループやセッション、制御端末はどうなるか? before(端末終了前) #pre||> $ ps wx -o pid,ppid,pgid,jobc,sess,tpgid,tsess,tty,stat,command PID PPID PGID JOBC SESS TGPID TSESS TTY STAT COMMAND 205 203 203 0 c8c5c0 30001 0 ?? S sshd: msakamoto@ttyp0 214 212 212 0 c8c880 30001 0 ?? S sshd: msakamoto@ttyp2 296 294 294 0 c99200 30001 0 ?? S sshd: msakamoto@ttyp1 531 529 529 0 cac780 30001 0 ?? S sshd: msakamoto@ttyp3 206 205 206 0 bf9000 542 c0bf9000 ttyp0 Ss -bash 533 206 533 1 bf9000 542 c0bf9000 ttyp0 T man ps 534 533 533 1 bf9000 542 c0bf9000 ttyp0 T sh -c less /usr/share/man//cat1/ps.0 535 534 533 1 bf9000 542 c0bf9000 ttyp0 T less /usr/share/man//cat1/ps.0 539 206 539 1 bf9000 542 c0bf9000 ttyp0 T man printf 540 539 539 1 bf9000 542 c0bf9000 ttyp0 T sh -c less /usr/share/man//cat1/printf.0 541 540 539 1 bf9000 542 c0bf9000 ttyp0 T less /usr/share/man//cat1/printf.0 542 206 542 1 bf9000 542 c0bf9000 ttyp0 S+ less /etc/hosts 297 296 297 0 c99f00 556 c0c99f00 ttyp1 Ss -bash 552 297 552 1 c99f00 556 c0c99f00 ttyp1 S ./nohuptest 553 297 553 1 c99f00 556 c0c99f00 ttyp1 T man chdir 554 553 553 1 c99f00 556 c0c99f00 ttyp1 T sh -c less /usr/share/man//cat2/chdir.0 555 554 553 1 c99f00 556 c0c99f00 ttyp1 T less /usr/share/man//cat2/chdir.0 556 297 556 1 c99f00 556 c0c99f00 ttyp1 S+ man bash 561 556 556 1 c99f00 556 c0c99f00 ttyp1 S+ sh -c less /tmp//man.00556a 562 561 556 1 c99f00 556 c0c99f00 ttyp1 S+ less /tmp//man.00556a 215 214 215 0 cacfc0 546 c0cacfc0 ttyp2 Ss -bash 543 215 543 1 cacfc0 546 c0cacfc0 ttyp2 T man sh 544 543 543 1 cacfc0 546 c0cacfc0 ttyp2 T sh -c less /usr/share/man//cat1/sh.0 545 544 543 1 cacfc0 546 c0cacfc0 ttyp2 T less /usr/share/man//cat1/sh.0 546 215 546 1 cacfc0 546 c0cacfc0 ttyp2 S+ man cat 547 546 546 1 cacfc0 546 c0cacfc0 ttyp2 S+ sh -c less /usr/share/man//cat1/cat.0 548 547 546 1 cacfc0 546 c0cacfc0 ttyp2 S+ less /usr/share/man//cat1/cat.0 532 531 532 0 c8c8c0 563 c0c8c8c0 ttyp3 Ss -bash 563 532 563 1 c8c8c0 563 c0c8c8c0 ttyp3 R+ ps wx -o pid ||< → nohuptest プロセスに関して整形: #pre||> sshd: msakamoto@ttyp1 (PID=296, 制御端末=??) > [セッショングループ SESS=c99200, 制御端末=ttyp1] -bash (PID=297, セッションリーダー) > [プログラムグループ PGID=552] : Sleeping (STAT=T) ./nohuptest (PID=552) > [プログラムグループ PGID=553 : Stopped (STAT=T) man chdir (PID=553) sh -c less /usr/share/man//cat2/chdir.0 less /usr/share/man//cat2/chdir.0 > [プログラムグループ PGID=556] : Foreground (STAT=S+) man bash (PID=556) sh -c less /tmp//man.00556a less /tmp//man.00556a ||< after(端末終了後) #pre||> $ ps wx -o pid,ppid,pgid,jobc,sess,tpgid,tsess,tty,stat,command PID PPID PGID JOBC SESS TGPID TSESS TTY STAT COMMAND 205 203 203 0 c8c5c0 30001 0 ?? S sshd: msakamoto@ttyp0 214 212 212 0 c8c880 30001 0 ?? S sshd: msakamoto@ttyp2 531 529 529 0 cac780 30001 0 ?? S sshd: msakamoto@ttyp3 206 205 206 0 bf9000 542 c0bf9000 ttyp0 Ss -bash 533 206 533 1 bf9000 542 c0bf9000 ttyp0 T man ps 534 533 533 1 bf9000 542 c0bf9000 ttyp0 T sh -c less /usr/share/man//cat1/ps.0 535 534 533 1 bf9000 542 c0bf9000 ttyp0 T less /usr/share/man//cat1/ps.0 539 206 539 1 bf9000 542 c0bf9000 ttyp0 T man printf 540 539 539 1 bf9000 542 c0bf9000 ttyp0 T sh -c less /usr/share/man//cat1/printf.0 541 540 539 1 bf9000 542 c0bf9000 ttyp0 T less /usr/share/man//cat1/printf.0 542 206 542 1 bf9000 542 c0bf9000 ttyp0 S+ less /etc/hosts 552 1 552 0 c99f00 30001 0 ttyp1 S ./nohuptest 215 214 215 0 cacfc0 546 c0cacfc0 ttyp2 Ss -bash 543 215 543 1 cacfc0 546 c0cacfc0 ttyp2 T man sh 544 543 543 1 cacfc0 546 c0cacfc0 ttyp2 T sh -c less /usr/share/man//cat1/sh.0 545 544 543 1 cacfc0 546 c0cacfc0 ttyp2 T less /usr/share/man//cat1/sh.0 546 215 546 1 cacfc0 546 c0cacfc0 ttyp2 S+ man cat 547 546 546 1 cacfc0 546 c0cacfc0 ttyp2 S+ sh -c less /usr/share/man//cat1/cat.0 548 547 546 1 cacfc0 546 c0cacfc0 ttyp2 S+ less /usr/share/man//cat1/cat.0 532 531 532 0 c8c8c0 564 c0c8c8c0 ttyp3 Ss -bash 564 532 564 1 c8c8c0 564 c0c8c8c0 ttyp3 R+ ps wx -o pid ||< nohuptestプロセスに注目すると、親プロセスIDがinit(PID=1)に変更され、制御端末のプロセスグループ(TGPID)・セッション(TSESS)がクリアされていることが分かる。TGPIDの"30001"というのは他のデーモンプロセスと同じ値。 nohup(1)の動作確認と、端末終了後のプロセスの状態を確認できたところで、いよいよソースを読んでみる。 * nohup(1)のソースコード 場所: $ locate nohup ... /usr/src/usr.bin/nohup/Makefile /usr/src/usr.bin/nohup/nohup.1 /usr/src/usr.bin/nohup/nohup.c $ wc -l /usr/src/usr.bin/nohup/nohup.c 149 /usr/src/usr.bin/nohup/nohup.c open(2), dup2(2), signal(3), isatty(3) をmanページなどで予習・復習しておくとよい。 ヘッダーやコメントを含めても149行と短い。さっそくmain()関数を見てみる。短いので、解説はコメントとして埋め込んだ。 #code|c|> int main(argc, argv) int argc; char **argv; { int exit_status; while (getopt(argc, argv, "") != -1) { usage(); } argc -= optind; argv += optind; if (argc < 1) usage(); /* 標準出力が端末に接続されていれば、 dofile()中でnohup.outに切り替える */ if (isatty(STDOUT_FILENO)) dofile(); /* 標準エラー出力が端末に接続されていれば、 標準エラー出力のファイル記述子を標準出力に接続する。 (上のdofile()で標準エラー出力はnohup.outに接続済み) */ if (isatty(STDERR_FILENO) && dup2(STDOUT_FILENO, STDERR_FILENO) == -1) { /* may have just closed stderr */ (void)fprintf(stdin, "nohup: %s\n", strerror(errno)); exit(EXIT_MISC); } /* 制御端末から送られるSIGHUPを無視する */ /* The nohup utility shall take the standard action for all signals except that SIGHUP shall be ignored. */ (void)signal(SIGHUP, SIG_IGN); /* プログラムの実行 */ execvp(argv[0], &argv[0]); exit_status = (errno == ENOENT) ? EXIT_NOTFOUND : EXIT_NOEXEC; (void)fprintf(stderr, "nohup: %s: %s\n", argv[0], strerror(errno)); exit(exit_status); } ||< dofile()のソースを見てみる。特に難しいところは無い。 #code|c|> static void dofile() { int fd; char *p, path[MAXPATHLEN]; /* If the standard output is a terminal, all output written to its standard output shall be appended to the end of the file nohup.out in the current directory. If nohup.out cannot be created or opened for appending, the output shall be appended to the end of the file nohup.out in the directory specified by the HOME environment variable. If a file is created, the file's permission bits shall be set to S_IRUSR | S_IWUSR. */ #define FILENAME "nohup.out" p = FILENAME; if ((fd = open(p, O_RDWR|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR)) >= 0) goto dupit; /* 現在ディレクトリにnohup.outを作成できなければ、$HOMEの下に作成してみる */ if ((p = getenv("HOME")) != NULL) { (void)strcpy(path, p); (void)strcat(path, "/"); (void)strcat(path, FILENAME); if ((fd = open(p = path, O_RDWR|O_CREAT|O_APPEND, S_IRUSR|S_IWUSR)) >= 0) goto dupit; } (void)fprintf(stderr, "nohup: can't open a nohup.out file.\n"); exit(EXIT_MISC); /* nohup.outのファイル記述子を標準出力のファイル記述子に接続 */ dupit: (void)lseek(fd, 0L, SEEK_END); if (dup2(fd, STDOUT_FILENO) == -1) { (void)fprintf(stderr, "nohup: %s\n", strerror(errno)); exit(EXIT_MISC); } (void)fprintf(stderr, "sending output to %s\n", p); } ||< ** fork-exec時のシグナルハンドラの引継ぎ nohup(1)ではSIGHUPをSIG_IGNに設定した後、exec(2)している。つまりnohup(1)で起動したプロセスは、別プロセスで書き換えられる。 となると気になるのが、「シグナルハンドラの引継ぎ」である。動作している以上は引き継がれているのだろうが、たとえばSIG_IGNやSIG_DFL以外、つまりカスタムのシグナルハンドラをインストールしていた場合はどうなるのか? 答えは"Advanced UNIX Programming"(AUP) 2nd Edition, p623, "9.1.10 Effect of fork, pthread_create, and exec on Singals"に書かれていた。見やすいようインデントを変更して引用する。 #pre||> 1. Signal actions: After a fork, the child inherits all signal actions. After an exec, singlas set to SIG_DFL remain that way: signals set to SIG_IGN remain that way, except for SIGCHLD, which may be set to SIG_IGN or SIG_DFL, as the implementation chooses; caught signals are set to SIG_DFL. As all actions are process-wide, pthread_create has no effect. ||< 上記と同様の説明はOpenGroupのexec(2)にも記載されている。"Advanced Programming in the UNIX Environment 2nd"(APUE)の方には残念ながら、ここまで詳しい説明は見当たらなかった。 日本語で。 + fork(2)はプロセスのコピーなので、シグナルハンドラは全て引き継がれる。 + exec(2)の場合、 ++ SIG_DFLは引継ぎ。 ++ SIG_IGN → SIGCHLD 以外は引継ぎ。 ++ 呼び出し元のプロセスイメージ上のシグナルハンドラが設定されているものはSIG_DFLに上書き。 nohup(1)はSIG_IGNをSIGHUPに設定しているので、exec(2)後も引き継がれていることが確認できた。 ---- 以上でnohup(1)が端末終了後もプロセスをバックグランドで続行させる仕組みが判明した。 + exec(2)でSIG_IGN設定が引き継がれる仕組みを使い、SIGHUPにSIG_IGNを設定後exec(2)している。 + 標準出力・標準エラー出力が端末に接続されていた場合は、nohup.outに切り替える。 今回のお題については、ここまで。 #navi_footer|C言語系|