JAVA 应用优雅关闭

Linux 信号

信号是在软件层次上对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。通俗来讲,信号就是进程间的一种异步通信机制。

对于内核来说,信号的意义在于通知进程让进程知道已经发生了某个特定事件,或/和强迫进程执行它的信号处理程序,比如本文讨论的就是通过信号完成对应用的优雅关闭(也就是关闭前进行一些扫尾工作)。

对于应用程序来说,信号是异步的,有点像CPU的异步中断,本质上也是依赖中断/异常处理来实现的。(参考:中断/异常、系统调用与信号

对信号的应答有以下三种方式:

  1. 显式地忽略信号
  2. 执行信号对应的默认操作
  3. 捕获信号

SIGKILL 和SIGSTOP 不能被显示地忽略和捕获,所以必须执行他们的默认操作:SIGKILL 强迫进程终止,SIGSTOP 停止进程执行。

C程序通过signal函数来捕获信号,并注册对应的处理函数

Linux kill 命令

kill 命令用于向线程组发送一个信号,其常用的信号选项如下:

  1. kill -2 pid 向指定 pid 发送 SIGINT 中断信号, 等同于 ctrl+c. 
  2. kill -9 pid, 向指定 pid 发送 SIGKILL 立即终止信号. 
  3. kill -15 pid, 向指定 pid 发送 SIGTERM 终止信号. 
  4. kill pid 等同于 kill 15 pid

SIGINT/SIGKILL/SIGTERM 信号的区别:

  1. SIGINT (ctrl+c) 信号 (信号编号为 2), 信号会被当前进程树接收到, 也就说, 不仅当前进程会收到该信号, 而且它的子进程也会收到. 
  2. SIGKILL 信号 (信号编号为 9), 程序不能捕获该信号, 最粗暴最快速结束程序的方法. (参考上一节,SIGKILL 和SIGSTOP 不能被显示地忽略和捕获)
  3. SIGTERM 信号 (信号编号为 15), 信号会被当前进程接收到, 但它的子进程不会收到, 如果当前进程被 kill 掉, 它的的子进程的父进程将变成 init 进程 (init 进程是那个 pid 为 1 的进程)

一般要结束某个进程, 我们应该优先使用 kill pid , 而不是 kill -9 pid. 如果对应程序提供优雅关闭机制的话, 在完全退出之前, 先可以做一些善后处理.

JAVA 原生应用

Java只支持一种信号量的捕获,即使用runtime.addShutdownHook()对退出信号做处理。按照Java标准,Java不支持其他信号的处理,因为这涉及到操作系统,而不是JVM层面的事情。另外一个原因是有的信号是JVM本身需要处理的,而不是暴露给Application

JAVA 虚拟机对于系统内核来说也是一个应用程序,本质上还是通过signal函数注册的信号处理函数,同样也绕不过操作系统的限制,即:SIGKILL 和SIGSTOP 不能被显示地忽略和捕获

但JAVA比C程序更加优雅的一点是,把shutdown hook封装到一个线程中去执行,这种优势是建立在JAVA这套完整的线程模型中的。

我们也可以在C程序中,使用signal函数注册SIGTERM的回调函数,而这个回调函数的内容是立即运行一个新的线程,在新的线程中才会真正处理这个信号

Spring boot应用

捕获容器关闭事件

Netty 应用

shutdownGracefully