JVM Attach實現原理剖析


轉載:https://www.cnblogs.com/scofield-1987/p/9347586.html 

前言


本文旨在從理論上分析JVM 在 Linux 環境下 Attach 操作的前因后果,以及 JVM 為此而設計並實現的解決方案,通過本文,我希望能夠講述清楚如下三個主要方面的內容。

原發布:我的博客

一、Attach 為什么而出現


Attach的出現究其根本原因,應該就是為了實現 Java 進程(A)與進程(B)之間的本地通信。一旦這個通信通道能夠成功建立,那么進程 A 就能通知進程 B 去執行某些操作,從而達到監控進程 B 或者控制進程 B 的某些行為的目的。如 jstack、jmap等 JDK 自帶的工具,基本都是通過 Attach 機制去達成各自想要的目的的。至於 jstack、jmap 能做什么、怎么做,就不再本文的討論范圍了,請自行百度或者 Google。

 

二、Attach 在 JVM 底層實現的根本原理是什么


Attach 實現的根本原理就是使用了 Linux 下是文件 Socket 通信(詳情可以自行百度或 Google)。有人也許會問,為什么要采用文件 socket 而不采用網絡 socket?我個人認為也許一方面是為了效率(避免了網絡協議的解析、數據包的封裝和解封裝等),另一方面是為了減少對系統資源的占用(如網絡端口占用)。采用文件 socket 通信,就好比兩個進程通過事先約定好的協議,對同一個文件進行讀寫操作,以達到信息的交互和共享。簡單理解成如下圖所示的模型

通過/tmp/.java.pid2345這個文件,實現客戶進程與目標進程2345的通信。

 

三、Attach 在 JVM 中實現的源碼分析


源碼的分析主要分三階段進行,這里要達到的目的是,弄 Attach 的清楚來龍去脈,本文的所有源碼都是基於 Open JDK 1.8的,大家可以自行去下載 Open JDK 1.8 的源碼。

 

3.1、目標JVM 對OS信號監聽的實現


或許你會想,在最開始的時候,目標 JVM 是怎么知道有某個進程想 attach 它自己的?答案很簡單,就是目標 JVM 在啟動的時候,在 JVM 內部啟動了一個監聽線程,這個線程的名字叫“Signal Dispatcher”,該線程的作用是,監聽並處理 OS 的信號。至於什么是 OS 的信號(可以自行百度或 Google),簡單理解就是,Linux系統允許進程與進程之間通過過信號的方式進行通信,如觸發某個操作(操作由接受到信號的進程自定義)。如平常我們用的最多的就是 kill -9 ${pid}來殺死某個進程,kill進程通過向${pid}的進程發送一個編號為“9”號的信號,來通知系統強制結束${pid}的生命周期。

接下來我們就通過源碼截圖的方式來呈現一下“Signal Dispatcher”線程的創建過程。

首先進入 JVM 的啟動類:/jdk/src/share/bin/main.c

復制代碼
 1 int
 2 main(int argc, char **argv)
 3 {
 4     int margc;
 5     char** margv;
 6     const jboolean const_javaw = JNI_FALSE;
 7 #endif /* JAVAW */
 8 #ifdef _WIN32
 9     {
10         int i = 0;
11         if (getenv(JLDEBUG_ENV_ENTRY) != NULL) {
12             printf("Windows original main args:\n");
13             for (i = 0 ; i < __argc ; i++) {
14                 printf("wwwd_args[%d] = %s\n", i, __argv[i]);
15             }
16         }
17     }
18     JLI_CmdToArgs(GetCommandLine());
19     margc = JLI_GetStdArgc();
20     // add one more to mark the end
21     margv = (char **)JLI_MemAlloc((margc + 1) * (sizeof(char *)));
22     {
23         int i = 0;
24         StdArg *stdargs = JLI_GetStdArgs();
25         for (i = 0 ; i < margc ; i++) {
26             margv[i] = stdargs[i].arg;
27         }
28         margv[i] = NULL;
29     }
30 #else /* *NIXES */
31     margc = argc;
32     margv = argv;
33 #endif /* WIN32 */
34     return JLI_Launch(margc, margv,
35                    sizeof(const_jargs) / sizeof(char *), const_jargs,
36                    sizeof(const_appclasspath) / sizeof(char *), const_appclasspath,
37                    FULL_VERSION,
38                    DOT_VERSION,
39                    (const_progname != NULL) ? const_progname : *margv,
40                    (const_launcher != NULL) ? const_launcher : *margv,
41                    (const_jargs != NULL) ? JNI_TRUE : JNI_FALSE,
42                    const_cpwildcard, const_javaw, const_ergo_class);
43 }
復制代碼

這個類里邊最重要的一個方法就是最后的JLI_Launch,這個方法的實現存在於jdk/src/share/bin/java.c 中(大家應該都不陌生平時我們運行 java 程序時,都是采用 java com.***.Main來啟動的吧)。

復制代碼
  1 /*
  2  * Entry point.
  3  */
  4 int
  5 JLI_Launch(int argc, char ** argv,              /* main argc, argc */
  6         int jargc, const char** jargv,          /* java args */
  7         int appclassc, const char** appclassv,  /* app classpath */
  8         const char* fullversion,                /* full version defined */
  9         const char* dotversion,                 /* dot version defined */
 10         const char* pname,                      /* program name */
 11         const char* lname,                      /* launcher name */
 12         jboolean javaargs,                      /* JAVA_ARGS */
 13         jboolean cpwildcard,                    /* classpath wildcard*/
 14         jboolean javaw,                         /* windows-only javaw */
 15         jint ergo                               /* ergonomics class policy */
 16 )
 17 {
 18     int mode = LM_UNKNOWN;
 19     char *what = NULL;
 20     char *cpath = 0;
 21     char *main_class = NULL;
 22     int ret;
 23     InvocationFunctions ifn;
 24     jlong start, end;
 25     char jvmpath[MAXPATHLEN];
 26     char jrepath[MAXPATHLEN];
 27     char jvmcfg[MAXPATHLEN];
 28 
 29     _fVersion = fullversion;
 30     _dVersion = dotversion;
 31     _launcher_name = lname;
 32     _program_name = pname;
 33     _is_java_args = javaargs;
 34     _wc_enabled = cpwildcard;
 35     _ergo_policy = ergo;
 36 
 37     InitLauncher(javaw);
 38     DumpState();
 39     if (JLI_IsTraceLauncher()) {
 40         int i;
 41         printf("Command line args:\n");
 42         for (i = 0; i < argc ; i++) {
 43             printf("argv[%d] = %s\n", i, argv[i]);
 44         }
 45         AddOption("-Dsun.java.launcher.diag=true", NULL);
 46     }
 47 
 48     /*
 49      * Make sure the specified version of the JRE is running.
 50      *
 51      * There are three things to note about the SelectVersion() routine:
 52      *  1) If the version running isn't correct, this routine doesn't
 53      *     return (either the correct version has been exec'd or an error
 54      *     was issued).
 55      *  2) Argc and Argv in this scope are *not* altered by this routine.
 56      *     It is the responsibility of subsequent code to ignore the
 57      *     arguments handled by this routine.
 58      *  3) As a side-effect, the variable "main_class" is guaranteed to
 59      *     be set (if it should ever be set).  This isn't exactly the
 60      *     poster child for structured programming, but it is a small
 61      *     price to pay for not processing a jar file operand twice.
 62      *     (Note: This side effect has been disabled.  See comment on
 63      *     bugid 5030265 below.)
 64      */
 65     SelectVersion(argc, argv, &main_class);
 66 
 67     CreateExecutionEnvironment(&argc, &argv,
 68                                jrepath, sizeof(jrepath),
 69                                jvmpath, sizeof(jvmpath),
 70                                jvmcfg,  sizeof(jvmcfg));
 71 
 72     ifn.CreateJavaVM = 0;
 73     ifn.GetDefaultJavaVMInitArgs = 0;
 74 
 75     if (JLI_IsTraceLauncher()) {
 76         start = CounterGet();
 77     }
 78 
 79     if (!LoadJavaVM(jvmpath, &ifn)) {
 80         return(6);
 81     }
 82 
 83     if (JLI_IsTraceLauncher()) {
 84         end   = CounterGet();
 85     }
 86 
 87     JLI_TraceLauncher("%ld micro seconds to LoadJavaVM\n",
 88              (long)(jint)Counter2Micros(end-start));
 89 
 90     ++argv;
 91     --argc;
 92 
 93     if (IsJavaArgs()) {
 94         /* Preprocess wrapper arguments */
 95         TranslateApplicationArgs(jargc, jargv, &argc, &argv);
 96         if (!AddApplicationOptions(appclassc, appclassv)) {
 97             return(1);
 98         }
 99     } else {
100         /* Set default CLASSPATH */
101         cpath = getenv("CLASSPATH");
102         if (cpath == NULL) {
103             cpath = ".";
104         }
105         SetClassPath(cpath);
106     }
107 
108     /* Parse command line options; if the return value of
109      * ParseArguments is false, the program should exit.
110      */
111     if (!ParseArguments(&argc, &argv, &mode, &what, &ret, jrepath))
112     {
113         return(ret);
114     }
115 
116     /* Override class path if -jar flag was specified */
117     if (mode == LM_JAR) {
118         SetClassPath(what);     /* Override class path */
119     }
120 
121     /* set the -Dsun.java.command pseudo property */
122     SetJavaCommandLineProp(what, argc, argv);
123 
124     /* Set the -Dsun.java.launcher pseudo property */
125     SetJavaLauncherProp();
126 
127     /* set the -Dsun.java.launcher.* platform properties */
128     SetJavaLauncherPlatformProps();
129 
130     return JVMInit(&ifn, threadStackSize, argc, argv, mode, what, ret);
131 }
復制代碼

這個方法中,進行了一系列必要的操作,如libjvm.so的加載、參數解析、Classpath 的獲取和設置、系統屬性的設置、JVM 初始化等等,不過和本文相關的主要是130行的 JVMInit 方法,接下來我們看下這個方法的實現(位於/jdk/src/solaris/bin/java_md_solinux.c)。

復制代碼
1 int
2 JVMInit(InvocationFunctions* ifn, jlong threadStackSize,
3         int argc, char **argv,
4         int mode, char *what, int ret)
5 {
6     ShowSplashScreen();
7     return ContinueInNewThread(ifn, threadStackSize, argc, argv, mode, what, ret);
8 }
復制代碼

