从 Java 程序优雅停机到 Linux 信号机制初窥

前不久,公司内部使用的一个 RPC 框架支持了优雅停机。优雅停机是很多框架非常重要的特征,在 Java 中是使用 Runtime.addShutdownHook 方法去注册关闭的钩子(Runtime 类代表了当前 JVM 进程的运行环境)。

The Java virtual machine shuts down in response to two kinds of events:

  • The program exits normally, when the last non-daemon thread exits or when the exit (equivalently, System.exit) method is invoked, or

  • The virtual machine is terminated in response to a user interrupt, such as typing ^C, or a system-wide event, such as user logoff or system shutdown.

Shutdown hooks should also finish their work quickly. When a program invokes exit the expectation is that the virtual machine will promptly shut down and exit. When the virtual machine is terminated due to user logoff or system shutdown the underlying operating system may only allow a fixed amount of time in which to shut down and exit. It is therefore inadvisable to attempt any user interaction or to perform a long-running computation in a shutdown hook.

In rare circumstances the virtual machine may abort, that is, stop running without shutting down cleanly. This occurs when the virtual machine is terminated externally, for example with the SIGKILL signal on Unix or the TerminateProcess call on Microsoft Windows. The virtual machine may also abort if a native method goes awry by, for example, corrupting internal data structures or attempting to access nonexistent memory. If the virtual machine aborts then no guarantee can be made about whether or not any shutdown hooks will be run.

  • 当 JVM 进程中最后一个非守护线程退出或者 System.exit 方法被调用时,或者是收到了操作系统的中断信号,如 Unix 上的 SIGTEYMSIGHUP 信号、<Ctrl+C> 产生的 SIGINT 信号等可以触发钩子(默认是这三种,可以自己定义,当然 JVM 对 Signal 也是会有限制的,后文会有介绍);
  • 要注意 SIGKILL 信号、Runtime.halt() 、断电等属于强制关闭,是无法触发的;
  • OOM 和其他 RuntimeException 是可以触发的;
  • 要注意的是尽量不要在 Hook 线程中执行耗时的操作,因为底层操作系统可能只允许在固定的时间内关闭和退出;可以看到参数传递的是一个线程,相当于多个钩子是并发执行的,顺序是无法保证的。

(2)SIGINT:用户按下 <Ctrl+C> 组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。

(9)SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。

(15)SIGTERM:程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行 shell 命令 kill 时,缺省产生这个信号。默认动作为终止进程。

几个例子

先看一个 JVM 正常退出的例子:

public class HookTest1 {

    public static void main(String[] args) throws InterruptedException {
        registHook();
        System.out.println(LocalDateTime.now() + "-->start...");
        Thread.sleep(3000);
        System.out.println(LocalDateTime.now() + "-->end...");
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            /*try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }*/
            System.out.println(LocalDateTime.now() + "-->Hook.run....");
        }));
    }
}

输出:

2020-05-23T19:36:45.304-->start...
2020-05-23T19:36:48.305-->end...
2020-05-23T19:36:48.307-->Hook.run....

这个例子中 JVM 进程属于正常退出的,可以看到执行了钩子。

再看使用 System.exit 方法退出的例子:

public class HookTest2 {

    public static void main(String[] args) throws InterruptedException {
        registHook();
        System.out.println(LocalDateTime.now()+"-->start...");
        Thread.sleep(3000);
        //0 为正常退出,非 0 为非正常退出
        System.exit(0);
        System.out.println(LocalDateTime.now()+"-->end...");
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now()+"-->Hook.run....")));
    }
}

输出:

2020-05-23T19:42:18.531-->start...
2020-05-23T19:42:21.536-->Hook.run....

可以看到也是可以正常触发钩子。

再使用 Runtime.halt 试试:

public class HookTest2 {

    public static void main(String[] args) throws InterruptedException {
        registHook();
        System.out.println(LocalDateTime.now()+"-->start...");
        Thread.sleep(3000);
        //Forcibly terminates the currently running Java virtual machine.
        Runtime.getRuntime().halt(0);
        System.out.println(LocalDateTime.now()+"-->end...");
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now()+"-->Hook.run....")));
    }
}

