繁体   English   中英

如何确保调用 `SIGINT` 信号处理程序的次数与按下 `Ctrl+C` 的次数相同(使用 `longjmp`)?

[英]How do I ensure the `SIGINT` signal handler is called as many times as `Ctrl+C` is pressed (with `longjmp`)?

设置

在下面的代码中,它只是打印一些文本直到超时,我为SIGINT添加了一个处理程序 ( onintr() )。 处理程序onintr()执行以下操作:

  1. 将自身重置为默认处理程序。
  2. 打印出一些文本。
  3. 调用longjmp()

问题

似乎只有第一个Ctrl+C被正确解释。

第一次按下Ctrl+C后, onintr()中的打印语句出现在屏幕上,执行返回到调用setjmp()的位置。 但是,随后的Ctrl+C调用将被忽略。

此外,将onintr()重置为处理程序似乎没有什么不同。 换句话说,如果我在onintr()被调用一次后让SIG_DFL成为默认处理程序,则后续的Ctrl+C -s 将被忽略,就像onintr()是处理程序时一样; 而且我无法终止该程序。

代码signal.c

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <setjmp.h>

jmp_buf sjbuf;
void onintr(int);

void 
onintr(int i) 
{
    signal(SIGINT, onintr);
    printf("\nInterrupt(%d)\n", i);
    longjmp(sjbuf, 0);
}

int 
main(int argc, char* argv[]) 
{
    int sleep_t = 1; 
    int ctr     = 0; 
    int timeout = 10;

    if (signal(SIGINT, SIG_IGN) != SIG_IGN)
        signal(SIGINT, onintr);

    setjmp(sjbuf);
    printf("Starting loop...\n");

    while (ctr < timeout) {
        printf("Going to sleep for %d second(s)\n", sleep_t);
        ctr++;
        sleep(sleep_t);
    }

    printf("\n");
    return 0;
}

行为

在 Ubuntu 22.04 上,我得到以下信息:

gomfy:signal$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

gomfy:signal$ gcc signal.c
gomfy:signal$
gomfy:signal$ ./a.out 
Starting loop...
Going to sleep for 1 second(s)
Going to sleep for 1 second(s)
^C
Interrupt(2)
Starting loop...
Going to sleep for 1 second(s)
Going to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
^CGoing to sleep for 1 second(s)
Going to sleep for 1 second(s)

gomfy:signal$ 

如您所见,随后的Ctrl+C -s ( ^C ) 将被忽略。 只有第一个似乎调用处理程序。

对于初学者,我们从signal-safety(7)中了解到:

异步信号安全函数是可以从信号处理程序中安全调用的函数。 许多功能不是异步信号安全的 特别是,不可重入函数通常从信号处理程序调用是不安全的。

这带来了两个问题:

  • printf不是异步信号安全的,并且

  • 混合信号处理程序、 longjmp(3)不安全函数会导致未定义的行为,如后面的注释部分所述:

如果信号处理程序中断不安全函数的执行,并且处理程序通过调用longjmp(3)siglongjmp(3)终止并且程序随后调用不安全函数,则程序的行为是未定义的。

这意味着如果SIGINT的传递碰巧中断了对printf的调用,那么程序就不能再可靠地推理了。


sleep(3)的通用 Linux 手册声称:

在 Linux 上, sleep() 是通过nanosleep(2)实现的。

可移植性说明
在某些系统上,可以使用 alarm(2) 和 SIGALRM(POSIX.1 允许这样做)来实现 sleep(); 混合调用 alarm(2) 和 sleep() 是个坏主意。

然后模棱两可地说:

从信号处理程序使用longjmp(3)或在休眠时修改 SIGALRM 的处理将导致未定义的结果。

尚不完全清楚这是否仅涉及前面提到的基于alarm的睡眠,但似乎很有可能。 sleep(3)的 POSIX 手册页似乎通过做出类似的声明来澄清这一点:

如果信号捕获函数中断 sleep() 并调用 siglongjmp() 或 longjmp() 以恢复在 sleep() 调用之前保存的环境,则与 SIGALRM 信号关联的操作以及 SIGALRM 信号被调度到的时间生成的是未指定的。

nanosleep(2)状态:

POSIX.1 明确规定 [nanosleep] 不与信号交互

可以说, sleep似乎不会导致这个问题,至少在 Linux 上是这样。


signal(2)的 Linux 手册强调了它的可移植性问题,并鼓励使用sigaction(2)

一个有趣的注释是:

