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

C言語系/「デーモン君のソース探検」読書メモ/A11(Intermission), exit(2) → wait4(2)

C言語系/「デーモン君のソース探検」読書メモ/A11(Intermission), exit(2) → wait4(2)

C言語系 / 「デーモン君のソース探検」読書メモ / A11(Intermission), exit(2) → wait4(2)
id: 851 所有者: msakamoto-sf    作成日: 2010-11-24 16:26:01
カテゴリ: BSD C言語 

お題:sys_wait4を中心に、だらだら、ぐだぐだと関連ソースを眺めてみる

・・・つもりが、気が付いたら"exit(2) → wait4(2)"の流れの舞台裏を調べていた。

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


Intermission : 一休み

今回は休憩回です。ちょっと振り返ってみます。

2010年の1-2月は、「デーモン君のソース探検」を読んでました。

2010-01 ~ 2010-02 : 01-06, A01-04

2010年の10月に "Understanding Linux/UNIX Programming" を読み終えました。

2010年の11月に "Advanced UNIX Programming" を読み終えました。

"Understanding Linux/UNIX Programming", "Advanced UNIX Programming" を読んでいて内部実装が気になって調べてみたのが、

A05, time(1) - A10, popen(3)

になります。

他に気になってる部分、もうちょっと深く調べたり、実験プログラムを組んでみたい部分もありますが、その殆どが "Advanced Programming in the UNIX Environment"(APUE) の方に載っていました。自分で1から調べる必要は無いでしょう。

つまり、ひとまず、「ひどく気になる部分」はこれにてお仕舞い、ということになります。

ということで繰り返しになりますが今回は休憩回です。たまたまA05でsys_write4のシステムコールを見てまして、exit(2)してから親プロセスにSIGCHLDが伝播し、wait4(2)を呼ぶまでの流れをだらだら、ぐだぐだ、べろんべろんと気楽に、ぶらぶらふらついてみることにします。

かなり当て推量とか知ったかぶりの半可通知識で読み飛ばしていくので、情報の正確性には期待しないでください。
じゃぁなんのために休憩回を用意したの?っていうと、もともとはwait()をもうちょっと深く調べてみる・・・予定だった、かもしれないんです。ただ、メモに「wait(), waitpid()」しか書き残してなかったので、wait()とかのどの辺が気になったのか、覚えてないんですよ。で、どうしようかな~とsys_write4のソースを眺めてるうちに、「あ、これってここでやってるんだ~」っていうのがぽろぽろ見つかってきちゃって。exit(2)とかwait4(2)周りのコードを読んでて「へ~」ボタンを押したくなるポイントが多かったので、メモしておきたかった次第です。

システムコールの内部、カーネルソースまで潜れるようになると、同じ風景でも見え方が違ってくるようです。
その楽しさの片鱗でもお伝えできればと思います。

「へ~」その1:セッションリーダーのプロセスが終了するとき、同じ制御端末のプロセス群にSIGHUP送信してる箇所

src/sys/kern/kern_exit.c をだらだら眺めてると、sys_exit()からexit1()というのをcallしていて、その中でセッションリーダーだった場合に、SIGHUPを送ってるところを見つけました。
あれってここで処理してたんだ・・・。

/*
 * exit --
 *      Death of process.
 */
int
sys_exit(struct proc *p, void *v, register_t *retval)
{
    struct sys_exit_args /* {
        syscallarg(int) rval;
    } */ *uap = v;

    exit1(p, W_EXITCODE(SCARG(uap, rval), 0));
    /* NOTREACHED */
    return (0);
}

/*
 * Exit: deallocate address space and other resources, change proc state
 * to zombie, and unlink proc from allproc and parent's lists.  Save exit
 * status and rusage for wait().  Check for child processes and orphan them.
 */
