探究用Linux信号处理函数安全退出进程

    • 来自glibc文档例子的困惑

查看glibc文档的 24.4.2 Handlers That Terminate the Process 一节时觉得它的例子有点奇怪,整个例子和我奇怪的地方如下:

volatile sig_atomic_t fatal_error_in_progress = 0;

void
fatal_error_signal (int sig)
{
  /* Since this handler is established for more than one kind of signal, 
     it might still get invoked recursively by delivery of some other kind
     of signal.  Use a static variable to keep track of that. */
  if (fatal_error_in_progress)
    raise (sig);    /// 当发现有信号处理函数已经在执行的时候直接发了一个接收到的信号,问题:这个信号一旦 raise 会不会立即中断当前的信号处理函数而重新触发一个信号处理函数,如果不是,这次的信号处理函数继续执行会不会导致 clean up 清理动作在不同的信号处理中执行多次。
  fatal_error_in_progress = 1;

  /* Now do the clean up actions:
     - reset terminal modes
     - kill child processes
     - remove lock files */
  …

  /* Now reraise the signal.  We reactivate the signal’s
     default handling, which is to terminate the process.
     We could just call exit or abort,
     but reraising the signal sets the return status
     from the process correctly. */
  signal (sig, SIG_DFL);
  raise (sig);
}

假设 fatal_error_signal 注册为 SIGINT 和 SIGTERM 的信号处理函数,它应该需要防止中间的清理操作 clean up actions 被多次执行。第一段的注释原文告诉我们因为这个处理函数为多种信号创建的,它本身可能被其它类型的信号再次触发,所以用一个静态变量 fatal_error_in_progress 来跟踪信号处理函数的状态,它的值为1时说明已经有一个本信号处理函数在执行了。

关于中止进程的信号处理函数的官方说明:

中止程序的信号处理函数通常用于程序接收到错误信号或者外部交互中断信号之后进行有序地清理或者恢复。信号处理函数终止一个进程最干净的方法是发起一个与最初触发信号处理函数的信号相同的信号。

只是靠上面这个例子难道就可以避免清理动作重复执行了吗?

既然有疑问,那么就做一下实验来搞清楚到底是怎么一回事情。

    • 实验

    • 第 1 个实验:什么预防措施都没有的信号处理函数

以下代码把 fatal_error_signal 函数设置成了SIGINT、SIGTERM、SIGUSER2 这3种信号的信号处理函数,fatal_error_signal 函数直接做clean_up没有做任何预防clean_up被多次执行的措施:

#include 
#include 
#include 
#include 

/// 信号处理函数 fatal_error_signal 每次被调用时都会取一个识别编号
static int handler_no = 10000;

/// 模拟清理动作的函数
void clean_up(int handler_no)
{
    struct timespec remaining, request = {1, 100};
    /// 模拟程序异常退出时的清理动作, 为了能够看清过程, 每个步骤中间都用 nanosleep 暂停 1 秒钟
    printf("Handler %d started.\n", handler_no);
    for (size_t i = 0; i < 5; i++)
    {
        nanosleep(&request, &remaining);
        printf("Handler %d clean up done %ld%%.\n", handler_no, (i + 1) * 20);
    }
    printf("Handler %d done.\n", handler_no);
}

/// 信号处理函数
void fatal_error_signal(int sig)
{
    /// 取当前编号值到 my_no 后将编号值增加 1, 以便于 fatal_error_signal 下次被调用时使用
    int my_no = handler_no++;
    printf("Handler %d received signal %d\n", my_no, sig);
    clean_up(my_no);
}

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

    struct timespec remaining, request = {5, 100};

    /// 把 fatal_error_signal 函数设置成 SIGINT、SIGTERM、SIGUSER2 这3种信号的信号处理函数
    struct sigaction sa, old_sa;
    sa.sa_handler = fatal_error_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGUSR2, &sa, NULL);

    /// 让主程序 sleep 50 秒, 便于我们有机会向它发送各种信号
    printf("Main fall asleep\n");
    for (size_t i = 0; i < 10; i++)
    {
        nanosleep(&request, &remaining);
        printf("Main has slept for %ld seconds\n", (i + 1) * 5);
    }
    printf("Main awake\n");

    return 0;
}

运行这个程序,用htop工具向这个程序依次连续快速发送 SIGINT(2)、SIGTERM(15)、SIGTERM(15)、SIGUSER2(12) 这 4 个信号给这个程序,其运行结果如下:

