LoginSignup
19
21

Daemonを召喚しよう

Last updated at Posted at 2023-09-27

モチベーション

  • サーバ周りの要素技術を触りたくなった
  • 手始めにデーモン化のためのDouble Forkを実装してみたくなった
  • 調べていくとよく使うNginxもphp-fpmも2回forkしてなくね?となった
  • どういうことなのかアマゾンの奥地へ旅立ったので、レポートをまとめようと思った

※NginxのコードがBLM運動の対応されてなさそうな関数名だけど、分かりやすさのためそのまま表記している
※プロセスの話で、親子を〇すみたいに書いてたら、不穏な文章になり、無理やり校正した関係で変な表現があるかもしれない

基本事項

Daemonとは?

デーモンプロセスを召喚するために何をすればよいのか。

とりあえずman daemonをしてみると以下のように記載してある

The daemon() function is for programs wishing to detach themselves from the controlling terminal and run in the background as system daemons.

If nochdir is zero, daemon() changes the process's current working directory to the root directory ("/"); otherwise, the current working directory is left unchanged.

If noclose is zero, daemon() redirects standard input, standard output and standard error to /dev/null; otherwise, no changes are made to these file descriptors.

最初の行によると、以下になっていればデーモンと呼べるらしい。

  • 制御端末に関連づいていないプロセスであること
  • バックグラウンドで実行されていること

制御端末はプログラムの入出力とデバイスへの表示の橋渡しのためのもの。
制御端末はOSがプロセスに対して割り当てる。
何かシグナルに該当する文字(Ctrl+cなど)が渡ってきたら、OSが、制御端末を通じて、プロセスに何かしら働きかけるためのもの。
(※コメントいただいて修正したけど、自分の理解に自信がないのでリンクを貼っておきます)
psコマンドで実行されるときに表示されるttyやptsなど。
以下のpts/0 などがそれにあたる。
?となっている場合は関連づいていない。

$ ps aux | grep ssh
root     1755  0.0  0.1  29104  8716 ?        Ss    9月08   0:00 sshd
root   501401  0.0  0.1  41880 10696 ?        Ss   14:41   0:00 sshd
user1  501405  0.0  0.0  41880  6340 ?        S    14:41   0:00 sshd
user1  501534  0.0  0.0 221940  2356 pts/0    S+   14:42   0:00 grep

この二つを実施するために利用される方法はforkとsetsidシステムコールの呼び出し。
イメージとしては以下のようなコードを書けばデーモンプロセスとなる。

forkは子プロセスを作成し、呼び出し元のプロセスは親プロセスとなる。
子プロセスはforkの呼び出し以後の命令からスタートする。

setsidは新しいセッションを開始する。
これにより新しいプロセスグループIDとセッションIDが割り当たる。
新しいセッションは制御端末を持たないため、もともと制御端末が割り当たっていたとしても外れた状態となる。

int pid = fork();
switch(pid){
  case -1:
    perror();
    exit(-1);
  case 0:
    setsid();
    break;
  default:
    exit(0);
}

プロセスに割り当たるID

プロセスには3つのIDが割り当たっている

  • PID(プロセスID)
  • PGID(プロセスグループID)
  • SID(セッションID)

具体的な値はps -ejHでそれぞれ確認できる。

$ ps -ejH | grep -E "ssh|PID"
    PID    PGID     SID TTY          TIME CMD
   1755    1755    1755 ?        00:00:00   sshd
 501401  501401  501401 ?        00:00:00     sshd
 501405  501401  501401 ?        00:00:00       sshd
  • コマンド部分は階層を表しており、1755 =fork=> 501401 =fork=> 501405 となっている。
  • PIDの1755と501401はセッションIDが異なることから、setsid相当のことが行われていることが分かる。
  • PIDの501405はセッションIDが501401であることから、PIDの501401と同じセッションであることが分かる。

Double forkは必要なのか?

