親が無い場合のゾンビの扱い
ゾンビ状態とは、親プロセスが子プロセスがどれくらいCPUを使用したとか等の統計情報を取得するために、子プロセスが親プロセスに終了シグナルを送信する間の状態でした。そうなると、子プロセスより親プロセスがすでに終了していた場合、あるいは子プロセスがゾンビ状態で親がwait関数でその処理をしないまま終了してしまった場合、子プロセスは永久にゾンビ状態ということになるわけですが。下記サンプルは上記の2つのことを確認するものです。
子プロセスがゾンビ状態にもかかわらず親がその処理をしないで終了してしまった場合。
補足
fork()で子プロセスを作成すると、親より先に実行されます。通常子プロセスはexecでメモリー空間を破棄し、新規に割り当てることになります。fork()では実メモリーは割り当てません。COWで書き込み処理発生毎に割り当てるものです。従って先に親プロセスが実行し、書き込み処理が発生毎に、親プロセス用にCOWで新規にメモリーを割り当てることになると、子プロセスがexec関数で全てのメモリーを破棄し新規に割り当てる可能性が高いことを考えると、メモリ割り当ての2度手間です。従って親より子プロセスが先に動作するようにスケジュールされるようです。
プロセス終了時、do_exit関数から親プロセスに知らせるためにexit_notify関数がコールされます。そこからforget_original_parent関数で、終了しようとするプロセスを親とする子プロセスの親を繋ぎかえる事で、上記の処理を実現しています。
forget_original_parent関数の引数struct task_struct *fatherは、終了プロセスです。まずfind_new_reaper関数で終了プロセスに代わる親を取得します。次にlist_for_each_entry_safe(p, n, &father->children, sibling) で、終了プロセスを親に持つ全子プロセスに対して、親を差し替えています。p->real_parentは本当の親で、p->parentは終了シグナルを送信する親です。通常この2つは同じプロセスですが、デバッガで動作されている場合、p->parentはデバッガプロセスであったりします。p->real_parentは無条件に新規の親に差し替えていますが、p->parentはif (p->parent == father)で、そのプロセスが終了プロセスの場合のみ、新規の親に差し替えています。
そしてreparent_thread関数で、親子リストのp->siblingをp->real_parent->childrenに繋ぎかえています。p->real_parentは新規の親です。これで親が代わりました。なおreparent_thread関数では、他にこのプロセスがスレッドで、しかも既にゾンビ状態だったらこの関数から新規の親に終了シグナルを送信しています。
CLONE_THREADによるプロセスがない場合、しかも削除プロセスがネームスペースの元親の場合、pid_ns->child_reaper = init_pid_ns.child_reaperで、init_pid_nsのネームスペースの元親すなわちinitにしています。その条件として、if (unlikely(pid_ns == &init_pid_ns))で、削除プロセスのネームスペースがinit_pid_nsでないことです。これだと終了プロセスがinitになってしまいます。
もし、削除プロセスがネームスペースの元親でない場合、この元親を新規の親としています。CLONE_NEWPIDで作成していないプロセスは、initのネームスペースの元親、すなわちinitということになります。
従って、この親が最終的に親がいなくなったプロセスの親となって、それらの終了処理を行います。削除されないinitが最終的には親になってくれるわけで、どのような状態削除過程であれ、ゾンビプロセスの処理が担保されるということです。
子プロセスがゾンビ状態にもかかわらず親がその処理をしないで終了してしまった場合。
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { pid_t pid; pid = fork(); if (pid == 0 ) {/* 子プロセス */ exit(0); } /* 親プロセス */ system("ps"); return 0; }子プロセスが終了した後の親プロセスのpsコマンドでは、子プロセスががゾンビになっています。
[root@localhost kitamura]# ./a.out PID TTY TIME CMD 12494 pts/4 00:00:00 bash 13371 pts/4 00:00:00 a.out 13372 pts/4 00:00:00 a.out <defunct>改めてpsしてみるとゾンビが無くなっています。
[root@localhost kitamura]# ps PID TTY TIME CMD 12494 pts/4 00:00:00 bash 13374 pts/4 00:00:00 ps子プロセスの終了前に親プロセスが終了してしまった場合。
#include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { pid_t pid; pid = fork(); if (pid == 0 ) {/* 子プロセス */ sleep(1); system("ps"); exit(0); } /* 親プロセス */ return 0; }親プロセスが終了し、子プロセスのみ動作しています。
[root@localhost kitamura]# ./a.out [root@localhost kitamura]# PID TTY TIME CMD 12494 pts/4 00:00:00 bash 14087 pts/4 00:00:00 a.out 14088 pts/4 00:00:00 ps子プロセスはゾンビになっていません。
[root@localhost kitamura]# ps PID TTY TIME CMD 12494 pts/4 00:00:00 bash 14089 pts/4 00:00:00 psどちらにしても子プロセスは最終的にはゾンビになっていないようです。
補足
fork()で子プロセスを作成すると、親より先に実行されます。通常子プロセスはexecでメモリー空間を破棄し、新規に割り当てることになります。fork()では実メモリーは割り当てません。COWで書き込み処理発生毎に割り当てるものです。従って先に親プロセスが実行し、書き込み処理が発生毎に、親プロセス用にCOWで新規にメモリーを割り当てることになると、子プロセスがexec関数で全てのメモリーを破棄し新規に割り当てる可能性が高いことを考えると、メモリ割り当ての2度手間です。従って親より子プロセスが先に動作するようにスケジュールされるようです。
プロセス終了時、do_exit関数から親プロセスに知らせるためにexit_notify関数がコールされます。そこからforget_original_parent関数で、終了しようとするプロセスを親とする子プロセスの親を繋ぎかえる事で、上記の処理を実現しています。
forget_original_parent関数の引数struct task_struct *fatherは、終了プロセスです。まずfind_new_reaper関数で終了プロセスに代わる親を取得します。次にlist_for_each_entry_safe(p, n, &father->children, sibling) で、終了プロセスを親に持つ全子プロセスに対して、親を差し替えています。p->real_parentは本当の親で、p->parentは終了シグナルを送信する親です。通常この2つは同じプロセスですが、デバッガで動作されている場合、p->parentはデバッガプロセスであったりします。p->real_parentは無条件に新規の親に差し替えていますが、p->parentはif (p->parent == father)で、そのプロセスが終了プロセスの場合のみ、新規の親に差し替えています。
そしてreparent_thread関数で、親子リストのp->siblingをp->real_parent->childrenに繋ぎかえています。p->real_parentは新規の親です。これで親が代わりました。なおreparent_thread関数では、他にこのプロセスがスレッドで、しかも既にゾンビ状態だったらこの関数から新規の親に終了シグナルを送信しています。
static void forget_original_parent(struct task_struct *father) { struct task_struct *p, *n, *reaper; LIST_HEAD(ptrace_dead); write_lock_irq(&tasklist_lock); reaper = find_new_reaper(father); ptrace_exit(father, &ptrace_dead); list_for_each_entry_safe(p, n, &father->children, sibling) { p->real_parent = reaper; if (p->parent == father) { BUG_ON(p->ptrace); p->parent = p->real_parent; } reparent_thread(p, father); } write_unlock_irq(&tasklist_lock); BUG_ON(!list_empty(&father->children)); ptrace_exit_finish(father, &ptrace_dead); }find_new_reaper関数で新しい親を取得します。まず終了プロセスにスレッド(CLONE_THREADで作成されたプロセス)があるか調べます。あればしかもそのプロセスがゾンビ状態でないなら、それを親といたします。なお、終了プロセスがプロセスIDのネームスペースの元親であるなら、新規の親を、このネームスペースの元親といたします。これはプロセス作成時CLONE_NEWPIDで作成したプロセスが設定されていると思います。
CLONE_THREADによるプロセスがない場合、しかも削除プロセスがネームスペースの元親の場合、pid_ns->child_reaper = init_pid_ns.child_reaperで、init_pid_nsのネームスペースの元親すなわちinitにしています。その条件として、if (unlikely(pid_ns == &init_pid_ns))で、削除プロセスのネームスペースがinit_pid_nsでないことです。これだと終了プロセスがinitになってしまいます。
もし、削除プロセスがネームスペースの元親でない場合、この元親を新規の親としています。CLONE_NEWPIDで作成していないプロセスは、initのネームスペースの元親、すなわちinitということになります。
従って、この親が最終的に親がいなくなったプロセスの親となって、それらの終了処理を行います。削除されないinitが最終的には親になってくれるわけで、どのような状態削除過程であれ、ゾンビプロセスの処理が担保されるということです。
static struct task_struct *find_new_reaper(struct task_struct *father) { struct pid_namespace *pid_ns = task_active_pid_ns(father); struct task_struct *thread; thread = father; while_each_thread(father, thread) { if (thread->flags & PF_EXITING) continue; if (unlikely(pid_ns->child_reaper == father)) pid_ns->child_reaper = thread; return thread; } if (unlikely(pid_ns->child_reaper == father)) { write_unlock_irq(&tasklist_lock); if (unlikely(pid_ns == &init_pid_ns)) panic("Attempted to kill init!"); zap_pid_ns_processes(pid_ns); write_lock_irq(&tasklist_lock); pid_ns->child_reaper = init_pid_ns.child_reaper; } return pid_ns->child_reaper; }