這里請關注兩個點,ContinueInNewThread方法 和 ifn 入參。ContinueInNewThread位於 java.c中,而 ifn 則攜帶了libjvm.so中的幾個非常重要的函數(CreateJavaVM/GetDefaultJavaVMInitArgs/GetCreatedJavaVMs),這里我們重點關注CreateJavaVM

復制代碼
 1 int
 2 ContinueInNewThread(InvocationFunctions* ifn, jlong threadStackSize,
 3                     int argc, char **argv,
 4                     int mode, char *what, int ret)
 5 {
 6 
 7     /*
 8      * If user doesn't specify stack size, check if VM has a preference.
 9      * Note that HotSpot no longer supports JNI_VERSION_1_1 but it will
10      * return its default stack size through the init args structure.
11      */
12     if (threadStackSize == 0) {
13       struct JDK1_1InitArgs args1_1;
14       memset((void*)&args1_1, 0, sizeof(args1_1));
15       args1_1.version = JNI_VERSION_1_1;
16       ifn->GetDefaultJavaVMInitArgs(&args1_1);  /* ignore return value */
17       if (args1_1.javaStackSize > 0) {
18          threadStackSize = args1_1.javaStackSize;
19       }
20     }
21 
22     { /* Create a new thread to create JVM and invoke main method */
23       JavaMainArgs args;
24       int rslt;
25 
26       args.argc = argc;
27       args.argv = argv;
28       args.mode = mode;
29       args.what = what;
30       args.ifn = *ifn;
31 
32       rslt = ContinueInNewThread0(JavaMain, threadStackSize, (void*)&args);
33       /* If the caller has deemed there is an error we
34        * simply return that, otherwise we return the value of
35        * the callee
36        */
37       return (ret != 0) ? ret : rslt;
38     }
39 }
復制代碼

可以看出,這里進行了 JavaMainArgs 參數設置,設置完成之后,在32行處調用了 ContinueInNewThread0 (位於/jdk/src/solaris/bin/java_md_solinux.c)方法,該方法中傳入了 JavaMain 函數指針和 args 參數,這二者至關重要。接下來看下其源碼

復制代碼
 1 /*
 2  * Block current thread and continue execution in a new thread
 3  */
 4 int
 5 ContinueInNewThread0(int (JNICALL *continuation)(void *), jlong stack_size, void * args) {
 6     int rslt;
 7 #ifdef __linux__
 8     pthread_t tid;
 9     pthread_attr_t attr;
10     pthread_attr_init(&attr);
11     pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
12 
13     if (stack_size > 0) {
14       pthread_attr_setstacksize(&attr, stack_size);
15     }
16 
17     if (pthread_create(&tid, &attr, (void *(*)(void*))continuation, (void*)args) == 0) {
18       void * tmp;
19       pthread_join(tid, &tmp);
20       rslt = (int)tmp;
21     } else {
22      /*
23       * Continue execution in current thread if for some reason (e.g. out of
24       * memory/LWP)  a new thread can't be created. This will likely fail
25       * later in continuation as JNI_CreateJavaVM needs to create quite a
26       * few new threads, anyway, just give it a try..
27       */
28       rslt = continuation(args);
29     }
30 
31     pthread_attr_destroy(&attr);
32 #else /* ! __linux__ */
33     thread_t tid;
34     long flags = 0;
35     if (thr_create(NULL, stack_size, (void *(*)(void *))continuation, args, flags, &tid) == 0) {
36       void * tmp;
37       thr_join(tid, NULL, &tmp);
38       rslt = (int)tmp;
39     } else {
40       /* See above. Continue in current thread if thr_create() failed */
41       rslt = continuation(args);
42     }
43 #endif /* __linux__ */
44     return rslt;
45 }
復制代碼

這里最關鍵的點在於,如果是 linux 環境下,則創建了一個 pthread_t 的線程來運行傳入的 JavaMain 函數,並且將 args 參數也一並傳入了。這時候,我們唯一要關注的便是 JavaMain (在jdk/src/share/bin/java.c )函數,請看源碼

復制代碼
  1 int JNICALL
  2 JavaMain(void * _args)
  3 {
  4     JavaMainArgs *args = (JavaMainArgs *)_args;
  5     int argc = args->argc;
  6     char **argv = args->argv;
  7     int mode = args->mode;
  8     char *what = args->what;
  9     InvocationFunctions ifn = args->ifn;
 10 
 11     JavaVM *vm = 0;
 12     JNIEnv *env = 0;
 13     jclass mainClass = NULL;
 14     jclass appClass = NULL; // actual application class being launched
 15     jmethodID mainID;
 16     jobjectArray mainArgs;
 17     int ret = 0;
 18     jlong start, end;
 19 
 20     RegisterThread();
 21 
 22     /* Initialize the virtual machine */
 23     start = CounterGet();
 24     if (!InitializeJVM(&vm, &env, &ifn)) {  25  JLI_ReportErrorMessage(JVM_ERROR1);  26         exit(1);  27  }  28 
 29     if (showSettings != NULL) {
 30         ShowSettings(env, showSettings);
 31         CHECK_EXCEPTION_LEAVE(1);
 32     }
 33 
 34     if (printVersion || showVersion) {
 35         PrintJavaVersion(env, showVersion);
 36         CHECK_EXCEPTION_LEAVE(0);
 37         if (printVersion) {
 38             LEAVE();
 39         }
 40     }
 41 
 42     /* If the user specified neither a class name nor a JAR file */
 43     if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
 44         PrintUsage(env, printXUsage);
 45         CHECK_EXCEPTION_LEAVE(1);
 46         LEAVE();
 47     }
 48 
 49     FreeKnownVMs();  /* after last possible PrintUsage() */
 50 
 51     if (JLI_IsTraceLauncher()) {
 52         end = CounterGet();
 53         JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
 54                (long)(jint)Counter2Micros(end-start));
 55     }
 56 
 57     /* At this stage, argc/argv have the application's arguments */
 58     if (JLI_IsTraceLauncher()){
 59         int i;
 60         printf("%s is '%s'\n", launchModeNames[mode], what);
 61         printf("App's argc is %d\n", argc);
 62         for (i=0; i < argc; i++) {
 63             printf("    argv[%2d] = '%s'\n", i, argv[i]);
 64         }
 65     }
 66 
 67     ret = 1;
 68 
 69     /*
 70      * Get the application's main class.
 71      *
 72      * See bugid 5030265.  The Main-Class name has already been parsed
 73      * from the manifest, but not parsed properly for UTF-8 support.
 74      * Hence the code here ignores the value previously extracted and
 75      * uses the pre-existing code to reextract the value.  This is
 76      * possibly an end of release cycle expedient.  However, it has
 77      * also been discovered that passing some character sets through
 78      * the environment has "strange" behavior on some variants of
 79      * Windows.  Hence, maybe the manifest parsing code local to the
 80      * launcher should never be enhanced.
 81      *
 82      * Hence, future work should either:
 83      *     1)   Correct the local parsing code and verify that the
 84      *          Main-Class attribute gets properly passed through
 85      *          all environments,
 86      *     2)   Remove the vestages of maintaining main_class through
 87      *          the environment (and remove these comments).
 88      *
 89      * This method also correctly handles launching existing JavaFX
 90      * applications that may or may not have a Main-Class manifest entry.
 91      */
 92     mainClass = LoadMainClass(env, mode, what);
 93     CHECK_EXCEPTION_NULL_LEAVE(mainClass);
 94     /*
 95      * In some cases when launching an application that needs a helper, e.g., a
 96      * JavaFX application with no main method, the mainClass will not be the
 97      * applications own main class but rather a helper class. To keep things
 98      * consistent in the UI we need to track and report the application main class.
 99      */
100     appClass = GetApplicationClass(env);
101     NULL_CHECK_RETURN_VALUE(appClass, -1);
102     /*
103      * PostJVMInit uses the class name as the application name for GUI purposes,
104      * for example, on OSX this sets the application name in the menu bar for
105      * both SWT and JavaFX. So we'll pass the actual application class here
106      * instead of mainClass as that may be a launcher or helper class instead
107      * of the application class.
108      */
109     PostJVMInit(env, appClass, vm);
110     /*
111      * The LoadMainClass not only loads the main class, it will also ensure
112      * that the main method's signature is correct, therefore further checking
113      * is not required. The main method is invoked here so that extraneous java
114      * stacks are not in the application stack trace.
115      */
116     mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
117                                        "([Ljava/lang/String;)V");
118     CHECK_EXCEPTION_NULL_LEAVE(mainID);
119 
120     /* Build platform specific argument array */
121     mainArgs = CreateApplicationArgs(env, argv, argc);
122     CHECK_EXCEPTION_NULL_LEAVE(mainArgs);
123 
124     /* Invoke main method. */
125     (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);
126 
127     /*
128      * The launcher's exit code (in the absence of calls to
129      * System.exit) will be non-zero if main threw an exception.
130      */
131     ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
132     LEAVE();
133 }
復制代碼

和本小節相關的函數為InitializeJVM函數,在這個函數中,調用CreateJavaVM方法,這個方法就是之前在加載 libjvm.so 的時候,從動態庫中獲取的,首先看InitializeJVM的源碼

復制代碼
 1 /*
 2  * Initializes the Java Virtual Machine. Also frees options array when
 3  * finished.
 4  */
 5 static jboolean
 6 InitializeJVM(JavaVM **pvm, JNIEnv **penv, InvocationFunctions *ifn)
 7 {
 8     JavaVMInitArgs args;
 9     jint r;
10 
11     memset(&args, 0, sizeof(args));
12     args.version  = JNI_VERSION_1_2;
13     args.nOptions = numOptions;
14     args.options  = options;
15     args.ignoreUnrecognized = JNI_FALSE;
16 
17     if (JLI_IsTraceLauncher()) {
18         int i = 0;
19         printf("JavaVM args:\n    ");
20         printf("version 0x%08lx, ", (long)args.version);
21         printf("ignoreUnrecognized is %s, ",
22                args.ignoreUnrecognized ? "JNI_TRUE" : "JNI_FALSE");
23         printf("nOptions is %ld\n", (long)args.nOptions);
24         for (i = 0; i < numOptions; i++)
25             printf("    option[%2d] = '%s'\n",
26                    i, args.options[i].optionString);
27     }
28 
29     r = ifn->CreateJavaVM(pvm, (void **)penv, &args); 30     JLI_MemFree(options);
31     return r == JNI_OK;
32 }
復制代碼

29行處,調用 CreateJavaVM(定義在hotspot/src/share/vm/prims/jni.cpp) 方法,來進行 JVM 虛擬機的真正創建過程,源碼如下