遠い昔、デーモンプロセスにするにはダブルフォークする必要があると聞いていた。
それなのに、デーモンのプロセスを作る際にforkを1回しか呼んでいなくてもデーモンの条件は満たされているように思う。

ダブルフォークの解説などは先達がいるので、そちらを参照した。
Double forkによるプロセスのデーモン化と、ファイル変更時の自動サーバーリロードの実装 (Python)

ここで疑問なのはダブルフォークとシングルフォークでどのような違いがあるのかということ。
親プロセスは2度死ぬ - デーモン化に使うダブルforkの謎
この記事によると、シングルフォークの場合、後から制御端末を割り当てられる可能性があるので問題だという。
制御端末はセッションリーダー(setsidしたプロセス)にしか割り当てられないため、さらにforkすればセッションリーダーではなくなるため、より確実なデーモンとなる、という理屈のようだ。

SolarisまたはSVR4じゃなければ気にしないで良いという話なので、自分の生きてる界隈に限れば問題にならないように思う。

ここまでで出てきた疑問

ここまで調べていろいろと疑問が出てきたので実験してみる。

* 親子孫プロセスにSIGTERM(killのデフォルト)を送ったときの挙動
    * 親、子、孫それぞれに送ったときに、それぞれのプロセスの状態は?
    * 親の処理が終了して、子、孫それぞれにシグナルを送ったときのプロセスの状態は?
    * 子の処理が終了して、親、孫それぞれにシグナルを送ったときのプロセスの状態は?
    * 子プロセス内でsetsidを呼出し、上記を行ったときのプロセスの状態は?
* SSH接続を切ったときの挙動
    * 制御端末を持たない、子プロセスと孫プロセスは強制終了するのか
    * 制御端末を持たない、子プロセスが終了した後の孫プロセスは強制終了するのか
    * 制御端末を持つ、子プロセスと孫プロセスは強制終了するのか
    * 制御端末を持つ、子プロセスが終了した後の孫プロセスは強制終了するのか
* 制御端末を持たない、セッションリーダーのプロセスに制御端末を割り当てる方法
* Nginxはどのような状態になっているのか
* php-fpmはどのような状態になっているのか

プロセスをkillした時の状態

  • 予想

    • 親子孫の間で、特に制御していないので、お互い影響しあわない。
    • 処理が終了したらpsコマンドから表示が消える。
    • 子を強制終了しても、親孫のプロセスは終了しない。
    • 孫を強制終了しても、親子のプロセスは終了しない。
    • 親を強制終了しても、子孫のプロセスは終了しない。
  • 実験用プログラム

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(){
    int child,gchild;
    if((child = fork()) < 0){
        perror("failed to fork.");
        return -1;
    }

    // terminate parent process.
    if( child != 0 ){
        sleep(30); // parent process wait 30s.
        printf("parent process finished\n");
        return 0;
    }

    /*
     * with and no
    if( child == 0 ){
        setsid();
    }
    */

    if((gchild = fork()) < 0){
        perror("failed to fork.");
        return -1;
    }

    // terminate child process.
    if( gchild != 0 ){
        sleep(40); // child process wait 40s.
        printf("child process finished\n");
        return 0;
    }

    sleep(50); // grand child process wait 40s.
    printf("grand child process finished\n");
}

killコマンドでプロセスを消す前に、実験する前に挙動を確認しておく。

## setsidのコメントアウトを外したものはdaemon-with-sess-idという名前でコンパイル
$ gcc daemon-no-sess-id.c -o daemon-no-sess-id
$ ./daemon-no-sess-id
parent process finished
$ child process finished
grand child process finished

制御端末を共有しているため、30s後にプロセスが終了し、制御が端末に戻った後、残ったプロセスが画面に出力している状態。

あとはこれらのプログラムを起動したら。

ps aux | grep daemon-no-sess # pidを調べる
kill [pid] # 親、子、孫を見たうえでkillしてみる
ps aux | grep daemon-no-sess # 状態を見てみる

