繁体   English   中英

当我发送两个 sigint 信号 Ctrl-C 时,为什么我的信号处理程序不工作?

[英]Why is my signal handler not working when I send two sigint signals Ctrl-C?

我正在学习在 C 中为 Linux 系统编写信号处理程序。 这是我的代码:

#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void sig_handler(int signum){


  //Return type of the handler function should be void
  printf("\nInside handler function\n");
}


int main(){
  signal(SIGINT,sig_handler); // Register signal handler
  for(int i=1;;i++){    //Infinite loop
    printf("%d : Inside main function\n",i);
    sleep(1);  // Delay for 1 second
  }
  return 0;
}

我的问题是,为什么当我按 Ctrl + C两次时,程序停止了? 难道不是每次我按下Ctrl + C时信号处理程序都会运行,所以程序应该永远运行吗?

实际上,这是我的 output,信号处理程序仅在第一次Ctrl + C中被调用,而不是第二次:

1 : Inside main function
2 : Inside main function
3 : Inside main function
4 : Inside main function
^C
 Inside handler function
5 : Inside main function
6 : Inside main function
7 : Inside main function
8 : Inside main function
9 : Inside main function
10 : Inside main function
^C

在 Linux 上,许多因素都会影响signal的行为。 根据 glibc 的版本、定义的feature_test_macros(7)以及是否使用 function 的 kernel 版本,结果将在两种极化行为之间有所不同,描述为System V 语义BSD 语义

使用System V 语义,当调用信号处理程序时,信号的处理将重置为其默认行为 ( SIG_DFL )。 此外,在信号处理程序执行期间,信号的进一步传递不会被阻塞。

使用BSD 语义,当调用信号处理程序时,不会重置信号的处理,并且在信号处理程序执行期间阻止进一步传递信号。 此外,如果被信号处理程序中断,某些系统调用将自动重新启动。

您的signal版本似乎提供了 System V 语义。 信号处理在第一个信号处理程序后重置,随后的SIGINT终止程序。

有关详细信息,请参阅signal(2)及其关于可移植性的注释。


请参阅signal-safety(7)以获取可从信号处理程序中安全调用的函数列表。 printf不是异步信号安全的function,不应从信号处理程序中调用。

请改用write(2)


最快的解决方法是在信号处理程序中调用signal以再次将信号处理设置为信号处理程序。

void sig_handler(int signum)
{
    (void) signum;

    char msg[] = "Signal handler called!\n";
    write(STDOUT_FILENO, msg, strlen(msg));
    signal(SIGINT, sig_handler);
}

更健壮的解决方案是使用sigaction(2)来建立信号处理程序,因为它的行为更一致并提供更好的可移植性。

一个基本的例子:

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

volatile sig_atomic_t sig_count = 0;

void sig_handler(int signum)
{
    (void) signum;

    sig_count++;
    char msg[] = "Signal handler called!\n";
    write(STDOUT_FILENO, msg, strlen(msg));
}

int main(void)
{
    struct sigaction sa = { .sa_handler = sig_handler };
    sigaction(SIGINT, &sa, NULL);

    while (sig_count < 5);
}

sigaction的默认行为类似于BSD 语义描述的行为,不同之处在于某些系统调用在被信号处理程序中断时不会重新启动。

要启用此行为, struct sigaction.sa_flags成员应包含SA_RESTART标志。

为了模仿System V语义, struct sigaction.sa_flags成员应该包含SA_RESETHANDSA_NODEFER标志。

TL;DR :使用sigaction()而不是signal()来安装信号处理程序。

正如评论和其他答案所观察到的那样,您的信号处理程序由于调用printf()而具有未定义的行为。 如果信号处理程序曾经被触发,则会给整个程序带来 UB,这使得很难推断其观察到的行为。

但是请考虑这种变化:

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

void sig_handler(int signum){
  static const char msg[] = "\nInside handler function\n";
  write(1, msg, sizeof(msg) - 1);
}

int main(){
  signal(SIGINT,sig_handler); // Register signal handler
  for(int i=1;;i++){    //Infinite loop
    printf("%d : Inside main function\n",i);
    sleep(1);  // Delay for 1 second
  }
  return 0;
}

我已经从printf切换到在信号处理程序内部write ,从而删除了 UB。 如果我编译它

gcc -std=c11 htest.c -o htest

那么生成的可执行文件仍然表现出您描述的行为:第一个 Ctrl-C 被处理,但第二个不是。

但是,如果我改为编译

gcc htest.c -o htest

