JVM源码剖析之信号处理机制

版本信息:

jdk版本:jdk8u40

写在前面:

在看到Saturn唯品会的分布式调度框架时,看到此框架使用了信号处理,并且外面关于Java信号处理机制的文章很少有写到JVM层面,所以笔者心血来潮写下了这篇关于Java信号处理机制的文章~

因为Java信号处理机制是依赖于底层操作系统的信号处理机制,本文重点关注于Java信号处理机制,所以并不会过度的去介绍操作系统的信号处理机制~

源码论证:

因为Java信号处理是可以Java层面自定义的,所以肯定分为Java层面的源码,和JVM层面源码的处理。由浅到深,所以接下来看到Java层面的处理。

一、Java层面:

先从一个案例看到Java层面如何自定义信号。

public class SignalTest {

    public static void main(String[] args) throws InterruptedException {

        SignalHandler signalHandler = signal -> System.out.println("关闭不了的,不要试啦");

        Signal.handle(new Signal("INT"), signalHandler);

        Thread.sleep(1000000);
    }

}

这里自定义了SIGINT信号(shell的ctrl+c),这样的话ctrl+c并不会关闭JVM,反而仅仅是输出一句话而已。所以接下来我们需要明白他是如何执行的。

看到Signal.java类的构造方法

public Signal(String name) {
    // findSignal方法会根据name找到JVM的映射编号
    number = findSignal(name);
    this.name = name;
    // 如果传入的名字有误,那么直接抛出非法逻辑异常
    if (number < 0) {
        throw new IllegalArgumentException("Unknown signal: " + name);
    }
}

构造方法很简单,会调用findSignal这个native方法去根据传入的名字找到对应的编码,如果没找到的话就直接抛出非法逻辑异常,本文后续JVM源码会介绍findSignal方法。

接下来继续看到Signal的静态方法handle。

public static synchronized SignalHandler handle(Signal sig,
                                                SignalHandler handler)
    throws IllegalArgumentException {
    // 如果类型是NativeSignalHandler就调用getHandler,无需关心,这个给JNI使用
    // 如果不是的话就是2.
    long newH = (handler instanceof NativeSignalHandler) ?
                  ((NativeSignalHandler)handler).getHandler() : 2;
    
    // 拿到原有sig.number对应的处理函数。
    // 这里顺带检测了当前Java程序是否有资格去修改某些信号。
    long oldH = handle0(sig.number, newH);

    signals.put(sig.number, sig);
    synchronized (handlers) {
        // 删除原有的SignalHandler
        SignalHandler oldHandler = handlers.get(sig);
        handlers.remove(sig);

        // 如果是2就代表不是NativeSignalHandler,所以直接添加到集合中。
        if (newH == 2) {
            handlers.put(sig, handler);
        }

        ………… // 省略一些判断
    }
}

这里也非常的简单,调用handle0这个native方法获取到sig.number之前注册的处理函数,并且会顺带去检测当前是否有资格去修改信息(因为有些信号是强制不能修改的,有些信号是JVM内部要使用的),如果不能修改的话会抛出非法逻辑异常。最终会把Signal对象作为key,SignalHandler作为value放入到Hashtable中(其实也很明显了,放入一张表中,后续查表)

// 此方法由JVM调用,传入的参数为信号的编号
private static void dispatch(final int number) {
    // 通过编号找到Signal对象
    final Signal sig = signals.get(number);
    // 通过Signal对象找到SignalHandler
    final SignalHandler handler = handlers.get(sig);
    // 包装一个Runnable
    Runnable runnable = new Runnable () {
        public void run() {
            handler.handle(sig);
        }
    };
    // 异步处理,不能影响后续信号的处理
    if (handler != null) {
        new Thread(runnable, sig + " handler").start();
    }
}

Signal.java类中存在dispatch方法,此方法由JVM调用,所以在Java层面你会找不到调用点,这个方法很简单,就是查表,然后异步执行对应的SignalHandler(而SignalHandler就是你自定义的逻辑代码)~

所以下文会介绍findSignal、handle0这两个native方法,以及JVM如何调用的dispatch方法。

二、JVM层面:

1.findSignal方法:

先介绍findSignal这个native方法,由上文我们得知,此方法入参为信号名称,返回值为JVM的信号编号,所以肯定是一个查表的过程,src/os/linux/vm/jvm_linux.cpp 文件中定义