分かりにくい表になってしまったが、実験結果だけ表にした。
生はプロセスがps画面に表示されていることを表している。
死はプロセスがps画面に表示されていないことを表している。
前は何したをする前、かつプログラムがreturnしていないプロセスの状態。
後は何したをした後、かつプログラムがreturnしていないプロセスの状態。
自分でまとめてても分かりにくいとは思ってる。

子でsetsid 親(前) 子(前) 孫(前) 何した 親(後) 子(後) 孫(後)
してない 親をkill
してない 子をkill 生※1
してない 孫をkill 生※2
してない 子をkill
してない 孫をkill 生※3
してない 死※4 親をkill 死※1
してない 死※4 孫をkill
した 親をkill
した 子をkill 生※1
した 孫をkill 生※2
した 子をkill
した 孫をkill 生※3
した 死※4 親をkill 死※1
した 死※4 孫をkill

※1: defunctになり、親プロセスの処理が終了したらdefunctプロセスもなくなる
※2: defunctになり、子プロセスの処理が終了したらdefunctプロセスもなくなる、親プロセスは強制終了しない
※3: defunctになり、子プロセスの処理が終了したらプロセスもなくなる
※4: 子が先に終了するよう親のsleep時間を調整している

  • 結論
    • 予想通り基本的にプロセスの終了は他のプロセスに影響しない。
    • ただ、予想に反し、fork元のプロセスが生きている状態で、fork先のプロセスが終了した場合、defunctになり、psコマンドに表示が残る(ゾンビプロセス)
    • ゾンビとなったプロセスはfork元のプロセスの消滅とともにpsコマンドの表示から消える。
    • ゾンビのことを忘れていた。

SSHをクローズしたときのプロセス

  • 予想

    • ssh実行後のログインシェルのプロセスが終了しても、そのシェルで実行したプログラムは動きっぱなしになる
    • ただ過去の経験から、nohupを実行しないとプロセスが残らないと知っている
    • おそらくシェルがkillを送ってくれていると予想して、PGIDが同一のものはkillしていると予想する
  • 実験時の状況

    • コードは流用して、コマンド実行時に&を付けてバックグラウンドプロセスにする(nohupはつけない)。
    • シェルはzsh
    • 実行したプログラムに対し外から何も操作しない

※ここもコメントいただいて、確かにdiwosnなどいろいろな条件で変わるため思いつく限りの実験条件を追記

これも実験結果だけ。

daemon-with-sess-id &
# fork -> setsid -> forkをするプログラム
# 1. すべてのプロセスが残っている時にsshを切断
# ==> 親だけ強制終了する、子と孫は生きている
#
# 2. 親の処理の終了後にsshを切断
# ==> 子と孫は生きている

daemon-no-sess-id &
# fork -> forkをするプログラム(setsidはしない)
# 3. すべてのプロセスが残っている時にsshを切断
# ==> 親も子も孫も強制終了する
#
# 4. 親の処理の終了後にsshを切断
# ==> 制御端末の紐づけは解除され、子と孫は生きている
  • 結果
    • 制御端末がありfork元が生きていると、芋づる式にプロセスが死んでいく
    • 制御端末がありfork元が終了していると、制御端末の関連がなくなるがプロセスは生きている
    • 制御端末が無いものはすべてのプロセスが残っている状態になる

制御端末の有無で大幅に挙動が変わった。

後から制御端末をプロセスに割り当てる方法

ググった例を見ると、setsidで外した後、プロセス自身が端末を取得しているように見え、外のプログラムから制御端末を割り当てるやり方が見つからなかった。

https://gist.github.com/zmwangx/2bac2af9195cad47069419ccd9ee98d8
https://q.hatena.ne.jp/1320139299

なので、そのようなことが技術的に可能なのかどうか分かっていない。
現状の理解では、プログラミング中に制御端末のことを忘れてしまったり、OSなりシステムの挙動の差で制御端末を取得してしまう事象が無い限り、無理に2回目のforkをする必要性は感じない。

