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");
}
所以,当 运行 运行这段代码时,我期望是这样的:
- 运行 代码.... ^C
- 进入handler,执行循环,命中^\
- 退出inthandler,进入quithandler,执行quithandler循环
- ^C 回到处理程序。如果我在 inthandler 中时再次执行 ^C,则忽略连续的 inthandler 信号,直到当前 inthandler 完成处理。
根据观察,我发现了一些信号,看起来像是嵌套的 2 队列深度 "scheduling"。例如,如果我快速连续输入以下中断:
- ^C, ^\, ^C, ^\, ^\, ^C
我将从代码中收到以下 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
换句话说,它似乎是这样处理的:
- 先接收到^C信号
- 收到^\信号,"delay"inthandler进入quithandler
- 接收下一个^C信号,但是因为我们已经"nested"在一个inthandler中,把它放在inthandler的后面"queue"
- 接收quithandler,放在quithandler队列的后面。
- 执行quithandler直到队列为空。忽略第三个 quithandler,因为它的队列深度似乎只有 2。
- 离开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)
之外,查看 sigprocmask
和 sigpending
文档应该会加深您对有关未决信号的保证的理解。
要从弱 "unspecified" 保证转移到您的 OpenSUSE 版本上实际发生的情况,您可能需要检查内核中的信号传递代码。
规则(对于非实时信号,例如您正在使用的 SIGQUIT
和 SIGINT
)是:
- 默认情况下,信号在进入其处理程序时被屏蔽,而在处理程序退出时取消屏蔽;
- 如果一个信号在被屏蔽时发出,它将被待处理,并且将被传送if/when信号被屏蔽。
pending 状态是二进制的 - 信号要么挂起,要么不挂起。如果一个信号在被屏蔽时被多次触发,它仍然只会在被屏蔽时被传递一次。
那么在您的示例中发生的情况是:
SIGINT
被引发并且 inthandler()
信号处理程序开始执行,SIGINT
被屏蔽。
SIGQUIT
被引发,quithandler()
信号处理程序开始执行(中断 inthandler()
),SIGQUIT
被屏蔽。
SIGINT
被引发,将 SIGINT
添加到挂起信号集(因为它被屏蔽)。
SIGQUIT
被引发,将 SIGQUIT
添加到挂起信号集(因为它被屏蔽)。
SIGQUIT
已引发,但没有任何反应,因为 SIGQUIT
已经挂起。
SIGINT
已引发,但没有任何反应,因为 SIGINT
已经挂起。
quithandler()
执行完毕,SIGQUIT
被揭露。因为 SIGQUIT
处于待处理状态,所以它随后被传送,并且 quithandler()
再次开始执行(SIGQUIT
再次被屏蔽)。
quithandler()
第二次执行完毕,SIGQUIT
被揭穿。 SIGQUIT
没有挂起,所以 inthandler()
然后继续执行(SIGINT
仍然被屏蔽)。
inthandler()
执行完毕,SIGINT
被揭露。因为 SIGINT
处于待处理状态,所以它随后被传送,并且 inthandler()
再次开始执行(SIGINT
再次被屏蔽)。
inthandler()
第二次执行完毕,SIGINT
被揭穿。然后主函数继续执行。
在 Linux 上,您可以通过检查 /proc/<PID>/status
查看进程的当前屏蔽信号和未决信号集。掩码信号显示在 SigBlk:
位掩码中,未决信号显示在 SigPnd:
位掩码中。
如果您使用 sigaction()
而不是 signal()
安装信号处理程序,您可以指定 SA_NODEFER
标志以请求信号在其处理程序执行时不被屏蔽。您可以在您的程序中尝试此操作 - 使用一个或两个信号 - 并尝试预测输出结果。
我们上周有一个讲座涉及 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");
}
所以,当 运行 运行这段代码时,我期望是这样的:
- 运行 代码.... ^C
- 进入handler,执行循环,命中^\
- 退出inthandler,进入quithandler,执行quithandler循环
- ^C 回到处理程序。如果我在 inthandler 中时再次执行 ^C,则忽略连续的 inthandler 信号,直到当前 inthandler 完成处理。
根据观察,我发现了一些信号,看起来像是嵌套的 2 队列深度 "scheduling"。例如,如果我快速连续输入以下中断:
- ^C, ^\, ^C, ^\, ^\, ^C
我将从代码中收到以下 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
换句话说,它似乎是这样处理的:
- 先接收到^C信号
- 收到^\信号,"delay"inthandler进入quithandler
- 接收下一个^C信号,但是因为我们已经"nested"在一个inthandler中,把它放在inthandler的后面"queue"
- 接收quithandler,放在quithandler队列的后面。
- 执行quithandler直到队列为空。忽略第三个 quithandler,因为它的队列深度似乎只有 2。
- 离开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)
之外,查看 sigprocmask
和 sigpending
文档应该会加深您对有关未决信号的保证的理解。
要从弱 "unspecified" 保证转移到您的 OpenSUSE 版本上实际发生的情况,您可能需要检查内核中的信号传递代码。
规则(对于非实时信号,例如您正在使用的 SIGQUIT
和 SIGINT
)是:
- 默认情况下,信号在进入其处理程序时被屏蔽,而在处理程序退出时取消屏蔽;
- 如果一个信号在被屏蔽时发出,它将被待处理,并且将被传送if/when信号被屏蔽。
pending 状态是二进制的 - 信号要么挂起,要么不挂起。如果一个信号在被屏蔽时被多次触发,它仍然只会在被屏蔽时被传递一次。
那么在您的示例中发生的情况是:
SIGINT
被引发并且inthandler()
信号处理程序开始执行,SIGINT
被屏蔽。SIGQUIT
被引发,quithandler()
信号处理程序开始执行(中断inthandler()
),SIGQUIT
被屏蔽。SIGINT
被引发,将SIGINT
添加到挂起信号集(因为它被屏蔽)。SIGQUIT
被引发,将SIGQUIT
添加到挂起信号集(因为它被屏蔽)。SIGQUIT
已引发,但没有任何反应,因为SIGQUIT
已经挂起。SIGINT
已引发,但没有任何反应,因为SIGINT
已经挂起。quithandler()
执行完毕,SIGQUIT
被揭露。因为SIGQUIT
处于待处理状态,所以它随后被传送,并且quithandler()
再次开始执行(SIGQUIT
再次被屏蔽)。quithandler()
第二次执行完毕,SIGQUIT
被揭穿。SIGQUIT
没有挂起,所以inthandler()
然后继续执行(SIGINT
仍然被屏蔽)。inthandler()
执行完毕,SIGINT
被揭露。因为SIGINT
处于待处理状态,所以它随后被传送,并且inthandler()
再次开始执行(SIGINT
再次被屏蔽)。inthandler()
第二次执行完毕,SIGINT
被揭穿。然后主函数继续执行。
在 Linux 上,您可以通过检查 /proc/<PID>/status
查看进程的当前屏蔽信号和未决信号集。掩码信号显示在 SigBlk:
位掩码中,未决信号显示在 SigPnd:
位掩码中。
如果您使用 sigaction()
而不是 signal()
安装信号处理程序,您可以指定 SA_NODEFER
标志以请求信号在其处理程序执行时不被屏蔽。您可以在您的程序中尝试此操作 - 使用一个或两个信号 - 并尝试预测输出结果。