home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/「デーモン君のソース探検」読書メモ/A07, nohup(1)

C言語系/「デーモン君のソース探検」読書メモ/A07, nohup(1)

C言語系 / 「デーモン君のソース探検」読書メモ / A07, nohup(1)
id: 847 所有者: msakamoto-sf    作成日: 2010-11-23 12:00:16
カテゴリ: BSD C言語 

お題:nohup(1)コマンドが、端末からログアウトしても終了しないように子プロセスを起動する仕組みを調査せよ

※この章は「デーモン君のソース探検」に載っていませんが、msakamoto-sf自身が個人的に興味を持って調べ、"Appendix"として読書メモシリーズに入れてありますのでご注意下さい。


nohup(1)を使ってみる

$ which nohup
/usr/bin/nohup
$ man 1 nohup

nohup(1)から起動されたプログラムはSIGHUPを無視する、つまり端末からログアウトしてもバックグランドで動き続ける。

簡単なプログラムでnohup(1)の効果を確認

標準出力と標準エラー出力に文字を出力するだけの簡単なプログラムを作成し、nohup(1)の効果を確認する。
nohuptest.c:

#include <stdio.h>
#include <unistd.h>
 
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)を使わずに実行してみる:

$ 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)を使ってみる。

$ 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

何も保存されていない。

いろいろ試してみたところ、たとえば次のようにプロセスが正常終了する場合は正常にリダイレクトされることが分かった。

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:

#include <stdio.h>
#include <unistd.h>
 
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)をバックグラウンドで使ってみる

バックグランドで動作させてみる:

$ 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(端末終了前)

$ 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 プロセスに関して整形:

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(端末終了後)

$ 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()関数を見てみる。短いので、解説はコメントとして埋め込んだ。

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()のソースを見てみる。特に難しいところは無い。

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"に書かれていた。見やすいようインデントを変更して引用する。

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)の方には残念ながら、ここまで詳しい説明は見当たらなかった。

日本語で。

  1. fork(2)はプロセスのコピーなので、シグナルハンドラは全て引き継がれる。
  2. exec(2)の場合、
    1. SIG_DFLは引継ぎ。
    2. SIG_IGN → SIGCHLD 以外は引継ぎ。
    3. 呼び出し元のプロセスイメージ上のシグナルハンドラが設定されているものはSIG_DFLに上書き。

nohup(1)はSIG_IGNをSIGHUPに設定しているので、exec(2)後も引き継がれていることが確認できた。


以上でnohup(1)が端末終了後もプロセスをバックグランドで続行させる仕組みが判明した。

  1. exec(2)でSIG_IGN設定が引き継がれる仕組みを使い、SIGHUPにSIG_IGNを設定後exec(2)している。
  2. 標準出力・標準エラー出力が端末に接続されていた場合は、nohup.outに切り替える。

今回のお題については、ここまで。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-11-23 12:02:59
md5:c22298c848d21de396a6226b016a4d70
sha1:72f0f0643b38943a663fa954dc6f19e615b575cf
コメント
コメントを投稿するにはログインして下さい。