あくまでも現状の理解としてであり、もしかしたら方法はあるのかもしれない。

無い。
原典をコメントいただいた。
credentials(7)

あるセッションの全プロセスは一つの 制御端末 を共有する。 セッションリーダーが最初に端末をオープンした際に制御端末は設定される (open(2) の呼び出しで O_NOCTTY フラグが指定された場合を除く)。 一つの端末は、最大でも一つのセッションの制御端末にしかなれない。

最初の一文ですでに明らかで、別のプロセスが制御端末を開こうと思っても、そもそもセッションが異なる。

結局サーバプログラムはどのようにプロセスを生成しているのか

Nginxの場合

$ git clone https://github.com/nginx/nginx --depth 1
$ cd nginx
$ ./auto/configure
$ make
$ make install
$ /usr/local/nginx/sbin/nginx

$ ps -ejH | grep nginx
 523925  523925  523925 ?        00:00:00   nginx
 523926  523925  523925 ?        00:00:00     nginx

前述の検証用コードを実行したときのpgidやsidは以下のようになっていた。

$ ps -ejH | grep daemon-with
 524569  524569  517514 pts/0    00:00:00           daemon-with-ses
 524570  524570  524570 ?        00:00:00             daemon-with-ses
 524571  524570  524570 ?        00:00:00               daemon-with-ses

nginxのpsとこれを比較したとき、子プロセスがmasterプロセスとして実行されていそうだと感じる。
nginxのmasterプロセスはPID/PGID/SIDがすべて一緒で、setsid後にforkしている様子が無いためだ。
※冒頭でのfork2回していないのでは?と言っていたのは、このIDを見たため。

nginxのコードを机上で追っていく(デバッガやprintfデバッグは利用しない)。

すごく長いので省略

まずはforkが呼び出されている箇所を探す。

$ grep -r fork src
src/os/unix/ngx_daemon.c:    switch (fork()) {
src/os/unix/ngx_daemon.c:        ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
src/os/unix/ngx_process.c:    pid = fork();
src/os/unix/ngx_process.c:                      "fork() failed while spawning \"%s\"", name);

これを見るとngx_daemon.cがデーモン化のためのmasterプロセス用のforkで、
ngx_process.cがworkerプロセス用のforkと予想できる。

src/core/nginx.cのmain関数を見てみると、ngx_daemonが呼ばれているので、daemonが有効になっている場合は予想通り、forkが1回だけ呼ばれそうだ。

    17      switch (fork()) {
    18      case -1:
    19          ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "fork() failed");
    20          return NGX_ERROR;
    21
    22      case 0:
    23          break;
    24
    25      default:
    26          exit(0);
    27      }
    28
    29      ngx_parent = ngx_pid;
    30      ngx_pid = ngx_getpid();
    31
    32      if (setsid() == -1) {
    33          ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "setsid() failed");
    34          return NGX_ERROR;
    35      }

子プロセスの戻り値はエラーが無ければ0。
親はエラーが無ければ-1と0以外でdefaultに行くため、親プロセスはすぐに終了するように見える。
その直後にsetsidも呼ばれているため、制御端末からも切り離されている。

次にsrc/os/unix/ngx_process.cの方だが、

   186      pid = fork();
   187
   188      switch (pid) {
   189
   190      case -1:
   191          ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
   192                        "fork() failed while spawning \"%s\"", name);
   193          ngx_close_channel(ngx_processes[s].channel, cycle->log);
   194          return NGX_INVALID_PID;
   195
   196      case 0:
   197          ngx_parent = ngx_pid;
   198          ngx_pid = ngx_getpid();
   199          proc(cycle, data);
   200          break;
   201
   202      default:
   203          break;
   204      }

ここのforkでの孫プロセスはproc(cycle, data)に到達し、関数ポインタの元を見てみるとngx_worker_process_cycleが呼び出されていそうで、ngx_worker_process_cycleを見ると以下にたどり着く。

 708     ngx_setproctitle("worker process");
 709
 710     for ( ;; ) {
 711
 712         if (ngx_exiting) {
 713             if (ngx_event_no_timers_left() == NGX_OK) {
 714                 ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "exiting");
 715                 ngx_worker_process_exit(cycle);
 716             }
 717         }