struct siglabel siglabels[] = {
  /* derived from /usr/include/bits/signum.h on RH7.2 */
   "HUP",       SIGHUP,         /* Hangup (POSIX).  */
  "INT",        SIGINT,         /* Interrupt (ANSI).  */
  "QUIT",       SIGQUIT,        /* Quit (POSIX).  */
  "ILL",        SIGILL,         /* Illegal instruction (ANSI).  */
  "TRAP",       SIGTRAP,        /* Trace trap (POSIX).  */
  "ABRT",       SIGABRT,        /* Abort (ANSI).  */
  "IOT",        SIGIOT,         /* IOT trap (4.2 BSD).  */
  "BUS",        SIGBUS,         /* BUS error (4.2 BSD).  */
  "FPE",        SIGFPE,         /* Floating-point exception (ANSI).  */
  "KILL",       SIGKILL,        /* Kill, unblockable (POSIX).  */
  "USR1",       SIGUSR1,        /* User-defined signal 1 (POSIX).  */
  "SEGV",       SIGSEGV,        /* Segmentation violation (ANSI).  */
  "USR2",       SIGUSR2,        /* User-defined signal 2 (POSIX).  */
  "PIPE",       SIGPIPE,        /* Broken pipe (POSIX).  */
  "ALRM",       SIGALRM,        /* Alarm clock (POSIX).  */
  "TERM",       SIGTERM,        /* Termination (ANSI).  */
#ifdef SIGSTKFLT
  "STKFLT",     SIGSTKFLT,      /* Stack fault.  */
#endif
  "CLD",        SIGCLD,         /* Same as SIGCHLD (System V).  */
  "CHLD",       SIGCHLD,        /* Child status has changed (POSIX).  */
  "CONT",       SIGCONT,        /* Continue (POSIX).  */
  "STOP",       SIGSTOP,        /* Stop, unblockable (POSIX).  */
  "TSTP",       SIGTSTP,        /* Keyboard stop (POSIX).  */
  "TTIN",       SIGTTIN,        /* Background read from tty (POSIX).  */
  "TTOU",       SIGTTOU,        /* Background write to tty (POSIX).  */
  "URG",        SIGURG,         /* Urgent condition on socket (4.2 BSD).  */
  "XCPU",       SIGXCPU,        /* CPU limit exceeded (4.2 BSD).  */
  "XFSZ",       SIGXFSZ,        /* File size limit exceeded (4.2 BSD).  */
  "VTALRM",     SIGVTALRM,      /* Virtual alarm clock (4.2 BSD).  */
  "PROF",       SIGPROF,        /* Profiling alarm clock (4.2 BSD).  */
  "WINCH",      SIGWINCH,       /* Window size change (4.3 BSD, Sun).  */
  "POLL",       SIGPOLL,        /* Pollable event occurred (System V).  */
  "IO",         SIGIO,          /* I/O now possible (4.2 BSD).  */
  "PWR",        SIGPWR,         /* Power failure restart (System V).  */
#ifdef SIGSYS
  "SYS",        SIGSYS          /* Bad system call. Only on some Linuxen! */
#endif
};

这里就已经限定了你传入的名称,比如操作系统SIGINT信号就只能传"INT"。

// src/share/native/sun/misc/Signal.c
JNIEXPORT jint JNICALL
Java_sun_misc_Signal_findSignal(JNIEnv *env, jclass cls, jstring name)
{
    ………… // 省略无关代码
    res = JVM_FindSignal(cname);
    return res;
}

// src/os/linux/vm/jvm_linux.cpp 文件
JVM_ENTRY_NO_ENV(jint, JVM_FindSignal(const char *name))

  // 查表
  for(uint i=0; i<ARRAY_SIZE(siglabels); i++)
    if(!strcmp(name, siglabels[i].name))
      return siglabels[i].number;

  return -1;

JVM_END

这里非常的简单,就是一个查表的过程,如果查到了就返回对应的编号,如果没查找就返回-1.

2.handle0方法:

接下来看到handle0这个native方法的处理细节 

// src/share/native/sun/misc/Signal.c
JNIEXPORT jlong JNICALL
Java_sun_misc_Signal_handle0(JNIEnv *env, jclass cls, jint sig, jlong handler)
{
  return ptr_to_jlong(JVM_RegisterSignal(sig, jlong_to_ptr(handler)));
}

