如何在Java中优雅地处理SIGKILL信号

126

程序接收到kill信号时如何进行清除操作?

例如,我连接的一个应用程序希望任何第三方应用程序(我的应用程序)在注销时发送finish命令。当我的应用程序被使用kill -9销毁时,最好的方法是发送哪个finish命令?

编辑1:无法捕获kill -9。谢谢大家指正。

编辑2:我想这种情况可能是当一个人只调用了“kill”(与ctrl-c相同)。


52
kill -9 对我来说意味着:“去吧,邪恶的进程,离开这里!” 这时进程将立即停止。 - ZoogieZork
12
在我所了解的大多数*nix系统中,无论用什么编程语言编写程序,都无法拦截并优雅地处理kill -9命令。 - President James K. Polk
3
除了其他人已经评论和回答的内容之外,如果你的Unix操作系统不会立即终止并重新获得被"kill -9"终止程序所使用的所有资源,那么操作系统就有问题。 - SyntaxT3rr0r
2
关于kill -9命令,man手册更精确地说:“9 KILL(不可捕获,不可忽略的kill)”。SIGKILL是由操作系统处理而非应用程序处理的信号。 - user1338062
真是太神奇了,SO是一个知识和乐趣的完整来源!@ZoogieZork用他非常独特的方式解释了-9开关,让我在沙发上独自笑了好几分钟,哈哈哈。确实让我的一天变得更美好了! - Gustavo Pinsard
6
仅仅使用"kill"命令并不等同于按下Ctrl-C键,因为未指定发送哪个信号的"kill"命令将发送SIGTERM信号,而Ctrl-C键发送的是SIGINT信号。 - Alesya Huzik
6个回答

166
任何语言的程序都无法处理 SIGKILL 信号,这是为了保证即使程序存在漏洞或恶意行为时也能强制终止程序。但 SIGKILL 并不是唯一终止程序的方式。另一种方式是使用 SIGTERM。程序可以处理该信号。程序应通过进行控制但快速的关闭来处理该信号。当计算机关闭时,关闭过程的最后阶段会向所有剩余进程发送 SIGTERM 信号,并给这些进程几秒钟的宽限期,然后发送 SIGKILL 信号。
除了 `kill -9` 之外,处理此问题的方法是注册一个 shutdown 钩子。如果您使用(SIGTERM)`kill -15`,则关闭钩子将起作用。 (SIGINT)`kill -2` 将导致程序优雅地退出并运行关闭钩子。
注册新的虚拟机关闭钩子。 Java 虚拟机响应两种事件而关闭: - 当最后一个非守护线程退出或调用 exit(等效于 System.exit)方法时,程序正常退出; - 在响应用户中断(例如键入 ^C)或系统范围事件(例如用户注销或系统关闭)时,虚拟机被终止。

我在OSX 10.6.3上尝试了以下测试程序,在执行kill -9时,它没有运行关闭挂钩,这是预期的。在执行kill -15时,每次都会运行关闭挂钩。

public class TestShutdownHook
{
    public static void main(String[] args) throws InterruptedException
    {
        Runtime.getRuntime().addShutdownHook(new Thread()
        {
            @Override
            public void run()
            {
                System.out.println("Shutdown hook ran!");
            }
        });

        while (true)
        {
            Thread.sleep(1000);
        }
    }
}

在任何程序中,没有真正优雅地处理kill -9的方法。

在极少数情况下,虚拟机可能会中止,即在不干净关闭的情况下停止运行。当虚拟机在外部终止时,例如在Unix上使用SIGKILL信号或在Microsoft Windows上使用TerminateProcess调用时,就会发生这种情况。

唯一真正处理kill -9的选择是使用另一个监视程序来监视您的主程序是否消失,或者使用包装脚本。您可以使用轮询ps命令查找程序列表并在其消失时相应地进行操作。

#!/usr/bin/env bash

java TestShutdownHook
wait
# notify your other app that you quit
echo "TestShutdownHook quit"

16
我会期望JVM优雅地中断(thread.interrupt())应用程序创建的所有运行线程,至少对于SIGINT (kill -2)SIGTERM (kill -15)信号。这样,信号将被转发到它们,允许在标准方式中进行线程取消和资源终止。
但事实并非如此(至少在我的JVM实现中:Java(TM) SE Runtime Environment (build 1.8.0_25-b17), Java HotSpot(TM) 64-Bit Server VM (build 25.25-b02, mixed mode))。正如其他用户所评论的那样,使用shutdown hooks似乎是强制性的。

