JVM Attach機制實現

杜老師說 2022-01-07 13:18:02 阅读数:627

jvm attach

感謝支付寶同事【寒泉子】的投稿

attach是什麼

在講這個之前,我們先來點大家都知道的東西,當我們感覺線程一直卡在某個地方,想知道卡在哪裏,首先想到的是進行線程dump,而常用的命令是jstack <pid>,我們就可以看到如下線程棧了

Snip20140314_179

大家是否注意過上面圈起來的兩個線程,”Attach Listener”和“Signal Dispatcher”,這兩個線程是我們這次要講的attach機制的關鍵,先偷偷告訴各比特,其實Attach Listener這個線程在jvm起來的時候可能並沒有的,後面會細說。

那attach機制是什麼?說簡單點就是jvm提供一種jvm進程間通信的能力,能讓一個進程傳命令給另外一個進程,並讓它執行內部的一些操作,比如說我們為了讓另外一個jvm進程把線程dump出來,那麼我們跑了一個jstack的進程,然後傳了個pid的參數,告訴它要哪個進程進行線程dump,既然是兩個進程,那肯定涉及到進程間通信,以及傳輸協議的定義,比如要執行什麼操作,傳了什麼參數等。

attach能做些什麼
     總結起來說,比如內存dump,線程dump,類信息統計(比如加載的類及大小以及實例個數等),動態加載agent(使用過btrace的應該不陌生),動態設置vm flag(但是並不是所有的flag都可以設置的,因為有些flag是在jvm啟動過程中使用的,是一次性的),打印vm flag,獲取系統屬性等,這些對應的源碼(attachListener.cpp)如下

static AttachOperationFunctionInfo funcs[] = { { &quot;agentProperties&quot;, get_agent_properties }, { &quot;datadump&quot;, data_dump }, { &quot;dumpheap&quot;, dump_heap }, { &quot;load&quot;, JvmtiExport::load_agent_library }, { &quot;properties&quot;, get_system_properties }, { &quot;threaddump&quot;, thread_dump }, { &quot;inspectheap&quot;, heap_inspection }, { &quot;setflag&quot;, set_flag }, { &quot;printflag&quot;, print_flag }, { &quot;jcmd&quot;, jcmd }, { NULL, NULL }};

後面是命令對應的處理函數。
attach在jvm裏如何實現的

Attach Listener線程的創建

   前面也提到了,jvm在啟動過程中可能並沒有啟動Attach Listener這個線程,可以通過jvm參數來啟動,代碼(Threads::create_vm)如下:

if (!DisableAttachMechanism) { if (StartAttachListener || AttachListener::init_at_startup()) { AttachListener::init(); } }bool AttachListener::init_at_startup() { if (ReduceSignalUsage) { return true; } else { return false; }}

其中DisableAttachMechanism,StartAttachListener ,ReduceSignalUsage均默認是false(globals.hpp)

product(bool, DisableAttachMechanism, false, \ &quot;Disable mechanism that allows tools to attach to this VM&rdquo;)product(bool, StartAttachListener, false, \ &quot;Always start Attach Listener at VM startup&quot;)product(bool, ReduceSignalUsage, false, \ &quot;Reduce the use of OS signals in Java and/or the VM&rdquo;)

因此AttachListener::init()並不會被執行,而Attach Listener線程正是在此方法裏創建的

// Starts the Attach Listener threadvoid AttachListener::init() { EXCEPTION_MARK; klassOop k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK); instanceKlassHandle klass (THREAD, k); instanceHandle thread_oop = klass-&gt;allocate_instance_handle(CHECK); const char thread_name[] = &quot;Attach Listener&quot;; 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(&amp;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(&amp;result, thread_group, group, vmSymbols::add_method_name(), vmSymbols::thread_void_signature(), thread_oop, // ARG 1 CHECK); { MutexLocker mu(Threads_lock); JavaThread* listener_thread = new JavaThread(&amp;attach_listener_thread_entry); // Check that thread and osthread were created if (listener_thread == NULL || listener_thread-&gt;osthread() == NULL) { vm_exit_during_initialization(&quot;java.lang.OutOfMemoryError&quot;, &quot;unable to create new native thread&quot;); } java_lang_Thread::set_thread(thread_oop(), listener_thread); java_lang_Thread::set_daemon(thread_oop()); listener_thread-&gt;set_threadObj(thread_oop()); Threads::add(listener_thread); Thread::start(listener_thread); }}

    既然在啟動的時候不會創建這個線程,那麼我們在上面看到的那個線程是怎麼創建的呢,這個就要關注另外一個線程“Signal Dispatcher”了,顧名思義是處理信號的,這個線程是在jvm啟動的時候就會創建的,具體代碼就不說了。

     下面以jstack的實現來說明觸發attach這一機制進行的過程,jstack命令的實現其實是一個叫做JStack.java的類,查看jstack代碼後會走到下面的方法裏

