tkokamoの日記

HPCの研究開発なのでそんなことをかきたい

シグナルのブロック/無視の覚書き

少し調べていたのでメモ。基本的にはカーネル内の話。コードはv4.9.6

シグナルに関しては言葉がややこしいので、以下のようにこの記事では使う。

  • シグナルの登録 シグナルが生成され対象のタスクのpendingリストに繋がった状態。

  • シグナルの受信 シグナルが登録されたことがタスクに通知された状態。(TIF_SIGPENDINGがたっている状態)

  • シグナルの配信 受信したシグナルに応じた処理を行う。

Linuxのシグナルには「ブロックする」と「無視する」の二つがある。 違いは以下の通り

  • ブロック

シグナルは対象のタスクに登録されるが、受信されない。 struct task_structblockedreal_blockedで指定されたシグナルがブロックされる。

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
        /*   
         * For reasons of header soup (see current_thread_info()), this
         * must be the first element of task_struct.
         */
...
        sigset_t blocked, real_blocked;
...
  • 無視

そもそもシグナルが登録されない。

シグナルを送る__send_signal()では以下のようにブロックと無視が実現されている。

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
                        int group, int from_ancestor_ns)
{
        struct sigpending *pending;
...
        result = TRACE_SIGNAL_IGNORED;
        if (!prepare_signal(sig, t, ★
                        from_ancestor_ns || (info == SEND_SIG_FORCED)))
                goto ret; 

        pending = group ? &t->signal->shared_pending : &t->pending;
...
        q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
                override_rlimit);
        if (q) {
                // ここでリストに登録
                list_add_tail(&q->list, &pending->list);
                switch ((unsigned long) info) {
...
out_set:
        signalfd_notify(t, sig);
        sigaddset(&pending->signal, sig);
        complete_signal(sig, t, group); ★
ret:
        trace_signal_generate(sig, info, t, group, result);
        return ret;
}

そもそもシグナルを登録するかどうかがprepare_signal()で判断される。 ブロックするものは登録される。無視されるものは登録すらされない。

static bool prepare_signal(int sig, struct task_struct *p, bool force)
{
        struct signal_struct *signal = p->signal;
        struct task_struct *t;
        sigset_t flush;
...
        return !sig_ignored(p, sig, force); ★
}


static int sig_ignored(struct task_struct *t, int sig, bool force)
{
        /*
         * Blocked signals are never ignored, since the
         * signal handler may change by the time it is
         * unblocked.
         */
// コメントに書かれている通り、blockedに登録されていてもシグナルは無視されない
// ブロック状態のシグナルでもprepare_signal()が真になり、登録される。
        if (sigismember(&t->blocked, sig) || sigismember(&t->real_blocked, sig))
                return 0;

        if (!sig_task_ignored(t, sig, force)) ★
                return 0;

        /*
         * Tracers may want to know about even ignored signals.
         */
// ptraceされていない場合はシグナルは無視される。
        return !t->ptrace;
}


static int sig_task_ignored(struct task_struct *t, int sig, bool force)
{
        void __user *handler;

        handler = sig_handler(t, sig);

        if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) &&
                        handler == SIG_DFL && !force)
                return 1;

        return sig_handler_ignored(handler, sig); ★
}

static int sig_handler_ignored(void __user *handler, int sig)
{
        /* Is it explicitly or implicitly ignored? */
// handlerがSIG_IGNになっている場合は無視
// handlerがSIG_DFLの場合は、sig_kernel_ignoreが真なら無視(↓参照)
        return handler == SIG_IGN ||
                (handler == SIG_DFL && sig_kernel_ignore(sig));
}

#define sig_kernel_ignore(sig)          siginmask(sig, SIG_KERNEL_IGNORE_MASK)

#define SIG_KERNEL_IGNORE_MASK (\
        rt_sigmask(SIGCONT)   |  rt_sigmask(SIGCHLD)   | \
        rt_sigmask(SIGWINCH)  |  rt_sigmask(SIGURG)    )

parepare_signal()が真の場合、シグナルが生成され登録される。 最後にタスクを起床し、受信させるかどうかの判断はcomplete_signal()で行われる。

static void complete_signal(int sig, struct task_struct *p, int group)
{
        struct signal_struct *signal = p->signal;
        struct task_struct *t;

        /*
         * Now find a thread we can wake up to take the signal off the queue.
         *
         * If the main thread wants the signal, it gets first crack.
         * Probably the least surprising to the average bear.
         */
        if (wants_signal(sig, p))  ★
                t = p;  
        else if (!group || thread_group_empty(p))
                /*
                 * There is just one thread and it does not need to be woken.
                 * It will dequeue unblocked signals before it runs again.
                 */
// wants_signal()が成立せず、プロセスグループ内にスレッドが1個なら何もせず復帰する。
                return;
...
        /*
         * The signal is already in the shared-pending queue.
         * Tell the chosen thread to wake up and dequeue it.
         */
        signal_wake_up(t, sig == SIGKILL); // ここで起床する。
        return;
}

static inline int wants_signal(int sig, struct task_struct *p)
{
// blockedになっていたら0が返る。
        if (sigismember(&p->blocked, sig)) 
                return 0; 
        if (p->flags & PF_EXITING)
                return 0;
        if (sig == SIGKILL)
                return 1;
        if (task_is_stopped_or_traced(p))
                return 0;
        return task_curr(p) || !signal_pending(p);
}

無視されず、ブロックされないシグナルの場合、対象のタスクはsignal_wake_up()で起床させられる。

カーネルスレッドでは親のkthreaddが全シグナルのハンドラをSIG_IGNにしているため、 通常は全シグナルが無視される。

int kthreadd(void *unused)
{
        struct task_struct *tsk = current;

        /* Setup a clean context for our children to inherit. */
        set_task_comm(tsk, "kthreadd");
        ignore_signals(tsk); ★
...

void ignore_signals(struct task_struct *t)
{
        int i; 

        for (i = 0; i < _NSIG; ++i)
                t->sighand->action[i].sa.sa_handler = SIG_IGN;

        flush_signals(t);
}

sigfillset()などの関数を使うと、blockedを一括してセットすることが可能。

static inline void sigfillset(sigset_t *set)
{
        switch (_NSIG_WORDS) {
        default:
                memset(set, -1, sizeof(sigset_t));
                break;
        case 2: set->sig[1] = -1;
        case 1: set->sig[0] = -1;
                break;
        }
}