信号间优先级及线程优先级对信号的影响

一、问题引出

为了精确定位一个任务退出的时候是何种原因,例如是看门狗复位,或者是受到了某些人为主动复位(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优先被处理。

事实上这个不是这里的原因:这个还是因为信号处理是优先从私有队列取信号导致的,但是上面说的这一点也没错,就是小数值信号优先被处理。

posted on 2019-03-06 20:41  tsecer  阅读(631)  评论(0编辑  收藏  举报

导航