ITPub博客

首页 > > > 技术连载 | Linux signal 那些事儿 (3)

技术连载 | Linux signal 那些事儿 (3)

作者:u_453e1a12781dea 时间:2016-07-07 16:34:54 0 删除 编辑
作者:Bean_lee
来源:ChinaUnix技术博客,本文版权由Bean_lee所有,如需转载,请注明出处。

往期回顾:连载1
                  连载2

这篇博客,想集中在signal 与线程的关系上,顺带介绍内核signal相关的结构。如何组织我其实并没想好,想到哪就写到哪里吧。主题一定会落在signal之内而不跑题。
    
提到signal与thread的关系,就得先提POSIX标准。POSIX标准决定了Linux为何将signal如此实现:

  • 信号处理函数必须在多线程应用的所有线程之间共享,但是,每个线程要有自己的挂起信号掩码和阻塞信号掩码
  • POSIX 函数kill/sigqueue必须面向所有的多线程应用而不是某个特殊的线程。
  • 每个发给多线程应用的信号仅传送给1个线程,这个线程是由内核从不会阻塞该信号的线程中随意选出。
  • 如果发送一个致命信号到多线程,那么内核将杀死该应用的所有线程,而不仅仅是接收信号的那个线程。

上面是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线程之线程 线程组 进程 轻量级进程(LWP)提到了这个问题。我不想多浪费笔墨赘述。
    
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;

 线程组里面的所有成员共享一个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);
    if (!sig)
        return -ENOMEM;
    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))
        goto ret;

    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_signo = sig;
            q->info.si_errno = 0;
            q->info.si_code = SI_KERNEL;
            q->info.si_pid = 0;
            q->info.si_uid = 0;
            break;
        default:
            copy_siginfo(&q->info, info);
            if (from_ancestor_ns)
                q->info.si_pid = 0;
            break;
        }

        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的信号为何不能传递,内核是怎么做到的?

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);
       。。。。
}

参考文献:
1 Linux2.6内核中的线程组初探
2 Linux kernel 3.8.0
只为一场完美的技术盛宴!
SACC2016
作为国内最受欢迎的架构师盛会,2016第八届中国系统架构师大会(SACC)将于2016年10月27日-29日在北京万达索菲特大酒店撼世来袭!

大会以"架构创新之路"为主题,共设置两个主场分享时段,24个技术交流专场时段;邀请来自互联网、电子商务、金融、电信、政府、行业协会等20多个领域,150多位技术专家及行业领袖来分享他们的经验;并将吸引4000多名系统运维、架构师、及各种企业的IT决策人士参会,为他们提供最具价值的交流平台。
最优折扣来袭!
7月28日前,订购SACC2016门票可立享6.8折优惠!团购更有折上折!
点击“阅读原文”立即购票!

原文作者:;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-07-17

  • 博文量
    1982
  • 访问量
    17924