【问题标题】:How does Linux prioritize custom signal handlers?Linux 如何确定自定义信号处理程序的优先级?
【发布时间】:2015-06-26 15:39:10
【问题描述】:

上周我们举办了一场讲座,内容涉及操作系统(在本例中为 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] = '\0';
      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. 进入inthandler,执行循环,点击^\
  3. 退出inthandler,进入quithandler,执行quithandler循环
  4. ^C 返回到 inhandler。如果我在 inthandler 中时再次执行 ^C,则忽略连续的 inthandler 信号,直到当前的 inthandler 完成处理。

根据观察,我发现一些信号似乎是嵌套的、2 队列深度的“调度”。例如,如果我快速连续输入以下中断:

  • ^C, ^\, ^C, ^\, ^\, ^C

我将从代码中收到以下行为/输出:

^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. 接收 ^\ 信号,“延迟” inthandler 并进入 quithandler
  3. 接收下一个 ^C 信号,但因为我们已经“嵌套”在一个 inthandler 中,所以将它放在 inthandler“队列”的后面
  4. 接收 quithandler,放在 quithandler 队列的后面。
  5. 执行退出处理程序,直到队列为空。忽略第三个退出处理程序,因为它的队列深度似乎只有 2。
  6. 离开quithandler,执行剩下的2个inhandler。忽略最终的 inthandler,因为队列深度为 2。

我向我的教授展示了这种行为,他似乎同意“嵌套 2 队列深度”行为是正在发生的事情,但我们不能 100% 确定原因(他来自硬件背景并且刚刚开始教这门课)。我想在 SO 上发帖,看看是否有人可以阐明 Linux 处理这些信号的原因/方式,因为我们不太期待某些行为,即嵌套。

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

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

我想留下额外的测试用例作为链接,因为它们有点大屏幕截图。

谢谢!

【问题讨论】:

  • 为了获得最佳效果,函数原型应该在任何函数之外。
  • 你应该显示你运行的代码,而不是它的近似值。您的“退出”处理程序代码打印“inth”,但记录显示打印“quith”的内容。
  • 参见How to avoid using printf() in a signal handler? 此外,与sigaction() 相比,您对signal() 所发生的事情的控制有限。见What is the difference between signal() and sigaction()?
  • 发布的代码编译不干净。出于多种原因,从 main 的两个未使用参数开始:ac 和 av。建议编译启用所有警告(或至少'-Wall -Wextra -pedantic')然后修复警告,然后重新发布代码。首先,代码缺少几个头文件 string.h 和 unistd.h
  • @JonathanLeffler 抱歉 - 我无法让 VPN 在我的桌面上运行(我在这里输入这篇文章,可能是因为我在 W10TP 上)所以我在重写时手动编写了其中的一些部分问题 - 我不小心写了 inth 而不是 Quith。我已经解决了。至于 printf(),这不是原始示例附带的(请参见此处:pastebin.com/k1CFRzPx 我只是想能够看到输出以确定它在做什么。如果我只使用睡眠。printf 是否与它的工作方式有关?

标签: c linux signals


【解决方案1】:

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

  1. 默认情况下,信号在进入其处理程序时被屏蔽,在处理程序退出时被取消屏蔽;
  2. 如果信号在被屏蔽时引发,则它会待处理,如果/当该信号被取消屏蔽时将被传递。

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 被取消屏蔽。然后 main 函数继续执行。

在 Linux 上,您可以通过检查 /proc/&lt;PID&gt;/status 来查看进程的当前屏蔽信号和待处理信号集。被屏蔽的信号显示在SigBlk: 位掩码中,待处理的信号显示在SigPnd: 位掩码中。

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

【讨论】:

  • 非常感谢!这非常完美地解释了它。一个后续问题:他们决定采用二进制“挂起/不挂起并忽略其他一切”行为是否有特殊原因?是某种约定还是易于设计?如果一个程序出于某种原因依赖于向另一个进程发送 SIGINT,但该进程已经挂起并且从未生成信号,那么第一个程序/进程将如何响应?
  • @karasaj:这种设计可以追溯到 UNIX 历史的早期。我认为这与易于实施以及资源处理问题有关 - (如果您有一个信号队列,您必须决定当该队列溢出时该怎么办)。无论如何,“待定”设计足以编写无竞争的处理程序 - 信号被视为发生感兴趣事件的通知,并且当您处理信号时,您处理 all 的实例自您上次检查以来发生的事件,而不仅仅是一个。这也是通常处理硬件中断的方式。
  • 作为观察结果,SIGRTMIN 和 SIGRTMAX(实时信号)之间的信号表现不同,因为它们实际上会排队,并且将处理尽可能多的接收到的信号(对于这些,您没有二进制挂起与非挂起标志)
【解决方案2】:

我在手册页 signal (7) 中发现了这个,这似乎很相关:

实时信号以有保证的顺序传递。多种的 同类型的实时信号按顺序送达 他们被派去。如果不同的实时信号被发送到一个 过程中,它们从编号最小的开始交付 信号。 (即,编号低的信号具有最高优先级。) 相比之下,如果一个进程有多个标准信号等待处理, 它们的交付顺序未指定。

查看sigprocmasksigpending 文档,以及signal (7),应该可以加深您对有关待处理信号的保证的理解。

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

【讨论】:

  • 都是正确的,但在这种情况下,订单是很好理解的,因为 OP 在任何时候都不会有两个未屏蔽的信号待处理。
猜你喜欢
  • 2016-12-23
  • 1970-01-01
  • 2017-11-30
  • 2022-06-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-01
  • 1970-01-01
相关资源
最近更新 更多