NetBSD 5.0 における pthread と fork

概要

NetBSD 5.0 において、複数の pthread が生きている状態で fork するとおかしくなる。

再現

再現プログラムではfork前に一度3つのpthreadを作り、その後1つ殺して lwpid 1 と 3 が残っている。この状態で3からforkすると子では3が残り、これにlwpid 1が割り当てられるのだが、どこかに3の情報が残っていて、その後呼びに行ってしまう

22612 2 a.out CALL _lwp_park(0xbb7ffd94,3,0x8049198,0x8049198)
22612 2 a.out RET _lwp_park -1 errno 3 No such process

詳細

NetBSD 5.0 ではカーネルスレッドに当たる lwp (light weight process) とユーザレベルを司る pthread が 1:1 対応している。プロセス内に存在する lwp の情報は、以下のあたりに保存されている。

// /usr/include/sys/lwp.h
struct lwp {
        ...
        /* Process level and global state, misc. */
        LIST_ENTRY(lwp) l_list;         /* a: entry on list of all LWPs */
        void            *l_ctxlink;     /* p: uc_link {get,set}context */
        struct proc     *l_proc;        /* p: parent process */
        LIST_ENTRY(lwp) l_sibling;      /* p: entry on proc's list of LWPs */
        ...
};
// /usr/include/sys/lwp.h
struct proc {
        ...
        pid_t           p_pid;          /* :: Process identifier. */
        LIST_ENTRY(proc) p_pglist;      /* l: List of processes in pgrp. */
        struct proc     *p_pptr;        /* l: Pointer to parent process. */
        LIST_ENTRY(proc) p_sibling;     /* l: List of sibling processes. */
        LIST_HEAD(, proc) p_children;   /* l: List of children. */
        LIST_HEAD(, lwp) p_lwps;        /* p: List of LWPs. */
        struct ras      *p_raslist;     /* a: List of RAS entries */

/* The following fields are all zeroed upon creation in fork. */
#define p_startzero     p_nlwps

        int             p_nlwps;        /* p: Number of LWPs */
        int             p_nzlwps;       /* p: Number of zombie LWPs */
        int             p_nrlwps;       /* p: Number running/sleeping LWPs */
        int             p_nlwpwait;     /* p: Number of LWPs in lwp_wait1() */
        int             p_ndlwps;       /* p: Number of detached LWPs */
        int             p_nlwpid;       /* p: Next LWP ID */
        ...
};

fork は実際には /sys/kern/kern_fork.c の fork1 で行われていて、ここでプロセスの複製と initial thread の作成を行っている模様。initial thread は fork を実行した lwp をテンプレートにして作られる。

        /*
         * Finish creating the child process.
         * It will return through a different path later.
         */
        lwp_create(l1, p2, uaddr, inmem, (flags & FORK_PPWAIT) ? LWP_VFORK : 0,
            stack, stacksize, (func != NULL) ? func : child_return, arg, &l2,
            l1->l_class);

        /*
         * It's now safe for the scheduler and other processes to see the
         * child process.
         */
        mutex_enter(proc_lock);

        if (p1->p_session->s_ttyvp != NULL && p1->p_lflag & PL_CONTROLT)
                p2->p_lflag |= PL_CONTROLT;

        LIST_INSERT_HEAD(&parent->p_children, p2, p_sibling);
        p2->p_exitsig = exitsig;                /* signal for parent on exit */

        LIST_INSERT_AFTER(p1, p2, p_pglist);
        LIST_INSERT_HEAD(&allproc, p2, p_list);

この lwp_create は /usr/src/sys/kern/kern_lwp.c にある。しかし、引用部の後半にある、プロセスの lwp リストに新しい lwp を追加するコードは、別のプロセスの lwp をテンプレートにした場合を考慮しているように見えない。

/*
 * Create a new LWP within process 'p2', using LWP 'l1' as a template.
 * The new LWP is created in state LSIDL and must be set running,
 * suspended, or stopped by the caller.
 */
int
lwp_create(lwp_t *l1, proc_t *p2, vaddr_t uaddr, bool inmem, int flags,
           void *stack, size_t stacksize, void (*func)(void *), void *arg,
           lwp_t **rnewlwpp, int sclass)
{
        ...

        if (isfree == NULL) {
                l2 = pool_cache_get(lwp_cache, PR_WAITOK);
                memset(l2, 0, sizeof(*l2));
                l2->l_ts = pool_cache_get(turnstile_cache, PR_WAITOK);
                SLIST_INIT(&l2->l_pi_lenders);
        } else {
                l2 = isfree;
                ts = l2->l_ts;
                KASSERT(l2->l_inheritedprio == -1);
                KASSERT(SLIST_EMPTY(&l2->l_pi_lenders));
                memset(l2, 0, sizeof(*l2));
                l2->l_ts = ts;
        }

        ...

        p2->p_nlwpid++;
        if (p2->p_nlwpid == 0)
                p2->p_nlwpid++;
        l2->l_lid = p2->p_nlwpid;
        LIST_INSERT_HEAD(&p2->p_lwps, l2, l_sibling);
        p2->p_nlwps++;
}

つまり推測するに、LIST_INSERT_HEAD(&p2->p_lwps, l2, l_sibling) で、l2 の next が 3 を指したまま insert してしまっているのではあるまいか。その結果、あとで pthread_cond_wait した時に存在しない lwpid 3 を見に行って困る、と。

ちなみに、回避法はあって、pthread_createを十分な数作って捨てることで、欠番になっている lwpid を使った上で消せばよい(ぉ

workaround

Bug #2724 に書いたパッチを Ruby 1.9 に当てることで回避できます。

send-pr

http://www.netbsd.org/cgi-bin/query-pr-single.pl?number=42772 How to Repeat と Fix を使いそこねた

謝辞

この問題の解決には @_enami さんの助けがありました。