// src/os/linux/vm/jvm_linux.cpp 文件
JVM_ENTRY_NO_ENV(void*, JVM_RegisterSignal(jint sig, void* handler))

  // 默认情况下为2,特殊情况下比如JNI信号处理可能是一个方法地址
  // 为2的情况下,使用JVM默认的信号处理函数os::user_handler()
  void* newHandler = handler == (void *)2
                   ? os::user_handler()
                   : handler;
  switch (sig) {
    
    // JVM内部需要使用的
    case INTERRUPT_SIGNAL:
    case SIGFPE:
    case SIGILL:
    case SIGSEGV:
    case BREAK_SIGNAL:
      return (void *)-1;
    // 这些信号需要判断JVM参数ReduceSignalUsage是否开启
    case SHUTDOWN1_SIGNAL:
    case SHUTDOWN2_SIGNAL:
    case SHUTDOWN3_SIGNAL:
      if (ReduceSignalUsage) return (void*)-1;
      if (os::Linux::is_sig_ignored(sig)) return (void*)1;
  }
  // 调用系统调用,将sig编号的处理函数变换成newHandler
  void* oldHandler = os::signal(sig, newHandler);
  if (oldHandler == os::user_handler()) {
      return (void *)2;
  } else {
      return oldHandler;
  }
JVM_END
  1. 判断传入的handler地址是否为2,默认都是2,为2的话使用JVM提供的默认信号处理函数os::user_handler(),当是JNI信号处理函数时,handler地址就为JNI信号处理函数。
  2. 判断信号是否是JVM内部使用的
  3. 如果是SIGHUP、SIGINT、SIGTERM这三个信号需要判断ReduceSignalUsage这个JVM参数是否关闭(ReduceSignalUsage用来控制减少其他进程对当前JVM的信号处理,默认是关闭)
  4. 调用系统调用将信号处理函数改变。

此时,我们调用handle0这个native 方法把信号对应的处理函数更改成os::user_handler(),所以当其他进程给当前JVM进程发送信号时,JVM会调用os::user_handler()方法。

3.JVM如何调用的dispatch方法

所以接下来看到os::user_handler()方法,src/os/linux/vm/os_linux.cpp 文件

void* os::user_handler() {
  return CAST_FROM_FN_PTR(void*, UserHandler);
}

// int sig            信号编号
// void *siginfo      信号信息   
// void *context      上下文信息
// 这三个参数都是操作系统传入的
static void
UserHandler(int sig, void *siginfo, void *context) {
  // 在Java层面的信号处理也看到了,如果来一个信号就会使用一个线程,
  // 如果其他进程一致发送SIGINT就会立马创建很多线程
  // 所以需要做限制。
  if (sig == SIGINT && Atomic::add(1, &sigint_count) > 1)
      return;

  // 通讯给signal dispatcher线程
  os::signal_notify(sig);
}

目前,我们得明白,当其他进程发送信号给JVM进程时,JVM会调用user_handler方法,转而调用UserHandler方法,而在UserHandler方法中会调用os::signal_notify(sig)方法,此方法会通过队列把当前信号编号传给signal dispatcher线程,所以接下来,我们需要分析signal dispatcher线程如何接受到数据,以及如何处理的数据。

在JVM启动时,会调用os::signal_init的方法初始化关于信号的处理,src/share/vm/runtime/os.cpp文件中os::signal_init方法