復制代碼
  1 _JNI_IMPORT_OR_EXPORT_ jint JNICALL JNI_CreateJavaVM(JavaVM **vm, void **penv, void *args) {
  2 #ifndef USDT2
  3   HS_DTRACE_PROBE3(hotspot_jni, CreateJavaVM__entry, vm, penv, args);
  4 #else /* USDT2 */
  5   HOTSPOT_JNI_CREATEJAVAVM_ENTRY(
  6                                  (void **) vm, penv, args);
  7 #endif /* USDT2 */
  8 
  9   jint result = JNI_ERR;
 10   DT_RETURN_MARK(CreateJavaVM, jint, (const jint&)result);
 11 
 12   // We're about to use Atomic::xchg for synchronization.  Some Zero
 13   // platforms use the GCC builtin __sync_lock_test_and_set for this,
 14   // but __sync_lock_test_and_set is not guaranteed to do what we want
 15   // on all architectures.  So we check it works before relying on it.
 16 #if defined(ZERO) && defined(ASSERT)
 17   {
 18     jint a = 0xcafebabe;
 19     jint b = Atomic::xchg(0xdeadbeef, &a);
 20     void *c = &a;
 21     void *d = Atomic::xchg_ptr(&b, &c);
 22     assert(a == (jint) 0xdeadbeef && b == (jint) 0xcafebabe, "Atomic::xchg() works");
 23     assert(c == &b && d == &a, "Atomic::xchg_ptr() works");
 24   }
 25 #endif // ZERO && ASSERT
 26 
 27   // At the moment it's only possible to have one Java VM,
 28   // since some of the runtime state is in global variables.
 29 
 30   // We cannot use our mutex locks here, since they only work on
 31   // Threads. We do an atomic compare and exchange to ensure only
 32   // one thread can call this method at a time
 33 
 34   // We use Atomic::xchg rather than Atomic::add/dec since on some platforms
 35   // the add/dec implementations are dependent on whether we are running
 36   // on a multiprocessor, and at this stage of initialization the os::is_MP
 37   // function used to determine this will always return false. Atomic::xchg
 38   // does not have this problem.
 39   if (Atomic::xchg(1, &vm_created) == 1) {
 40     return JNI_EEXIST;   // already created, or create attempt in progress
 41   }
 42   if (Atomic::xchg(0, &safe_to_recreate_vm) == 0) {
 43     return JNI_ERR;  // someone tried and failed and retry not allowed.
 44   }
 45 
 46   assert(vm_created == 1, "vm_created is true during the creation");
 47 
 48   /**
 49    * Certain errors during initialization are recoverable and do not
 50    * prevent this method from being called again at a later time
 51    * (perhaps with different arguments).  However, at a certain
 52    * point during initialization if an error occurs we cannot allow
 53    * this function to be called again (or it will crash).  In those
 54    * situations, the 'canTryAgain' flag is set to false, which atomically
 55    * sets safe_to_recreate_vm to 1, such that any new call to
 56    * JNI_CreateJavaVM will immediately fail using the above logic.
 57    */
 58   bool can_try_again = true;
 59 
 60   result = Threads::create_vm((JavaVMInitArgs*) args, &can_try_again);  61   if (result == JNI_OK) {
 62     JavaThread *thread = JavaThread::current();
 63     /* thread is thread_in_vm here */
 64     *vm = (JavaVM *)(&main_vm);
 65     *(JNIEnv**)penv = thread->jni_environment();
 66 
 67     // Tracks the time application was running before GC
 68     RuntimeService::record_application_start();
 69 
 70     // Notify JVMTI
 71     if (JvmtiExport::should_post_thread_life()) {
 72        JvmtiExport::post_thread_start(thread);
 73     }
 74 
 75     EventThreadStart event;
 76     if (event.should_commit()) {
 77       event.set_javalangthread(java_lang_Thread::thread_id(thread->threadObj()));
 78       event.commit();
 79     }
 80 
 81 #ifndef PRODUCT
 82   #ifndef TARGET_OS_FAMILY_windows
 83     #define CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(f) f()
 84   #endif
 85 
 86     // Check if we should compile all classes on bootclasspath
 87     if (CompileTheWorld) ClassLoader::compile_the_world();
 88     if (ReplayCompiles) ciReplay::replay(thread);
 89 
 90     // Some platforms (like Win*) need a wrapper around these test
 91     // functions in order to properly handle error conditions.
 92     CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(test_error_handler);
 93     CALL_TEST_FUNC_WITH_WRAPPER_IF_NEEDED(execute_internal_vm_tests);
 94 #endif
 95 
 96     // Since this is not a JVM_ENTRY we have to set the thread state manually before leaving.
 97     ThreadStateTransition::transition_and_fence(thread, _thread_in_vm, _thread_in_native);
 98   } else {
 99     if (can_try_again) {
100       // reset safe_to_recreate_vm to 1 so that retrial would be possible
101       safe_to_recreate_vm = 1;
102     }
103 
104     // Creation failed. We must reset vm_created
105     *vm = 0;
106     *(JNIEnv**)penv = 0;
107     // reset vm_created last to avoid race condition. Use OrderAccess to
108     // control both compiler and architectural-based reordering.
109     OrderAccess::release_store(&vm_created, 0);
110   }
111 
112   return result;
113 }
復制代碼

這里只關注最核心的方法是60行的Threads::create_vm(hotspot/src/share/vm/runtime/Thread.cpp) 方法,在這個方法中,進行了大量的初始化操作,不過,這里我們只關注其中的一個點,就是 os::signal_init() 方法的調用,這就是啟動“Signal Dispatcher”線程的地方。先看 create_vm 的源碼

