注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

Tsecer的回音岛

Tsecer的博客

 
 
 

日志

 
 

kill信号由谁接收处理  

2012-05-08 22:30:39|  分类: Linux内核 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
一、信号发送方式:片发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\n",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\n",getpid());
//创建线程
pthread_create(&uncle,NULL,reaper,NULL);
//创建子进程,休息10s,等待父进程中所有线程创建完成并开始执行。然后退出,退出之后产生SIGCHLD发送给主线程
if(0 == fork())
{
sleep(10);
printf("forked child will exit\n");
exit(0);
}
//通过vfork创建无限休眠子进程,从而让主进程进入不可唤醒休眠中,这个状态满足wants_signal判断
if(0 == vfork())
{
sleep(10000);
_exit(0);
}
printf("parent exit\n");
}
//生成可执行文件
[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_userspace) do_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循环测试,用户态堆栈就会多出一些信号处理函数堆栈(这一点对于一些线程堆栈受限系统可能要考虑一下)。
  评论这张
 
阅读(1499)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017