天天看點

Linux signal 那些事兒 (3)【轉】

這篇部落格,想集中在signal 與線程的關系上,順帶介紹核心signal相關的結構。如何組織我其實并沒想好,想到哪就寫到哪裡吧。主題一定會落在signal之内而不跑題。

    提到signal與thread的關系,就得先提POSIX标準。POSIX标準決定了Linux為何将signal如此實作:

   1 信号處理函數必須在多線程應用的所有線程之間共享,但是,每個線程要有自己的挂起信号掩碼和阻塞信号掩碼。

   2 POSIX 函數kill/sigqueue必須面向所有的多線程應用而不是某個特殊的線程。

   3 每個發給多線程應用的信号僅傳送給1個線程,這個線程是由核心從不會阻塞該信号的線程中随意選出。

   4 如果發送一個緻命信号到多線程,那麼核心将殺死該應用的所有線程,而不僅僅是接收信号的那個線程。

    上面是POSIX标準,也就是提出來的要求,Linux要遵循POSIX标準,那Linux是怎麼做到的呢?

    到了此處,我們需要理清一些基本的概念:

struct task_struct {

    pid_t pid;

    pid_t tgid

      .....

    struct task_struct *group_leader;    /* threadgroup leader */

      ......

    struct list_head thread_group;

        ....

}

    從字面意思上看 pid,是process id,其則不然,pid是thread id。從字面意思上看,tgid是thread group id,其則是真正的pid。

    有點繞是不是?對于一個多線程的程式,無論是哪個線程執行getpid,結果都是一樣的,最終傳回的同一個值 tgid。如果我們實作了gettid(很不幸的是glibc沒有這個函數,是以我們要用syscall),我們就會發現,各個線程傳回的值不同,此時,傳回的值是核心task_struct中的pid。對于多線程應用/proc/pid/task可以看到的,就是線程的thread id,也就是task_struct中的pid。

Linux signal 那些事兒 (3)【轉】

    group leader字段,指向線程組的第一個線程。對于我們自己的程式而言,main函數所在的線程,也就是線程組的第一個線程,是以group leader就會他自己。一旦用pthread_create建立了線程,那麼main所在的線程,還有建立出來的線程,隸屬于同一個線程組,線程的group leader還是main函數所在的線程id。

    thread_group,同一線程組的所有線程的隊列。對于group_leader,這是一個隊列頭,對于同一線程組的其他線程,通過這個字段挂入隊列。可以根據這個隊列,周遊線程組的所有線程。

     是時候看看核心代碼了,下面的代碼屬于do_fork函數及copy_process函數的一些代碼。       

    p->pid = pid_nr(pid);

    p->tgid = p->pid;

    if (clone_flags & CLONE_THREAD)//建立線程,tgid等于目前線程的

        p->tgid = current->tgid;

    p->group_leader = p;

    INIT_LIST_HEAD(&p->thread_group);

    if (clone_flags & CLONE_THREAD) { //線程處理部分,group_leader都是第一個線程。同時挂入隊列

        current->signal->nr_threads++;

        atomic_inc(&current->signal->live);

        atomic_inc(&current->signal->sigcnt);

        p->group_leader = current->group_leader;

        list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);

    }

    代碼表明,第一個線程呢,pid和tgid相同,都是配置設定的那個pid,group_leader也是自己。後面第二個線程,pid是自己的,但是tgid 等于建立者的tgid,group_leader指向第一個線程的task_struct. 後面建立的所有的線程,都會挂入隊列,友善遍線程組的所有線程。

    有了線程組的概念,我們就可以進一步解釋signal相關的内容了。 

    /* signal handlers */

    struct signal_struct *signal;

    struct sighand_struct *sighand;

    sigset_t blocked, real_blocked;

    sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */

    struct sigpending pending;

Linux signal 那些事兒 (3)【轉】

    線程組裡面的所有成員共享一個signal_struct類型結構,同一線程組的多線程的task_struct 中的signal指針都是指向同一個signal_struct。sighand成員變量也是如此,統一個線程組的多個線程指向同一個signalhand_struct結構。  

static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)

{

    struct signal_struct *sig;

    if (clone_flags & CLONE_THREAD) //線程,直接傳回,表明同一線程組共享

        return 0;

    sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);

    tsk->signal = sig;

    if (!sig)

        return -ENOMEM;

    sig->nr_threads = 1;

    atomic_set(&sig->live, 1);

    atomic_set(&sig->sigcnt, 1);

    init_waitqueue_head(&sig->wait_chldexit);

    sig->curr_target = tsk;

        。。。。

