從 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

歡迎關注公衆號
​​​​​​在這裏插入圖片描述

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