C: SIGALRM - 每秒显示消息的闹钟信号

11

所以我正在尝试调用一个闹钟来每秒显示一条消息“仍在工作...”。 我包含了signal.h。

在我的主函数外面,我有一个函数:(我从未声明/定义int s)

void display_message(int s);   //Function for alarm set up
void display_message(int s) {
     printf("copyit: Still working...\n" );
     alarm(1);    //for every second
     signal(SIGALRM, display_message);
}

然后,在我的主要部分

while(1)
{
    signal(SIGALRM, display_message);
    alarm(1);     //Alarm signal every second.

当循环开始时,这个东西就已经在那里了。但是程序从来没有输出“仍在工作…”的消息。我做错了什么?非常感谢。


可能是 https://dev59.com/cHI-5IYBdhLWcg3wlpUM 的重复问题。 - txtechhelp
6个回答

19

信号处理程序不应包含“业务逻辑”或调用诸如printf的库函数。请参见C11 §7.1.4/4及其脚注:

因此,一般情况下,信号处理程序不能调用标准库函数。

所有信号处理程序应该做的是设置一个标志以供非中断代码处理,并取消等待系统调用的阻塞。即使添加了一些I / O或其他功能,该程序也能正确运行而不会崩溃:

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

volatile sig_atomic_t print_flag = false;

void handle_alarm( int sig ) {
    print_flag = true;
}

int main() {
    signal( SIGALRM, handle_alarm ); // Install handler first,
    alarm( 1 ); // before scheduling it to be called.
    for (;;) {
        sleep( 5 ); // Pretend to do something. Could also be read() or select().
        if ( print_flag ) {
            printf( "Hello\n" );
            print_flag = false;
            alarm( 1 ); // Reschedule.
        }
    }
}

如果您可以“保证”不能通过进程(直接或间接)正常调用非信号安全函数,通常可以调用它们。 signalalarm都是信号安全的,只要程序在构造函数/析构函数、共享库或其他线程中没有调用任何信号不安全的函数,该程序实际上是完全安全的。“信号不安全”表示一个函数可能不安全,而不是它就是不安全的,“您仅可以设置s ig_atomic_t标志”的说法自POSIX以来已经不再正确。 - yyny
构造函数和析构函数并非特殊,因此您需要在该列表中包含相同的(中断)线程。您可能会找到一种方法来在信号处理程序中执行所有 I/O,但这不是一种合理的编程方式。 - Potatoswatter
1
sleep()alarm()不应该同时使用。因此,你的例子并不是很好。Linux的man页面提到了这个问题。 - Alexis Wilke
@AlexisWilke 是的,但这仅是因为“睡眠”永远不知道是什么唤醒了它。我之所以选择这个作为例子,只是因为1)它是我可以随意添加更多虚拟代码的唯一阻塞功能,以及2)缩短延迟实际上并不会破坏“仍在工作”的消息。 - Potatoswatter
如果出现问题,例如发现高频率打印此类消息,则解决方案是使用 usleep 来跟踪时间。 - Potatoswatter

5

将对 signalalarm 的调用移到循环之前。高速重复调用 alarm 会不断将闹钟重置为距离该点一秒钟的时间,因此永远无法到达那一秒的结束点!

例如:

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

void display_message(int s) {
     printf("copyit: Still working...\n" );
     alarm(1);    //for every second
     signal(SIGALRM, display_message);
}

int main(void) {
    signal(SIGALRM, display_message);
    alarm(1);
    int n = 0;
    while (1) {
        ++n;
    }
    return 0;
}

我按照你建议的做了,但没有成功。还有什么可能会导致这种情况? - vivav
1
你为什么要在display_message中再次调用signal呢?一旦信号被设置,只需要另一个alarm调用即可。 - tgwaste
1
虽然许多示例展示了在信号处理程序中使用printf(),但这并不是一个好主意。它可能在简单的程序中运行良好,但随着程序变得更加复杂,很可能会出现问题。 - Alexis Wilke
@AlexisWilke printf会因为不可重入而崩溃吗?(显示交错字符在我看来不是完全的崩溃,但是很麻烦) - Paul Stelian
1
@PaulStelian 你会注意到的主要问题是输出被穿插。然而,printf() 可以被重定向到使用互斥锁/锁机制的文件系统,当该锁处于打开状态时发生另一个信号...下一个信号处理程序尝试自己的 printf()...并失败,因为第二个锁生成死锁。更多信息:https://docs.oracle.com/cd/E19455-01/806-5257/gen-26/index.html - Alexis Wilke
@AlexisWilke 所以基本上是的,它不是可重入的。 - Paul Stelian

3
不要调用 alarm() 两次,在 main() 中调用一次以启动回调,然后在 display_message() 中再调用一次。 在 Linux (Debian 7.8) 上尝试以下代码:
#include  <stdio.h>
#include  <signal.h>

void display_message(int s);   //Function for alarm set up

void display_message(int s)
{
     printf("copyit: Still working...\n" );
     alarm(1);    //for every second
     signal(SIGALRM, display_message);
}

int main()
{
    signal(SIGALRM, display_message);
    alarm(1);     // Initial timeout setting

     while (1) 
     {   
          pause();
     }   
}

结果将如下所示:
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...
copyit: Still working...

2
alarm()调用是用于单次信号的。
要重复一个闹钟,您必须在每次发生信号时再次调用alarm()
另一个问题是,您很可能会收到EINTR错误。许多系统函数在接收到信号时会被中断。这使得编程变得更加复杂,因为许多操作系统函数会受到影响。
无论如何,等待下一个SIGALRM的正确方法是使用pause()函数。其他人没有提到的一件事是(他们使用紧密循环,很丑陋!)
话虽如此,如果您只想要一个简单的sleep()调用,那么您尝试做的事情将变得更加容易:
// print a message every second (simplified version)
for(;;)
{
    printf("My Message\n");
    sleep(1);
}