那么,我该如何处理它呢?

首先,我并不关心所有程序中的它,只在我想跟踪用户取消和意外结束的程序中关注它。例如,假设您的Java程序是由其他进程管理的进程。您可能希望区分它是否已经优雅地终止(来自管理进程的SIGTERM),或者是否发生了关闭(以便在启动时自动重新启动作业)。

作为基础,我总是让我的长时间运行的线程周期性地知道被中断的状态,并在被中断时抛出InterruptedException。这使得执行终止由开发人员控制(也产生与标准阻塞操作相同的结果)。然后,在线程堆栈的顶层,捕获InterruptedException并执行适当的清理。这些线程编码为知道如何响应中断请求。高cohesion设计。

因此,在这些情况下,我添加了一个关机钩子,它执行我认为JVM应默认执行的操作:中断仍在运行的应用程序创建的所有非守护线程:

Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
        System.out.println("Interrupting threads");
        Set<Thread> runningThreads = Thread.getAllStackTraces().keySet();
        for (Thread th : runningThreads) {
            if (th != Thread.currentThread() 
                && !th.isDaemon() 
                && th.getClass().getName().startsWith("org.brutusin")) {
                System.out.println("Interrupting '" + th.getClass() + "' termination");
                th.interrupt();
            }
        }
        for (Thread th : runningThreads) {
            try {
                if (th != Thread.currentThread() 
                && !th.isDaemon() 
                && th.isInterrupted()) {
                    System.out.println("Waiting '" + th.getName() + "' termination");
                    th.join();
                }
            } catch (InterruptedException ex) {
                System.out.println("Shutdown interrupted");
            }
        }
        System.out.println("Shutdown finished");
    }
});

在 github 上完整的测试应用程序: https://github.com/idelvall/kill-test


12

在某些JVM中,有办法处理自己的信号--请参见关于HotSpot JVM的这篇文章

通过使用Sun内部的sun.misc.Signal.handle(Signal, SignalHandler)方法调用,您还可以注册信号处理程序,但可能无法处理像INTTERM这样由JVM使用的信号。

要能够处理任何信号,您必须跳出JVM并进入操作系统领域。

通常我所做的(例如)是在Perl脚本中启动我的JVM,但通过使用waitpid系统调用使脚本等待JVM。

然后我会被告知JVM何时退出以及退出的原因,并采取必要的措施。


4
请注意,您可以使用sun.misc.Signal捕获INTTERM信号,但无法处理QUIT信号,因为JVM将其保留用于调试,也无法处理KILL信号,因为操作系统会立即终止JVM。尝试处理任何一个信号都会引发IllegalArgumentException异常。 - dimo414

7
您可以使用Runtime.getRuntime().addShutdownHook(...),但不能保证它会在任何情况下都被调用。

13
但是在使用kill -9命令时,几乎可以肯定它不会运行。 - President James K. Polk

6

参考 https://aws.amazon.com/blogs/containers/graceful-shutdowns-with-ecs/

本文介绍了在 Amazon Elastic Container Service (ECS) 中使用信号实现优雅停机的方法。通过这种方式,您可以确保在停止容器前,应用程序有足够的时间来完成正在执行的任务,并保存状态。

import sun.misc.Signal;
import sun.misc.SignalHandler;
 
public class ExampleSignalHandler {
    public static void main(String... args) throws InterruptedException {
        final long start = System.nanoTime();
        Signal.handle(new Signal("TERM"), new SignalHandler() {
            public void handle(Signal sig) {
                System.out.format("\nProgram execution took %f seconds\n", (System.nanoTime() - start) / 1e9f);
                System.exit(0);
            }
        });
        int counter = 0;
        while(true) {
            System.out.println(counter++);
            Thread.sleep(500);
        }
    }
}

3

对于kill -9的一种应对方式是,有一个单独的进程来监视被杀死的进程,并在必要时进行清理。这可能涉及IPC并且需要相当多的工作,而且您仍然可以通过同时杀死两个进程来覆盖它。我认为,在大多数情况下,这将不值得麻烦。

使用-9杀死进程的人理论上应该知道自己在做什么,并且可能会使事情处于不一致状态。


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