【发布时间】:2020-09-30 13:08:46
【问题描述】:
我正在尝试为我的事件驱动应用程序添加一个信号处理程序以进行适当的清理。
我的 SIGINT 信号处理程序仅更改全局标志变量的值,然后在主循环中对其进行检查。为避免竞争,信号始终被阻止,pselect() 呼叫期间除外。这应该会导致在pselect() 调用期间仅传递未决信号,该调用应该被中断并以EINTR 失败。
这通常可以正常工作,除非在受监视的文件描述符上已经有未决的事件(例如,在重负载下,当文件描述符上总是有活动时)。
这个示例程序重现了这个问题:
#include <assert.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t stop_requested = 0;
void handle_signal(int sig)
{
// Use write() and strlen() instead of printf(), which is not async-signal-safe
const char * out = "Caught stop signal. Exiting.\n";
size_t len = strlen (out);
ssize_t writelen = write(STDOUT_FILENO, out, len);
assert(writelen == (ssize_t) len);
stop_requested = 1;
}
int main(void)
{
int ret;
// Install signal handler
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = handle_signal;
ret = sigaction(SIGINT, &sa, NULL);
assert(ret == 0);
}
// Block SIGINT
sigset_t old_sigmask;
{
sigset_t blocked;
sigemptyset(&blocked);
sigaddset(&blocked, SIGINT);
ret = sigprocmask(SIG_BLOCK, &blocked, &old_sigmask);
assert(ret == 0);
}
ret = raise(SIGINT);
assert(ret == 0);
// Create pipe and write data to it
int pipefd[2];
ret = pipe(pipefd);
assert(ret == 0);
ssize_t writelen = write(pipefd[1], "foo", 3);
assert(writelen == 3);
while (stop_requested == 0)
{
printf("Calling pselect().\n");
fd_set fds;
FD_ZERO(&fds);
FD_SET(pipefd[0], &fds);
struct timespec * timeout = NULL;
int ret = pselect(pipefd[0] + 1, &fds, NULL, NULL, timeout, &old_sigmask);
assert(ret >= 0 || errno == EINTR);
printf("pselect() returned %d.\n", ret);
if (FD_ISSET(pipefd[0], &fds))
printf("pipe is readable.\n");
sleep(1);
}
printf("Event loop terminated.\n");
}
此程序为SIGINT 安装一个处理程序,然后阻塞SIGINT,将SIGINT 发送给它自己(由于SIGINT 被阻塞,它还不会被传递),创建一个管道并将一些数据写入管道,然后监视管道的读取端的可读性。
这种可读性监控是使用pselect() 完成的,它应该解除对SIGINT 的阻塞,然后应该中断pselect() 并调用信号处理程序。
但是,在 Linux 上(我在 5.6 和 4.19 上测试过),pselect() 调用会返回 1 并指示管道的可读性,无需调用信号处理程序。由于该测试程序不会读取写入管道的数据,因此文件描述符将永远无法读取,并且永远不会调用信号处理程序。在实际程序中,在重负载下可能会出现类似的情况,其中可能会在不同的文件描述符(例如套接字)上读取大量数据。
另一方面,在 FreeBSD(我在 12.1 上测试)上,调用信号处理程序,然后 pselect() 返回 -1 并将 errno 设置为 EINTR。这也是我期望在 Linux 上发生的事情。
是我误解了什么,还是我错误地使用了这些接口?还是我应该退回到旧的self-pipe trick,(我相信)会更好地处理这种情况?
【问题讨论】:
-
附注:以这种方式使用
assert()是自找麻烦,因为表达式在非调试版本中都被删除了:断言不应该有副作用。但首先要感谢使用它们。 -
您可能会考虑使用
signalfd()而不是信号处理程序。 -
在 Linux 上,使用
eventfd()比使用自管道更好。 -
@SteveFriedl 你是绝对正确的。我不会在生产代码中这样做;这可以 100% 归因于懒惰。我只是认为这是在这个快速而肮脏的测试程序中添加“错误检查”的最简单方法。我将修复代码以消除断言的副作用。
-
@Shawn 在
signalfd()中的一些精彩历史:https://lwn.net/Articles/414618/。还有一个幽默的(至少对我来说)咆哮:https://ldpreload.com/blog/signalfd-is-useless