输出:

2020-05-24T00:49:55.024-->start...

可以看到并未触发钩子。

接下来试试使用 kill 命令(SIGTERM 信号)去关闭 JVM 进程:

public class HookTest3 {

    public static void main(String[] args) throws InterruptedException {
        registHook();
        System.out.println(LocalDateTime.now()+"-->start...");
        while (true){

        }
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now()+"-->Hook.run....")));
    }
}

运行后使用 kill 命令关闭进程:

➜  ~ jps
16913 Jps
706 
16242 KotlinCompileDaemon
16876 Launcher
16877 HookTest3
➜  ~ kill 16877

控制台输出:

2020-05-23T19:44:28.048-->start...
2020-05-23T19:45:03.809-->Hook.run....
  
Process finished with exit code 143 (interrupted by signal 15: SIGTERM)

也是可以触发钩子。再试试使用 kill -9 去关闭进程,重新启动程序后执行:

➜  ~ jps          
16992 Launcher
16993 HookTest3
706 
17026 Jps
16242 KotlinCompileDaemon
➜  ~ kill -9 16993

控制台输出:

2020-05-23T19:47:17.616-->start...

Process finished with exit code 137 (interrupted by signal 9: SIGKILL)

可以看到并没有触发钩子。

再试试 <Ctrl+C>(MacOS 下是 <control+c>),运行上面的程序:

➜  hook git:(master) ✗ /Users/dongguabai/IdeaProjects/java-socket/target/classes
➜  classes git:(master) ✗ java com.dongguabai.socket.demo.hook.HookTest3
2020-05-23T19:57:24.594-->start...

使用 <control+c> 关闭进程:

➜  hook git:(master) ✗ /Users/dongguabai/IdeaProjects/java-socket/target/classes
➜  classes git:(master) ✗ java com.dongguabai.socket.demo.hook.HookTest3
2020-05-23T19:57:24.594-->start...
^C2020-05-23T20:01:27.322-->Hook.run....

可以发现触发了钩子。

最后再演示一个 OOM 的例子:

public class HookTest3 {

    public static void main(String[] args) throws InterruptedException {
        registHook();
        List<String> list = new ArrayList<>(1000000);
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now()+"-->Hook.run....")));
    }
}

运行程序:

➜  hook git:(master)/Users/dongguabai/IdeaProjects/java-socket/target/classes
➜  classes git:(master) ✗ java -Xmx1M -Xms1M  com.dongguabai.socket.demo.hook.HookTest3
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.ArrayList.<init>(ArrayList.java:153)
	at com.dongguabai.socket.demo.hook.HookTest3.main(HookTest3.java:16)
2020-05-23T20:49:18.660-->Hook.run....

可以看到发生了 OOM 也会触发钩子。

钩子执行分析

可以看到是可以通过 Runtime.addShutdownHook 方法去注册关闭的钩子,那么这个钩子注册到哪里去了呢,下面简单分析一下。

先看 java.lang.ApplicationShutdownHooks#add 方法:

static synchronized void add(Thread hook) {
        if(hooks == null)
            throw new IllegalStateException("Shutdown in progress");

        if (hook.isAlive())
            throw new IllegalArgumentException("Hook already running");

        if (hooks.containsKey(hook))
            throw new IllegalArgumentException("Hook previously registered");

        hooks.put(hook, hook);
    }

可以看到最终是放到 ApplicationShutdownHooks 类中的 hooks 中去:

/* The set of registered hooks */
private static IdentityHashMap<Thread, Thread> hooks;

那么这个 hooks 是在哪里被调用的呢,在 ApplicationShutdownHooks 中有一个静态代码:

