POSIX规定只有一个信号可以打断pselect吗?

9

POSIX pselect函数需要一个信号掩码参数。在函数开始执行之前,信号掩码被“原子性”设置为当前掩码,并在函数返回时恢复。

这允许在函数执行时取消掩码的信号再次被掩码。如果以这种方式取消掩码的信号被捕获,则保证* pselect函数将被该信号中断,并且(除非使用SA_RESTART标志指定了信号动作)将返回EINTR错误。

(*:或者呢?上面链接的文档中的语言似乎允许在pselect由于看到文件准备就绪或超时而解除阻塞和替换信号掩码与原始掩码之间接收到信号不一定会导致EINTR,因为如果“函数在阻塞时被中断...”,则需要EINTR - 但是,这最终不影响这个问题。)

我的问题是:假设在pselect执行期间,两个单独的信号被暂时取消屏蔽,是否可能在pselect函数返回之前都捕获到两个信号并恢复以前的信号掩码 - 或者在这种情况下只有一个信号会被捕获(留下另一个信号挂起)?(为了此问题的目的,假设对于信号操作未设置SA_RESTART,并且在通过sigaction建立信号处理程序时指定了所有信号在执行期间被屏蔽)。
我找不到任何表明只能处理一个信号的迹象,但我可能错过了什么,并且我正在编写一些代码,这将是一个非常有用的保证。我很想知道POSIX本身是否提供任何保证,以及不同的操作系统是否独立地提供这样的保证。
2个回答