ここで無限ループに突入して、ワーカーとして働いているように見える。
forkを呼び出している関数の、ngx_spawn_processの呼び出し元を見てみると、
src/os/unix/ngx_process_cycle.cのngx_start_worker_processesに行きつく。
先ほど関数ポインタでngx_worker_process_cycleが呼び出されていると言っていたのは、ngx_spawn_processの第2引数の部分を見たためだ。

 335 static void
 336 ngx_start_worker_processes(ngx_cycle_t *cycle, ngx_int_t n, ngx_int_t type)
 337 {
 338     ngx_int_t  i;
 339
 340     ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "start worker processes");
 341
 342     for (i = 0; i < n; i++) {
 343
 344         ngx_spawn_process(cycle, ngx_worker_process_cycle,
 345                           (void *) (intptr_t) i, "worker process", type);
 346
 347         ngx_pass_open_channel(cycle);
 348     }
 349 }

ngx_start_worker_processesはngx_master_process_cycleから呼び出されている。
ngx_master_process_cycleはmain関数から呼ばれる。

  • 結論
    • 2回フォークしていなさそう
* main
  * ngx_daemon
    * fork(親プロセスはすぐにexit)
    * setsid(子プロセスはここで制御端末を捨て、masterプロセスとなる)
  * ngx_master_process_cycle
    * ngx_start_worker_processes
      * ngx_spawn_process
        * fork(ここでforkしたものがworkerプロセスとなる)
        * ngx_worker_proces_cycle
          * 無限ループのfor(workerプロセス(孫)の処理)
    * 無限ループのfor(masterプロセス(子)の処理)

php-fpmの場合

下準備

git clone https://github.com/php/php-src --depth 1
cd php-src/sapi/fpm/fpm
これも長いので省略

nginxと同様にまずforkを探してみる。

$ grep -r "fork()"
fpm_children.c:         pid = fork();
fpm_children.c:                         zlog(ZLOG_SYSERROR, "fork() failed");
fpm_signals.c:                  around fork(). This execution branch is a last resort trap
fpm_unix.c:             pid_t pid = fork();

fpm_unix.cのforkを呼び出している関数はfpm_unix_init_mainで、
fpm_children.cのforkを呼び出している関数はfpm_children_make。

前者のほうがmain関数から呼び出されているmasterプロセスのようで、

                pid_t pid = fork();
                switch (pid) {

                        case -1 : /* error */
                                zlog(ZLOG_SYSERROR, "failed to daemonize");
                                return -1;

                        case 0 : /* children */
                                close(fpm_globals.send_config_pipe[0]); /* close the read side of the pipe */
                                break;

                        default : /* parent */
                                close(fpm_globals.send_config_pipe[1]); /* close the write side of the pipe */

                                [....masterプロセス()の起動を待っているような処理を中略]

                                exit(FPM_EXIT_SOFTWARE);
                }
        }

        /* continue as a child */
        setsid();

推測通りで、親はすぐに終了し、子プロセスがsetsidを実行している。
コメントからもプロセスがどうなっているか読み取れる。
先にmainから呼ばれているかを追ってみる。
結果的には呼び出されていた。

# fpm.cで呼び出され、呼び出し元の関数はfpm_init
$ grep -r fpm_unix_init_main .
./fpm.c:            0 > fpm_unix_init_main()          ||
./fpm_unix.h:int fpm_unix_init_main(void);
./fpm_unix.c:int fpm_unix_init_main(void)