復制代碼
  1 jint Threads::create_vm(JavaVMInitArgs* args, bool* canTryAgain) {
  2 
  3   extern void JDK_Version_init();
  4 
  5   // Check version
  6   if (!is_supported_jni_version(args->version)) return JNI_EVERSION;
  7 
  8   // Initialize the output stream module
  9   ostream_init();
 10 
 11   // Process java launcher properties.
 12   Arguments::process_sun_java_launcher_properties(args);
 13 
 14   // Initialize the os module before using TLS
 15   os::init();
 16 
 17   // Initialize system properties.
 18   Arguments::init_system_properties();
 19 
 20   // So that JDK version can be used as a discrimintor when parsing arguments
 21   JDK_Version_init();
 22 
 23   // Update/Initialize System properties after JDK version number is known
 24   Arguments::init_version_specific_system_properties();
 25 
 26   // Parse arguments
 27   jint parse_result = Arguments::parse(args);
 28   if (parse_result != JNI_OK) return parse_result;
 29 
 30   os::init_before_ergo();
 31 
 32   jint ergo_result = Arguments::apply_ergo();
 33   if (ergo_result != JNI_OK) return ergo_result;
 34 
 35   if (PauseAtStartup) {
 36     os::pause();
 37   }
 38 
 39 #ifndef USDT2
 40   HS_DTRACE_PROBE(hotspot, vm__init__begin);
 41 #else /* USDT2 */
 42   HOTSPOT_VM_INIT_BEGIN();
 43 #endif /* USDT2 */
 44 
 45   // Record VM creation timing statistics
 46   TraceVmCreationTime create_vm_timer;
 47   create_vm_timer.start();
 48 
 49   // Timing (must come after argument parsing)
 50   TraceTime timer("Create VM", TraceStartupTime);
 51 
 52   // Initialize the os module after parsing the args
 53   jint os_init_2_result = os::init_2();
 54   if (os_init_2_result != JNI_OK) return os_init_2_result;
 55 
 56   jint adjust_after_os_result = Arguments::adjust_after_os();
 57   if (adjust_after_os_result != JNI_OK) return adjust_after_os_result;
 58 
 59   // intialize TLS
 60   ThreadLocalStorage::init();
 61 
 62   // Bootstrap native memory tracking, so it can start recording memory
 63   // activities before worker thread is started. This is the first phase
 64   // of bootstrapping, VM is currently running in single-thread mode.
 65   MemTracker::bootstrap_single_thread();
 66 
 67   // Initialize output stream logging
 68   ostream_init_log();
 69 
 70   // Convert -Xrun to -agentlib: if there is no JVM_OnLoad
 71   // Must be before create_vm_init_agents()
 72   if (Arguments::init_libraries_at_startup()) {
 73     convert_vm_init_libraries_to_agents();
 74   }
 75 
 76   // Launch -agentlib/-agentpath and converted -Xrun agents
 77   if (Arguments::init_agents_at_startup()) {
 78     create_vm_init_agents();
 79   }
 80 
 81   // Initialize Threads state
 82   _thread_list = NULL;
 83   _number_of_threads = 0;
 84   _number_of_non_daemon_threads = 0;
 85 
 86   // Initialize global data structures and create system classes in heap
 87   vm_init_globals();
 88 
 89   // Attach the main thread to this os thread
 90   JavaThread* main_thread = new JavaThread();
 91   main_thread->set_thread_state(_thread_in_vm);
 92   // must do this before set_active_handles and initialize_thread_local_storage
 93   // Note: on solaris initialize_thread_local_storage() will (indirectly)
 94   // change the stack size recorded here to one based on the java thread
 95   // stacksize. This adjusted size is what is used to figure the placement
 96   // of the guard pages.
 97   main_thread->record_stack_base_and_size();
 98   main_thread->initialize_thread_local_storage();
 99 
100   main_thread->set_active_handles(JNIHandleBlock::allocate_block());
101 
102   if (!main_thread->set_as_starting_thread()) {
103     vm_shutdown_during_initialization(
104       "Failed necessary internal allocation. Out of swap space");
105     delete main_thread;
106     *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again
107     return JNI_ENOMEM;
108   }
109 
110   // Enable guard page *after* os::create_main_thread(), otherwise it would
111   // crash Linux VM, see notes in os_linux.cpp.
112   main_thread->create_stack_guard_pages();
113 
114   // Initialize Java-Level synchronization subsystem
115   ObjectMonitor::Initialize() ;
116 
117   // Second phase of bootstrapping, VM is about entering multi-thread mode
118   MemTracker::bootstrap_multi_thread();
119 
120   // Initialize global modules
121   jint status = init_globals();
122   if (status != JNI_OK) {
123     delete main_thread;
124     *canTryAgain = false; // don't let caller call JNI_CreateJavaVM again
125     return status;
126   }
127 
128   // Should be done after the heap is fully created
129   main_thread->cache_global_variables();
130 
131   HandleMark hm;
132 
133   { MutexLocker mu(Threads_lock);
134     Threads::add(main_thread);
135   }
136 
137   // Any JVMTI raw monitors entered in onload will transition into
138   // real raw monitor. VM is setup enough here for raw monitor enter.
139   JvmtiExport::transition_pending_onload_raw_monitors();
140 
141   // Fully start NMT
142   MemTracker::start();
143 
144   // Create the VMThread
145   { TraceTime timer("Start VMThread", TraceStartupTime);
146     VMThread::create();
147     Thread* vmthread = VMThread::vm_thread();
148 
149     if (!os::create_thread(vmthread, os::vm_thread))
150       vm_exit_during_initialization("Cannot create VM thread. Out of system resources.");
151 
152     // Wait for the VM thread to become ready, and VMThread::run to initialize
153     // Monitors can have spurious returns, must always check another state flag
154     {
155       MutexLocker ml(Notify_lock);
156       os::start_thread(vmthread);
157       while (vmthread->active_handles() == NULL) {
158         Notify_lock->wait();
159       }
160     }
161   }
162 
163   assert (Universe::is_fully_initialized(), "not initialized");
164   if (VerifyDuringStartup) {
165     // Make sure we're starting with a clean slate.
166     VM_Verify verify_op;
167     VMThread::execute(&verify_op);
168   }
169 
170   EXCEPTION_MARK;
171 
172   // At this point, the Universe is initialized, but we have not executed
173   // any byte code.  Now is a good time (the only time) to dump out the
174   // internal state of the JVM for sharing.
175   if (DumpSharedSpaces) {
176     MetaspaceShared::preload_and_dump(CHECK_0);
177     ShouldNotReachHere();
178   }
179 
180   // Always call even when there are not JVMTI environments yet, since environments
181   // may be attached late and JVMTI must track phases of VM execution
182   JvmtiExport::enter_start_phase();
183 
184   // Notify JVMTI agents that VM has started (JNI is up) - nop if no agents.
185   JvmtiExport::post_vm_start();
186 
187   {
188     TraceTime timer("Initialize java.lang classes", TraceStartupTime);
189 
190     if (EagerXrunInit && Arguments::init_libraries_at_startup()) {
191       create_vm_init_libraries();
192     }
193 
194     initialize_class(vmSymbols::java_lang_String(), CHECK_0);
195 
196     // Initialize java_lang.System (needed before creating the thread)
197     initialize_class(vmSymbols::java_lang_System(), CHECK_0);
198     initialize_class(vmSymbols::java_lang_ThreadGroup(), CHECK_0);
199     Handle thread_group = create_initial_thread_group(CHECK_0);
200     Universe::set_main_thread_group(thread_group());
201     initialize_class(vmSymbols::java_lang_Thread(), CHECK_0);
202     oop thread_object = create_initial_thread(thread_group, main_thread, CHECK_0);
203     main_thread->set_threadObj(thread_object);
204     // Set thread status to running since main thread has
205     // been started and running.
206     java_lang_Thread::set_thread_status(thread_object,
207                                         java_lang_Thread::RUNNABLE);
208 
209     // The VM creates & returns objects of this class. Make sure it's initialized.
210     initialize_class(vmSymbols::java_lang_Class(), CHECK_0);
211 
212     // The VM preresolves methods to these classes. Make sure that they get initialized
213     initialize_class(vmSymbols::java_lang_reflect_Method(), CHECK_0);
214     initialize_class(vmSymbols::java_lang_ref_Finalizer(),  CHECK_0);
215     call_initializeSystemClass(CHECK_0);
216 
217     // get the Java runtime name after java.lang.System is initialized
218     JDK_Version::set_runtime_name(get_java_runtime_name(THREAD));
219     JDK_Version::set_runtime_version(get_java_runtime_version(THREAD));
220 
221     // an instance of OutOfMemory exception has been allocated earlier
222     initialize_class(vmSymbols::java_lang_OutOfMemoryError(), CHECK_0);
223     initialize_class(vmSymbols::java_lang_NullPointerException(), CHECK_0);
224     initialize_class(vmSymbols::java_lang_ClassCastException(), CHECK_0);
225     initialize_class(vmSymbols::java_lang_ArrayStoreException(), CHECK_0);
226     initialize_class(vmSymbols::java_lang_ArithmeticException(), CHECK_0);
227     initialize_class(vmSymbols::java_lang_StackOverflowError(), CHECK_0);
228     initialize_class(vmSymbols::java_lang_IllegalMonitorStateException(), CHECK_0);
229     initialize_class(vmSymbols::java_lang_IllegalArgumentException(), CHECK_0);
230   }
231 
232   // See        : bugid 4211085.
233   // Background : the static initializer of java.lang.Compiler tries to read
234   //              property"java.compiler" and read & write property "java.vm.info".
235   //              When a security manager is installed through the command line
236   //              option "-Djava.security.manager", the above properties are not
237   //              readable and the static initializer for java.lang.Compiler fails
238   //              resulting in a NoClassDefFoundError.  This can happen in any
239   //              user code which calls methods in java.lang.Compiler.
240   // Hack :       the hack is to pre-load and initialize this class, so that only
241   //              system domains are on the stack when the properties are read.
242   //              Currently even the AWT code has calls to methods in java.lang.Compiler.
243   //              On the classic VM, java.lang.Compiler is loaded very early to load the JIT.
244   // Future Fix : the best fix is to grant everyone permissions to read "java.compiler" and
245   //              read and write"java.vm.info" in the default policy file. See bugid 4211383
246   //              Once that is done, we should remove this hack.
247   initialize_class(vmSymbols::java_lang_Compiler(), CHECK_0);
248 
249   // More hackery - the static initializer of java.lang.Compiler adds the string "nojit" to
250   // the java.vm.info property if no jit gets loaded through java.lang.Compiler (the hotspot
251   // compiler does not get loaded through java.lang.Compiler).  "java -version" with the
252   // hotspot vm says "nojit" all the time which is confusing.  So, we reset it here.
253   // This should also be taken out as soon as 4211383 gets fixed.
254   reset_vm_info_property(CHECK_0);
255 
256   quicken_jni_functions();
257 
258   // Must be run after init_ft which initializes ft_enabled
259   if (TRACE_INITIALIZE() != JNI_OK) {
260     vm_exit_during_initialization("Failed to initialize tracing backend");
261   }
262 
263   // Set flag that basic initialization has completed. Used by exceptions and various
264   // debug stuff, that does not work until all basic classes have been initialized.
265   set_init_completed();
266 
267 #ifndef USDT2
268   HS_DTRACE_PROBE(hotspot, vm__init__end);
269 #else /* USDT2 */
270   HOTSPOT_VM_INIT_END();
271 #endif /* USDT2 */
272 
273   // record VM initialization completion time
274 #if INCLUDE_MANAGEMENT
275   Management::record_vm_init_completed();
276 #endif // INCLUDE_MANAGEMENT
277 
278   // Compute system loader. Note that this has to occur after set_init_completed, since
279   // valid exceptions may be thrown in the process.
280   // Note that we do not use CHECK_0 here since we are inside an EXCEPTION_MARK and
281   // set_init_completed has just been called, causing exceptions not to be shortcut
282   // anymore. We call vm_exit_during_initialization directly instead.
283   SystemDictionary::compute_java_system_loader(THREAD);
284   if (HAS_PENDING_EXCEPTION) {
285     vm_exit_during_initialization(Handle(THREAD, PENDING_EXCEPTION));
286   }
287 
288 #if INCLUDE_ALL_GCS
289   // Support for ConcurrentMarkSweep. This should be cleaned up
290   // and better encapsulated. The ugly nested if test would go away
291   // once things are properly refactored. XXX YSR
292   if (UseConcMarkSweepGC || UseG1GC) {
293     if (UseConcMarkSweepGC) {
294       ConcurrentMarkSweepThread::makeSurrogateLockerThread(THREAD);
295     } else {
296       ConcurrentMarkThread::makeSurrogateLockerThread(THREAD);
297     }
298     if (HAS_PENDING_EXCEPTION) {
299       vm_exit_during_initialization(Handle(THREAD, PENDING_EXCEPTION));
300     }
301   }
302 #endif // INCLUDE_ALL_GCS
303 
304   // Always call even when there are not JVMTI environments yet, since environments
305   // may be attached late and JVMTI must track phases of VM execution
306   JvmtiExport::enter_live_phase();
307 
308   // Signal Dispatcher needs to be started before VMInit event is posted
309  os::signal_init(); 310 
311   // Start Attach Listener if +StartAttachListener or it can't be started lazily
312   if (!DisableAttachMechanism) {
313     AttachListener::vm_start();
314     if (StartAttachListener || AttachListener::init_at_startup()) {
315       AttachListener::init();
316     }
317   }
318 
319   // Launch -Xrun agents
320   // Must be done in the JVMTI live phase so that for backward compatibility the JDWP
321   // back-end can launch with -Xdebug -Xrunjdwp.
322   if (!EagerXrunInit && Arguments::init_libraries_at_startup()) {
323     create_vm_init_libraries();
324   }
325 
326   // Notify JVMTI agents that VM initialization is complete - nop if no agents.
327   JvmtiExport::post_vm_initialized();
328 
329   if (TRACE_START() != JNI_OK) {
330     vm_exit_during_initialization("Failed to start tracing backend.");
331   }
332 
333   if (CleanChunkPoolAsync) {
334     Chunk::start_chunk_pool_cleaner_task();
335   }
336 
337   // initialize compiler(s)
338 #if defined(COMPILER1) || defined(COMPILER2) || defined(SHARK)
339   CompileBroker::compilation_init();
340 #endif
341 
342   if (EnableInvokeDynamic) {
343     // Pre-initialize some JSR292 core classes to avoid deadlock during class loading.
344     // It is done after compilers are initialized, because otherwise compilations of
345     // signature polymorphic MH intrinsics can be missed
346     // (see SystemDictionary::find_method_handle_intrinsic).
347     initialize_class(vmSymbols::java_lang_invoke_MethodHandle(), CHECK_0);
348     initialize_class(vmSymbols::java_lang_invoke_MemberName(), CHECK_0);
349     initialize_class(vmSymbols::java_lang_invoke_MethodHandleNatives(), CHECK_0);
350   }
351 
352 #if INCLUDE_MANAGEMENT
353   Management::initialize(THREAD);
354 #endif // INCLUDE_MANAGEMENT
355 
356   if (HAS_PENDING_EXCEPTION) {
357     // management agent fails to start possibly due to
358     // configuration problem and is responsible for printing
359     // stack trace if appropriate. Simply exit VM.
360     vm_exit(1);
361   }
362 
363   if (Arguments::has_profile())       FlatProfiler::engage(main_thread, true);
364   if (MemProfiling)                   MemProfiler::engage();
365   StatSampler::engage();
366   if (CheckJNICalls)                  JniPeriodicChecker::engage();
367 
368   BiasedLocking::init();
369 
370   if (JDK_Version::current().post_vm_init_hook_enabled()) {
371     call_postVMInitHook(THREAD);
372     // The Java side of PostVMInitHook.run must deal with all
373     // exceptions and provide means of diagnosis.
374     if (HAS_PENDING_EXCEPTION) {
375       CLEAR_PENDING_EXCEPTION;
376     }
377   }
378 
379   {
380       MutexLockerEx ml(PeriodicTask_lock, Mutex::_no_safepoint_check_flag);
381       // Make sure the watcher thread can be started by WatcherThread::start()
382       // or by dynamic enrollment.
383       WatcherThread::make_startable();
384       // Start up the WatcherThread if there are any periodic tasks
385       // NOTE:  All PeriodicTasks should be registered by now. If they
386       //   aren't, late joiners might appear to start slowly (we might
387       //   take a while to process their first tick).
388       if (PeriodicTask::num_tasks() > 0) {
389           WatcherThread::start();
390       }
391   }
392 
393   // Give os specific code one last chance to start
394   os::init_3();
395 
396   create_vm_timer.end();
397 #ifdef ASSERT
398   _vm_complete = true;
399 #endif
400   return JNI_OK;
401 }
復制代碼