static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
static void runHooks() {
        Collection<Thread> threads;
        synchronized(ApplicationShutdownHooks.class) {
            threads = hooks.keySet();
            hooks = null;
        }

        for (Thread hook : threads) {
            hook.start();
        }
        for (Thread hook : threads) {
            while (true) {
                try {
                    hook.join();
                    break;
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

可以看到这里会开启线程去执行钩子,也就解释了文章开头说的“多个钩子是并发执行的,顺序是无法保证的”。

也就是 ApplicationShutdownHooks 类加载的时候就会往 Shutdownhooks 中设置钩子:

private static final Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS];

可以看到被设置到 hooks 中的索引为 1 元素了,那么索引为 0 的被设置了什么呢,可以看到在 Console 类中会设置:

static {
        try {
            // Add a shutdown hook to restore console's echo state should
            // it be necessary.
            sun.misc.SharedSecrets.getJavaLangAccess()
                .registerShutdownHook(0 /* shutdown hook invocation order */,
                    false /* only register if shutdown is not in progress */,
                    new Runnable() {
                        public void run() {
                            try {
                                if (echoOff) {
                                    echo(true);
                                }
                            } catch (IOException x) { }
                        }
                    });
        } catch (IllegalStateException e) {
            // shutdown is already in progress and console is first used
            // by a shutdown hook
        }

DeleteOnExitHook 会设置到索引为 2 的位置:

static {
        // DeleteOnExitHook must be the last shutdown hook to be invoked.
        // Application shutdown hooks may add the first file to the
        // delete on exit list and cause the DeleteOnExitHook to be
        // registered during shutdown in progress. So set the
        // registerShutdownInProgress parameter to true.
        sun.misc.SharedSecrets.getJavaLangAccess()
            .registerShutdownHook(2 /* Shutdown hook invocation order */,
                true /* register even if shutdown in progress */,
                new Runnable() {
                    public void run() {
                       runHooks();
                    }
                }
        );
    }

ConsoleDeleteOnExitHook 只是题外话。继续分析钩子的执行。在文章开头提到过“JVM 默认只能处理 SIGTEYMSIGHUPSIGINT 信号”,那这个是为什么呢。我们知道 System 类在初始化的时候,首先会注册所有的 native 方法:

    static {
        registerNatives();
    }

在这之后会调用 java.lang.System#initializeSystemClass 方法:

 // Setup Java signal handlers for HUP, TERM, and INT (where available).
        Terminator.setup();

根据注释可以看出处理的是 HUPTERMINT 这三种 Signal。再看看 java.lang.Terminator#setup 方法:

static void setup() {
        if (handler != null) return;
        SignalHandler sh = new SignalHandler() {
            public void handle(Signal sig) {
                Shutdown.exit(sig.getNumber() + 0200);
            }
        };
        handler = sh;
        // When -Xrs is specified the user is responsible for
        // ensuring that shutdown hooks are run by calling
        // System.exit()
        try {
            Signal.handle(new Signal("HUP"), sh);
        } catch (IllegalArgumentException e) {
        }
        try {
            Signal.handle(new Signal("INT"), sh);
        } catch (IllegalArgumentException e) {
        }
        try {
            Signal.handle(new Signal("TERM"), sh);
        } catch (IllegalArgumentException e) {
        }
    }

可以看到使用 Signal.handle 去处理 HUPTERMINT 这三种 Signal。具体处理是在 java.lang.Shutdown#exit 方法中(关于 Signal.handle 后文有详细说明):

synchronized (Shutdown.class) {
            /* Synchronize on the class object, causing any other thread
             * that attempts to initiate shutdown to stall indefinitely
             */
            sequence();
            halt(status);
        }

可以看到在 java.lang.Shutdown#halt 方法被调用之前会先执行 java.lang.Shutdown#sequence 方法,而 java.lang.Shutdown#sequence 方法就是去处理注册的钩子。这也能够解释上文例子中为什么 Runtime.halt() 无法触发钩子。

java.lang.Shutdown#sequence 方法会调用 java.lang.Shutdown#runHooks 方法,这里会执行所有的 Hook:

/* Run all registered shutdown hooks
     */
    private static void runHooks() {
        for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
            try {
                Runnable hook;
                synchronized (lock) {
                    // acquire the lock to make sure the hook registered during
                    // shutdown is visible here.
                    currentRunningHook = i;
                    hook = hooks[i];
                }
                if (hook != null) hook.run();
            } catch(Throwable t) {
                if (t instanceof ThreadDeath) {
                    ThreadDeath td = (ThreadDeath)t;
                    throw td;
                }
            }
        }
    }

这里会循环执行 hooks 里面的钩子,根据上文的介绍,我们自定义的钩子索引为 1,会被执行,而里面实际上是多个线程并发执行。

在其他框架中的使用

在 Spring Boot 中的应用

SpringApplication 在启动的时候会向 JVM 注册 钩子:

org.springframework.boot.SpringApplication#run
-> org.springframework.boot.SpringApplication#refreshContext -> org.springframework.context.support.AbstractApplicationContext#registerShutdownHook

	@Override
	public void registerShutdownHook() {
		if (this.shutdownHook == null) {
			// No shutdown hook registered yet.
			this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
				@Override
				public void run() {
					synchronized (startupShutdownMonitor) {
						doClose();
					}
				}
			};
			Runtime.getRuntime().addShutdownHook(this.shutdownHook);
		}
	}

Spring 容器的生命周期会在调用 org.springframework.context.support.AbstractApplicationContext#close 方法的时候终止:

@Override
	public void close() {
		synchronized (this.startupShutdownMonitor) {
			doClose();
			// If we registered a JVM shutdown hook, we don't need it anymore now:
			// We've already explicitly closed the context.
			if (this.shutdownHook != null) {
				try {
					Runtime.getRuntime().removeShutdownHook(this.shutdownHook);
				}
				catch (IllegalStateException ex) {
					// ignore - VM is already shutting down
				}
			}
		}
	}

主要是为了合理的资源销毁,确保 Spring 容器内部的 Bean 都能够执行 Spring Bean 中的生命周期的相应回调。

在 Tomcat 中的应用

org.apache.catalina.startup.Catalina#start 方法中会注册一个 CatalinaShutdownHook

... 
 // Register shutdown hook
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);

            // If JULI is being used, disable JULI's shutdown hook since
            // shutdown hooks run in parallel and log messages may be lost
            // if JULI's hook completes before the CatalinaShutdownHook()
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        false);
            }
        }