# fpm_main.cで呼び出され、呼び出し元の関数はmain
$ grep -r "fpm_init(" .
./fpm.c:enum fpm_init_return_status fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root, int force_daemon, int force_stderr) /* {{{ */
./fpm.h:enum fpm_init_return_status fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root, int force_daemon, int force_stderr);
./fpm_main.c:   enum fpm_init_return_status ret = fpm_init(argc, argv, fpm_config ? fpm_config : CGIG(fpm_config), fpm_prefix, fpm_pid, test_conf, php_allow_to_run_as_root, force_daemon, force_stderr);

続いて、fpm_children_makeの呼び出し元を追う。

$ grep -r "fpm_children_make(" .
./fpm_children.c:                               fpm_children_make(wp, 1 /* in event loop */, 1, 0);
./fpm_children.c:int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) /* {{{ */
./fpm_children.c:       return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);
./fpm_children.h:int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug);
./fpm_process_ctl.c:                    fpm_children_make(wp, 1, children_to_fork, 1);
./fpm_process_ctl.c:    fpm_children_make(wp, 1, 1, 1);

呼び出し元は複数あるが、関数名だけで判断すると、fpm_children.cのfpm_children_create_initialが子プロセスから呼び出されていそうな名前をしている。

# fpm.cのfpm_runから呼び出されている
$ grep -r "fpm_children_create_initial(" .
./fpm.c:                is_parent = fpm_children_create_initial(wp);
./fpm_children.c:int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */
./fpm_children.h:int fpm_children_create_initial(struct fpm_worker_pool_s *wp);

# fpm_main.cのmainから呼び出されている。
#  確認してみるとちゃんとfpm_initより後に呼ばれていた。
$ grep -r "fpm_run(" .
./fpm.c:int fpm_run(int *max_requests) /* {{{ */
./fpm.h:int fpm_run(int *max_requests);
./fpm_main.c:   fcgi_fd = fpm_run(&max_requests);

プロセスの無限ループがどこで突入するかまで追ってみる。
fpm_children_makeを抜けた後、fpm_children_create_initialに制御が戻り、
呼び出し元のfpm_runにまで制御が戻る。
その後fpm_event_loopが呼び出され、親プロセスは無限ループに突入する。
ワーカーとなる子プロセスはgotoでmain関数にまで制御が戻る。

int fpm_run(int *max_requests) /* {{{ */
{
        struct fpm_worker_pool_s *wp;

        /* create initial children in all pools */
        for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
                int is_parent;

                is_parent = fpm_children_create_initial(wp);

                if (!is_parent) {
                        goto run_child;
                }

                /* handle error */
                if (is_parent == 2) {
                        fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
                        fpm_event_loop(1);
                }
        }

        /* run event loop forever */
        fpm_event_loop(0);

run_child: /* only workers reach this point */

        fpm_cleanups_run(FPM_CLEANUP_CHILD);

        *max_requests = fpm_globals.max_requests;
        return fpm_globals.listening_socket;
}