309 行處,看到了os::signal_init() 的調用(hotspot/src/share/vm/runtime/os.cpp),這就是我們要找的。接着,我們看下其具體實現

復制代碼
 1 void os::signal_init() {
 2   if (!ReduceSignalUsage) {
 3     // Setup JavaThread for processing signals
 4     EXCEPTION_MARK;
 5     Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
 6     instanceKlassHandle klass (THREAD, k);
 7     instanceHandle thread_oop = klass->allocate_instance_handle(CHECK);
 8 
 9     const char thread_name[] = "Signal Dispatcher";
10     Handle string = java_lang_String::create_from_str(thread_name, CHECK);
11 
12     // Initialize thread_oop to put it into the system threadGroup
13     Handle thread_group (THREAD, Universe::system_thread_group());
14     JavaValue result(T_VOID);
15     JavaCalls::call_special(&result, thread_oop,
16                            klass,
17                            vmSymbols::object_initializer_name(),
18                            vmSymbols::threadgroup_string_void_signature(),
19                            thread_group,
20                            string,
21                            CHECK);
22 
23     KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
24     JavaCalls::call_special(&result,
25                             thread_group,
26                             group,
27                             vmSymbols::add_method_name(),
28                             vmSymbols::thread_void_signature(),
29                             thread_oop,         // ARG 1
30                             CHECK);
31 
32     os::signal_init_pd();
33 
34     { MutexLocker mu(Threads_lock);
35       JavaThread* signal_thread = new JavaThread(&signal_thread_entry); 36 
37       // At this point it may be possible that no osthread was created for the
38       // JavaThread due to lack of memory. We would have to throw an exception
39       // in that case. However, since this must work and we do not allow
40       // exceptions anyway, check and abort if this fails.
41       if (signal_thread == NULL || signal_thread->osthread() == NULL) {
42         vm_exit_during_initialization("java.lang.OutOfMemoryError",
43                                       "unable to create new native thread");
44       }
45 
46       java_lang_Thread::set_thread(thread_oop(), signal_thread);
47       java_lang_Thread::set_priority(thread_oop(), NearMaxPriority);
48       java_lang_Thread::set_daemon(thread_oop());
49 
50       signal_thread->set_threadObj(thread_oop());
51       Threads::add(signal_thread);
52       Thread::start(signal_thread);
53     }
54     // Handle ^BREAK
55     os::signal(SIGBREAK, os::user_handler());
56   }
57 }
復制代碼

這里的完全可以看出來,在此函數中35行處,創建了一個 java 線程,用於執行signal_thread_entry 函數,那我們來看看,這個 signal_thread_entry 函數到底做了什么?

復制代碼
 1 // sigexitnum_pd is a platform-specific special signal used for terminating the Signal thread.
 2 static void signal_thread_entry(JavaThread* thread, TRAPS) {
 3   os::set_priority(thread, NearMaxPriority);
 4   while (true) {
 5     int sig;
 6     {
 7       // FIXME : Currently we have not decieded what should be the status
 8       //         for this java thread blocked here. Once we decide about
 9       //         that we should fix this.
10       sig = os::signal_wait(); 11     }
12     if (sig == os::sigexitnum_pd()) {
13        // Terminate the signal thread
14        return;
15     }
16 
17     switch (sig) {
18       case SIGBREAK: {
19         // Check if the signal is a trigger to start the Attach Listener - in that
20         // case don't print stack traces.
21         if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
22           continue;
23         }
24         // Print stack traces
25         // Any SIGBREAK operations added here should make sure to flush
26         // the output stream (e.g. tty->flush()) after output.  See 4803766.
27         // Each module also prints an extra carriage return after its output.
28         VM_PrintThreads op;
29         VMThread::execute(&op);
30         VM_PrintJNI jni_op;
31         VMThread::execute(&jni_op);
32         VM_FindDeadlocks op1(tty);
33         VMThread::execute(&op1);
34         Universe::print_heap_at_SIGBREAK();
35         if (PrintClassHistogram) {
36           VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */);
37           VMThread::execute(&op1);
38         }
39         if (JvmtiExport::should_post_data_dump()) {
40           JvmtiExport::post_data_dump();
41         }
42         break;
43       }
44       default: {
45         // Dispatch the signal to java
46         HandleMark hm(THREAD);
47         Klass* k = SystemDictionary::resolve_or_null(vmSymbols::sun_misc_Signal(), THREAD);
48         KlassHandle klass (THREAD, k);
49         if (klass.not_null()) {
50           JavaValue result(T_VOID);
51           JavaCallArguments args;
52           args.push_int(sig);
53           JavaCalls::call_static(
54             &result,
55             klass,
56             vmSymbols::dispatch_name(),
57             vmSymbols::int_void_signature(),
58             &args,
59             THREAD
60           );
61         }
62         if (HAS_PENDING_EXCEPTION) {
63           // tty is initialized early so we don't expect it to be null, but
64           // if it is we can't risk doing an initialization that might
65           // trigger additional out-of-memory conditions
66           if (tty != NULL) {
67             char klass_name[256];
68             char tmp_sig_name[16];
69             const char* sig_name = "UNKNOWN";
70             InstanceKlass::cast(PENDING_EXCEPTION->klass())->
71               name()->as_klass_external_name(klass_name, 256);
72             if (os::exception_name(sig, tmp_sig_name, 16) != NULL)
73               sig_name = tmp_sig_name;
74             warning("Exception %s occurred dispatching signal %s to handler"
75                     "- the VM may need to be forcibly terminated",
76                     klass_name, sig_name );
77           }
78           CLEAR_PENDING_EXCEPTION;
79         }
80       }
81     }
82   }
83 }
復制代碼

函數里面意思已經很清晰明了了,首先在10行處,有一個os::signal_wait()的調用,該調用的主要是阻塞當前線程,並等待接收系統信號,然后再根據接收到的信號 sig 做 switch 邏輯,對於不同的信號做不同的處理。至此,關於“目標 JVM 對OS信號監聽的實現”這一點,就已經分析結束了。簡單的一句話總結就是,JVM 在啟動的時候,會創建一個名為“Signal Dispatcher”的線程用於接收os 的信號,以便對不同信號分別做處理。

 

3.2、文件 Socket 通信的通道的創建


經過3.1的分析,我們已經知道在 JVM 啟動之后,內部會有線程監聽並處理 os 的信號,那么,這個時候,如果我們想和已經啟動的 JVM 建立通信,當然就可以毫不猶豫的使用信號來進行了。不過,基於信號的通信,也是存在限制的,一方面,os 支持的信號是有限的,二來信號的通信往往是單向的,不方便通信雙方進行高效的通信。基於這些,筆者認為,為了使得 Client JVM 和 Target JVM 更好的通信,就采用了 Socket 通信來實現二者的通信。那接下來我們看看,這個通道究竟是如何創建的?

當我們需要 attach 到某個目標 JVM 進程上去的時候,我們通常會寫如下代碼

1 VirtualMachine vm = VirtualMachine.attach(pid);

這樣我們就能得到目標 JVM 的相關信息了,是不是很簡單?不過,今天我們要做的可不是這么簡單的事情,我們需要深入其后,了解其根本。接下來我們就以com.sun.tools.attach.VirtualMachine的 attach 方法入手,逐層揭開其神秘面紗。

復制代碼
 1 public static VirtualMachine attach(String id)
 2         throws AttachNotSupportedException, IOException
 3     {
 4         if (id == null) {
 5             throw new NullPointerException("id cannot be null");
 6         }
 7         List<AttachProvider> providers = AttachProvider.providers();
 8         if (providers.size() == 0) {
 9             throw new AttachNotSupportedException("no providers installed");
10         }
11         AttachNotSupportedException lastExc = null;
12         for (AttachProvider provider: providers) {
13             try {
14                 return provider.attachVirtualMachine(id);
15             } catch (AttachNotSupportedException x) {
16                 lastExc = x;
17             }
18         }
19         throw lastExc;
20     }
復制代碼

這是attach的源碼,入參為目標 JVM 的進程 ID,其實現委派給了 AttachProvider 了,通過provider.attachVirtualMachine(id);來實現真正的 attach 操作。由於 AttachProvider 是個抽象類,所以這個方法的真正實現在子類中,在 Linux 環境下,我們看 sun.tools.attach.BsdAttachProvider.java 的實現。

復制代碼
 1 public VirtualMachine attachVirtualMachine(String vmid)
 2         throws AttachNotSupportedException, IOException
 3     {
 4         checkAttachPermission();
 5 
 6         // AttachNotSupportedException will be thrown if the target VM can be determined
 7         // to be not attachable.
 8         testAttachable(vmid);
 9 
10         return new BsdVirtualMachine(this, vmid);
11     }
復制代碼

這個方法非常簡單,就是 new 了一個 BsdVirtualMachine 對象,並且把目標進程 ID 帶過去了。看sun.tools.attach.BsdVirtualMachine.java 的構造函數