Main fall asleep
Main has slept for 5 seconds
Handler 10000 received signal 2
Handler 10000 started.
Handler 10000 clean up done 20%.
Handler 10001 received signal 15
Handler 10001 started.
Handler 10001 clean up done 20%.
Handler 10001 clean up done 40%.
Handler 10002 received signal 12
Handler 10002 started.
Handler 10002 clean up done 20%.
Handler 10002 clean up done 40%.
Handler 10002 clean up done 60%.
Handler 10002 clean up done 80%.
Handler 10002 clean up done 100%.
Handler 10002 done.
Handler 10001 clean up done 60%.
Handler 10001 clean up done 80%.
Handler 10001 clean up done 100%.
Handler 10001 done.
Handler 10003 received signal 15
Handler 10003 started.
Handler 10003 clean up done 20%.
Handler 10003 clean up done 40%.
Handler 10003 clean up done 60%.
Handler 10003 clean up done 80%.
Handler 10003 clean up done 100%.
Handler 10003 done.
Handler 10000 clean up done 40%.
Handler 10000 clean up done 60%.
Handler 10000 clean up done 80%.
Handler 10000 clean up done 100%.
Handler 10000 done.
Main has slept for 10 seconds
Main has slept for 15 seconds
Main has slept for 20 seconds
Main has slept for 25 seconds
Main has slept for 30 seconds
Main has slept for 35 seconds
Main has slept for 40 seconds
Main has slept for 45 seconds
Main has slept for 50 seconds
Main awake

从这个运行结果可以解读到在没有任何特殊处理的情况下,会发生以下的事情:

  1. linux系统默认会阻塞 (block) 与当前正在处理的信号相同的信号

我们连续发送了 2 个 SIGTERM(15) 信号给这个进程,第 1 个 SIGTERM(15) 信号触发了 10001 号信号处理函数,而第2个 SIGTERM(15) 信号在整个 10001 号处理函数运行过程中都没有触发信号处理函数,反而是第 2 个 SIGTERM(15) 之后的 SIGUSER2(12) 信号先触发了 10002 号处理函数运行。我们看到 10001 号处理函数的运行被 SIGUSER2(12) 的 10002号处理函数给中断了,10002号处理函数运行完成之后 10001号处理函数继续运行,当 10001 号处理函数运行完成之后才收到第 2 个 SIGTERM(15) 信号并触发了 10003 号处理函数的运行。

这说明,当第 1 个 SIGTERM(15) 信号的处理函数在运行时,linux系统会阻塞 (block) 掉 SIGTERM(15) 信号,如果此时又发生同样的SIGTERM(15) 信号,系统会等当前此信号的处理函数执行完之后再解除阻塞将信号发送给进程。

  1. clean_up 被执行了 4 次,而且每次执行过程都可能被另外的信号中断

SIGINT(2) 触发的 10000 号处理函数运行到20%时被第1个 SIGTERM(15) 的 10001 号处理函数中断,10001号处理函数执行到 40%时又被SIGUSER2(12) 处理函数10002号中断,10002号处理函数执行完成之后10001号处理函数继续执行,10001号完成时系统又发送了第 2 个SIGTERM(15)触发了10003号处理函数,当10003号处理函数完成时才继续运行 10000号处理函数。

假设清理函数中包含回收内存之类的操作,被执行多次肯定会出异常。

  1. 信号处理函数做完清理之后,不会使进程结束

在所有信号处理函数执行完成之后,会继续执行 main 函数中的语句,并没有使进程退出。

    • 第 2 个实验:按照 glibc 文档提供的例子来做预防

修改第 1 个实验的代码,按照 glibc 文档的例子改动了 fatal_error_signal 函数:

#include 
#include 
#include 
#include 
/// 跟踪处理函数是不是已经在执行的静态变量
volatile sig_atomic_t fatal_error_in_progress = 0;
/// 信号处理函数 fatal_error_signal 每次被调用时都会取一个识别编号
static int handler_no = 10000;

/// 模拟清理动作的函数
void clean_up(int handler_no)
{
    struct timespec remaining, request = {1, 100};
    /// 模拟程序异常退出时的清理动作, 为了能够看清过程, 每个步骤中间都用 nanosleep 暂停 1 秒钟
    printf("Handler %d started.\n", handler_no);
    for (size_t i = 0; i < 5; i++)
    {
        nanosleep(&request, &remaining);
        printf("Handler %d clean up done %ld%%.\n", handler_no, (i + 1) * 20);
    }
    printf("Handler %d done.\n", handler_no);
}