このあたりいまいち分からなかったが、mainの制御に戻ったのち、fpm_run後whileでfastcgiのリクエストをacceptして処理しているように見える。

        fcgi_fd = fpm_run(&max_requests);
        parent = 0;

        /* onced forked tell zlog to also send messages through sapi_cgi_log_fastcgi() */
        zlog_set_external_logger(sapi_cgi_log_fastcgi);

        /* make php call us to get _ENV vars */
        php_php_import_environment_variables = php_import_environment_variables;
        php_import_environment_variables = cgi_php_import_environment_variables;

        /* library is already initialized, now init our request */
        request = fpm_init_request(fcgi_fd);

        zend_first_try {
                while (EXPECTED(fcgi_accept_request(request) >= 0)) {
  • 結論
    • これも2回呼んでなさそう
* main
  * fpm_init
    * fpm_unix_init_main
      * fork(親プロセスは子プロセス(master)の起動を待ってexit)
  * fpm_run
    * fpm_children_create_initial
      * fpm_children_make
  * fpm_event_loop(子プロセス(master)はここで無限ループに突入)
  * 孫プロセス(worker)はmainの最後のほうにfastcgiの処理のための無限ループに突入

残る疑問

  • nginxやphp-fpmのmasterプロセスは制御端末を後からつけられるのか?
    • コメントいただいて追記したけど、制御端末はあとからつけられない
  • コードを読んだだけでスタックトレース的なものを結論として書いたが、これは正しいか?
    • これもstraceで見たら?とコメントいただいたので、やってみる
    • コマンド叩いて、あれ?システムコールだけしか追えなくね?と思ったが、-kオプションというものがあった
strace(nginxだけ)

以下を実行

strace -D -k -ff /usr/local/nginx/sbin/nginx > strace.log 2>&1

一部抜粋

すぐexitする親プロセス

forkするところはmain => ngx_daemonで正しいそう

2121 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLDstrace: Process 911251 attached
2122 , child_tidptr=0x7f6d429a4f10) = 911251
2123  > /usr/lib64/libc.so.6(_Fork+0x27) [0x118a27]
2124  > /usr/lib64/libc.so.6(__fork+0x4c) [0x1185bc]
2125  > /usr/local/nginx/sbin/nginx(ngx_daemon+0xd) [0x431a81]
2126  > /usr/local/nginx/sbin/nginx(main+0x995) [0x40cfa9]
2127  > /usr/lib64/libc.so.6(__libc_start_call_main+0x7f) [0x3feaf]
2128  > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x7f) [0x3ff5f]
2129  > /usr/local/nginx/sbin/nginx(_start+0x24) [0x40b904]

masterから生み出されるworkerプロセス

ngx_master_process_cycle => ngx_start_worker_processes => ngx_spawn_processで、正しそう。

2310 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f6d429a4f10) = 91122
     55
2311  > /usr/lib64/libc.so.6(_Fork+0x27) [0x118a27]
2312  > /usr/lib64/libc.so.6(__fork+0x4c) [0x1185bc]
2313  > /usr/local/nginx/sbin/nginx(ngx_spawn_process+0x5d) [0x431364]
2314  > /usr/local/nginx/sbin/nginx(ngx_start_worker_processes+0x3e) [0x4322b3]
2315  > /usr/local/nginx/sbin/nginx(ngx_master_process_cycle+0x1b6) [0x4335bd]
2316  > /usr/local/nginx/sbin/nginx(main+0xa60) [0x40d074]
2317  > /usr/lib64/libc.so.6(__libc_start_call_main+0x7f) [0x3feaf]
2318  > /usr/lib64/libc.so.6(__libc_start_main@@GLIBC_2.34+0x7f) [0x3ff5f]
2319  > /usr/local/nginx/sbin/nginx(_start+0x24) [0x40b904]
2320 rt_sigsuspend([], 8strace: Process 911255 attached
2321  <unfinished ...>
  • ログインシェルがシグナルを送っているのか?
  • 実はバックグラウンド実行のことは全く触れてないが、この概念はどこから来ているのか?
    • これもコメントいただいたcredentials(7)にある
    • 自分の理解だと、setsidするとそもそも制御端末がないんだから、全部バックグラウンド実行になるという理解
    • なおフォアグラウンド、バックグラウンドの違いは判ったが、その状態にするための方法はよくわからなかった

フォアグラウンドジョブだけが端末からの読み込みを行える

  • systemdと昔(SysVinit)のプロセス管理の違い
    • コメントいただいた部分のイメージがつかなかった部分
    • おそらくsystemdはフォアグラウンド実行しても並列実行してくれるという理解
    • 昔はプロセスをfork,forkでやってた影響でデーモン化しないと次のプログラムを起動できなかった?

まとめ

  • 親プロセスが生きている状態で子プロセスがなくなるとゾンビとなる
  • ログインシェルが子プロセスに対し、PGIDかSIDかを見てプロセスを終了させる処理をしているっぽい
  • nginx/php-fpmはforkを2回したものがmasterプロセスになっていなさそう
  • ただし机上で追っているだけなので実験的に正しいかは未確認
  • デーモンは結構簡単に召喚できる

おわり

19
21
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
21