復制代碼
 1 /**
 2      * Attaches to the target VM
 3      */
 4     BsdVirtualMachine(AttachProvider provider, String vmid)
 5         throws AttachNotSupportedException, IOException
 6     {
 7         super(provider, vmid);
 8 
 9         // This provider only understands pids
10         int pid;
11         try {
12             pid = Integer.parseInt(vmid);
13         } catch (NumberFormatException x) {
14             throw new AttachNotSupportedException("Invalid process identifier");
15         }
16 
17         // Find the socket file. If not found then we attempt to start the
18         // attach mechanism in the target VM by sending it a QUIT signal.
19         // Then we attempt to find the socket file again.
20         path = findSocketFile(pid); 21         if (path == null) {
22             File f = new File(tmpdir, ".attach_pid" + pid);
23  createAttachFile(f.getPath()); 24             try {
25  sendQuitTo(pid); 26 
27                 // give the target VM time to start the attach mechanism
28                 int i = 0;
29                 long delay = 200;
30                 int retries = (int)(attachTimeout() / delay);
31                 do {
32                     try {
33                         Thread.sleep(delay);
34                     } catch (InterruptedException x) { }
35                     path = findSocketFile(pid);
36                     i++;
37                 } while (i <= retries && path == null);
38                 if (path == null) {
39                     throw new AttachNotSupportedException(
40                         "Unable to open socket file: target process not responding " +
41                         "or HotSpot VM not loaded");
42                 }
43             } finally {
44                 f.delete();
45             }
46         }
47 
48         // Check that the file owner/permission to avoid attaching to
49         // bogus process
50         checkPermissions(path);
51 
52         // Check that we can connect to the process
53         // - this ensures we throw the permission denied error now rather than
54         // later when we attempt to enqueue a command.
55         int s = socket();
56         try {
57             connect(s, path);
58         } finally {
59             close(s);
60         }
61     }
復制代碼

首先看20行處的findSocketFile(pid);這里是找對應的 socket (/tmp/.java_pid${pid})文件,這個文件就是我們在第二大點圖中畫出來的,用於進程間通信的 socket 文件,如果不存在,即第一次進入該方法的時候。這時會運行到74行的createAttachFile(f.getPath());來創建一個attach 文件,socket 文件的命名方式為:/tmp/../.attach_pid${pid},關於這兩個方法(findSocketFile和createAttachFile)的具體實現,這里就不展開了,感興趣的可以直接去查看jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c的相關源碼。然后就會運行到一個非常關鍵的方法25行的sendQuitTo(pid);這個方法的實現,我們等會進入BsdVirtualMachine.c看下源碼,其主要目的就是給該進程發送一個信號。之后會進入到31行處的 do...while循環,自旋反復輪詢指定的次數來獲取該 socket 文件的路徑,直到超時或者 path(即 socket 文件路徑) 不為空,最后在55行處,建立一個 socket,並且在57行處通過 path 進行 socket 的連接,從而完成了客戶端(Client JVM)到目標進程(Target JVM)的 socket 通道建立。不過,請打住,這里是不是少了點什么?我相信細心的你肯定發現了,至少還存2個問題,

1. Target JVM 的 socket 服務端是何時創建的?

2. 用於通信的 socket 文件是在哪里創建的?

帶着這兩個問題,我們進入25行關鍵方法sendQuitTo(pid);的源碼解讀,該方法是個本地方法,位於jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c中

復制代碼
 1 /*
 2  * Class:     sun_tools_attach_BsdVirtualMachine
 3  * Method:    sendQuitTo
 4  * Signature: (I)V
 5  */
 6 JNIEXPORT void JNICALL Java_sun_tools_attach_BsdVirtualMachine_sendQuitTo
 7   (JNIEnv *env, jclass cls, jint pid)
 8 {
 9     if (kill((pid_t)pid, SIGQUIT)) {
10         JNU_ThrowIOExceptionWithLastError(env, "kill");
11     }
12 }
復制代碼

看到第9行的時候,是不是覺得這里必然和前面3.1中大篇幅分析的信號處理線程“Signal Dispatcher”有種必然聯系了?沒錯,這里就是通過 kill 這個系統調用像目標 JVM,發送了一個 SIGQUIT 的信號,該信號是個#define,即宏,表示的數字“3”,即類似在 linux 命令行執行了“kill -3 ${pid}”的操做(其實,這個命令正是獲取目標 JVM 線程 dump 文件的一種方式,讀者可以試試)。既然這里向目標 JVM 發送了這么個信號,那么我們現在就移步到3.1中講到過的 signal_thread_entry 方法中去。

復制代碼
 1 static void signal_thread_entry(JavaThread* thread, TRAPS) {
 2   os::set_priority(thread, NearMaxPriority);
 3   while (true) {
 4     int sig;
 5     {
 6       // FIXME : Currently we have not decieded what should be the status
 7       //         for this java thread blocked here. Once we decide about
 8       //         that we should fix this.
 9       sig = os::signal_wait(); 10     }
11     if (sig == os::sigexitnum_pd()) {
12        // Terminate the signal thread
13        return;
14     }
15 
16     switch (sig) {
17       case SIGBREAK: {
18         // Check if the signal is a trigger to start the Attach Listener - in that
19         // case don't print stack traces.
20         if (!DisableAttachMechanism && AttachListener::is_init_trigger()) {
21           continue;
22         }
23         // Print stack traces
24         // Any SIGBREAK operations added here should make sure to flush
25         // the output stream (e.g. tty->flush()) after output.  See 4803766.
26         // Each module also prints an extra carriage return after its output.
27         VM_PrintThreads op;
28         VMThread::execute(&op);
29         VM_PrintJNI jni_op;
30         VMThread::execute(&jni_op);
31         VM_FindDeadlocks op1(tty);
32         VMThread::execute(&op1);
33         Universe::print_heap_at_SIGBREAK();
34         if (PrintClassHistogram) {
35           VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */);
36           VMThread::execute(&op1);
37         }
38         if (JvmtiExport::should_post_data_dump()) {
39           JvmtiExport::post_data_dump();
40         }
41         break;
42       }
43       default: {
44         // Dispatch the signal to java
45         HandleMark hm(THREAD);
46         Klass* k = SystemDictionary::resolve_or_null(vmSymbols::sun_misc_Signal(), THREAD);
47         KlassHandle klass (THREAD, k);
48         if (klass.not_null()) {
49           JavaValue result(T_VOID);
50           JavaCallArguments args;
51           args.push_int(sig);
52           JavaCalls::call_static(
53             &result,
54             klass,
55             vmSymbols::dispatch_name(),
56             vmSymbols::int_void_signature(),
57             &args,
58             THREAD
59           );
60         }
61         if (HAS_PENDING_EXCEPTION) {
62           // tty is initialized early so we don't expect it to be null, but
63           // if it is we can't risk doing an initialization that might
64           // trigger additional out-of-memory conditions
65           if (tty != NULL) {
66             char klass_name[256];
67             char tmp_sig_name[16];
68             const char* sig_name = "UNKNOWN";
69             InstanceKlass::cast(PENDING_EXCEPTION->klass())->
70               name()->as_klass_external_name(klass_name, 256);
71             if (os::exception_name(sig, tmp_sig_name, 16) != NULL)
72               sig_name = tmp_sig_name;
73             warning("Exception %s occurred dispatching signal %s to handler"
74                     "- the VM may need to be forcibly terminated",
75                     klass_name, sig_name );
76           }
77           CLEAR_PENDING_EXCEPTION;
78         }
79       }
80     }
81   }
82 }
復制代碼

這里的17行,我們看到了有個對 SIGBREAK(宏定義) 信號處理的 case,事實上,這個SIGBREAK和前面客戶端發過來的SIGQUIT 的值是一樣的,都是“3”,熟悉 C語言的讀者應該不難理解。所以,當客戶端發送這個信號給目標 JVM 時,就理所應當的進入了這個 case 的處理邏輯。這里的27行到40行,事實上就是對“kill -3 ${pid}”執行時對應的處理邏輯“進行目標 JVM 進程的線程 dump 操作”。現在我們重點關注一下20行的 if 語句,第一個 boolean 值,某認情況下是false(可通過/hotspot/src/share/vm/runtime/globals.c)查看,表示某認情況下是不禁止attach 機制的,於是就會進入第二個條件的判斷AttachListener::is_init_trigger(),這里的判斷還是比較有意思的(即判斷當前殺是不是需要進行 attach 的初始化操作),我們進入源碼,源碼的文件為:hotspot/src/os/bsd/vm/attachListener_bsd.cpp

復制代碼
 1 // If the file .attach_pid<pid> exists in the working directory
 2 // or /tmp then this is the trigger to start the attach mechanism
 3 bool AttachListener::is_init_trigger() {
 4   if (init_at_startup() || is_initialized()) {
 5     return false;               // initialized at startup or already initialized
 6   }
 7   char path[PATH_MAX + 1];
 8   int ret;
 9   struct stat st;
10 
11   snprintf(path, PATH_MAX + 1, "%s/.attach_pid%d",
12            os::get_temp_directory(), os::current_process_id());
13   RESTARTABLE(::stat(path, &st), ret);
14   if (ret == 0) {
15     // simple check to avoid starting the attach mechanism when
16     // a bogus user creates the file
17     if (st.st_uid == geteuid()) {
18       init();
19       return true;
20     }
21   }
22   return false;
23 }
復制代碼

方法進入的第一行,即判斷是不是在 JVM 啟動時就初始化或者之前已經初始化過,如果是,則直接返回,否則繼續當前方法。方法的第11行,是在處理/tmp/attach_pid${pid}路徑(這個文件就是 Client JVM 在attach 時創建的),並把 path 傳入13行定義的宏進行判斷,如果這個文件存在,且剛好是當前用戶的創建的 attach_pid 文件,則進入18行的 init() 方法,否則什么也不做,返回 false。接着我們進入 init 的源碼(hotspot/src/share/vm/services/attachListener.cpp)

復制代碼
 1 // Starts the Attach Listener thread
 2 void AttachListener::init() {
 3   EXCEPTION_MARK;
 4   Klass* k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK);
 5   instanceKlassHandle klass (THREAD, k);
 6   instanceHandle thread_oop = klass->allocate_instance_handle(CHECK);
 7 
 8   const char thread_name[] = "Attach Listener";
 9   Handle string = java_lang_String::create_from_str(thread_name, CHECK);