...
protected class CatalinaShutdownHook extends Thread {

        @Override
        public void run() {
            try {
                if (getServer() != null) {
                    Catalina.this.stop();
                }
            } catch (Throwable ex) {
                ExceptionUtils.handleThrowable(ex);
                log.error(sm.getString("catalina.shutdownHookFail"), ex);
            } finally {
                // If JULI is used, shut JULI down *after* the server shuts down
                // so log messages aren't lost
                LogManager logManager = LogManager.getLogManager();
                if (logManager instanceof ClassLoaderLogManager) {
                    ((ClassLoaderLogManager) logManager).shutdown();
                }
            }
        }
    }

就是调用 org.apache.catalina.startup.Catalina#stop 方法去关闭服务实例。

Linux 和 JVM 对 Signal 的处理

Linux 系统对 Signal 的处理主要是由 signalsigaction 函数完成。我们常用的 kill 命令成为“杀死”进程其实是不太准确的,它是发送指定的信号到对应进程。可以看一下 signal 函数:

NAME
     signal -- simplified software signal facilities

LIBRARY
     Standard C Library (libc, -lc)

SYNOPSIS
     #include <signal.h>

     void (*signal(int sig, void (*func)(int)))(int);

     or in the equivalent but easier to read typedef'd version:

     typedef void (*sig_t) (int);

     sig_t
     signal(int sig, sig_t func);

这个函数的第二个参数是一个函数指针,指向用户编写的信号处理函数的地址,当然也是可以忽略,使用默认的处理方式。

也就是说当我们执行 kill 命令的时候,从 OS 到 JVM 进程大体流程可以理解为:

执行 kill 命令 -> OS 收到信号 -> 跳转到(JVM 定义的) signal 函数 -> 执行 JVM 钩子

下面是一个处理 SIGINT 信号的例子:

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

void handler_sigint(int signo)
{
    printf("Hello World\n");
}

int main()
{
    //注册
    signal(SIGINT,handler_sigint);
    while (1);
    
    //返回给 OS,0 是正常,非 0 是异常 
    return 0;
}

编译执行,在键盘输入 <Control+c>

➜  Desktop gcc test.c -o test
➜  Desktop ./test  
^CHello World
^CHello World

同样地,我在新开的控制台上执行:

➜  ~ kill -2 23060

进程也是无法被终止的(因为 Signal 是一样的):

➜  Desktop ./test  
^CHello World
^CHello World
Hello World

发现此时进程无法被 <Control+c> 终止。新开一个控制台,kill 这个进程:

➜  ~ ps
  PID TTY           TIME CMD
16021 ttys000    0:00.33 /bin/zsh --login -i
22569 ttys001    0:00.31 -zsh
22698 ttys001    1:32.40 ./test
21027 ttys002    0:00.23 /bin/zsh --login -i
22734 ttys003    0:00.17 -zsh
➜  ~ kill 22698

这时候发现进程被终止:

➜  Desktop ./test  
^CHello World
^CHello World
Hello World
[1]    22698 terminated  ./test

由于能力不足,对 JVM 源码理解有限,以下仅为猜测。

在 OpenJDK 源码中也能找到端倪:

void* os::signal(int signal_number, void* handler) {
  struct sigaction sigAct, oldSigAct;

  sigfillset(&(sigAct.sa_mask));
  sigAct.sa_flags   = SA_RESTART|SA_SIGINFO;
  sigAct.sa_handler = CAST_TO_FN_PTR(sa_handler_t, handler);

  if (sigaction(signal_number, &sigAct, &oldSigAct)) {
    // -1 means registration failed
    return (void *)-1;
  }

  return CAST_FROM_FN_PTR(void*, oldSigAct.sa_handler);
}

在 C 中 signal 函数本质也是通过 sigaction 函数进行调用。JDK 的 sun.misc.NativeSignalHandler#handle0 会调用 JVM_RegisterSignal 函数,在 jvm_linux.cpp 中:

JVM_ENTRY_NO_ENV(void*, JVM_RegisterSignal(jint sig, void* handler))
  // Copied from classic vm
  // signals_md.c       1.4 98/08/23
  void* newHandler = handler == (void *)
                   ? os::user_handler()
                   : handler;
  switch (sig) {
    /* The following are already used by the VM. */
    case INTERRUPT_SIGNAL:
    case SIGFPE:
    case SIGILL:
    case SIGSEGV:

    /* The following signal is used by the VM to dump thread stacks unless
       ReduceSignalUsage is set, in which case the user is allowed to set
       his own _native_ handler for this signal; thus, in either case,
       we do not allow JVM_RegisterSignal to change the handler. */
    case BREAK_SIGNAL:
      return (void *)-1;

    /* The following signals are used for Shutdown Hooks support. However, if
       ReduceSignalUsage (-Xrs) is set, Shutdown Hooks must be invoked via
       System.exit(), Java is not allowed to use these signals, and the the
       user is allowed to set his own _native_ handler for these signals and
       invoke System.exit() as needed. Terminator.setup() is avoiding
       registration of these signals when -Xrs is present.
       - If the HUP signal is ignored (from the nohup) command, then Java
         is not allowed to use this signal.
     */

    case SHUTDOWN1_SIGNAL:
    case SHUTDOWN2_SIGNAL:
    case SHUTDOWN3_SIGNAL:
      if (ReduceSignalUsage) return (void*)-1;
      if (os::Linux::is_sig_ignored(sig)) return (void*)1;
  }

  void* oldHandler = os::signal(sig, newHandler);
  if (oldHandler == os::user_handler()) {
      return (void *)2;
  } else {
      return oldHandler;
  }
JVM_END