/// 信号处理函数
void fatal_error_signal(int sig)
{
    /// 取当前编号值到 my_no 后将编号值增加 1, 以便于 fatal_error_signal 下次被调用时使用
    int my_no = handler_no++;
    printf("Handler %d received signal %d\n", my_no, sig);

    /* Since this handler is established for more than one kind of signal,
       it might still get invoked recursively by delivery of some other kind
       of signal.  Use a static variable to keep track of that. */
    if (fatal_error_in_progress)
    {
        printf("%d fatal_error_in_progress is true\n", my_no);
        raise(sig);
    }
    fatal_error_in_progress = 1;

    /* Now do the clean up actions:
       - reset terminal modes
       - kill child processes
       - remove lock files */
    clean_up(my_no);

    /* Now reraise the signal.  We reactivate the signal’s
       default handling, which is to terminate the process.
       We could just call exit or abort,
       but reraising the signal sets the return status
       from the process correctly. */
    signal(sig, SIG_DFL);
    raise(sig);
}

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

    struct timespec remaining, request = {5, 100};

    /// 把 fatal_error_signal 函数设置成 SIGINT、SIGTERM、SIGUSER2 这3种信号的信号处理函数
    struct sigaction sa, old_sa;
    sa.sa_handler = fatal_error_signal;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGUSR2, &sa, NULL);

    /// 让主程序 sleep 50 秒, 便于我们有机会向它发送各种信号
    printf("Main fall asleep\n");
    for (size_t i = 0; i < 10; i++)
    {
        nanosleep(&request, &remaining);
        printf("Main has slept for %ld seconds\n", (i + 1) * 5);
    }
    printf("Main awake\n");

    return 0;
}

和第1个实验一样,运行这个程序,用htop工具向这个程序依次连续快速发送 SIGINT(2)、SIGTERM(15)、SIGTERM(15)、SIGUSER2(12) 这 4 个信号给这个程序,其运行结果如下:

Main fall asleep
Main has slept for 5 seconds
Main has slept for 10 seconds
Handler 10000 received signal 2
Handler 10000 started.
Handler 10000 clean up done 20%.
Handler 10001 received signal 15
10001 fatal_error_in_progress is true
Handler 10001 started.
Handler 10001 clean up done 20%.
Handler 10001 clean up done 40%.
Handler 10001 clean up done 60%.
Handler 10002 received signal 12
10002 fatal_error_in_progress is true
Handler 10002 started.
Handler 10002 clean up done 20%.
Handler 10002 clean up done 40%.
Handler 10002 clean up done 60%.
Handler 10002 clean up done 80%.
Handler 10002 clean up done 100%.
Handler 10002 done.
User defined signal 2

从运行结果来看,虽然有跟踪本信号处理函数是否已经在执行了的机制,但是 clean_up 还是被执行了 3 次(前 2 次执行中途被中断没有完整执行)。

最开始关于第37行的raise语句的疑问得到部分解答,也就是由于这个raise语句发送了一个与本次接收到的信号相同的信号,而这个信号触发的信号处理函数当前正在执行,所以raise发送的信号会暂时被阻塞直到本次信号处理函数执行完成,因此raise所发送的信号不会立即中断当前正在运行的信号处理函数。比如在 10001号执行中,接收到的是SIGTERM(15)信号,那这里的raise又会发送一个SIGTERM(15)给本进程,因为当前正在执行SIGTERM(15)信号触发的处理函数,所以新发送的信号被阻塞,没有立即中断当前信号处理函数的执行。

但是,当 fatal_error_in_progress 标明已经有信号处理函数正在执行了的时候为什么又 raise 一个相同的信号呢?从实验结果来看,它也没有起到作用,因为在 raise 语句执行之后,这个打断了原先正在执行的10000处理函数而新执行的10001处理函数还是执行了它自己的clean_up。

和10001号执行一样SIGUSER2(12)触发的10002号执行的处理函数中断了10001号。只有10002号执行完成,重置了默认的信号处理函数,然后又发送了一个SIGUSER2(12)信号。

    • 第3个实验: 用return替换掉 raise

根据第2个实验的结果,似乎包含在if (fatal_error_in_progress)判断内的raise更应该是直接退出新的信号处理函数而让被中断的老的信号处理函数继续执行。我们试一下:

/// 信号处理函数
void fatal_error_signal(int sig)
{
...
    if (fatal_error_in_progress)
    {
        printf("%d fatal_error_in_progress is true\n", my_no);
        // 用 return 替换 raise(sig);
        return;
    }
    fatal_error_in_progress = 1;
...
}

和上面2个实验一样,运行这个程序,用htop工具向这个程序依次连续快速发送 SIGINT(2)、SIGTERM(15)、SIGTERM(15)、SIGUSER2(12) 这 4 个信号给这个程序,其运行结果如下:

Main fall asleep
Main has slept for 5 seconds
Handler 10000 received signal 2
Handler 10000 started.
Handler 10001 received signal 15
10001 fatal_error_in_progress is true
Handler 10000 clean up done 20%.
Handler 10002 received signal 15
10002 fatal_error_in_progress is true
Handler 10000 clean up done 40%.
Handler 10000 clean up done 60%.
Handler 10003 received signal 12
10003 fatal_error_in_progress is true
Handler 10000 clean up done 80%.
Handler 10000 clean up done 100%.
Handler 10000 done.

