Linux 如何确定自定义信号处理程序的优先级?

How does Linux prioritize custom signal handlers?

我们上周有一个讲座涉及 OS(在本例中为 Linux,在本例中我们学校的服务器使用 SUSE Linux 11)如何处理中断。需要注意的一件事是,对于大多数信号,您可以捕获中断并将自己的信号处理程序定义为 运行 而不是默认值。我们用一个例子来说明这一点,我发现了最初在我看来很有趣的行为。这是代码:

#include <stdio.h>
#include <signal.h>

#define INPUTLEN 100

main(int ac, char *av[])

{
  void inthandler (int);
  void quithandler (int);
  char input[INPUTLEN];
  int nchars;

  signal(SIGINT, inthandler);
  signal(SIGQUIT, quithandler);

  do {
    printf("\nType a message\n");
    nchars = read(0, input, (INPUTLEN - 1));
    if ( nchars == -1)
      perror("read returned an error");
    else {
      input[nchars] = '[=11=]';
      printf("You typed: %s", input);
    }
  }
  while(strncmp(input, "quit" , 4) != 0); 
}

void inthandler(int s)
{
  printf(" Received Signal %d ....waiting\n", s);
  int i = 0;
  for(int i; i<3; ++i){
    sleep(1);
    printf("inth=%d\n",i);
  }
  printf(" Leaving inthandler \n");
}

void quithandler(int s)
{
  printf(" Received Signal %d ....waiting\n", s);
  for(int i; i<7; ++i){
    sleep(1);
    printf("quith=%d\n",i);
  }  printf(" Leaving quithandler \n");
}

所以,当 运行 运行这段代码时,我期望是这样的:

  1. 运行 代码.... ^C
  2. 进入handler,执行循环,命中^\
  3. 退出inthandler,进入quithandler,执行quithandler循环
  4. ^C 回到处理程序。如果我在 inthandler 中时再次执行 ^C,则忽略连续的 inthandler 信号,直到当前 inthandler 完成处理。

根据观察,我发现了一些信号,看起来像是嵌套的 2 队列深度 "scheduling"。例如,如果我快速连续输入以下中断:

我将从代码中收到以下 behavior/output:

^CReceived signal 2 ....waiting
^\Received Signal 3 ....waiting
^C^\^\^C quith=0
quith=1
quith=2
quith=3
quith=4
quith=5
quith=6
quith=7
Leaving quithandler
Received Signal 3 ....waiting
quith=1
quith=2
quith=3
quith=4
quith=5
quith=6
quith=7
Leaving quithandler
inth=0
inth=1
inth=2
inth=3
Leaving inthandler
Received Signal 2 ....waiting
inth=0
inth=1
inth=2
inth=3
Leaving inthandler

换句话说,它似乎是这样处理的:

  1. 先接收到^C信号
  2. 收到^\信号,"delay"inthandler进入quithandler
  3. 接收下一个^C信号,但是因为我们已经"nested"在一个inthandler中,把它放在inthandler的后面"queue"
  4. 接收quithandler,放在quithandler队列的后面。
  5. 执行quithandler直到队列为空。忽略第三个 quithandler,因为它的队列深度似乎只有 2。
  6. 离开quithandler,执行剩下的2个inthandlers。忽略最终的处理程序,因为队列深度为 2。

我向我的教授展示了这种行为,他似乎同意 "nested 2 queue depth" 行为就是正在发生的事情,但我们不能 100% 确定原因(他来自硬件背景并且只有开始教这个class)。我想 post 看看是否有人可以阐明 why/how Linux 处理这些信号,因为我们不太期待某些行为,即嵌套。

我认为我写的测试用例应该足以说明发生了什么,但这里有一堆额外的测试用例的截图:

http://imgur.com/Vya7JeY,fjfmrjd,30YRQfk,uHHXFu5,Pj35NbF

我想将额外的测试用例保留为 link,因为它们是一种大屏幕截图。

谢谢!

我在联机帮助页 signal (7) 中找到了这个似乎相关的内容:

Real-time signals are delivered in a guaranteed order. Multiple real-time signals of the same type are delivered in the order they were sent. If different real-time signals are sent to a process, they are delivered starting with the lowest-numbered signal. (I.e., low-numbered signals have highest priority.) By contrast, if multiple standard signals are pending for a process, the order in which they are delivered is unspecified.

除了 signal (7) 之外,查看 sigprocmasksigpending 文档应该会加深您对有关未决信号的保证的理解。

要从弱 "unspecified" 保证转移到您的 OpenSUSE 版本上实际发生的情况,您可能需要检查内核中的信号传递代码。

规则(对于非实时信号,例如您正在使用的 SIGQUITSIGINT)是:

  1. 默认情况下,信号在进入其处理程序时被屏蔽,而在处理程序退出时取消屏蔽;
  2. 如果一个信号在被屏蔽时发出,它将被待处理,并且将被传送if/when信号被屏蔽。

pending 状态是二进制的 - 信号要么挂起,要么不挂起。如果一个信号在被屏蔽时被多次触发,它仍然只会在被屏蔽时被传递一次。

那么在您的示例中发生的情况是:

  1. SIGINT 被引发并且 inthandler() 信号处理程序开始执行,SIGINT 被屏蔽。
  2. SIGQUIT 被引发,quithandler() 信号处理程序开始执行(中断 inthandler()),SIGQUIT 被屏蔽。
  3. SIGINT 被引发,将 SIGINT 添加到挂起信号集(因为它被屏蔽)。
  4. SIGQUIT 被引发,将 SIGQUIT 添加到挂起信号集(因为它被屏蔽)。
  5. SIGQUIT 已引发,但没有任何反应,因为 SIGQUIT 已经挂起。
  6. SIGINT 已引发,但没有任何反应,因为 SIGINT 已经挂起。
  7. quithandler() 执行完毕,SIGQUIT 被揭露。因为 SIGQUIT 处于待处理状态,所以它随后被传送,并且 quithandler() 再次开始执行(SIGQUIT 再次被屏蔽)。
  8. quithandler()第二次执行完毕,SIGQUIT被揭穿。 SIGQUIT 没有挂起,所以 inthandler() 然后继续执行(SIGINT 仍然被屏蔽)。
  9. inthandler() 执行完毕,SIGINT 被揭露。因为 SIGINT 处于待处理状态,所以它随后被传送,并且 inthandler() 再次开始执行(SIGINT 再次被屏蔽)。
  10. inthandler()第二次执行完毕,SIGINT被揭穿。然后主函数继续执行。

在 Linux 上,您可以通过检查 /proc/<PID>/status 查看进程的当前屏蔽信号和未决信号集。掩码信号显示在 SigBlk: 位掩码中,未决信号显示在 SigPnd: 位掩码中。

如果您使用 sigaction() 而不是 signal() 安装信号处理程序,您可以指定 SA_NODEFER 标志以请求信号在其处理程序执行时不被屏蔽。您可以在您的程序中尝试此操作 - 使用一个或两个信号 - 并尝试预测输出结果。