如果配置设置为函数,则首先将配置重置为 SIG_DFL,或者信号被阻止(请参阅下面的可移植性),然后使用参数signum调用处理程序 如果处理程序的调用导致信号被阻塞,则信号在从处理程序返回时解除阻塞。

可移植性部分稍后详细介绍了 System V(重置)和 BSD(块)语义之间的差异,并指出 glibc 2+ 默认使用 BSD 语义(通过环绕sigaction(2) )。

因此,如果我们从处理程序中longjmp ,信号是否会被解除阻塞?

Linux 的信号概述signal(7)最终在标记为Execution of signal handlers的部分中阐明了内容,详细说明了五个步骤的过程。

第一步的最后一部分涉及:

当使用sigprocmask(2)注册处理程序时,在 act->sa_mask 中指定的任何信号都将添加到线程的信号掩码中。 传递的信号也被添加到信号掩码中,除非在注册处理程序时指定了 SA_NODEFER。 这些信号因此在处理程序执行时被阻塞。

而第四步和第五步是:

  1. 当信号处理程序返回时,控制传递给信号蹦床代码。
  1. 信号蹦床调用 sigreturn(2),这是一个系统调用,它使用在步骤 1 中创建的堆栈帧中的信息将线程恢复到调用信号处理程序之前的状态。 作为此过程的一部分,线程的信号掩码和备用信号堆栈设置将被恢复。 完成对 sigreturn(2) 的调用后,内核将控制权交还给用户空间,线程从被信号处理程序中断的位置重新开始执行。

同样,如果我们longjmp从处理程序中跳出会发生什么? 这是最重要的信息:

请注意,如果信号处理程序没有返回(例如,使用siglongjmp(3)将控制权从处理程序中移出,或者处理程序使用 execve(2) 执行新程序),则不会执行最后一步。 特别是,在这种情况下,如果希望解除阻止在进入信号处理程序时被阻止的信号,则程序员有责任恢复信号掩码的状态(使用sigprocmask(2) )。 (请注意,siglongjmp(3) 可能会或可能不会恢复信号掩码,具体取决于在对 sigsetjmp(3) 的相应调用中指定的 savesigs 值。)

所以答案是,通过跳出信号处理程序,信号掩码保留SIGINT并阻止后续信号的传递。 手册中提到使用sigprocmask(2)sigsetjmp(3)siglongjmp(3)来解决这个问题。

这是使用后者的一个简单示例。 sigsetjmp需要一个非零值作为它的第二个参数,它告诉这对函数保存和恢复信号掩码。 siglongjmp只是替换了longjmp ,而sigjmp_buf替换了jmpbuf

signalsigsetjmp之后移动,以避免在sigsetjmp执行之前交付SIGINT事件中的未定义行为,这会导致siglongjmp在垃圾sigjmp_buf上运行。

此外, ctr必须声明为volatile ,否则它的值是未指定的。 来自setjmp(3) (也适用于sigsetjmp ):

[...] 如果自动变量满足以下所有条件,则在调用 longjmp() 后未指定自动变量的值:

  • 它们对于进行相应 setjmp() 调用的函数是本地的;
  • 它们的值在调用 setjmp() 和 longjmp() 之间改变;
  • 它们没有被声明为volatile
#include <setjmp.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

sigjmp_buf sjbuf;

void dump(const char *s)
{
    write(STDOUT_FILENO, s, strlen(s));
}

void onintr(int i)
{
    dump("SIGINT caught. Jumping away...\n");
    siglongjmp(sjbuf, 42);
}

int main(void)
{
    volatile int ctr = 0;
    int timeout = 10;

    if (0 != sigsetjmp(sjbuf, 1))
        dump("Stuck the landing!\n");
    else
        signal(SIGINT, onintr);

    dump("Starting loop...\n");

    while (ctr < timeout) {
        dump("Going to sleep...\n");
        ctr++;
        sleep(1);
    }

    dump("\n");
}
Starting loop...
Going to sleep...
^CSIGINT caught. Jumping away...
Stuck the landing!
Starting loop...
Going to sleep...
^CSIGINT caught. Jumping away...
Stuck the landing!
Starting loop...
Going to sleep...
Going to sleep...
Going to sleep...
^CSIGINT caught. Jumping away...
Stuck the landing!
Starting loop...
Going to sleep...
Going to sleep...
Going to sleep...
^CSIGINT caught. Jumping away...
Stuck the landing!
Starting loop...
Going to sleep...
Going to sleep...

如果这仍然不起作用,您可能需要定义一个宏:glibc 2.19 及更早版本上的_BSD_SOURCE或 glibc 2.19 及更高版本中的_DEFAULT_SOURCE 请参阅: feature_test_macros(7)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM