zoukankan      html  css  js  c++  java
  • kill信号由谁接收处理

    一、信号发送方式:片发VS点发
    通常信号发送都是使用kill系统调用来实现,这个功能其实相对粗糙一些,它的第一个参数指明了接受者,但是这个接受者在多线程中并不总是最终的处理者。那么通过这个现象可以解释这个参数的意义:那就是首选(prefer)这个线程,但是如果这个线程实在是有些难言之隐,那么它所在的线程组中其它线程也可以代劳。这一点和之后新添加的tkill系统调用不同,从使用场景上看,这个tkill是为了支持POSIX 线程库中pthread_kill而添加的内核支持函数,所以它是定向的信号发送,所以即使是目标线程现在不方便,信号还是只能等目标线程在适当的时候来处理这个信号,而其它线程没有办法染指。
    在很多时候,我们都希望能够在信号发送的时候使用tkill,从而减少不确定性,但是这个美好的愿望并不是每次都能奏效的。例如,当子进程退出的时候,SIGCHLD信号到底发给谁,这个通常是确定的,那就是执行fork/vfork的线程,而且通常情况下最终由哪个线程来执行信号处理函数可能都不重要。但是有时候这个确实很重要的,最明显的场景就是对线程私有数据的访问是线程相关的。当然这并不是我在工程中遇到的问题,工程中遇到的问题可能比这个情况更为复杂,使用线程私有数据说明这个问题只是觉得更加典型而已。
    二、kill信号接受者何时确定
    在信号发送的时候,信号接受者已经选中:
    sys_kill-->>>kill_something_info---->>>kill_pid_info--->>>group_send_sig_info--->>>__group_send_sig_info--->>>__group_complete_signal
    __group_complete_signal(int sig, struct task_struct *p) {
    ……
    if (wants_signal(sig, p))
            t = p;
        else if (thread_group_empty(p))
            return;
        else {
            /*
             * Otherwise try to find a suitable thread.
             */
      ……
        }
    ……
        /*
         * 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);
    }
    这个kill在很多时候还是非常忠实的尊重调用者的意愿,如果发送者可以接收信号,那么就铁定选择这个信号,不含糊,但是如果说目标线程不方便(由wants_signal确定),那么就在线程组中其它线程选择一个来处理这个信号
    static inline int wants_signal(int sig, struct task_struct *p)
    {
        if (sigismember(&p->blocked, sig)) 如果目标线程屏蔽了信号,那么不方便。
            return 0;
        if (p->flags & PF_EXITING)正在退出,不方便。
            return 0;
        if (sig == SIGKILL) 杀死信号,必须处理。
            return 1;
        if (p->state & (TASK_STOPPED | TASK_TRACED))被调试或者暂定线程不方便。
            return 0;
        return task_curr(p) || !signal_pending(p);
    }
    由于通常那些希望处理这些信号的线程都是能通过这个检测的,所以kill的目标线程通常(例如对于SIGCHLD)是如愿以偿的处理这个信号的,由于没有执行__group_complete_signal函数最后的signal_wake_up(t, sig == SIGKILL);,所以其它线程的TIF_SIGPENDING未被置位,可以认为这个信号如果首选目标可以处理,那么这个信号对其它线程透明。
    三、节外生枝
    那么是不是在这里确定这个信号的接收者之后,这个信号一定会在这个线程的上下文下执行呢?答案同样是不确定。我们看一下当一个线程去去信号的处理函数
    get_signal_to_deliver--->>dequeue_signal
    {
        int signr = __dequeue_signal(&tsk->pending, mask, info);
        if (!signr) {
            signr = __dequeue_signal(&tsk->signal->shared_pending,
                         mask, info);
    ……
    }
    recalc_sigpending_tsk(tsk);
    ……
    }
    fastcall void recalc_sigpending_tsk(struct task_struct *t)
    {
        if (t->signal->group_stop_count > 0 ||
            (freezing(t)) ||
            PENDING(&t->pending, &t->blocked) ||
            PENDING(&t->signal->shared_pending, &t->blocked))
            set_tsk_thread_flag(t, TIF_SIGPENDING);
        else
            clear_tsk_thread_flag(t, TIF_SIGPENDING);
    }
    从这个信号处理函数中可以看到,当一个线程处理信号的时候,它首先从自己的私有延迟队列中取信号,如果取到则返回,娶不到则到共享队列中取信号。当信号取出之后,它会自觉的通过recalc_sigpending_tsk来再次检测是否有信号可以处理,同样是检测了私有和共享延迟队列
    现在假设在第二节的kill调用时发送信号SIGX选择了线程A,但是A一直没有机会得到调度,然后线程B收到一个信号SIGY被唤醒,那么线程B在处理完SIGY这个信号之后将会再次从共享队列中看到本来确定给A处理的SIGX信号,所以他会先于A线程来消耗掉这个信号,然后A线程真正获得执行的时候这个信号已经消失
    四、验证
    上节描述的场景不太容易复现,因为线程A和线程B的内核态抢占不太容易模拟,它通常在可抢占实时系统中偶现,但是这个场景在理论上是存在(如果有不同意的同学可以指点一下)。所以我们使用一个必现的场景来模拟展示一下这种情况。
    [tsecer@Harry Uncertain]$ cat Uncertain.c
    /*
     * Author: tsecer@163.com
     * Date  :2012.05.08
     * Desc  :Illuminate signal-handling race
     */

    #include <sched.h>
    #include <sys/types.h>
    #include <signal.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <pthread.h>
    #include <stdio.h>
    #include <stdlib.h>
    //简单信号处理函数,主要用来打印信号值和处理线程
    void sighandler(int sig)
    {
    printf("signo is %d, my threadid is %d ",sig,syscall(SYS_gettid));
    }
    //简单线程,用来作为真正处理信号处理线程
    void * reaper(void * arg)
    {
    while (1) sleep(100000);
    }
    //主函数
    int main()
    {
    pthread_t uncle;

    //安装SIGCHLD信号和SIGINT处理函数
    signal(SIGCHLD,sighandler);
    signal(SIGINT,sighandler);
    printf("main thread pid is %d ",getpid());
    //创建线程
    pthread_create(&uncle,NULL,reaper,NULL);
    //创建子进程,休息10s,等待父进程中所有线程创建完成并开始执行。然后退出,退出之后产生SIGCHLD发送给主线程
    if(0 == fork())
    {
    sleep(10);
    printf("forked child will exit ");
    exit(0);
    }
    //通过vfork创建无限休眠子进程,从而让主进程进入不可唤醒休眠中,这个状态满足wants_signal判断
    if(0 == vfork())
    {
    sleep(10000);
    _exit(0);
    }
    printf("parent exit ");
    }
    //生成可执行文件
    [tsecer@Harry Uncertain]$ gcc Uncertain.c -o Uncertain.c.exe -lpthread -static
    /usr/lib/gcc/i686-redhat-linux/4.4.2/../../../libpthread.a(libpthread.o): In function `sem_open':
    (.text+0x6d1a): warning: the use of `mktemp' is dangerous, better use `mkstemp'
    //后台运行可执行程序
    [tsecer@Harry Uncertain]$ ./Uncertain.c.exe &
    main thread pid is 19204
    [2] 19204
    //一段时间后,子进程退出,向父进程发送SIGCHLD信号,注意:此时uncle线程未被唤醒
    [tsecer@Harry Uncertain]$ forked child will exit
    //主动发送一个信号SIGINT到uncle线程,从而触发它的信号处理函数
    [tsecer@Harry Uncertain]$ kill -INT 19205
    //uncle线程同时处理了两个信号,它抢先处理了本来发送给主线程的SIGCHLD(17)号信号,然后处理了主动发送的SIGINT
    [tsecer@Harry Uncertain]$ signo is 17, my threadid is 19205
    signo is 2, my threadid is 19205
    [tsecer@Harry Uncertain]$ 
    五、为什么先处理大数值的SIGCHLD后处理小数值的SIGINT
    这里有一个细节,就是信号处理函数执行的时候是先处理17号信号,然后处理2号信号,但是在信号摘取函数中是从小到大取信号的:
    dequeue_signal--->>__dequeue_signal---->>next_signal
            for (i = 0; i < _NSIG_WORDS; ++i, ++s, ++m)
                if ((x = *s &~ *m) != 0) {
                    sig = ffz(~x) + i*_NSIG_BPW + 1;
                    break;
                }
            break;
    其中都是通过ffz来选择第一置位的bit,所以理论上升或应该是先执行2号信号,然后才是17信号。为了确认这一点,我同样调试了内核进行了验证,的确是先取到2号信号,然后是17号,这点大家不用怀疑。
    所以要从其它地方看,那就是内核对信号处理函数的判断。
    #ifdef CONFIG_VM86
    #define resume_userspace_sig    check_userspace
    #else
    #define resume_userspace_sig    resume_userspace
    #endif

    work_pending:
        testb $_TIF_NEED_RESCHED, %cl
        jz work_notifysig
    ……
        call do_notify_resume 从信号处理返回,再次跳转到resume_userspace_sig处,通过前面宏知道它等价于resume_userspace
        jmp resume_userspace_sig
    END(work_pending)
    ……
    ENTRY(resume_userspacedo_notify_resume函数之后跳转到这个地方
         DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                        # setting need_resched or sigpending
                        # between sampling and the iret
        movl TI_flags(%ebp), %ecx
        andl $_TIF_WORK_MASK, %ecx    # is there any work to be done on
                        # int/exception return?
        jne work_pending 再次跳转到前面判断是否有信号
        jmp restore_all
    END(ret_from_exception)
    可以看到,在内核返回到用户态之前,线程所有的信号堆栈都已经被压到用户态堆栈中,它们的执行顺序是和get_signal_to_deliver中取到的信号顺序刚好相反。这个用户态堆栈的创建是在do_notify_resume--->>>do_signal--->>handle_signal--->>setup_rt_frame中完成,所以上面每执行一次work_pending循环测试,用户态堆栈就会多出一些信号处理函数堆栈(这一点对于一些线程堆栈受限系统可能要考虑一下)。
     
     
     
     
     
  • 相关阅读:
    HDU 1813 Escape from Tetris
    BZOJ 2276 Temperature
    BZOJ 4499 线性函数
    BZOJ 3131 淘金
    HDU 5738 Eureka
    POJ 2409 Let it Bead
    POJ 1286 Necklace of Beads
    POJ 1696 Space Ant
    Fox And Jumping
    Recover the String
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486368.html
Copyright © 2011-2022 走看看