这次的结果似乎更加符合我们希望的,虽然每个信号都触发了一次信号处理函数的运行,但是只有 10000 号执行的信号处理函数中的 clean_up被完整的执行了。

    • 第4个实验:为信号处理函数添加阻塞信号

根据glibc文档的 24.7.5 Blocking Signals for a Handler 节的说明,可以使用 sa_mask 成员来使信号处理函数执行的时候自动阻塞(block)指定的几种信号。

修改第2个实验的main函数如下(为了清晰我就不节选代码了):

#include 
#include 
#include 
#include 
/// 跟踪处理函数是不是已经在执行的静态变量
volatile sig_atomic_t fatal_error_in_progress = 0;
/// 信号处理函数 fatal_error_signal 每次被调用时都会取一个识别编号
static int handler_no = 10000;

/// 模拟清理动作的函数
void clean_up(int handler_no)
{
    struct timespec remaining, request = {1, 100};
    /// 模拟程序异常退出时的清理动作, 为了能够看清过程, 每个步骤中间都用 nanosleep 暂停 1 秒钟
    printf("Handler %d started.\n", handler_no);
    for (size_t i = 0; i < 5; i++)
    {
        nanosleep(&request, &remaining);
        printf("Handler %d clean up done %ld%%.\n", handler_no, (i + 1) * 20);
    }
    printf("Handler %d done.\n", handler_no);
}

/// 信号处理函数
void fatal_error_signal(int sig)
{
    /// 取当前编号值到 my_no 后将编号值增加 1, 以便于 fatal_error_signal 下次被调用时使用
    int my_no = handler_no++;
    printf("Handler %d received signal %d\n", my_no, sig);

    /* Since this handler is established for more than one kind of signal,
       it might still get invoked recursively by delivery of some other kind
       of signal.  Use a static variable to keep track of that. */
    if (fatal_error_in_progress)
    {
        printf("%d fatal_error_in_progress is true\n", my_no);
        raise(sig);
    }
    fatal_error_in_progress = 1;

    /* Now do the clean up actions:
       - reset terminal modes
       - kill child processes
       - remove lock files */
    clean_up(my_no);

    /* Now reraise the signal.  We reactivate the signal’s
       default handling, which is to terminate the process.
       We could just call exit or abort,
       but reraising the signal sets the return status
       from the process correctly. */
    signal(sig, SIG_DFL);
    raise(sig);
}

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

    struct timespec remaining, request = {5, 100};

    /// 把 fatal_error_signal 函数设置成 SIGINT、SIGTERM、SIGUSER2 这3种信号的信号处理函数
    struct sigaction sa, old_sa;
    sa.sa_handler = fatal_error_signal;
    sigemptyset(&sa.sa_mask);
    /// fatal_error_signal 处理函数执行时阻塞 SIGINT SIGTERM SIGUSR2
    sigaddset(&sa.sa_mask, SIGINT);
    sigaddset(&sa.sa_mask, SIGTERM);
    sigaddset(&sa.sa_mask, SIGUSR2);
    sa.sa_flags = 0;
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
    sigaction(SIGUSR2, &sa, NULL);

    /// 让主程序 sleep 50 秒, 便于我们有机会向它发送各种信号
    printf("Main fall asleep\n");
    for (size_t i = 0; i < 10; i++)
    {
        nanosleep(&request, &remaining);
        printf("Main has slept for %ld seconds\n", (i + 1) * 5);
    }
    printf("Main awake\n");

    return 0;
}

和上面所有实验一样,运行这个程序,用htop工具向这个程序依次连续快速发送 SIGINT(2)、SIGTERM(15)、SIGTERM(15)、SIGUSER2(12) 这 4 个信号给这个程序,其运行结果如下:

Main fall asleep
Main has slept for 5 seconds
Main has slept for 10 seconds
Handler 10000 received signal 2
Handler 10000 started.
Handler 10000 clean up done 20%.
Handler 10000 clean up done 40%.
Handler 10000 clean up done 60%.
Handler 10000 clean up done 80%.
Handler 10000 clean up done 100%.
Handler 10000 done.

这次只有 SIGINT(2) 触发的 10000号完整地执行,其它3个信号根本就没有打断它。原因就是因为有66、67、68行的sigaddset,当10000号信号处理函数执行的时候,SIGINT、SIGTERM、SIGUSER2这3种信号都被系统阻塞(block)了。

    • 结论

我看到有个帖子也表达了对glibc文档 24.4.2 Handlers That Terminate the Process的疑惑,预防clean_up多次执行或者不能完整执行的方法应该看 24.7.5 Blocking Signals for a Handler 这节。当然用fatal_error_in_progress来跟踪也是可以的,但是不能像 24.4.2中说的那样简单的在if(fatal_error_in_progress) 内 raise 一个相同的信号。

你可能感兴趣的:(linux,c++,信号处理)