private static void runThreadDump(String pid, String args[]) throws Exception { VirtualMachine vm = null; try { vm = VirtualMachine.attach(pid); } catch (Exception x) { String msg = x.getMessage(); if (msg != null) { System.err.println(pid + &quot;: &quot; + msg); } else { x.printStackTrace(); } if ((x instanceof AttachNotSupportedException) &amp;&amp; (loadSAClass() != null)) { System.err.println(&quot;The -F option can be used when the target &quot; + &quot;process is not responding&quot;); } System.exit(1); } // Cast to HotSpotVirtualMachine as this is implementation specific // method. InputStream in = ((HotSpotVirtualMachine)vm).remoteDataDump((Object[])args); // read to EOF and just print output byte b[] = new byte[256]; int n; do { n = in.read(b); if (n &gt; 0) { String s = new String(b, 0, n, &quot;UTF-8&quot;); System.out.print(s); } } while (n &gt; 0); in.close(); vm.detach(); }

    請注意VirtualMachine.attach(pid);這行代碼,觸發attach pid的關鍵,如果是在linux下會走到下面的構造函數

LinuxVirtualMachine(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException { super(provider, vmid); // This provider only understands pids int pid; try { pid = Integer.parseInt(vmid); } catch (NumberFormatException x) { throw new AttachNotSupportedException(&quot;Invalid process identifier&quot;); } // Find the socket file. If not found then we attempt to start the // attach mechanism in the target VM by sending it a QUIT signal. // Then we attempt to find the socket file again. path = findSocketFile(pid); if (path == null) { File f = createAttachFile(pid); try { // On LinuxThreads each thread is a process and we don't have the // pid of the VMThread which has SIGQUIT unblocked. To workaround // this we get the pid of the &quot;manager thread&quot; that is created // by the first call to pthread_create. This is parent of all // threads (except the initial thread). if (isLinuxThreads) { int mpid; try { mpid = getLinuxThreadsManager(pid); } catch (IOException x) { throw new AttachNotSupportedException(x.getMessage()); } assert(mpid &gt;= 1); sendQuitToChildrenOf(mpid); } else { sendQuitTo(pid); } // give the target VM time to start the attach mechanism int i = 0; long delay = 200; int retries = (int)(attachTimeout() / delay); do { try { Thread.sleep(delay); } catch (InterruptedException x) { } path = findSocketFile(pid); i++; } while (i &lt;= retries &amp;&amp; path == null); if (path == null) { throw new AttachNotSupportedException( &quot;Unable to open socket file: target process not responding &quot; + &quot;or HotSpot VM not loaded&quot;); } } finally { f.delete(); } } // Check that the file owner/permission to avoid attaching to // bogus process checkPermissions(path); // Check that we can connect to the process // - this ensures we throw the permission denied error now rather than // later when we attempt to enqueue a command. int s = socket(); try { connect(s, path); } finally { close(s); } }

     這裏要解釋下代碼了,首先看到調用了createAttachFile方法在目標進程的cwd目錄下創建了一個文件/proc/<pid>/cwd/.attach_pid<pid>,這個在後面的信號處理過程中會取出來做判斷(為了安全),另外我們知道在linux下線程是用進程實現的,在jvm啟動過程中會創建很多線程,比如我們上面的信號線程,也就是會看到很多的pid(應該是LWP),那麼如何找到這個信號處理線程呢,從上面實現來看是找到我們傳進去的pid的父進程,然後給它的所有子進程都發送一個SIGQUIT信號,而jvm裏除了vm thread,其他線程都設置了對此信號的屏蔽,因此收不到該信號,於是該信號就傳給了“Signal Dispatcher”,在傳完之後作輪詢等待看目標進程是否創建了某個文件,attachTimeout默認超時時間是5000ms,可通過設置系統變量sun.tools.attach.attachTimeout來指定,下面是Signal Dispatcher線程的entry實現

static void signal_thread_entry(JavaThread* thread, TRAPS) { os::set_priority(thread, NearMaxPriority); while (true) { int sig; { // FIXME : Currently we have not decieded what should be the status // for this java thread blocked here. Once we decide about // that we should fix this. sig = os::signal_wait(); } if (sig == os::sigexitnum_pd()) { // Terminate the signal thread return; } switch (sig) { case SIGBREAK: { // Check if the signal is a trigger to start the Attach Listener - in that // case don't print stack traces. if (!DisableAttachMechanism &amp;&amp; AttachListener::is_init_trigger()) { continue; } // Print stack traces // Any SIGBREAK operations added here should make sure to flush // the output stream (e.g. tty-&gt;flush()) after output. See 4803766. // Each module also prints an extra carriage return after its output. VM_PrintThreads op; VMThread::execute(&amp;op); VM_PrintJNI jni_op; VMThread::execute(&amp;jni_op); VM_FindDeadlocks op1(tty); VMThread::execute(&amp;op1); Universe::print_heap_at_SIGBREAK(); if (PrintClassHistogram) { VM_GC_HeapInspection op1(gclog_or_tty, true /* force full GC before heap inspection */, true /* need_prologue */); VMThread::execute(&amp;op1); } if (JvmtiExport::should_post_data_dump()) { JvmtiExport::post_data_dump(); } break; } &hellip;. } } }}

    當信號是SIGBREAK(在jvm裏做了#define,其實就是SIGQUIT)的時候,就會觸發AttachListener::is_init_trigger()的執行

bool AttachListener::is_init_trigger() { if (init_at_startup() || is_initialized()) { return false; // initialized at startup or already initialized } char fn[PATH_MAX+1]; sprintf(fn, &quot;.attach_pid%d&quot;, os::current_process_id()); int ret; struct stat64 st; RESTARTABLE(::stat64(fn, &amp;st), ret); if (ret == -1) { snprintf(fn, sizeof(fn), &quot;%s/.attach_pid%d&quot;, os::get_temp_directory(), os::current_process_id()); RESTARTABLE(::stat64(fn, &amp;st), ret); } if (ret == 0) { // simple check to avoid starting the attach mechanism when // a bogus user creates the file if (st.st_uid == geteuid()) { init(); return true; } } return false;}

    一開始會判斷當前進程目錄下是否有個.attach_pid<pid>文件(前面提到了),如果沒有就會在/tmp下創建一個/tmp/.attach_pid<pid>,當那個文件的uid和自己的uid是一致的情况下(為了安全)再調用init方法

// Starts the Attach Listener threadvoid AttachListener::init() { EXCEPTION_MARK; klassOop k = SystemDictionary::resolve_or_fail(vmSymbols::java_lang_Thread(), true, CHECK); instanceKlassHandle klass (THREAD, k); instanceHandle thread_oop = klass-&gt;allocate_instance_handle(CHECK); const char thread_name[] = &quot;Attach Listener&quot;; 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(&amp;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(&amp;result, thread_group, group, vmSymbols::add_method_name(), vmSymbols::thread_void_signature(), thread_oop, // ARG 1 CHECK); { MutexLocker mu(Threads_lock); JavaThread* listener_thread = new JavaThread(&amp;attach_listener_thread_entry); // Check that thread and osthread were created if (listener_thread == NULL || listener_thread-&gt;osthread() == NULL) { vm_exit_during_initialization(&quot;java.lang.OutOfMemoryError&quot;, &quot;unable to create new native thread&quot;); } java_lang_Thread::set_thread(thread_oop(), listener_thread); java_lang_Thread::set_daemon(thread_oop()); listener_thread-&gt;set_threadObj(thread_oop()); Threads::add(listener_thread); Thread::start(listener_thread); }}

此時水落石出了,看到創建了一個線程,並且取名為Attach Listener。再看看其子類LinuxAttachListener的init方法

int LinuxAttachListener::init() { char path[UNIX_PATH_MAX]; // socket file char initial_path[UNIX_PATH_MAX]; // socket file during setup int listener; // listener socket (file descriptor) // register function to cleanup ::atexit(listener_cleanup); int n = snprintf(path, UNIX_PATH_MAX, &quot;%s/.java_pid%d&quot;, os::get_temp_directory(), os::current_process_id()); if (n &lt; (int)UNIX_PATH_MAX) { n = snprintf(initial_path, UNIX_PATH_MAX, &quot;%s.tmp&quot;, path); } if (n &gt;= (int)UNIX_PATH_MAX) { return -1; } // create the listener socket listener = ::socket(PF_UNIX, SOCK_STREAM, 0); if (listener == -1) { return -1; } // bind socket struct sockaddr_un addr; addr.sun_family = AF_UNIX; strcpy(addr.sun_path, initial_path); ::unlink(initial_path); int res = ::bind(listener, (struct sockaddr*)&amp;addr, sizeof(addr)); if (res == -1) { RESTARTABLE(::close(listener), res); return -1; } // put in listen mode, set permissions, and rename into place res = ::listen(listener, 5); if (res == 0) { RESTARTABLE(::chmod(initial_path, S_IREAD|S_IWRITE), res); if (res == 0) { res = ::rename(initial_path, path); } } if (res == -1) { RESTARTABLE(::close(listener), res); ::unlink(initial_path); return -1; } set_path(path); set_listener(listener); return 0;}

     看到其創建了一個監聽套接字,並創建了一個文件/tmp/.java_pid<pid>,這個文件就是客戶端之前一直在輪詢等待的文件,隨著這個文件的生成,意味著attach的過程圓滿結束了。

attach listener接收請求

      看看它的entry實現attach_listener_thread_entry

static void attach_listener_thread_entry(JavaThread* thread, TRAPS) { os::set_priority(thread, NearMaxPriority); thread-&gt;record_stack_base_and_size(); if (AttachListener::pd_init() != 0) { return; } AttachListener::set_initialized(); for (;;) { AttachOperation* op = AttachListener::dequeue(); if (op == NULL) { return; // dequeue failed or shutdown } ResourceMark rm; bufferedStream st; jint res = JNI_OK; // handle special detachall operation if (strcmp(op-&gt;name(), AttachOperation::detachall_operation_name()) == 0) { AttachListener::detachall(); } else { // find the function to dispatch too AttachOperationFunctionInfo* info = NULL; for (int i=0; funcs[i].name != NULL; i++) { const char* name = funcs[i].name; assert(strlen(name) &lt;= AttachOperation::name_length_max, &quot;operation &lt;= name_length_max&quot;); if (strcmp(op-&gt;name(), name) == 0) { info = &amp;(funcs[i]); break; } } // check for platform dependent attach operation if (info == NULL) { info = AttachListener::pd_find_operation(op-&gt;name()); } if (info != NULL) { // dispatch to the function that implements this operation res = (info-&gt;func)(op, &amp;st); } else { st.print(&quot;Operation %s not recognized!&quot;, op-&gt;name()); res = JNI_ERR; } } // operation complete - send result and output to client op-&gt;complete(res, &amp;st); }}

      從代碼來看就是從隊列裏不斷取AttachOperation,然後找到請求命令對應的方法進行執行,比如我們一開始說的jstack命令,找到 { “threaddump”,       thread_dump }的映射關系,然後執行thread_dump方法  再來看看其要調用的AttachListener::dequeue()

AttachOperation* AttachListener::dequeue() { JavaThread* thread = JavaThread::current(); ThreadBlockInVM tbivm(thread); thread-&gt;set_suspend_equivalent(); // cleared by handle_special_suspend_equivalent_condition() or // java_suspend_self() via check_and_wait_while_suspended() AttachOperation* op = LinuxAttachListener::dequeue(); // were we externally suspended while we were waiting? thread-&gt;check_and_wait_while_suspended(); return op;}

     最終調用的是LinuxAttachListener::dequeue()

LinuxAttachOperation* LinuxAttachListener::dequeue() { for (;;) { int s; // wait for client to connect struct sockaddr addr; socklen_t len = sizeof(addr); RESTARTABLE(::accept(listener(), &amp;addr, &amp;len), s); if (s == -1) { return NULL; // log a warning? } // get the credentials of the peer and check the effective uid/guid // - check with jeff on this. struct ucred cred_info; socklen_t optlen = sizeof(cred_info); if (::getsockopt(s, SOL_SOCKET, SO_PEERCRED, (void*)&amp;cred_info, &amp;optlen) == -1) { int res; RESTARTABLE(::close(s), res); continue; } uid_t euid = geteuid(); gid_t egid = getegid(); if (cred_info.uid != euid || cred_info.gid != egid) { int res; RESTARTABLE(::close(s), res); continue; } // peer credential look okay so we read the request LinuxAttachOperation* op = read_request(s); if (op == NULL) { int res; RESTARTABLE(::close(s), res); continue; } else { return op; } }}

     我們看到如果沒有請求的話,會一直accept在那裏,當來了請求,然後就會創建一個套接字,並讀取數據,構建出LinuxAttachOperation返回並執行。
   整個過程就這樣了,從attach線程創建到接收請求,處理請求,希望對大家有幫助。

原文地址:http://lovestblog.cn/2014/03/14/jvm/jvm_attach/

原創文章,轉載請注明: 轉載自並發編程網 – ifeve.com本文鏈接地址: JVM Attach機制實現

FavoriteLoading添加本文到我的收藏
版权声明:本文为[杜老師說]所创,转载请带上原文链接,感谢。 https://gsmany.com/2022/01/202201071318023630.html