大致可以看出 JDK 应该也是定义了 signal 函数去处理 Signal,就跟上面的例子一样。JDK 允许用户自定义去定义信号处理,但是部分信号属于保护信号,用户无法修改。

-Xrs 参数

这是官方文档中的描述:

Reduces use of operating-system signals by the Java VM.

In an earlier release, the Shutdown Hooks facility was added to enable orderly shutdown of a Java application. The intent was to enable user cleanup code (such as closing database connections) to run at shutdown, even if the Java VM terminates abruptly.

The Java VM watches for console control events to implement shutdown hooks for unexpected Java VM termination. Specifically, the Java VM registers a console control handler which begins shutdown-hook processing and returns TRUE for CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, and CTRL_SHUTDOWN_EVENT.

The JVM uses a similar mechanism to implement the feature of dumping thread stacks for debugging purposes. The JVM uses CTRL_BREAK_EVENT to perform thread dumps.

If the Java VM is run as a service (for example, the servlet engine for a web server), then it can receive CTRL_LOGOFF_EVENT but should not initiate shutdown because the operating system will not actually terminate the process. To avoid possible interference such as this, the -Xrs command-line option was added beginning with J2SE 1.3.1. When the -Xrs option is used on the Java VM, the Java VM does not install a console control handler, implying that it does not watch for or process CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, or CTRL_SHUTDOWN_EVENT.

There are two consequences of specifying -Xrs:

  • Ctrl-Break thread dumps are not available.
  • User code is responsible for causing shutdown hooks to run, for example by calling System.exit() when the Java VM is to be terminated.

就是说由于一些原因,从 1.3.1 提供了这个参数,能够减少 JVM 对操作系统 Signal 的使用。还是以这个 Demo 为例:

public class HookTest3 {

    public static void main(String[] args) throws InterruptedException {
        registHook();
        while (true){}
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println(LocalDateTime.now()+"-->Hook.run....")));
    }
}

增加 -Xrs 参数运行:

➜  classes git:(master) ✗ /Users/dongguabai/IdeaProjects/java-socket/target/classes
➜  classes git:(master) ✗ java -Xrs com.dongguabai.socket.demo.hook.HookTest3

在键盘输入 <Control+c> 或者新开窗口执行 kill -2 命令,会发现进程是直接关闭的,并未触发钩子:

➜  classes git:(master) ✗ /Users/dongguabai/IdeaProjects/java-socket/target/classes
➜  classes git:(master) ✗ java -Xrs com.dongguabai.socket.demo.hook.HookTest3
^C
➜  classes git:(master)

而没有 -Xrs 参数则可以触发钩子。

SignalHandler

到目前为止其实关于 JVM 退出的钩子已经介绍完毕,但是我总感觉缺少点什么,对,就是缺少直接通过 Java 去控制 Signal 的操作,因为就算是添加了钩子,但本质上还是 C/C++ 去处理的 Signal。本来准备基于 JNI 去调用 C 再试图去处理 Signal 的,后来无意中发现了 SignalHandler,这个接口是在 sun.misc 中。看名称就知道它可以处理 SIganl。

/**
 * This is the signal handler interface expected in <code>Signal.handle</code>.
 */

public interface SignalHandler {

    /**
     * Handle the given signal
     *
     * @param sig a signal object
     */
    public void handle(Signal sig);
}

需要与 Signal.handle 结合一起使用。这个方法需要传入一个 Signal 对象:

    public Signal(String name) {
        number = findSignal(name);
        this.name = name;
        if (number < 0) {
            throw new IllegalArgumentException("Unknown signal: " + name);
        }
    }

可以看到需要传如一个 name,这个 name 就是当前操作系统支持的 Signal 的名称,可以通过 kill -l 命令查看:

➜  ~ kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2

看一个例子:

package com.dongguabai.socket.demo.hook.signl;

import sun.misc.Signal;
import sun.misc.SignalHandler;

import java.time.LocalDateTime;

/**
 * @author Dongguabai
 * @Description
 * @Date 创建于 2020-05-24 01:20
 */