10 
11   // Initialize thread_oop to put it into the system threadGroup
12   Handle thread_group (THREAD, Universe::system_thread_group());
13   JavaValue result(T_VOID);
14   JavaCalls::call_special(&result, thread_oop,
15                        klass,
16                        vmSymbols::object_initializer_name(),
17                        vmSymbols::threadgroup_string_void_signature(),
18                        thread_group,
19                        string,
20                        THREAD);
21 
22   if (HAS_PENDING_EXCEPTION) {
23     tty->print_cr("Exception in VM (AttachListener::init) : ");
24     java_lang_Throwable::print(PENDING_EXCEPTION, tty);
25     tty->cr();
26 
27     CLEAR_PENDING_EXCEPTION;
28 
29     return;
30   }
31 
32   KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
33   JavaCalls::call_special(&result,
34                         thread_group,
35                         group,
36                         vmSymbols::add_method_name(),
37                         vmSymbols::thread_void_signature(),
38                         thread_oop,             // ARG 1
39                         THREAD);
40 
41   if (HAS_PENDING_EXCEPTION) {
42     tty->print_cr("Exception in VM (AttachListener::init) : ");
43     java_lang_Throwable::print(PENDING_EXCEPTION, tty);
44     tty->cr();
45 
46     CLEAR_PENDING_EXCEPTION;
47 
48     return;
49   }
50 
51   { MutexLocker mu(Threads_lock);
52     JavaThread* listener_thread = new JavaThread(&attach_listener_thread_entry); 53 
54     // Check that thread and osthread were created
55     if (listener_thread == NULL || listener_thread->osthread() == NULL) {
56       vm_exit_during_initialization("java.lang.OutOfMemoryError",
57                                     "unable to create new native thread");
58     }
59 
60     java_lang_Thread::set_thread(thread_oop(), listener_thread);
61     java_lang_Thread::set_daemon(thread_oop());
62 
63     listener_thread->set_threadObj(thread_oop());
64     Threads::add(listener_thread);
65     Thread::start(listener_thread);
66   }
67 }
復制代碼

從源碼中,我們可以看出來,這里最主要的功能是,創建一個名為“Attach Listener”的 Java 線程,該線程啟動后會調用attach_listener_thread_entry這個方法(52行),來完成有關的任務處理。進入attach_listener_thread_entry方法

復制代碼
 1 // The Attach Listener threads services a queue. It dequeues an operation
 2 // from the queue, examines the operation name (command), and dispatches
 3 // to the corresponding function to perform the operation.
 4 
 5 static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
 6   os::set_priority(thread, NearMaxPriority);
 7 
 8   thread->record_stack_base_and_size();
 9 
10   if (AttachListener::pd_init() != 0) {
11     return;
12   }
13   AttachListener::set_initialized();
14 
15   for (;;) {
16     AttachOperation* op = AttachListener::dequeue(); 17     if (op == NULL) {
18       return;   // dequeue failed or shutdown
19     }
20 
21     ResourceMark rm;
22     bufferedStream st;
23     jint res = JNI_OK;
24 
25     // handle special detachall operation
26     if (strcmp(op->name(), AttachOperation::detachall_operation_name()) == 0) {
27       AttachListener::detachall();
28     } else {
29       // find the function to dispatch too
30       AttachOperationFunctionInfo* info = NULL;
31       for (int i=0; funcs[i].name != NULL; i++) {
32         const char* name = funcs[i].name;
33         assert(strlen(name) <= AttachOperation::name_length_max, "operation <= name_length_max");
34         if (strcmp(op->name(), name) == 0) {
35           info = &(funcs[i]);
36           break;
37         }
38       }
39 
40       // check for platform dependent attach operation
41       if (info == NULL) {
42         info = AttachListener::pd_find_operation(op->name());
43       }
44 
45       if (info != NULL) { 46         // dispatch to the function that implements this operation
47         res = (info->func)(op, &st); 48       } else {
49         st.print("Operation %s not recognized!", op->name());
50         res = JNI_ERR;
51       }
52     }
53 
54     // operation complete - send result and output to client
55     op->complete(res, &st);
56   }
57 }
復制代碼

這里需要關注兩個方面的內容,

第一、第10行的AttachListener::pd_init();

第二、第15行開始的 for 循環里面的內容。

 

首先看AttachListener::pd_init()

復制代碼
 1 int AttachListener::pd_init() {
 2   JavaThread* thread = JavaThread::current();
 3   ThreadBlockInVM tbivm(thread);
 4 
 5   thread->set_suspend_equivalent();
 6   // cleared by handle_special_suspend_equivalent_condition() or
 7   // java_suspend_self() via check_and_wait_while_suspended()
 8 
 9   int ret_code = BsdAttachListener::init(); 10 
11   // were we externally suspended while we were waiting?
12   thread->check_and_wait_while_suspended();
13 
14   return ret_code;
15 }
復制代碼

以上的 pd_init() 方法是在hotspot/src/os/bsd/vm/attachListener_bsd.cpp中實現的,我們看第9行的代碼,調用了BsdAttachListener::init()一個這樣的方法,該方法的主要作用就是生產 socket 通信文件的。源碼如下

復制代碼
 1 // Initialization - create a listener socket and bind it to a file
 2 
 3 int BsdAttachListener::init() {
 4   char path[UNIX_PATH_MAX];          // socket file
 5   char initial_path[UNIX_PATH_MAX];  // socket file during setup
 6   int listener;                      // listener socket (file descriptor)
 7 
 8   // register function to cleanup
 9   ::atexit(listener_cleanup);
10 
11   int n = snprintf(path, UNIX_PATH_MAX, "%s/.java_pid%d",
12                    os::get_temp_directory(), os::current_process_id());
13   if (n < (int)UNIX_PATH_MAX) {
14     n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);
15   }
16   if (n >= (int)UNIX_PATH_MAX) {
17     return -1;
18   }
19 
20   // create the listener socket
21   listener = ::socket(PF_UNIX, SOCK_STREAM, 0);
22   if (listener == -1) {
23     return -1;
24   }
25 
26   // bind socket
27   struct sockaddr_un addr;
28   addr.sun_family = AF_UNIX;
29   strcpy(addr.sun_path, initial_path);
30   ::unlink(initial_path);
31   int res = ::bind(listener, (struct sockaddr*)&addr, sizeof(addr));
32   if (res == -1) {
33     ::close(listener);
34     return -1;
35   }
36 
37   // put in listen mode, set permissions, and rename into place
38   res = ::listen(listener, 5);
39   if (res == 0) {
40     RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res);
41     if (res == 0) {
42       // make sure the file is owned by the effective user and effective group
43       // (this is the default on linux, but not on mac os)
44       RESTARTABLE(::chown(initial_path, geteuid(), getegid()), res);
45       if (res == 0) {
46         res = ::rename(initial_path, path);
47       }
48     }
49   }
50   if (res == -1) {
51     ::close(listener);
52     ::unlink(initial_path);
53     return -1;
54   }
55   set_path(path);
56   set_listener(listener);
57 
58   return 0;
59 }
復制代碼

從方法的注釋,就能看出這個方法就是用來創建一個基於文件的 socket 的 listener 端,即服務端的。具體的創建過程,代碼寫的已經很清楚了,我做下簡單描述,11行處,構建 socket 通信文件的路徑(/tmp/.java_pid${pid}),21 行,創建一個 socket,其中關注 socket 函數的第一個參數,當為 PF_UNIX 時,表示創建文件 socket,詳情可以參考 linux 的 socket 函數說明,然后到29行,將 socket 文件 path 拷貝到 socket 通信地址中,即以此文件作為通信地址,然后在31行時,將 socket 和該 socket 文件地址做一個綁定,38行,表示對當前 socket 進行監聽(數字5表示監聽時可容納客戶端連接的隊列的大小),如果有Client JVM 的客戶端連接上來,並且發送了相關消息,該服務端就可以對其進行相應處理了。至此,進程間 socket 的通信的通道就建立了。

 

其次看下 for 循環做了什么?其實很簡單,16行,BsdAttachListener::dequeue() 從監聽器的隊列中拿到一個 Client JVM 的AttachOperation(當客戶端 attach 上 target JVM 之后,往目標 JVM 發送任意 socket 信息,都會被放置到這個隊列中,等待被處理),此處會被阻塞,直到收到請求,如下源碼的13行, socket 的 accept 函數處於等待狀態,等待來之客戶端 JVM 的相關請求,一旦獲取到請求,則將請求組裝好返回給調用者一個BadAttachOperation 對象。

復制代碼
 1 // Dequeue an operation
 2 //
 3 // In the Bsd implementation there is only a single operation and clients
 4 // cannot queue commands (except at the socket level).
 5 //
 6 BsdAttachOperation* BsdAttachListener::dequeue() {
 7   for (;;) {
 8     int s;
 9 
10     // wait for client to connect
11     struct sockaddr addr;
12     socklen_t len = sizeof(addr);
13     RESTARTABLE(::accept(listener(), &addr, &len), s); 14     if (s == -1) {
15       return NULL;      // log a warning?
16     }
17 
18     // get the credentials of the peer and check the effective uid/guid
19     // - check with jeff on this.
20     uid_t puid;
21     gid_t pgid;
22     if (::getpeereid(s, &puid, &pgid) != 0) {
23       ::close(s);
24       continue;
25     }
26     uid_t euid = geteuid();
27     gid_t egid = getegid();
28 
29     if (puid != euid || pgid != egid) {
30       ::close(s);
31       continue;
32     }
33 
34     // peer credential look okay so we read the request
35     BsdAttachOperation* op = read_request(s);
36     if (op == NULL) {
37       ::close(s);
38       continue;
39     } else {
40       return op;
41     }
42   }
43 }
復制代碼

所以,只要收到一個AttachOperation不為“detachall”的操縱請求就會進入到45行處進行處理,這里的目的就是為了拿到對應的操作AttachOperationFunctionInfo對象,如果不為空,則調用其func,來完成對客戶端的響應,如47行所示。AttachOperationFunctionInfo(/hotspot/src/share/vm/services/attachListener.cpp)的定義如下

復制代碼
 1 // names must be of length <= AttachOperation::name_length_max
 2 static AttachOperationFunctionInfo funcs[] = {
 3   { "agentProperties",  get_agent_properties },
 4   { "datadump",         data_dump },
 5   { "dumpheap",         dump_heap },
 6   { "load",             JvmtiExport::load_agent_library },
 7   { "properties",       get_system_properties },
 8   { "threaddump", thread_dump },  9   { "inspectheap",      heap_inspection },
10   { "setflag",          set_flag },
11   { "printflag",        print_flag },
12   { "jcmd",             jcmd },
13   { NULL,               NULL }
14 };
復制代碼

從這里,我們可以看到,threaddump、dumpheap 等我們常用的操縱。到此為止,水落石出,涉及到 attach 操縱的服務端的原理基本已經理清楚了。接下來我們以 jstack 為例,來看下客戶端 JVM 是不是確實是以我們上面分析出來的方式與服務端 JVM 進行通信,並獲取到它想要的內容的。