static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)

    struct sighand_struct *sig;

    if (clone_flags & CLONE_SIGHAND) {

        atomic_inc(&current->sighand->count); //如果發現是線程,直接講引用計數++,無需配置設定sighand_struct結構

        return 0;

    sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);

    rcu_assign_pointer(tsk->sighand, sig);

    atomic_set(&sig->count, 1);

    memcpy(sig->action, current->sighand->action, sizeof(sig->action));

    return 0;

    這就基本實作了多線程應用中,信号處理程式是共享的,因為他們共用一個signalhand_struct。

    上一篇博文提到,signal->shared_pending 和pending兩個挂起信号相關的資料結構,此處我們可以具體講解了。signal是線程組共享的結構,自然下屬的shared_pending也是線程組共享的。就像POSIX提到的,kill/sigqueue發送信号,發送的對象并不是線程組某個特定的線程,而是整個線程組。自然,如果kernel會将信号記錄在全線程組共享的signal->shared_pending,表示,線程組收到信号X一枚。

    有筒子說了,我就要給某個特定的線程發信号,有沒有辦法,核心怎麼辦?這是個好問題。   

  int tkill(int tid, int sig);

  int tgkill(int tgid, int tid, int sig)

    這兩個API是給線程組特定線程發信号的,毫不意外,核心會将信号記錄線上程自己的結構pending中。

