【问题标题】:pselect() on Linux does not deliver signals if events are pending如果事件未决,Linux 上的 pselect() 不会传递信号
【发布时间】: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

标签: c unix select signals


【解决方案1】:

这是一种由于始终以相同顺序检查活动资源而导致的资源匮乏。当资源总是以相同的顺序检查时,如果首先检查的资源足够繁忙,那么后面检查的资源可能永远不会得到任何关注。

What is starvation?

pselect() 的 Linux 实现显然会在检查信号之前检查文件描述符。 BSD 实现则相反。

对于它的价值,POSIX documentation for pselect() 声明:

如果选定的描述符都没有为请求的操作做好准备,pselect()select() 函数将阻塞,直到至少有一个请求的操作准备好,直到发生超时,或者直到被信号中断。

要严格阅读该描述,首先需要检查描述符。如果有任何描述符处于活动状态,pselect() 将返回该描述符,而不是在 errno 设置为 EINTR 时失败。

在这种情况下,如果描述符太忙以至于一个总是处于活动状态,那么信号处理就会变得匮乏。

如果信号来得太快,BSD 实现可能会饿死活动描述符。

一种常见的解决方案是每次select() 调用或类似返回时始终处理所有活动资源。但是您不能使用当前将信号与描述符混合的设计来做到这一点,因为如果有活动的描述符,pselect() 甚至不会检查待处理的信号。正如 @Shawn 在 cmets 中提到的,您可以使用 signalfd() 将信号映射到文件描述符。然后将来自signalfd() 的描述符添加到传递给pselect() 的文件描述符集中。

【讨论】:

  • 这是有道理的。我想我期待 BSD 行为,因为它在我的用例中更有意义(接收到信号时优雅地退出),其中文件描述符上可能有很多活动,而信号很少传递(如果有的话)。在这种情况下,信号处理不足不太可能成为问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-04
  • 2019-05-04
  • 1970-01-01
  • 1970-01-01
  • 2016-08-09
相关资源
最近更新 更多