1
不,但它也没有指定可以或必须使用多个信号。由于未指定,最好遵循通用规则,允许处理所有待处理未屏蔽信号。如果你试图严格依赖此规则,则可能会走上错误的道路,因为异步事件的时间难以预测。
一般来说,要实现一个“仅限一个”的限制将非常困难,因为操作系统运行时必须保留一个或多个挂起但未屏蔽的信号,直到某个未指定的时间点。请记住,在 pselect 被中断时运行的信号处理程序可以执行 siglongjmp 而不是返回,因此内核必须保持一个复杂的、可能无界的数据结构来跟踪要强制执行哪个信号掩码。
下面是您的测试程序的修改版。在这个版本中,每个事件通过write()发出一个字符串,因此没有缓冲问题。该程序将其“主”环境设置为掩盖SIGUSR1、SIGUSR2;但是当pselect正在运行时,它允许SIGUSR1、SIGUSR2、SIGTERM。 该程序forks,父进程(默认)坐在循环中调用pselect(),然后在完成后输出“。”。 子进程坐在循环中,向父进程传递SIGUSR1、SIGUSR2,然后睡眠一会儿。它在传递信号后输出“^”。 处理程序发出前缀“(1”或“(2”以响应SIGUSR1、SIGUSR2 resp;然后睡眠一会儿,并输出“)”以指示睡眠已完成。
我在macos(10.12.6,但我怀疑它并不重要)上看到的输出是: ^(2)(1).^(2)(1).^(2)(1).^(2)(1).终止:15 这表明每次调用pselect()时都会运行SIGUSR1和SIGUSR2的信号处理程序。这正是我所期望的;因为它被设计为不承认不确定性窗口,就像使用sigprocmasks()来括起select()一样。
#include <stdio.h>
#include <signal.h>
#include <sys/select.h>
#include <unistd.h>

void handle(int signo)
{
    char s[2];
    s[0] = '(';
    s[1] = signo == SIGUSR1? '1' : '2';
    write(1, s, 2);
    sleep(1);
    write(1, ")", 1);
}

int main(int argc, char **argv)
{
  sigset_t mask;
  sigemptyset(&mask);
  sigaddset(&mask, SIGUSR1);
  sigaddset(&mask, SIGUSR2);
  sigprocmask(SIG_SETMASK, &mask, NULL);

  sigfillset(&mask);
  sigdelset(&mask, SIGUSR1);
  sigdelset(&mask, SIGUSR2);
  sigdelset(&mask, SIGTERM);
  signal(SIGUSR1, handle);
  signal(SIGUSR2, handle);
  pid_t t = fork();
  switch (t) {
  default:
    while (1) {
            /* no USR1, USR2 */
            pselect(0, NULL, NULL, NULL, NULL, &mask);
            /* no USR1, USR2 */
            write(1, ".", 1);
    }
    break;
  case 0:
    t = getppid();
    for (int i = 0; i < 4; i++) {
        kill(t, SIGUSR1);
        kill(t, SIGUSR2);
        write(1, "^", 1);
        sleep(5);
    }
    kill(t, SIGTERM);
    break;
  case -1:
    perror("fork\n");
  }
  return 0;
}

我得出了类似的结论,但我不明白你关于“只有一个”限制难度的逻辑,因为如果处理的信号执行了siglongjmp,那么在pselect执行时肯定会排除其他信号被接收到(即手动强制执行“只有一个”限制)。实际上,这就是我最终使用的解决方案,以确保我在pselect中仅检测到一个信号。如果您感兴趣,可以在此处查看代码:https://github.com/davmac314/dasynq/blob/master/dasynq-pselect.h - davmac
在pselect内部,它必须选择允许哪个信号,并等待一段时间后再允许哪个信号,因为不能保证信号处理程序会返回(siglongjmp),所以它需要将该决策存储在某个地方,以便最终运行该处理程序。这相当于一个“向上”调用,即使您不会将其实现为这样,每个其他调用都必须检查是否到了运行该处理程序的时候。 - mevets
随着我深入挖掘,我重新使用了之前发生但未交付的两个信号的掩码,内核再次必须选择一个并存储另一个。 - mevets
让我们在聊天中继续这个讨论 - mevets
是的,抱歉,我没有仔细阅读你的问题。当信号处理程序返回时,“main”信号掩码将是生效的信号掩码。我以为你在提出完全不同的建议。 - mevets
显示剩余3条评论

0

我已经继续搜索并没有找到额外的信息,所以我只能得出结论:在POSIX中通常没有保证。

在Linux下,如果我正确理解下面的代码,只能处理一个信号(假设信号处理程序本身不会取消屏蔽信号):相关代码和一个揭示性的注释位于fs/select.c中的do_pselect函数中:

ret = core_sys_select(n, inp, outp, exp, to);
ret = poll_select_copy_remaining(&end_time, tsp, 0, ret);

if (ret == -ERESTARTNOHAND) {
    /*
     * Don't restore the signal mask yet. Let do_signal() deliver
     * the signal on the way back to userspace, before the signal
     * mask is restored.
     */
    if (sigmask) {
        memcpy(&current->saved_sigmask, &sigsaved,
                sizeof(sigsaved));
        set_restore_sigmask();
    }
} else ...

它基本上从系统调用返回,允许信号处理程序执行,之后原始信号掩码将立即恢复(来自current->saved_sigmask,因为set_restore_sigmask()设置了一个标志,指示应该发生这种情况)。

以下测试程序验证了这一点:

#include <stdio.h>
#include <signal.h>
#include <sys/select.h>

volatile sig_atomic_t got_usr1 = 0;
volatile sig_atomic_t got_usr2 = 0;

void handle_usr1(int signo, siginfo_t *info, void *v)
{
  got_usr1 = 1;
}

void handle_usr2(int signo, siginfo_t *info, void *v)
{
  got_usr2 = 1;
}

int main(int argc, char **argv)
{
  // mask SIGUSR1 and SIGUSR2:
  sigset_t curmask;
  sigemptyset(&curmask);
  sigaddset(&curmask, SIGUSR1);
  sigaddset(&curmask, SIGUSR2);
  sigprocmask(SIG_SETMASK, &curmask, NULL);

  // Create a mask for all but SIGUSR1 and SIGUSR2:
  sigset_t mask;
  sigfillset(&mask);
  sigdelset(&mask, SIGUSR1);
  sigdelset(&mask, SIGUSR2);

  // Set up signal handlers:
  struct sigaction action;
  action.sa_sigaction = handle_usr1;
  sigfillset(&action.sa_mask);
  action.sa_flags = SA_SIGINFO;
  sigaction(SIGUSR1, &action, NULL);

  action.sa_sigaction = handle_usr2;
  sigaction(SIGUSR2, &action, NULL);

  // Make signals pending:
  raise(SIGUSR1);
  raise(SIGUSR2);

  // pselect with no file descriptors and no timeout:
  pselect(0, NULL, NULL, NULL, NULL, &mask);

  int count = got_usr1 + got_usr2;

  printf("Handled %d signals while in pselect.\n", count);
  return 0;
}

在Linux上,以上代码的输出结果始终如一:

在pselect中处理了1个信号。

这在FreeBSD上似乎也是这样,但我不想指望所有其他平台都是这种情况。我找到的确保只能处理一个信号的解决方案是使用siglongjmp跳出信号处理程序以及pselect调用,同时恢复信号掩码,以便不再处理任何其他信号。

基本上,代码看起来像这样:

    jmp_buf jbuf; // signal handlers have access to this

    if (sigsetjmp(jbuf, 1) != 0) {
        // We received a signal while in pselect ...
    }

    int r = pselect(nfds, &read_set_c, &write_set_c, &err_set, wait_ts, &sigmask);

信号处理程序必须执行 siglongjmp:
void signal_handler(int signo, siginfo_t *siginfo, void *v)
{
    siglongjmp(jbuf, 1);
}

这感觉有些老旧,但似乎在我测试过的所有平台上都可以工作(Linux、MacOS 和 FreeBSD) - 此外,它似乎受到 POSIX 的普遍支持。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接