public class MySignlHandler implements SignalHandler {
    @Override
    public void handle(Signal sig) {
        System.out.println(LocalDateTime.now()+"--->receive signale:"+sig);
    }
}
package com.dongguabai.socket.demo.hook.signl;

import sun.misc.Signal;

import java.time.LocalDateTime;

/**
 * @author Dongguabai
 * @Description
 * @Date 创建于 2020-05-24 01:23
 */
public class SignlTest {

    public static void main(String[] args) {
        MySignlHandler mySignlHandler = new MySignlHandler();
        //注册 Signal 处理器,处理 INT
        Signal.handle(new Signal("INT"),mySignlHandler);
        registHook();
        while (true){}
    }

    private static void registHook() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println(LocalDateTime.now() + "-->Hook.run....");
        }));
    }
}

启动程序,使用 kill -2 关闭程序:

➜  ~ jps           
29137 Jps
706 
25308 KotlinCompileDaemon
29132 Launcher
29133 SignlTest
➜  ~ kill -2 29133 
➜  ~ kill -2 29133
➜  ~ kill -2 29133

控制台输出:

2020-05-24T01:50:44.744--->receive signale:SIGINT
2020-05-24T01:50:47.280--->receive signale:SIGINT
2020-05-24T01:50:48.026--->receive signale:SIGINT

可以看到此时程序根本无法关闭。那么使用 kill -15 试试:

2020-05-24T01:50:44.744--->receive signale:SIGINT
2020-05-24T01:50:47.280--->receive signale:SIGINT
2020-05-24T01:50:48.026--->receive signale:SIGINT
2020-05-24T01:51:21.625-->Hook.run....

Process finished with exit code 143 (interrupted by signal 15: SIGTERM)

发现程序是可以被关闭了,同时触发了钩子。在收到了 SIGTEYM 信号后可以触发钩子,这是因为我这里并未覆盖 SIGTEYM 信号的处理 Handler。而我这里定义的 SIGINT 信号的 Handler 会覆盖 JVM 启动的时候默认注册的 SIGINT 信号的 Handler:

 /**
     * Registers a signal handler.
     *
     * @param sig a signal
     * @param handler the handler to be registered with the given signal.
     * @result the old handler
     * @exception IllegalArgumentException the signal is in use by the VM
     * @see sun.misc.Signal#raise(Signal sig)
     * @see sun.misc.SignalHandler
     * @see sun.misc.SignalHandler#SIG_DFL
     * @see sun.misc.SignalHandler#SIG_IGN
     */
    public static synchronized SignalHandler handle(Signal sig,
                                                    SignalHandler handler)
        throws IllegalArgumentException {
        long newH = (handler instanceof NativeSignalHandler) ?
                      ((NativeSignalHandler)handler).getHandler() : 2;
        long oldH = handle0(sig.number, newH);
        if (oldH == -1) {
            throw new IllegalArgumentException
                ("Signal already used by VM or OS: " + sig);
        }
        signals.put(sig.number, sig);
        synchronized (handlers) {
            SignalHandler oldHandler = handlers.get(sig);
            handlers.remove(sig);
            if (newH == 2) {
                handlers.put(sig, handler);
            }
            if (oldH == 0) {
                return SignalHandler.SIG_DFL;
            } else if (oldH == 1) {
                return SignalHandler.SIG_IGN;
            } else if (oldH == 2) {
                return oldHandler;
            } else {
                return new NativeSignalHandler(oldH);
            }
        }
    }

而根据上文的分析,钩子之所以能够执行,是由于 JVM 默认的 Hander 会去调用钩子。

References

  • https://docs.oracle.com/javase/8/docs/api/java/lang/Runtime.html#addShutdownHook-java.lang.Thread-
  • 《Linux C 编程实战》
  • https://docs.oracle.com/javase/7/docs/technotes/tools/windows/java.html?cm_mc_uid=30731201786714525992590&cm_mc_sid_50200000=1461656557
  • https://blog.csdn.net/raintungli/article/details/7310141

欢迎关注公众号
​​​​​​在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章