pending = group ? &t->signal->shared_pending : &t->pending;

    對于kill/sigqueue,__send_signal傳進來的是group是true,對于tkill/tgkill傳進來的是false。會将信号寫入相應的挂起信号位圖。

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,

            int group, int from_ancestor_ns)

    struct sigpending *pending;

    struct sigqueue *q;

    int override_rlimit;

    int ret = 0, result;

    assert_spin_locked(&t->sighand->siglock);

    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;   // tkill用的自己的pending,

                                                                  // kill/sigqueue用的線程組共享的signal->shared_pending

    /*

     * Short-circuit ignored signals and support queuing

     * exactly one non-rt signal, so that we can get more

     * detailed information about the cause of the signal.

     */

    result = TRACE_SIGNAL_ALREADY_PENDING;

    if (legacy_queue(pending, sig))

    result = TRACE_SIGNAL_DELIVERED;

     * fast-pathed signals for kernel-internal things like SIGSTOP

     * or SIGKILL.

    if (info == SEND_SIG_FORCED)

        goto out_set;

     * Real-time signals must be queued if sent by sigqueue, or

     * some other real-time mechanism. It is implementation

     * defined whether kill() does so. We attempt to do so, on

     * the principle of least surprise, but since kill is not

     * allowed to fail with EAGAIN when low on memory we just

     * make sure at least one signal gets delivered and don't

     * pass on the info struct.

    if (sig < SIGRTMIN)

        override_rlimit = (is_si_special(info) || info->si_code >= 0);

    else

        override_rlimit = 0;

    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) {

        case (unsigned long) SEND_SIG_NOINFO:

            q->info.si_signo = sig;

            q->info.si_errno = 0;

            q->info.si_code = SI_USER;

            q->info.si_pid = task_tgid_nr_ns(current,

                            task_active_pid_ns(t));

            q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());

            break;

        case (unsigned long) SEND_SIG_PRIV:

            q->info.si_code = SI_KERNEL;

            q->info.si_pid = 0;

            q->info.si_uid = 0;

        default:

            copy_siginfo(&q->info, info);

            if (from_ancestor_ns)

                q->info.si_pid = 0;

        }

        userns_fixup_signal_uid(&q->info, t);

    } else if (!is_si_special(info)) {

        if (sig >= SIGRTMIN && info->si_code != SI_USER) {

            /*

             * Queue overflow, abort. We may abort if the

             * signal was rt and sent by user using something

             * other than kill().

             */

            result = TRACE_SIGNAL_OVERFLOW_FAIL;

            ret = -EAGAIN;

            goto ret;

        } else {

             * This is a silent loss of information. We still

             * send the signal, but the *info bits are lost.

            result = TRACE_SIGNAL_LOSE_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;

    線程存在一個很讓人迷惑的問題,如何讓線程組的所有線程一起退出。我們都知道,多線程的程式有一個線程通路了非法位址,引發段錯誤,會造成所有線程一起退出。這也是多線程程式脆弱的地方。但是如何做到的呢?

    do_signal--->get_signal_to_deliver中,會選擇信号,如果發現需要退出,會執行do_group_exit。這個名字顧名思義了,線程組退出。   

void

do_group_exit(int exit_code)

    struct signal_struct *sig = current->signal;

    BUG_ON(exit_code & 0x80); /* core dumps don't get here */

    if (signal_group_exit(sig))

        exit_code = sig->group_exit_code;

    else if (!thread_group_empty(current)) {

        struct sighand_struct *const sighand = current->sighand;

        spin_lock_irq(&sighand->siglock);

        if (signal_group_exit(sig))

            /* Another thread got here before we took the lock. */

            exit_code = sig->group_exit_code;

        else {

            sig->group_exit_code = exit_code;

            sig->flags = SIGNAL_GROUP_EXIT;

            zap_other_threads(current);

        spin_unlock_irq(&sighand->siglock);

    do_exit(exit_code);

    /* NOTREACHED */

    如果是多線程,會走入到else中,主要的操作都在zap_other_threads函數中:

/*

 * Nuke all other threads in the group.

 */

int zap_other_threads(struct task_struct *p)

    struct task_struct *t = p;

    int count = 0;

    p->signal->group_stop_count = 0;

    while_each_thread(p, t) {

        task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);

        count++;

        /* Don't bother with already dead threads */

        if (t->exit_state)

            continue;

        sigaddset(&t->pending.signal, SIGKILL);

        signal_wake_up(t, 1);

    return count;

    不多說了,就是給每一個線程都挂上一個SIGKILL的信号,當CPU選擇線程執行時候的時候,自然會處理這個信号,而對SIGKILL的處理,會再次調用do_group_exit。這一次會調用do_exit退出。當線程組所有程序都執行過之後,整個線程組就消亡了。

    講完這些,需要講block了。我第一篇就講到,我們有時候需要阻塞某些信号。POSIX說了多線程中每個線程要有自己的阻塞信号。不必說,task_struct中的blocked就是阻塞信号位圖。我們的glibc的sigprocmask函數,就是設定程序的blocked。

    那些block的信号為何不能傳遞,核心是怎麼做到的?

Linux signal 那些事兒 (3)【轉】

int next_signal(struct sigpending *pending, sigset_t *mask)

    unsigned long i, *s, *m, x;

    int sig = 0;

    s = pending->signal.sig;

    m = mask->sig;

     * Handle the first word specially: it contains the

     * synchronous signals that need to be dequeued first.

    x = *s &~ *m;

    if (x) {

        if (x & SYNCHRONOUS_MASK)

            x &= SYNCHRONOUS_MASK;

        sig = ffz(~x) + 1;

        return sig;

    switch (_NSIG_WORDS) {

    default:

        for (i = 1; i < _NSIG_WORDS; ++i) {

            x = *++s &~ *++m;

            if (!x)

                continue;

            sig = ffz(~x) + i*_NSIG_BPW + 1;

        break;

    case 2:

        x = s[1] &~ m[1];

        if (!x)

        sig = ffz(~x) + _NSIG_BPW + 1;

    case 1:

        /* Nothing to do */

    return sig;

    m就是task_struct中的blocked,阻塞的信号就不會不會被取出傳遞了。很有意思的一點是信号傳遞的順序。在Linux programming interface一書中提到小signo優先的政策,比如SIGINT(2)和SIGQUIT(3)同時存在,SIGINT(2) 先deliver,然後才是SIGQUIT(3).我們看代碼,很有意思的是有同步信号:

#define SYNCHRONOUS_MASK \

    (sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \

     sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))

    有SIGSEGV SIGBUS SIGILL SIGTRAP SIGFPE SIGSYS,那麼這幾個信号優先。沒有這幾個信号,按照小信号優先。當然了,這些是Linux kernel的實作,畢竟不是POSIX标準,不可依賴這種順序。

   另外,dequeue很有意思,先去task_struct中的pending中取,取不到再去整個線程組共享的shered_pending位圖去取。    

int dequeue_signal(struct task_struct *tsk, sigset_t *mask, siginfo_t *info)

    int signr;

    /* We only dequeue private signals from ourselves, we don't let

     * signalfd steal them

    signr = __dequeue_signal(&tsk->pending, mask, info);

    if (!signr) {

        signr = __dequeue_signal(&tsk->signal->shared_pending,

                     mask, info);

       。。。。

參考文獻:

本文轉自張昺華-sky部落格園部落格,原文連結:http://www.cnblogs.com/sky-heaven/p/6844580.html,如需轉載請自行聯系原作者

繼續閱讀