void
exit1(struct proc *p, int rv)
{
/* (省略) */
if (SESS_LEADER(p)) {
    struct session *sp = p->p_session;

    if (sp->s_ttyvp) {
        /*
         * Controlling process.
         * Signal foreground pgrp,
         * drain controlling terminal
         * and revoke access to controlling terminal.
         */
        if (sp->s_ttyp->t_session == sp) {
            if (sp->s_ttyp->t_pgrp)
                pgsignal(sp->s_ttyp->t_pgrp, SIGHUP, 1);
            (void) ttywait(sp->s_ttyp);
            /*
             * The tty could have been revoked
             * if we blocked.
             */
            if (sp->s_ttyvp)
                VOP_REVOKE(sp->s_ttyvp, REVOKEALL);
        }
        if (sp->s_ttyvp)
            vrele(sp->s_ttyvp);
        sp->s_ttyvp = NULL;
        /*
         * s_ttyp is not zero'd; we use this to indicate
         * that the session once had a controlling terminal.
         * (for logging and informational purposes)
         */
    }
    sp->s_leader = NULL;
}

「へ~」その2:シグナル関連のソースはkern_sig.c

上のexit1()を読んでいて、

pgsignal(sp->...);

というのが出てきました。
grepしてみると、シグナル関連のソースはkern_sig.cにまとめられているようです。

src/sys/kern/kern_sig.c:

...
/*
 * Send a signal to a process group. If checktty is 1,
 * limit to members which have a controlling terminal.
 */
void
pgsignal(struct pgrp *pgrp, int signum, int checkctty)
{
    struct proc *p;

    if (pgrp)
        for (p = pgrp->pg_members.lh_first; p != 0;
          p = p->p_pglist.le_next)
            if (checkctty == 0 || p->p_flag & P_CONTROLT)
                psignal(p, signum);
}
...
/*
 * Send the signal to the process.  If the signal has an action, the action
 * is usually performed by the target process rather than the caller; we add
 * the signal to the set of pending signals for the process.
 *
 * Exceptions:
 *   o When a stop signal is sent to a sleeping process that takes the
 *   default action, the process is stopped without awakening it.
 *   o SIGCONT restarts stopped processes (or puts them back to sleep)
 *   regardless of the signal action (eg, blocked or ignored).
 *
 * Other ignored signals are discarded immediately.
 *
 * XXXSMP: Invoked as psignal() or sched_psignal().
 */
void
psignal1(struct proc *p, int signum,
    int dolock)       /* XXXSMP: works, but icky */
{
...

psignal()とpsignal1()は sys/signalvar.hで見つかります。

$ grep psignal *
...
signalvar.h:void        psignal1 __P((struct proc *p, int sig, int dolock));
signalvar.h:#define     psignal(p, sig)         psignal1((p), (sig), 1)
...

「へ~」その3:NetBSD1.6では、wait/waitpid/wait3全てがwait4で実装されている

なので、waitを調べるにはwait4のシステムコールに辿り着いてしまうんですね。

$ locate wait
/usr/include/sys/wait.h
...
/usr/src/lib/libc/gen/wait.c
/usr/src/lib/libc/gen/wait3.c
/usr/src/lib/libc/gen/waitpid.c
...

$ wc -l /usr/src/lib/libc/gen/wait.c
      61 /usr/src/lib/libc/gen/wait.c
$ wc -l /usr/src/lib/libc/gen/wait3.c
      62 /usr/src/lib/libc/gen/wait3.c
$ wc -l /usr/src/lib/libc/gen/waitpid.c
      66 /usr/src/lib/libc/gen/waitpid.c

man 2 wait:

WAIT(2)                   NetBSD Programmer's Manual                   WAIT(2)

NAME
  wait, waitpid, wait4, wait3 - wait for process termination

LIBRARY
  Standard C Library (libc, -lc)

SYNOPSIS
  #include <sys/wait.h>

  pid_t wait(int *status);
  pid_t waitpid(pid_t wpid, int *status, int options);

  #include <sys/resource.h>
  pid_t wait3(int *status, int options, struct rusage *rusage);
  pid_t wait4(pid_t wpid, int *status, int options, struct rusage *rusage);

/usr/src/lib/libc/gen/wait.c:

pid_t
wait(istat)
        int *istat;
{
        return (wait4(WAIT_ANY, istat, 0, (struct rusage *)0));
}

/usr/src/lib/libc/gen/waitpid.c:

pid_t
#if __STDC__
waitpid(pid_t pid, int *istat, int options)
#else
waitpid(pid, istat, options)
        pid_t pid;
        int *istat;
        int options;
#endif
{
        return (wait4(pid, istat, options, (struct rusage *)0));
}

/usr/src/lib/libc/gen/wait3.c:

pid_t
wait3(istat, options, rup)
        int *istat;
        int options;
        struct rusage *rup;
{
        return (wait4(WAIT_ANY, istat, options, rup));
}

全部wait4()に委譲されてます。

「へ~」その4:終了プロセスをゾンビにしてる箇所発見(kern_exit.c)

kern_exit.cをだらだら眺めてると、カーネルスレッドから呼ばれる"reaper"という関数が見つかりました。
"deadproc"というリストをfor()で走査してるようです。見ていくと、"SZOMB"をセットしている箇所が見つかりました。src/sys/kern/中のソースコードでも、SZOMBを代入しているのはここだけのようです。
コメントからも、どうやらここが、終了後まだ親プロセスでwait()されていないプロセスに"ゾンビマーク"をつけている箇所と推測できます。

/*
 * Process reaper.  This is run by a kernel thread to free the resources
 * of a dead process.  Once the resources are free, the process becomes
 * a zombie, and the parent is allowed to read the undead's status.
 */
void
reaper(void *arg)
{
    struct proc *p;

    KERNEL_PROC_UNLOCK(curproc);

    for (;;) {
        simple_lock(&deadproc_slock);
        p = LIST_FIRST(&deadproc);

/* (省略) */

        /* Process is now a true zombie. */
        p->p_stat = SZOMB;

        /* Wake up the parent so it can get exit status. */
        if ((p->p_flag & P_FSTRACE) == 0 && p->p_exitsig != 0)
            psignal(p->p_pptr, P_EXITSIG(p));
        KERNEL_PROC_UNLOCK(curproc);
        wakeup((caddr_t)p->p_pptr);
    }
}

「へ~」その5:親プロセスが終了するとき、子プロセスの親を"init"に変更している箇所発見

これもkern_exit.cのexit1()中で見つけたコードです。コメントもそのものズバリです。

/*
 * Give orphaned children to init(8).
 */
q = p->p_children.lh_first;
if (q)      /* only need this if any child is S_ZOMB */
    wakeup((caddr_t)initproc);
for (; q != 0; q = nq) {
    nq = q->p_sibling.le_next;

    /*
     * Traced processes are killed since their existence
     * means someone is screwing up. Since we reset the
     * trace flags, the logic in sys_wait4() would not be
     * triggered to reparent the process to its
     * original parent, so we must do this here.
     */
    if (q->p_flag & P_TRACED) {
        if (q->p_opptr != q->p_pptr) {
            struct proc *t = q->p_opptr;
            proc_reparent(q, t ? t : initproc);
            q->p_opptr = NULL;
        } else
            proc_reparent(q, initproc);
        q->p_flag &= ~(P_TRACED|P_WAITED|P_FSTRACE);
        psignal(q, SIGKILL);
    } else {
        proc_reparent(q, initproc);
    }
}

ここで処理してたんですね・・・。

proc_reparent()も、kern_exit.cの最後に定義されています。

/*
 * make process 'parent' the new parent of process 'child'.
 */
void
proc_reparent(struct proc *child, struct proc *parent)
{
    if (child->p_pptr == parent)
        return;

    if (parent == initproc)
        child->p_exitsig = SIGCHLD;

    LIST_REMOVE(child, p_sibling);
    LIST_INSERT_HEAD(&parent->p_children, child, p_sibling);
    child->p_pptr = parent;
}

「へ~」その6:親プロセスが"SA_NOCLDWAIT"をsigaction()に使っていたら、initに親を変更する箇所発見

kern_exit.cの終わりの方で見つかりました。プロセスが終了するとき、親プロセスにP_NOCLDWAITフラグが立っていれば、親プロセスをinitに変更しています。これにより、ゾンビプロセスの回収などはinit側が受け持つことになります。

/*
 * Notify parent that we're gone.  If parent has the P_NOCLDWAIT
 * flag set, notify init instead (and hope it will handle
 * this situation).
 */
if (p->p_pptr->p_flag & P_NOCLDWAIT) {
    struct proc *pp = p->p_pptr;
    proc_reparent(p, initproc);
    /*
     * If this was the last child of our parent, notify
     * parent, so in case he was wait(2)ing, he will
     * continue.
     */
    if (pp->p_children.lh_first == NULL)
            wakeup((caddr_t)pp);
}

ここで処理してたんですね・・・。

「へ~」その7:wait4()中でゾンビプロセスを回収している箇所を発見

sys_wait4()中で見つけました。

int
sys_wait4(struct proc *q, void *v, register_t *retval)
{

/* (省略) */

 loop:
    nfound = 0;
    for (p = q->p_children.lh_first; p != 0; p = p->p_sibling.le_next) {

/* (省略) */

        if (p->p_stat == SZOMB) {

/* (省略) */

            /*
             * Finally finished with old proc entry.
             * Unlink it from its process group and free it.
             */
            leavepgrp(p);

            s = proclist_lock_write();
            LIST_REMOVE(p, p_list); /* off zombproc */
            proclist_unlock_write(s);

            LIST_REMOVE(p, p_sibling);

まとめ:exit() → sys_wait4()での回収まで

あっちこっちつまみ食いしてきましたが、そろそろ絵が描けそうなので、exit()からの流れをまとめてみます。簡単にするため、親プロセスはNOCLDWAITを使っておらず、礼儀正しくSIGCHLDでwait4()を呼ぶパターンで見ていきます。

1.exit()システムコール → exit1()

int sys_exit(struct proc *p, void *v, register_t *retval) {
    ...
    exit1(p, W_EXITCODE(SCARG(uap, rval), 0));

2.exit1() → SDEAD, 終了コードの設定, zombprocへ追加, cpu_exit()

void exit1(struct proc *p, int rv) {
    ...
    p->p_stat = SDEAD;
    ...
    LIST_REMOVE(p, p_hash);
    LIST_REMOVE(p, p_list);
    LIST_INSERT_HEAD(&zombproc, p, p_list);
    ...
    p->p_xstat = rv;
    ...
    /*
     * Finally, call machine-dependent code to switch to a new
     * context (possibly the idle context).  Once we are no longer
     * using the dead process's vmspace and stack, exit2() will be
     * called to schedule those resources to be released by the
     * reaper thread.
     *
     * Note that cpu_exit() will end with a call equivalent to
     * cpu_switch(), finishing our execution (pun intended).
     */
    cpu_exit(p);
}

"「へ~」その4:終了プロセスをゾンビにしてる箇所発見(kern_exit.c)"で reaper() がゾンビにしている、と書きましたが、実際にreaper()が呼ばれるまでにはタイムラグが存在するようです。

3.cpu_exit() → (scheduling) → exit2() → (kernel thread) → reaper()

reaper()では"deadproc"のリストを手繰っていますが、exit1()の中では"zombproc"に追加しているだけです。
実際に"deadproc"に追加しているのは、cpu_exit()から呼ばれるexit2()です。

kern_exit.c:

/*
 * We are called from cpu_exit() once it is safe to schedule the
 * dead process's resources to be freed (i.e., once we've switched to
 * the idle PCB for the current CPU).
 *
 * NOTE: One must be careful with locking in this routine.  It's
 * called from a critical section in machine-dependent code, so
 * we should refrain from changing any interrupt state.
 *
 * We lock the deadproc list (a spin lock), place the proc on that
 * list (using the p_hash member), and wake up the reaper.
 */
void
exit2(struct proc *p)
{

    simple_lock(&deadproc_slock);
    LIST_INSERT_HEAD(&deadproc, p, p_hash);
    simple_unlock(&deadproc_slock);

    wakeup(&deadproc);
}

これで、終了プロセスが"deadproc"に追加されます。

その後さらにカーネルスレッドからreaper()が呼ばれ、deadprocから外され、ゾンビマークが付けられ、親プロセスにシグナルが送信されます。

/*
 * Process reaper.  This is run by a kernel thread to free the resources
 * of a dead process.  Once the resources are free, the process becomes
 * a zombie, and the parent is allowed to read the undead's status.
 */
void
reaper(void *arg)
{
    struct proc *p;

    KERNEL_PROC_UNLOCK(curproc);

    for (;;) {
        simple_lock(&deadproc_slock);
        p = LIST_FIRST(&deadproc);

        /* ... */

        /* Remove us from the deadproc list. */
        LIST_REMOVE(p, p_hash);
        simple_unlock(&deadproc_slock);
        KERNEL_PROC_LOCK(curproc);

        /* ... */

        /* Process is now a true zombie. */
        p->p_stat = SZOMB;

        /* Wake up the parent so it can get exit status. */
        if ((p->p_flag & P_FSTRACE) == 0 && p->p_exitsig != 0)
            psignal(p->p_pptr, P_EXITSIG(p));

        /* ... */
    }
}

結論から言うと、これが親プロセスへSIGCHLDを送信している箇所です。

psignal(p->p_pptr, P_EXITSIG(p));
親プロセスへのSIGCHLD送信箇所を特定
P_EXITSIG(p)

このマクロ定義は sys/proc.h にあります。
sys/proc.h:

/*
 * Macro to compute the exit signal to be delivered.
 */
#define P_EXITSIG(p)    (((p)->p_flag & (P_TRACED|P_FSTRACE)) ? SIGCHLD : \
                         p->p_exitsig)

"TRACE"はデバッグ絡みですのでこの際無視します。すると、

P_EXITSIG(p)
=
p->p_exitsig

ということになります。

名前からして、いかにも「終了時のシグナル」という感じですが、これはどこで設定されているのか?適当にsrc/sys/kern/内をgrepしてみます。
すると、kern_fork.c内でそれらしき行がヒットしました。

int
fork1(struct proc *p1, int flags, int exitsig, void *stack, size_t stacksize,
    void (*func)(void *), void *arg, register_t *retval,
    struct proc **rnewprocp)
{
    struct proc     *p2, *tp;


    /* Record the signal to be delivered to the parent on exit. */
    p2->p_exitsig = exitsig;

コメントの通りで、fork1()の三番目の引数exitsigを、プロセス終了時に親プロセスに送られるシグナル番号としてp_exitsigに設定しています。

あとは src/sys/kern/ 内を"fork1"でgrepすれば、明らかにexitsigがSIGCHLDであることが分かります。

$ grep fork1 *
init_main.c:    if (fork1(p, 0, SIGCHLD, NULL, 0, start_init, NULL, NULL, &initproc))
init_sysent.c:      sys___vfork14 },                    /* 282 = __vfork14 */
kern_fork.c:    return (fork1(p, 0, SIGCHLD, NULL, 0, NULL, NULL, retval, NULL));
kern_fork.c:    return (fork1(p, FORK_PPWAIT, SIGCHLD, NULL, 0, NULL, NULL,
kern_fork.c:sys___vfork14(struct proc *p, void *v, register_t *retval)
kern_fork.c:    return (fork1(p, FORK_PPWAIT|FORK_SHAREVM, SIGCHLD, NULL, 0,
kern_fork.c:    return (fork1(p, flags, sig, SCARG(uap, stack), 0,
kern_fork.c:fork1(struct proc *p1, int flags, int exitsig, void *stack, size_t stacksize,
kern_kthread.c: error = fork1(&proc0, FORK_SHAREVM | FORK_SHARECWD | FORK_SHAREFILES |
syscalls.c:     "__vfork14",                    /* 282 = __vfork14 */
syscalls.master:282     STD             { int sys___vfork14(void); }

kern_exit.c のreaper()に戻すと、親プロセスにシグナルを送信している

psignal(p->p_pptr, P_EXITSIG(p));

ですが、これまでの調査により一般的には

psignal(p->p_pptr, SIGCHLD);

と同じことが分かりました。

ようやく、exit(2) → 親プロセスへのSIGCHLD送信の道筋が掴めました!!

4.親プロセスのSIGCHLD受信 → シグナルハンドラ

この辺はkern_sig.cの話になりますが、さすがに追いきれませんでした。
とりあえずシグナルハンドラ → wait4()が呼ばれ、sys_wait4()に入ってきたものとして進めます。

5.シグナルハンドラ → sys_wait4() → ゾンビ回収

"「へ~」その7:wait4()中でゾンビプロセスを回収している箇所を発見"に書いてある通りです。

お疲れ様でした。

これでようやく、exit(2) → wait4(2) までの流れを、舞台裏から見ることが出来ました。
シグナル送受信の箇所については、タスク切り替えなども絡みそうで追いきれませんでしたが、それでも、「あ~、manページ載ってたあの動作って、ここで処理してたのか~」という驚きは感じられました。・・・よね?

今回はIntermissionということで、ぐだぐだ、だらだらとソースを眺めてみました。
個人的には、ソースを調べてでも中身を確認しておきたい機能はひとまず、消化完了です。ですのでこのシリーズは暫くお休みになります。
「どうしてもソースを調べて中身を確認しないと気になって夜も眠れない」ようなトピックが出てきたら、またその時再開します。

では・・・

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



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-11-24 16:31:09
md5:5ddd5d61b1cfe46941cf55d8d312ecb0
sha1:83ddc6732a627a97ba7463cee7fed4a9b447d1aa
コメント
コメントを投稿するにはログインして下さい。