void os::signal_init() {
  if (!ReduceSignalUsage) {
    EXCEPTION_MARK;
    Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
    instanceKlassHandle klass (THREAD, k);
    instanceHandle thread_oop = klass->allocate_instance_handle(CHECK);

    const char thread_name[] = "Signal Dispatcher";
    Handle string = java_lang_String::create_from_str(thread_name, CHECK);

    // Initialize thread_oop to put it into the system threadGroup
    Handle thread_group (THREAD, Universe::system_thread_group());
    JavaValue result(T_VOID);
    JavaCalls::call_special(&result, thread_oop,
                           klass,
                           vmSymbols::object_initializer_name(),
                           vmSymbols::threadgroup_string_void_signature(),
                           thread_group,
                           string,
                           CHECK);

    KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
    JavaCalls::call_special(&result,
                            thread_group,
                            group,
                            vmSymbols::add_method_name(),
                            vmSymbols::thread_void_signature(),
                            thread_oop,         // ARG 1
                            CHECK);

    os::signal_init_pd();

    { MutexLocker mu(Threads_lock);
      JavaThread* signal_thread = new JavaThread(&signal_thread_entry);

      java_lang_Thread::set_thread(thread_oop(), signal_thread);
      java_lang_Thread::set_priority(thread_oop(), NearMaxPriority);
      java_lang_Thread::set_daemon(thread_oop());

      signal_thread->set_threadObj(thread_oop());
      Threads::add(signal_thread);
      Thread::start(signal_thread);
    }
    // 注册SIGQUIT信号的处理函数为os::user_handler()
    os::signal(SIGBREAK, os::user_handler());
  }
}

这里代码虽然很多,但是非常的简单,就是用c++的代码在创建一个Java线程,等同于在Java层面new Thread一致。而不管是在c++层面还是Java层面创建一个线程,都需要有一个线程启动后的回调点。这里的回调点为signal_thread_entry函数。

static void signal_thread_entry(JavaThread* thread, TRAPS) {
  os::set_priority(thread, NearMaxPriority);
  while (true) {
    int sig;
    {
      // 来信号后,其他线程 会往这个线程发送信号编号,这边就会被唤醒。
      // 获取到信号的编号。
      sig = os::signal_wait();
    }

    ………… // 省略无关代码

    // 这里调用Java的Signal类的dispatch方法
    // 到此为止,我们就整个环都闭上了
    HandleMark hm(THREAD);
    Klass* k = SystemDictionary::resolve_or_null(vmSymbols::sun_misc_Signal(), THREAD);
    KlassHandle klass (THREAD, k);
    if (klass.not_null()) {
      JavaValue result(T_VOID);
      JavaCallArguments args;
      args.push_int(sig);
      JavaCalls::call_static(
        &result,
        klass,
        vmSymbols::dispatch_name(),
        vmSymbols::int_void_signature(),
        &args,
        THREAD
      );
    }
  }
}
  1. signal_thread_entry函数作为signal dispatcher线程的执行方法
  2. 在没有其他线程投递信号编号给我signal dispatcher线程时,会调用signal_wait方法睡眠,等待其他线程投递信号编号。而其他线程也是等待操作系统回调,而操作系统是等待其他线程往JVM进程发送信号~
  3. 获取到信号编号后,调用Java的Signal类中dispatch方法,去根据信号编号查表,最终执行用户自定义的SignalHander逻辑
  4. 到此为止,我们就把整个信号处理机制完美闭环~

总结:

从Java到JVM,再从JVM到Java,跨度是非常的大,所以笔者也只能把Java和JVM分为2部分来讲解,尽量用简单的方式讲述明白~ 并且当我们研究Java的信号处理时,就可以把操作系统的信号处理当作黑盒来理解即可,只需要明白当来信号时操作系统会回调JVM中那一个方法即可~

相关推荐

  1. JVM剖析信号处理机制

    2023-12-16 21:20:01       25 阅读
  2. JVM剖析registerNatives方法

    2023-12-16 21:20:01       32 阅读
  3. Zookeeper剖析:深入理解Leader选举机制

    2023-12-16 21:20:01       38 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-16 21:20:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-16 21:20:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-16 21:20:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-16 21:20:01       20 阅读

热门阅读

  1. Ubuntu 命令行安装 Clang 16或者Clang 17的过程

    2023-12-16 21:20:01       38 阅读
  2. js-正则表达式

    2023-12-16 21:20:01       25 阅读
  3. 学习软件测试建议看些什么书?

    2023-12-16 21:20:01       31 阅读
  4. Ceph入门到精通-Ceph Pacific 版本磁盘预测模块

    2023-12-16 21:20:01       30 阅读
  5. 【LeetCode】第18题. 四数之和

    2023-12-16 21:20:01       42 阅读
  6. MySQL之修改表

    2023-12-16 21:20:01       36 阅读
  7. Lost connection to MySQL server during query

    2023-12-16 21:20:01       38 阅读
  8. Vue中this.$nextTick的执行时机

    2023-12-16 21:20:01       43 阅读