3.3 JVM 對 Attach 上來的進程的命令的響應,以 jstack -l 為例


我們首先進入 jstack 的源碼,源碼目錄為jdk/src/share/classes/sun/tools/jstack/JStack.java。進入 main 函數

復制代碼
 1 public static void main(String[] args) throws Exception {
 2         if (args.length == 0) {
 3             usage(1); // no arguments
 4         }
 5 
 6         boolean useSA = false;
 7         boolean mixed = false;
 8         boolean locks = false;
 9 
10         // Parse the options (arguments starting with "-" )
11         int optionCount = 0;
12         while (optionCount < args.length) {
13             String arg = args[optionCount];
14             if (!arg.startsWith("-")) {
15                 break;
16             }
17             if (arg.equals("-help") || arg.equals("-h")) {
18                 usage(0);
19             }
20             else if (arg.equals("-F")) {
21                 useSA = true;
22             }
23             else {
24                 if (arg.equals("-m")) {
25                     mixed = true;
26                 } else {
27                     if (arg.equals("-l")) {
28                        locks = true;
29                     } else {
30                         usage(1);
31                     }
32                 }
33             }
34             optionCount++;
35         }
36 
37         // mixed stack implies SA tool
38         if (mixed) {
39             useSA = true;
40         }
41 
42         // Next we check the parameter count. If there are two parameters
43         // we assume core file and executable so we use SA.
44         int paramCount = args.length - optionCount;
45         if (paramCount == 0 || paramCount > 2) {
46             usage(1);
47         }
48         if (paramCount == 2) {
49             useSA = true;
50         } else {
51             // If we can't parse it as a pid then it must be debug server
52             if (!args[optionCount].matches("[0-9]+")) {
53                 useSA = true;
54             }
55         }
56 
57         // now execute using the SA JStack tool or the built-in thread dumper
58         if (useSA) {
59             // parameters (<pid> or <exe> <core>
60             String params[] = new String[paramCount];
61             for (int i=optionCount; i<args.length; i++ ){
62                 params[i-optionCount] = args[i];
63             }
64             runJStackTool(mixed, locks, params);
65         } else { 66             // pass -l to thread dump operation to get extra lock info
67             String pid = args[optionCount];
68             String params[];
69             if (locks) {
70                 params = new String[] { "-l" };
71             } else {
72                 params = new String[0];
73             }
74  runThreadDump(pid, params); 75         }
76     }
復制代碼

當采用 jstack -l 時,會走65行的 else 分支,最終執行77行的runThreadDump方法

復制代碼
 1 // Attach to pid and perform a thread dump
 2     private static void runThreadDump(String pid, String args[]) throws Exception {
 3         VirtualMachine vm = null;
 4         try {
 5             vm = VirtualMachine.attach(pid);  6         } catch (Exception x) {
 7             String msg = x.getMessage();
 8             if (msg != null) {
 9                 System.err.println(pid + ": " + msg);
10             } else {
11                 x.printStackTrace();
12             }
13             if ((x instanceof AttachNotSupportedException) &&
14                 (loadSAClass() != null)) {
15                 System.err.println("The -F option can be used when the target " +
16                     "process is not responding");
17             }
18             System.exit(1);
19         }
20 
21         // Cast to HotSpotVirtualMachine as this is implementation specific
22         // method.
23         InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args); 24 
25         // read to EOF and just print output
26         byte b[] = new byte[256];
27         int n;
28         do {
29             n = in.read(b);
30             if (n > 0) {
31                 String s = new String(b, 0, n, "UTF-8");
32                 System.out.print(s);
33             }
34         } while (n > 0);
35         in.close();
36         vm.detach();
37     }
復制代碼

5行:執行VirtualMachine.attach(pid);則會達到3.1、3.2的效果,即服務端已經做好了所有 attach 所需的准備,如 socket 服務端、socket 通信文件、socket 請求處理線程“Attach Listener”。

23行:通過調用 HotSpotVirtualMachine 對象的 remoteDataDump 函數進行遠程 dump,獲得輸入流 InputStream in,最后通過讀取輸入流的內容,來通過標准輸出流輸出從服務端獲取的數據。至此,jstack -l 命令完成所有操作。

接下來,我們重點分析HotSpotVirtualMachine 對象的 remoteDataDump 函數。首先上HotSpotVirtualMachine(/jdk/src/share/classes/sun/tools/attach/HotSpotVirtualMachine.java) 對象的 remoteDataDump的源碼

1 // Remote ctrl-break. The output of the ctrl-break actions can
2     // be read from the input stream.
3     public InputStream remoteDataDump(Object ... args) throws IOException {
4         return executeCommand("threaddump", args); 5     }

請注意4行的 cmd 字符串為“threaddump”,這個和3.2中 AttachOperationFunctionInfo 的定義是吻合的,也就是說最終在服務端會調用 thread_dump 方法,來執行線程 dump,並將結果返回給客戶端。接着我們看下下 executeCommand方法,該方法只是簡單的調用 execute 方法,如下

復制代碼
 1 /*
 2      * Convenience method for simple commands
 3      */
 4     private InputStream executeCommand(String cmd, Object ... args) throws IOException {
 5         try {
 6             return execute(cmd, args);
 7         } catch (AgentLoadException x) {
 8             throw new InternalError("Should not get here", x);
 9         }
10     }
復制代碼

exectue 方法在該類中為抽象方法,其具體實現放在了sun.tools.attach.BsdVirtualMachine.java中,我們看下在其具體實現這里可是最最關鍵的地方了

復制代碼
 1 /**
 2      * Execute the given command in the target VM.
 3      */
 4     InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
 5         assert args.length <= 3;                // includes null
 6 
 7         // did we detach?
 8         String p;
 9         synchronized (this) {
10             if (this.path == null) {
11                 throw new IOException("Detached from target VM");
12             }
13             p = this.path; 14         }
15 
16         // create UNIX socket
17         int s = socket(); 18 
19         // connect to target VM
20         try {
21  connect(s, p); 22         } catch (IOException x) {
23             close(s);
24             throw x;
25         }
26 
27         IOException ioe = null;
28 
29         // connected - write request
30         // <ver> <cmd> <args...>
31         try { 32  writeString(s, PROTOCOL_VERSION); 33  writeString(s, cmd); 34 
35             for (int i=0; i<3; i++) { 36                 if (i < args.length && args[i] != null) { 37  writeString(s, (String)args[i]); 38                 } else { 39                     writeString(s, ""); 40  } 41  } 42         } catch (IOException x) { 43             ioe = x; 44  } 45 
46 
47         // Create an input stream to read reply
48         SocketInputStream sis = new SocketInputStream(s); 49 
50         // Read the command completion status
51         int completionStatus;
52         try {
53             completionStatus = readInt(sis);
54         } catch (IOException x) {
55             sis.close();
56             if (ioe != null) {
57                 throw ioe;
58             } else {
59                 throw x;
60             }
61         }
62 
63         if (completionStatus != 0) {
64             sis.close();
65 
66             // In the event of a protocol mismatch then the target VM
67             // returns a known error so that we can throw a reasonable
68             // error.
69             if (completionStatus == ATTACH_ERROR_BADVERSION) {
70                 throw new IOException("Protocol mismatch with target VM");
71             }
72 
73             // Special-case the "load" command so that the right exception is
74             // thrown.
75             if (cmd.equals("load")) {
76                 throw new AgentLoadException("Failed to load agent library");
77             } else {
78                 throw new IOException("Command failed in target VM");
79             }
80         }
81 
82         // Return the input stream so that the command output can be read
83         return sis; 84     }
復制代碼

17行:創建一個 socket,這個是 socket 是一個 jni 本地方法,有興趣的可以去看對應的實現,源碼在jdk/src/solaris/native/sun/tools/attach/BsdVirtualMachine.c中,其關鍵操作就一個return socket(PF_UNIX, SOCK_STREAM, 0) 客戶端 socket 連接。

21行:這里也是一個本地方法,調用了 connect(s,p),這里的 p 就是 attach 時產生的/tmp/.java_pid${pid}的 socket 文件路徑,這樣,客戶端就和目標 JVM 連接上了,該方法同樣是一個 native 方法,可以通過查看BsdVirtualMachine.c的源碼來進行查看,如下,重點在16行使用 socket 文件路徑作為連接地址 和 18 行與目標 JVM 端啟動的 socket server 建立連接;

復制代碼
 1 /*
 2  * Class:     sun_tools_attach_BsdVirtualMachine
 3  * Method:    connect
 4  * Signature: (ILjava/lang/String;)I
 5  */
 6 JNIEXPORT void JNICALL Java_sun_tools_attach_BsdVirtualMachine_connect
 7   (JNIEnv *env, jclass cls, jint fd, jstring path)
 8 {
 9     jboolean isCopy;
10     const char* p = GetStringPlatformChars(env, path, &isCopy);
11     if (p != NULL) {
12         struct sockaddr_un addr;
13         int err = 0;
14 
15         addr.sun_family = AF_UNIX; 16  strcpy(addr.sun_path, p); 17 
18         if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) { 19             err = errno; 20  } 21 
22         if (isCopy) {
23             JNU_ReleaseStringPlatformChars(env, path, p);
24         }
25 
26         /*
27          * If the connect failed then we throw the appropriate exception
28          * here (can't throw it before releasing the string as can't call
29          * JNI with pending exception)
30          */
31         if (err != 0) {
32             if (err == ENOENT) {
33                 JNU_ThrowByName(env, "java/io/FileNotFoundException", NULL);
34             } else {
35                 char* msg = strdup(strerror(err));
36                 JNU_ThrowIOException(env, msg);
37                 if (msg != NULL) {
38                     free(msg);
39                 }
40             }
41         }
42     }
復制代碼

31~44行:想 Target JVM 端發送命令 threaddump、以及可能存在的相關參數,如-l;這里的 writeString 同樣是一個本地方法,涉及到的底層操作就是一個 C 語言庫的 write 操作,感興趣的可以自己看源碼,不再贅述;

48~83行:這里就是對當前 socket 連接,構建一個 SocketInputStream 對象,並等待Target JVM 端數據完全返回,最后將這個 InputStream 對象作為方法返回參數返回。

四、總結


本文結合 Attach 的原理和使用案例(jstack -l),對 Attach 的各個方面都進行了深入的分析和總結,希望能對有需要的同學有所幫助。當然,以上均為本人個人所學,所以難免會有錯誤和疏忽的地方,如果您發現了,還麻煩指出。

 

本文參考連接:http://lovestblog.cn/blog/2014/06/18/jvm-attach/

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM