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循环测试,用户态堆栈就会多出一些信号处理函数堆栈(这一点对于一些线程堆栈受限系统可能要考虑一下)。
     
     
     
     
     
  • 相关阅读:
    GitLab 重置认证和添加账号缓存
    PHP 正则匹配IP
    git 删除指定版本
    PostgreSQL 9.2 日期运算
    postgre 已有字段修改为自增
    postgresql 导入导出
    PHP TS 和 NTS 版本选择
    background-image属性的设置
    SQLServer 附加数据库后只读或报错解决方法
    IIS 6.0 发布网站使用教程
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486368.html
Copyright © 2011-2022 走看看