1. 程式人生 > >JVM Attach機制實現

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 thread
void 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 thread
void 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執行緒建立到接收請求,處理請求,希望對大家有幫助。