然后生成的程序会拦截我键入的每个Ctrl-C,正如我猜你所期望的那样。 发生什么了?


问题围绕着这样一个事实,即signal() function 的行为细节在历史上有所不同。 有关详细信息,请参阅signal ( 2 ) 手册页中的可移植性说明,但这里有一个简短的摘要:

在最初的 System V UNIX 中,通过signal()安装的自定义信号处理程序是一次性的:当此类处理程序被触发时,信号的配置将重置为其默认值,并且信号在处理程序执行期间不会被阻塞.

System V 行为存在一些问题,因此 BSD 在这些方面以不同方式实现了signal() :信号处理不会自动重置,并且信号在处理程序执行期间阻塞。 此外,在 BSD 中,如果被不会导致程序终止的信号中断,某些阻塞系统调用会自动重新启动。

这些差异意味着signal() function 的唯一可移植用途是将信号处置设置为SIG_DFLSIG_IGN

Glibc 支持这两种选择,你得到的是哪一个是由特性测试宏控制的,它可以受编译器命令行选项的影响。 只要定义了_DEFAULT_SOURCE宏,它就默认为 BSD 语义(glibc 2.19 之前的_BSD_SOURCE )。 Gcc 默认定义该宏,但一些命令行选项,特别是严格一致性-std选项,导致 Gcc 不定义它。 这就是为什么我可以根据编译程序的方式获得不同的行为。

在 POSIX 系统上,解决方案是使用sigaction() function 而不是signal()来注册信号处理程序。 这个 function 对上述所有行为差异领域都有明确定义的语义,包括在它们之间进行选择的明确定义方式。 但是,要在所有情况下都包含它的声明,您需要确保定义了不同的功能测试宏。 例如:

// Manipulation of feature-test macros should precede all header inclusions
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 1
#endif

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

void sig_handler(int signum) {
    static const char msg[] = "\nInside handler function\n";
    write(1, msg, sizeof(msg) - 1);
}

int main(void) {
    // Register signal handler
    struct sigaction sa = { .sa_handler = sig_handler /* default sa_mask and sa_flags */ };
    sigaction(SIGINT, &sa, NULL);

    while (int i = 1; ; i++) {
        printf("%d : Inside main function\n",i);
        sleep(1);
    }

    return 0;
}

这将可靠地为您提供一个处理程序,该处理程序在触发时不会重置,在处理时确实阻塞了SIGINT ,并且不会自动重新启动被信号中断的系统调用。 这将防止程序通过SIGINT被杀死,尽管还有其他方法可以杀死它,例如通过SIGKILL (无法阻止或处理)。

我的问题是,为什么当我按两次 Ctrl+C 时,程序停止了?


未定义的行为:

您的代码调用未定义的行为,因为它调用了一个非异步信号安全的 function ( printf )。¹

来自 C11:

如果信号不是由于调用中止或引发 function 而发生,如果信号处理程序引用任何 object 和 static 或线程存储持续时间不是无锁原子 object 不是通过分配值,则行为未定义to an object declared as volatile sig_atomic_t, or the signal handler calls any function in the standard library other than the abort function, the _Exit function, the quick_exit function, or the signal function with the first argument equal to the signal number corresponding to the signal这导致了处理程序的调用。

根据 C 标准,您只能拨打:

abort()
_Exit()
quick_exit()
signal()  /* With the first argument equal to the signal number the handler caught */

安全地在信号处理程序中。


然而,POSIX 标准指定了更多的函数。 所以你可以有write() ,但不能有printf()


[1] —异步信号安全 function 是一种可以从信号处理程序中安全调用的信号处理程序。 (Linux 手册页)

不要使用signal() 而是使用sigaction()

根据规范, [w]当信号发生时,[ signal的第二个参数] 指向 function,实现定义是否等同于signal(sig, SIG_DFL); 被执行...

从历史上看,某些 UNIX 风格会在进入用户定义的信号处理程序时将信号处理重置为默认行为。 这意味着您的第一个 SIGFOO 将调用您的处理程序,但您的下一个将触发 SIG_DFL 行为。 重要的是,这与您观察到的一致。

是的,在信号处理程序中调用printf()是不安全的(未定义的行为)。 在您提供的代码中,它可能会自行中断,这会导致各种麻烦。 实际上,人们总是在琐碎的程序中调用printf()并无大碍。 你的行为是可重复的这一事实表明你正在获得确定性行为,而不是可怕的魔法未定义行为。

sigaction()强制您在调用用户提供的处理程序时指定您想要的行为类型,因此优于signal()

暂无
暂无

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

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