这样的循环可以出现在一个单独的线程中。然后,您不需要Unix信号来实现此功能。

注意:sleep()函数实际上使用与alarm()相同的定时器,明确指出您不应在同一代码中混合使用两个函数。

sleep(3)可能使用SIGALRM实现;调用alarm()sleep(3)混合使用是不好的做法。

(来自Linux man alarm)

void alarm_handler(int)
{
    alarm(1);    // recurring alarm
}
    
int main(int argc, char *argv[])
{
    signal(SIGALRM, alarm_handler);
    alarm(1);

    for(;;)
    {
        printf("My Message\n");
        // ...do other work here if needed...
        pause();
    }

    // not reached (use Ctrl-C to exit)
    return 0;
}

您可以创建变量。例如,如果您希望第一条消息在1秒后而不是立即发生,请将pause()移至printf()之前。
“其他工作”注释假设您的其他工作不超过1秒。
如果需要并行工作,则可以在特定线程上获得警报信号,但是如果需要任何其他计时器(即,您不能轻松地与其他函数共享alarm()计时器),则可能会变得复杂。
附言:正如其他人所提到的,将您的printf()放在信号处理程序中根本不是一个好主意。
还有另一个版本,在其中alarm()main()内被重置,第一条消息出现在1秒后,并且循环运行60秒(1分钟):
void alarm_handler(int)
{
}
    
int main(int argc, char *argv[])
{
    signal(SIGALRM, alarm_handler);

    for(int seconds(0); seconds < 60; ++seconds)
    {
        alarm(1);
        // ...do other work here if needed...
        pause();
        printf("My Message\n");
    }

    // reached after 1 minute
    return 0;
}

请注意,使用此方法打印消息的时间将会被扭曲。在重新启动闹钟之前,打印消息的时间被添加到时钟中...因此每次调用之间始终会有略微超过1秒的间隔。另一个循环在这方面更好,但仍然存在偏差。对于一个完美(更好)的定时器,poll()函数要好得多,因为您可以指定下一次唤醒的时间。poll()只能与计时器一起使用。我的Snap库使用了这个功能(在文件底部查找run()函数)。在2019年,我将那个.cpp文件移动到eventdispatcher库中。 run()函数在communicator.cpp文件中。

0

POSIX允许从信号处理上下文中调用其某些函数,这些函数是异步信号安全的,请在此处搜索"async-signal safe"(这些函数可以被理解为"系统调用"而不是库调用)。值得注意的是,这包括write(2)

因此,您可以执行以下操作

void
display_message (int s) {
     static char const working_message [] = "copyit: Still working...\n";
     write (1, working_message, sizeof working_message - sizeof "");
     alarm(1);    /* for every second */
}

顺便提一下,精确的周期性闹钟最好使用setitimer(2)来实现, 因为这些不会受到漂移的影响。通过软件重新触发警报,就像在这里所做的那样,将不可避免地随着时间的推移累积误差,因为执行软件以及调度延迟所花费的时间。

在POSIX中,sigaction(2)出于很好的原因取代了signal(2): 原始的Unix信号处理模型非常简单。特别是, 一旦触发信号处理程序,它就会被重置为其原始“放置”状态(例如,终止进程)。您需要通过在display_message()中调用signal()之前调用alarm()来重新关联SIGALRMdisplay_message()

使用sigaction(2)的更重要的原因是SA_RESTART标志。通常,当调用信号处理程序时,系统调用会被中断。也就是说,当信号处理程序返回时,系统调用会返回一个错误指示(通常为-1),并且errno被设置为EINTR,即中断的系统调用。(这样做的一个原因是能够使用SIGALRM来实现超时,另一个原因是让更高级别的实例,比如用户,通过向当前进程发送信号(例如,在终端上按下control-C发送SIGINT)来“解除阻塞”当前进程)。
在您的情况下,您希望信号处理对代码的其余部分透明,因此在调用sigaction(2)时应设置SA_RESTART标志。这意味着内核应自动重新启动中断的系统调用。

-1

ooga 是正确的,你要不断重载警报器,这样它才不会响起。这个方法是有效的。我在这里加了一个 sleep,这样你就不用在循环中一直卡住自己了,但是根据你的目的地,你可能需要用更有用的东西来代替它。

void display_message(int s)
{
     printf("copyit: Still working...\n" );
    // alarm(1);    //for every second
    // signal(SIGALRM, display_message);
}

int main(int argc, char *argv[])
{
    int ret;

    while(1)
    {
        signal(SIGALRM, display_message);
        alarm(1);

        if ((ret = sleep(3)) != 0)
        {
            printf("sleep was interrupted by SIGALRM\n");
        }
    }

    return (0);
}

4
从信号处理程序调用printf是非常不安全的。 - Potatoswatter
在这个一次性程序中使用async-unsafe调用并不不安全。但好吧,@vivav,在信号处理程序中不要使用async-unsafe调用。 - Duck
1
真的吗?应该先打印哪一个,still working 还是 sleep was interrupted?也许机器会尝试同时打印两个。 - Potatoswatter
是的,真的。处理程序将运行并打印,然后sleep将返回EINTR并打印。由于在执行处理程序时几乎没有被另一个信号击中的机会,所以每次都是如此。 - Duck
1
不,不能保证信号处理程序将在与“main”相同的线程上运行(除非POSIX禁止OS私有线程,但我怀疑),也不能保证sleep将等待处理程序先返回。根据sleep的POSIX规范,对进程的任何信号都会导致立即返回。无论如何,这不是正确的编码方式。 - Potatoswatter
显示剩余2条评论

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