Contents

Linux中的SIGINT和其他终止信号

1. 概述

在 Linux 系统中,进程可以接收到多种信号 ,例如 SIGINT 或 SIGKILL。每个信号在不同情况下发送,每个信号都有不同的行为。

在本文中,我们将讨论 SIGINT、SIGTERM、SIGQUIT 和 SIGKILL。我们还将看到它们之间的区别。

2. 信号简介

信号是进程 间通信的一种方法。当进程接收到信号时,进程中断其执行并执行信号处理程序。

程序的行为通常取决于接收到的信号类型。处理完信号后,进程可能会也可能不会继续其正常执行。

Linux 内核可以发送信号,例如,当进程尝试除以零时,它会收到 SIGFPE 信号。

我们还可以使用kill 程序发送信号。让我们在后台运行一个简单的脚本并停止它:

$ (sleep 30; echo "Ready!") &
[1] 26929
$ kill -SIGSTOP 26929
[1]+  Stopped                 ( sleep 30; echo "Ready!" )

现在,我们可以使用 SIGCONT 恢复它:

$ kill -SIGCONT 26929
Ready!
[1]+  Done                    ( sleep 30; echo "Ready!" )

或者,我们可以使用组合键在终端中发送信号。例如,Ctrl+C 发送 SIGINT,Ctrl+S 发送 SIGSTOP,Ctrl+Q 发送 SIGCONT。

**每个信号都有一个默认操作,但是进程可以覆盖默认操作并以不同方式处理它,或者忽略它。**但是,有些信号既不能忽略也不能以不同方式处理,并且始终执行默认操作。

我们可以使用trap命令在bash中处理信号。例如,我们可以在脚本中添加trap date SIGINT ,它会在收到 SIGINT 时打印日期。

3. 信号情报

SIGINT 是我们按下 Ctrl+C 时发出的信号。默认操作是终止进程。但是,某些程序会覆盖此操作并以不同方式处理它。

一个常见的例子是bash解释器。当我们按下 Ctrl+C 时,它不会退出,而是打印一个新的空提示行。另一个例子是当我们使用gdb 调试程序时。我们可以使用 Ctrl+C 发送 SIGINT 来停止执行并将它返回给gdb的解释器。

**我们可以把 SIGINT 看成是用户发出的中断请求。**如何处理通常取决于过程和情况。

让我们使用trap命令编写handle_sigint.sh来处理 SIGINT 并打印当前日期:

#!/bin/bash
trap date SIGINT
read input
echo User input: $input
echo Exiting now

我们使用read input来等待用户交互。现在,让我们运行脚本并按下 Ctrl+C:

$ ./handle_sigint.sh 
^CSat Apr 10 15:32:07 -03 2021

我们可以看到脚本不存在。我们现在可以通过编写一些输入来终止脚本:

$ ./handle_sigint.sh 
^CSat Apr 10 15:32:07 -03 2021
live long and prosper
User input: live long and prosper
Exiting now

如果我们想使用一个信号来终止它,我们不能在这个脚本中使用 SIGINT。我们应该改用 SIGTERM、SIGQUIT 或 SIGKILL。

4. SIGTERM 和 SIGQUIT

**SIGTERM 和 SIGQUIT 信号用于终止进程。**在这种情况下,我们特别要求完成它。SIGTERM 是我们使用kill 命令时的默认信号。

这两个信号的默认操作是终止进程。但是,SIGQUIT 也会在退出前生成核心转储

当我们发送 SIGTERM 时,进程有时会在退出前执行清理例程。

我们也可以在退出前处理 SIGTERM 请求确认。让我们编写一个名为handle_sigterm.sh的脚本来仅在用户发送信号两次时终止:

#!/bin/bash
SIGTERM_REQUESTED=0
handle_sigterm() {
    if [ $SIGTERM_REQUESTED -eq 0 ]; then
        echo "Send SIGTERM again to terminate"
        SIGTERM_REQUESTED=1
    else
        echo "SIGTERM received, exiting now"
        exit 0
    fi
}
trap handle_sigterm SIGTERM
TIMEOUT=$(date +%s)
TIMEOUT=$(($TIMEOUT + 60))
echo "This script will exit in 60 seconds"
while [ $(date +%s) -lt $TIMEOUT ]; do
    sleep 1;
done
echo Timeout reached, exiting now

现在,让我们在后台执行*$ ./handle_sigterm.sh &来运行它。然后,我们运行$ kill PID*两次:

$ ./handle_sigterm.sh &
[1] 6092
$ kill 6092
Send SIGTERM again to terminate
$ kill 6092
SIGTERM received, exiting now
[1]+  Done                    ./handle_sigterm.sh

如我们所见,脚本在收到第二个 SIGTERM 后退出。

5. 信号终止

当进程收到 SIGKILL 时,它会终止。这是一个特殊的信号,因为它不能被忽略,我们也不能改变它的行为。

我们使用这个信号来强行终止进程。我们应该小心,因为该进程将无法执行任何清理例程。

使用 SIGKILL 的一种常见方式是先发送 SIGTERM。我们给进程一些时间来终止,我们也可以发送 SIGTERM 几次。如果进程没有自行完成,那么我们发送 SIGKILL 来终止它。

让我们重写前面的例子来尝试处理 SIGKILL 并请求确认:

#!/bin/bash
SIGKILL_REQUESTED=0
handle_sigkill() {
    if [ $SIGKILL_REQUESTED -eq 0 ]; then
        echo "Send SIGKILL again to terminate"
        SIGKILL_REQUESTED=1
    else
        echo "Exiting now"
        exit 0
    fi
}
trap handle_sigkill SIGKILL
read input
echo User input: $input

现在,让我们在终端上运行它,并使用*$ kill -SIGKILL pid*只发送一次 SIGKILL :

$ ./handle_sigkill.sh
Killed
$

我们可以看到它立即终止而没有要求重新发送信号

6. SIGINT 与 SIGTERM、SIGQUIT 和 SIGKILL 的关系

现在我们对信号有了更多了解,我们可以了解它们之间的关系。

SIGINT、SIGTERM、SIGQUIT 和 SIGKILL 的默认操作是终止进程。但是,SIGTERM、SIGQUIT 和 SIGKILL 被定义为终止进程的信号,而 SIGINT 被定义为用户请求的中断。

特别是,如果我们根据进程和情况发送 SIGINT(或按 Ctrl+C),它的行为可能会有所不同。所以,我们不应该仅仅依靠 SIGINT 来完成一个过程。

由于 SIGINT 旨在作为用户发送的信号,因此通常进程使用其他信号相互通信。例如,父进程通常会向其子进程发送 SIGTERM 以终止它们,即使 SIGINT 具有相同的效果。

对于 SIGQUIT,它会生成一个对调试有用的核心转储。

现在我们记住了这一点,我们可以看到我们应该在 SIGKILL 之上选择 SIGTERM 来终止进程。SIGTERM 是首选方式,因为进程有机会正常终止。

由于进程可以覆盖 SIGINT、SIGTERM 和 SIGQUIT 的默认操作,因此可能出现它们都没有完成进程的情况。此外,如果进程挂起,它可能不会响应任何这些信号。在这种情况下,我们将 SIGKILL 作为终止进程的最后手段。