信号间优先级及线程优先级对信号的影响
一、问题引出
为了精确定位一个任务退出的时候是何种原因,例如是看门狗复位,或者是受到了某些人为主动复位(kill 指定任务,或者reboot导致的简介SIGTERM+SIGKILL组合),或者是某些第三方库中执行了exit导致了线程的退出等原因。这就需要内核进行介入,记录指定感兴趣任务(之后称为受控任务)它处理的最后一个信号,就好像警察查案的时候,最后一个和死者接触的人可能对死因有重要参考价值。
二、定位添加
这个是一个纯技术问题,就是在线程进行信号处理的时候加上监控,更准确的说就是在dequeue_signal函数中记录一个线程从自己有权利、有义务处理的信号队列中是否有未被屏蔽的信号需要处理,只需要记录这个函数中__dequeue_signal到的信号就好了,记录入一个线程私有变量中,每个线程一个。这个具体实现就不废话了。
三、一个奇怪的现象
这里再补充一下背景,目标任务是一个典型的多线程任务,其中一般有几十个线程,并且嵌入式系统中的实时Linux都是分优先级的。
当我们通过kill target(kill 默认发送的是SIGTERM信号)杀死受控任务之后,从记录上看,可以看到记录的结果所有的任务记录到的信号都是SIGKILL,而不是SIGTERM,也就是对各个线程完成致命一击的并不是我们发送的SIGTERM而是SIGKILL,并且一般是最高优先级的线程最早罹难。
四、分析
1、kill的目标线程
当通过kill发送信号的时候,事实上是所有的线程都是有机会获得这个信号的,此时要看目标线程所在线程组哪个线程比较合适,比方说没有屏蔽这个信号、正在运行、或者轮流坐庄轮到了某一个线程等,具体策略可以参考complete_signal函数。但是不管是哪个线程最终有幸来处理这个信号,它们的信号处理函数都是相同的,也就是每个线程不能有自己的信号处理函数,虽然它们可以各自屏蔽不同的信号、有自己的私有信号队列等。而内核的前32个信号(也就是所谓的非实时信号,相对于32--64之间的实时信号)大部分都是不能忽略的,并且如果线程不安装这些信号的处理函数,那么如果线程受到这个信号,将会是致命问题(除了SIGCHLD可以忽略,其它的大部分都是不速之客)。
当通过kill 发送SIGTERM给受控任务之后,受控任务没有注册这个信号的处理函数,这就引起了严重的后果:这个目标任务中的所有线程都必须退出。这个在实际应用中是合理的,因为一个线程出问题,可能问题已经很糟糕了,继续勉强运行可能问题更加诡异。内核在complete_signal中会进行判断
if (sig_fatal(p, sig) &&
!(signal->flags & (SIGNAL_UNKILLABLE | SIGNAL_GROUP_EXIT)) &&
!sigismember(&t->real_blocked, sig) &&
(sig == SIGKILL ||
!tracehook_consider_fatal_signal(t, sig))) {
/*
* This signal will be fatal to the whole group.
*/
if (!sig_kernel_coredump(sig)) {
/*
* Start a group exit and wake everybody up.
* This way we don't have other threads
* running and doing things after a slower
* thread has the fatal signal pending.
*/
signal->flags = SIGNAL_GROUP_EXIT;
signal->group_exit_code = sig;
signal->group_stop_count = 0;
t = p;
do {这里给受控任务中的每个线程都发送一个SIGKILL信号,让它们体面的结束运行,注意这里是一个循环,也就是受控任务中的每个线程都有份。
sigaddset(&t->pending.signal, SIGKILL);这里信号是放在一个任务的私有队列中的,这个SIGKILL是一个特殊信号,内核能够感知到这个信号,保证用户不能屏蔽掉对这个信号的处理。
signal_wake_up(t, 1);唤醒目标线程,这一步将会对结果有决定性影响。
} while_each_thread(p, t);
return;
}
2、唤醒及执行
在上面可以看到它是在循环中给线程发送SIGKILL之后,接着又一口气通过signal_wake_up唤醒这个线程。但是这个唤醒在实时系统中将有可能导致抢占的发生,或者可以认为这个函数会执行到一个抢占点,如果唤醒线程的优先级高于唤醒者,那么唤醒者会被当场抢占。
signal_wake_up--->>>wake_up_state--->>>try_to_wake_up---->>>ttwu_post_activation--->>>check_preempt_curr
在这个函数中,新唤醒的线程可能会直接抢占唤醒者。这样,受控任务中最高优先级的任务就会在上面的while_each_thread中间获得调度权,它就会在返回用户态之前从自己的信号队列(准确的说是私有信号队列)中取信号来处理。
get_signal_to_deliver--->>>>dequeue_signal
signr = __dequeue_signal(&tsk->pending, mask, info);
if (!signr) {
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info);
可以看到,它是先从自己的私有队列中取信号,然后才取公共信号,看来私有制无处不在啊。这样这个最高优先级的线程就会取到刚才放入的那个SIGKILL,从而导致了“高优先级线程先死”的现象。
3、真正SIGTERM接受者为什么也是SIGKILL而不是SIGTERM杀死
还是刚才那个while_each_thread循环,非常实在的给自己也放了一个。但是在dequeue_signal--->>__dequeue_signal--->>>next_signal可以看到,对于信号队列的扫描是从小到大依次扫描的,所以排在前面的信号将会优先被处理。由于SIGKILL是9,而SIGTERM是15,所以SIGKILL优先被处理。
事实上这个不是这里的原因:这个还是因为信号处理是优先从私有队列取信号导致的,但是上面说的这一点也没错,就是小数值信号优先被处理。