0 前言
前面文章根蟹,我們已講述了《基于JVMTI的Agent實(shí)現(xiàn)》和《基于Java Instrument的Agent實(shí)現(xiàn)》兩種Agent的實(shí)現(xiàn)方式,其中每種方式都會分為:啟動時Agent莲祸、運(yùn)行時Agent踩窖。
對于 啟動時Agent的觸發(fā)機(jī)制,在上一節(jié)《JVMTI Agent 工作原理及核心源碼分析》中泉瞻,已經(jīng)在源碼級進(jìn)行了分析系吭,具體如下:
但是對于 運(yùn)行時Agent的觸發(fā)機(jī)制,卻沒有進(jìn)行詳細(xì)說明村斟,本節(jié)的主要目標(biāo)就是在源碼級分析下JVMTI Attach 工作機(jī)制。
1 Attach是什么
Attach機(jī)制是JVM提供一種JVM進(jìn)程間通信的能力抛猫,能讓一個進(jìn)程傳命令給另外一個進(jìn)程蟆盹,并讓它執(zhí)行內(nèi)部的一些操作。
比如:為了讓另外一個JVM進(jìn)程把線程dump出來闺金,那么首先跑了一個jstack的進(jìn)程逾滥,然后傳了個pid的參數(shù),告訴它要哪個進(jìn)程進(jìn)行線程dump败匹,既然是兩個進(jìn)程寨昙,那肯定涉及到進(jìn)程間通信,以及傳輸協(xié)議的定義掀亩,比如:要執(zhí)行什么操作舔哪,傳了什么參數(shù)等。
有時當(dāng)我們感覺線程一直卡在某個地方槽棍,想知道卡在哪里捉蚤,首先想到的是進(jìn)行 線程dump,而常用的命令是jstack炼七,我們就可以看到如下線程棧:
2014-06-18 12:56:14 Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.51-b03 mixed mode):
"Attach Listener" daemon prio=5 tid=0x00007fb0c6800800 nid=0x440b waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Service Thread" daemon prio=5 tid=0x00007fb0c584d800 nid=0x5303 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread1" daemon prio=5 tid=0x00007fb0c482e000 nid=0x5103 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"C2 CompilerThread0" daemon prio=5 tid=0x00007fb0c482c800 nid=0x4f03 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Signal Dispatcher" daemon prio=5 tid=0x00007fb0c4815800 nid=0x4d03 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Finalizer" daemon prio=5 tid=0x00007fb0c4813800 nid=0x3903 in Object.wait() [0x00000001187d2000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007aaa85568> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:135)
- locked <0x00000007aaa85568> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:151)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:189)
"Reference Handler" daemon prio=5 tid=0x00007fb0c4800000 nid=0x3703 in Object.wait() [0x00000001186cf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007aaa850f0> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:503)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:133)
- locked <0x00000007aaa850f0> (a java.lang.ref.Reference$Lock)
"main" prio=5 tid=0x00007fb0c5800800 nid=0x1903 waiting on condition [0x0000000107962000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at Test.main(Test.java:5)
"VM Thread" prio=5 tid=0x00007fb0c583d800 nid=0x3503 runnable
"GC task thread#0 (ParallelGC)" prio=5 tid=0x00007fb0c401e000 nid=0x2503 runnable
"GC task thread#1 (ParallelGC)" prio=5 tid=0x00007fb0c401e800 nid=0x2703 runnable
"GC task thread#2 (ParallelGC)" prio=5 tid=0x00007fb0c401f800 nid=0x2903 runnable
"GC task thread#3 (ParallelGC)" prio=5 tid=0x00007fb0c4020000 nid=0x2b03 runnable
"GC task thread#4 (ParallelGC)" prio=5 tid=0x00007fb0c4020800 nid=0x2d03 runnable
"GC task thread#5 (ParallelGC)" prio=5 tid=0x00007fb0c4021000 nid=0x2f03 runnable
"GC task thread#6 (ParallelGC)" prio=5 tid=0x00007fb0c4022000 nid=0x3103 runnable
"GC task thread#7 (ParallelGC)" prio=5 tid=0x00007fb0c4022800 nid=0x3303 runnable
"VM Periodic Task Thread" prio=5 tid=0x00007fb0c5845000 nid=0x5503 waiting on condition
在上面的Thread Dump日志中缆巧,出現(xiàn)了兩個線程:“Attach Listener” 和 “Signal Dispatcher”,這兩個線程便是Attach機(jī)制的關(guān)鍵豌拙。
那么JVM是如何啟動這兩個線程呢陕悬?JVM有很多線程主要在thread.cpp里的create_vm方法體里實(shí)現(xiàn):
JvmtiExport::enter_live_phase();
// 1. Signal Dispatcher 需要在發(fā)布VMInit事件之前啟動
os::signal_init();
// 2. Start Attach Listener 如果配置 +StartAttachListener; 否則會延遲啟動
if (!DisableAttachMechanism) {
if (StartAttachListener || AttachListener::init_at_startup()) {
AttachListener::init();
}
}
其中JVM相關(guān)參數(shù):DisableAttachMechanism,StartAttachListener 按傅,ReduceSignalUsage 均默認(rèn)是 false:
product(bool, DisableAttachMechanism, false, "Disable mechanism that allows tools to Attach to this VM”);
product(bool, StartAttachListener, false, "Always start Attach Listener at VM startup");
product(bool, ReduceSignalUsage, false, "Reduce the use of OS signals in Java and/or the VM”);
如上面create_vm源碼所示捉超,在啟動的時候有可能不會創(chuàng)建AttachListener線程,那么 在上面Thread Stack日志中看到的AttachListener線程是怎么創(chuàng)建的呢唯绍,這個就要關(guān)注另外一個線程“Signal Dispatcher”了狂秦,顧名思義是處理信號的,這個線程是在JVM啟動的時候肯定會創(chuàng)建的推捐。
1.1 Signal Dispatcher 線程
在os.cpp中的 signal_init()
函數(shù)中裂问,啟動了signal dispatcher 線程,對signal dispather線程主要是用于處理信號,等待信號并且分發(fā)處理堪簿,可以詳細(xì)看 signal_thread_entry
的方法:
// 該方法用于Signal Dispatcher線程處理接受到的信號
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 && AttachListener::is_init_trigger()) {
continue;
}
// Print stack traces
// Any SIGBREAK operations added here should make sure to flush
// the output stream (e.g. tty->flush()) after output. See 4803766\.
// Each module also prints an extra carriage return after its output.
VM_PrintThreads op;
VMThread::execute(&op);
VM_PrintJNI jni_op;
VMThread::execute(&jni_op);
VM_FindDeadlocks op1(tty);
VMThread::execute(&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(&op1);
}
if (JvmtiExport::should_post_data_dump()) {
JvmtiExport::post_data_dump();
}
break;
}
default: {
// Dispatch the signal to java
HandleMark hm(THREAD);
klassOop k = SystemDictionary::resolve_or_null(vmSymbolHandles::sun_misc_Signal(), THREAD);
KlassHandle klass (THREAD, k);
if (klass.not_null()) {
JavaValue result(T_VOID);
JavaCallArguments args;
args.push_int(sig);
JavaCalls::call_static(
&result,
klass,
vmSymbolHandles::dispatch_name(),
vmSymbolHandles::int_void_signature(),
&args,
THREAD
);
}
if (HAS_PENDING_EXCEPTION) {
// tty is initialized early so we don't expect it to be null, but
// if it is we can't risk doing an initialization that might
// trigger additional out-of-memory conditions
if (tty != NULL) {
char klass_name[256];
char tmp_sig_name[16];
const char* sig_name = "UNKNOWN";
instanceKlass::cast(PENDING_EXCEPTION->klass())->
name()->as_klass_external_name(klass_name, 256);
if (os::exception_name(sig, tmp_sig_name, 16) != NULL)
sig_name = tmp_sig_name;
warning("Exception %s occurred dispatching signal %s to handler"
"- the VM may need to be forcibly terminated",
klass_name, sig_name );
}
CLEAR_PENDING_EXCEPTION;
}
}
}
}
}
可以看到通過 os::signal_wait();
等待信號痊乾,而在Linux里是通過 sem_wait()
來實(shí)現(xiàn),當(dāng)接受到信號是SIGBREAK(在JVM里做了#define椭更,其實(shí)就是SIGQUIT)的時候哪审,就會觸發(fā) AttachListener::is_init_trigger()的執(zhí)行初始化attach listener線程。
- 第一次收到信號虑瀑,會開始初始化湿滓,當(dāng)初始化成功,將會直接返回舌狗,而且 不返回任何線程stack的信息(通過socket file的操作返回)叽奥,并且第二次將不在需要初始化。如果初始化不成功痛侍,將直接在控制臺的outputstream中打印線程棧信息朝氓;
- 第二次收到信號,如果已經(jīng)初始化過主届,將直接在控制臺中打印線程的棧信息赵哲。如果沒有初始化,繼續(xù)初始化君丁,走和第一次相同的流程枫夺;
比如:我們經(jīng)常會 使用 kill -3 pid的操作打印出線程棧信息,可以看到具體的實(shí)現(xiàn)是在Signal Dispatcher 線程中完成的绘闷,因?yàn)閗ill -3 pid 并不會創(chuàng)建.attach_pid#pid文件筷屡,所以一直初始化不成功,從而線程的棧信息被打印到控制臺中簸喂。
1.2 Attach Listener 線程
Attach Listener 線程是負(fù)責(zé)接收到外部的命令毙死,而對該命令進(jìn)行執(zhí)行的并且把結(jié)果返回給發(fā)送者。在JVM啟動的時候喻鳄,如果沒有指定 +StartAttachListener
扼倘,該Attach Listener線程是不會啟動的。
在接受到 quit
信號之后除呵,會調(diào)用 AttachListener::is_init_trigger()
方法再菊, AttachListener::is_init_trigger()
內(nèi)會調(diào)用AttachListener::init()
啟動了Attach Listener 線程,在不同的操作系統(tǒng)下初始化實(shí)現(xiàn)是不同的颜曾,在linux中是在attachListener_Linux.cpp文件中實(shí)現(xiàn)的纠拔。
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, ".Attach_pid%d", os::current_process_id());
int ret;
struct stat64 st;
RESTARTABLE(::stat64(fn, &st), ret);
if (ret == -1) {
snprintf(fn, sizeof(fn), "%s/.Attach_pid%d",
os::get_temp_directory(), os::current_process_id());
RESTARTABLE(::stat64(fn, &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()) {
// 創(chuàng)建AttachListener線程
init();
return true;
}
}
return false;
}
一開始會 判斷當(dāng)前進(jìn)程目錄下是否有個.Attach_pid文件,如果沒有就會在/tmp下創(chuàng)建一個/tmp/.Attach_pid
泛豪,當(dāng)那個文件的uid和自己的uid是一致的情況下(為了安全)再調(diào)用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->allocate_instance_handle(CHECK);
const char thread_name[] = "Attach Listener";
Handle string = java_lang_String::create_from_str(thread_name, CHECK);
// Initialize thread_oop to put it into the system threadGroup
Handle thread_group (THREAD, Universe::system_thread_group());
JavaValue result(T_VOID);
JavaCalls::call_special(&result, thread_oop,
klass,
vmSymbols::object_initializer_name(),
vmSymbols::threadgroup_string_void_signature(),
thread_group,
string,
CHECK);
KlassHandle group(THREAD, SystemDictionary::ThreadGroup_klass());
JavaCalls::call_special(&result,
thread_group,
group,
vmSymbols::add_method_name(),
vmSymbols::thread_void_signature(),
thread_oop, // ARG 1
CHECK);
{ MutexLocker mu(Threads_lock);
JavaThread* listener_thread = new JavaThread(&Attach_listener_thread_entry);
// Check that thread and osthread were created
if (listener_thread == NULL || listener_thread->osthread() == NULL) {
vm_exit_during_initialization("java.lang.OutOfMemoryError",
"unable to create new native thread");
}
java_lang_Thread::set_thread(thread_oop(), listener_thread);
java_lang_Thread::set_daemon(thread_oop());
listener_thread->set_threadObj(thread_oop());
Threads::add(listener_thread);
Thread::start(listener_thread);
}
}
此時水落石出了侦鹏,看到創(chuàng)建了一個線程,并且取名為Attach Listener臀叙。再看看Linux系統(tǒng)下其子類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, "%s/.java_pid%d",
os::get_temp_directory(), os::current_process_id());
if (n < (int)UNIX_PATH_MAX) {
n = snprintf(initial_path, UNIX_PATH_MAX, "%s.tmp", path);
}
if (n >= (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*)&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;
}
看到其創(chuàng)建了一個監(jiān)聽套接字略水,并創(chuàng)建了一個文件/tmp/.java_pid,這個文件就是客戶端之前一直在輪詢等待的文件劝萤,隨著這個文件的生成渊涝,意味著Attach的創(chuàng)建過程圓滿結(jié)束了。
Attach Listener線程接收到請求時床嫌,具體的請求處理在 attach_listener_thread_entry 方法體中實(shí)現(xiàn):
static void attach_listener_thread_entry(JavaThread* thread, TRAPS) {
os::set_priority(thread, NearMaxPriority);
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->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) <= AttachOperation::name_length_max, "operation <= name_length_max");
if (strcmp(op->name(), name) == 0) {
info = &(funcs[i]);
break;
}
}
// check for platform dependent attach operation
if (info == NULL) {
info = AttachListener::pd_find_operation(op->name());
}
if (info != NULL) {
// dispatch to the function that implements this operation
res = (info->func)(op, &st);
} else {
st.print("Operation %s not recognized!", op->name());
res = JNI_ERR;
}
}
// operation complete - send result and output to client
op->complete(res, &st);
}
}
從代碼來看就是 從隊(duì)列里不斷取AttachOperation跨释,然后找到請求命令對應(yīng)的方法進(jìn)行執(zhí)行,比如一開始說的jstack命令厌处,找到 { “threaddump”, thread_dump }的映射關(guān)系鳖谈,然后執(zhí)行thread_dump方法。
AttachOperation有很多種類嘱蛋,比如:內(nèi)存dump蚯姆,線程dump五续,類信息統(tǒng)計(jì)(比如加載的類及大小以及實(shí)例個數(shù)等)洒敏,動態(tài)加載agent,動態(tài)設(shè)置vm flag(但是并不是所有的flag都可以設(shè)置的疙驾,因?yàn)橛行ゝlag是在jvm啟動過程中使用的凶伙,是一次性的),打印vm flag它碎,獲取系統(tǒng)屬性等函荣,這些對應(yīng)的源碼(AttachListener.cpp
)如下:
static AttachOperationFunctionInfo funcs[] = {
// 第二個參數(shù)是命令對應(yīng)的處理函數(shù)
{ "agentProperties", get_agent_properties },
{ "datadump", data_dump },
{ "dumpheap", dump_heap },
{ "load", JvmtiExport::load_agent_library },
{ "properties", get_system_properties },
{ "threaddump", thread_dump },
{ "inspectheap", heap_inspection },
{ "setflag", set_flag },
{ "printflag", print_flag },
{ "jcmd", jcmd },
{ NULL, NULL }
};
再來看看其要調(diào)用的 AttachListener::dequeue();
:
AttachOperation* AttachListener::dequeue() {
JavaThread* thread = JavaThread::current();
ThreadBlockInVM tbivm(thread);
thread->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->check_and_wait_while_suspended();
return op;
}
最終會調(diào)用的是 LinuxAttachListener::dequeue()
:
LinuxAttachOperation* LinuxAttachListener::dequeue() {
for (;;) {
int s;
// wait for client to connect
struct sockaddr addr;
socklen_t len = sizeof(addr);
// 如果沒有請求的話,會一直accept在那里
RESTARTABLE(::accept(listener(), &addr, &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*)&cred_info, &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在那里,當(dāng)來了請求挖息,然后就會創(chuàng)建一個套接字金拒,并讀取數(shù)據(jù),構(gòu)建出LinuxAttachOperation返回套腹,找到請求對應(yīng)的操作绪抛,調(diào)用操作得到結(jié)果并把結(jié)果寫到這個socket的文件,如果你把socket的文件刪除电禀,jstack/jmap會出現(xiàn)錯誤信息 unable to open socket file:........
1.3 jstack/jmap命令流程圖
以jstack的實(shí)現(xiàn)來說明觸發(fā)Attach這一機(jī)制進(jìn)行的過程幢码,jstack命令的實(shí)現(xiàn)其實(shí)是一個叫做JStack.java的類,jstack命令首先會attach到目標(biāo)JVM進(jìn)程尖飞,產(chǎn)生VirtualMachine類症副;Linux系統(tǒng)下店雅,其實(shí)現(xiàn)類為LinuxVirtualMachine,調(diào)用其remoteDataDump方法瓦糕,打印堆棧信息底洗;查看JStack.java
代碼后會走到下面的方法里:
private static void runThreadDump(String pid, String args[]) throws Exception {
VirtualMachine vm = null;
try {
// jstack命令首先會attach到目標(biāo)JVM進(jìn)程
vm = VirtualMachine.Attach(pid);
} catch (Exception x) {
String msg = x.getMessage();
if (msg != null) {
System.err.println(pid + ": " + msg);
} else {
x.printStackTrace();
}
if ((x instanceof AttachNotSupportedException) &&
(loadSAClass() != null)) {
System.err.println("The -F option can be used when the target " +
"process is not responding");
}
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 > 0) {
String s = new String(b, 0, n, "UTF-8");
System.out.print(s);
}
} while (n > 0);
in.close();
vm.detach();
}
那么VirtualMachine是如何連接到目標(biāo)JVM進(jìn)程的呢?請注意 VirtualMachine.Attach(pid);
這行代碼咕娄,觸發(fā)Attach pid的關(guān)鍵亥揖,如果是在Linux下具體的實(shí)現(xiàn)邏輯在 sun.tools.attach.LinuxVirtualMachine
的構(gòu)造函數(shù):
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("Invalid process identifier");
}
// 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 "manager thread" 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 >= 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 <= retries && path == null);
if (path == null) {
throw new AttachNotSupportedException(
"Unable to open socket file: target process not responding " +
"or HotSpot VM not loaded");
}
} 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);
}
}
- 查找/tmp目錄下是否存在
".java_pid"+pid
文件;- 如果文件不存在圣勒,則首先創(chuàng)建
"/proc/" + pid + "/cwd/" + ".attach_pid" + pid
文件费变;- 通過
kill
命令發(fā)送SIGQUIT
信號給目標(biāo)JVM進(jìn)程,由于JVM里除了信號線程圣贸,其他線程都設(shè)置了對此信號的屏蔽挚歧,因此收不到該信號,于是該信號就傳給了“Signal Dispatcher”
吁峻;- 目標(biāo)JVM進(jìn)程接收到信號之后滑负,會在
/tmp
目錄下創(chuàng)建".java_pid"+pid
文件;- 當(dāng)發(fā)現(xiàn)
/tmp
目錄下存在".java_pid"+pid
文件用含,LinuxVirtualMachine
會通過connect系統(tǒng)調(diào)用連接到該文件描述符矮慕,后續(xù)通過該fd
進(jìn)行雙方的通訊;
JVM接受SIGQUIT信號的相關(guān)邏輯處理啄骇,則是在前面 signal_thread_entry
方法中進(jìn)行實(shí)現(xiàn)痴鳄。
前面JStack.java
源碼中,輸出堆棧信息是通過調(diào)用remoteDataDump
方法實(shí)現(xiàn)的缸夹,該方法就是通過往前面提到的fd中寫入threaddump指令痪寻,讀取返回結(jié)果,從而得到目標(biāo)JVM的堆棧信息虽惭。
2 Java 代碼實(shí)現(xiàn)動態(tài) attach Agent
Java動態(tài)attach Agent與上面所講到的JStack.java
實(shí)現(xiàn)基本類似橡类,在 attach 的java代碼中,使用sun自用的tool.jar中的VirtualMachine的attach的方式:
VirtualMachine vm = VirtualMachine.attach(processid);
vm.loadAgent(agentpath, args)
在HotSpotVirtualMachine.java
中芽唇,loadAgent
方法源碼如下:
public void loadAgent(String agent, String options) throws AgentLoadException, AgentInitializationException, IOException
{
String args = agent;
if (options != null) {
args = args + "=" + options;
}
try {
loadAgentLibrary("instrument", args);
} .....
}
private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options) throws AgentLoadException, AgentInitializationException, IOException
{
InputStream in = execute("load", agentLibrary, isAbsolute ? "true" : "false", options);
try {
int result = readInt(in);
if (result != 0) {
throw new AgentInitializationException("Agent_OnAttach failed", result);
}
} finally {
in.close();
}
}
在LinuxVirtualMachine.java
中的execute
方法:
InputStream execute(String cmd, Object ... args) throws AgentLoadException, IOException {
assert args.length <= 3; // includes null
// did we detach?
String p;
synchronized (this) {
if (this.path == null) {
throw new IOException("Detached from target VM");
}
p = this.path;
}
// create UNIX socket
int s = socket();
// connect to target VM
try {
connect(s, p);
} catch (IOException x) {
close(s);
throw x;
}
IOException ioe = null;
// connected - write request
// <ver> <cmd> <args...>
try {
writeString(s, PROTOCOL_VERSION);
writeString(s, cmd);
for (int i=0; i<3; i++) {
if (i < args.length && args[i] != null) {
writeString(s, (String)args[i]);
} else {
writeString(s, "");
}
}
} catch (IOException x) {
ioe = x;
}
// Create an input stream to read reply
SocketInputStream sis = new SocketInputStream(s);
// Read the command completion status
int completionStatus;
try {
completionStatus = readInt(sis);
} catch (IOException x) {
sis.close();
if (ioe != null) {
throw ioe;
} else {
throw x;
}
}
....
}
也就是向socket的中寫入了顾画,格式為:
<ver> <cmd> <args...>
具體內(nèi)容為:
1 load instrument agentPath=path.jar
既然Load Agent 往socket里發(fā)了load指令,匹配到JVM的操作:
static AttachOperationFunctionInfo funcs[] = {
{ "agentProperties", get_agent_properties },
{ "datadump", data_dump },
#ifndef SERVICES_KERNEL
{ "dumpheap", dump_heap },
#endif // SERVICES_KERNEL
{ "load", JvmtiExport::load_agent_library },
{ "properties", get_system_properties },
{ "threaddump", thread_dump },
{ "inspectheap", heap_inspection },
{ "setflag", set_flag },
{ "printflag", print_flag },
{ NULL, NULL }
};
"load", JvmtiExport::load_agent_library
披摄,具體源碼如下:
jint JvmtiExport::load_agent_library(AttachOperation* op, outputStream* st) {
char ebuf[1024];
char buffer[JVM_MAXPATHLEN];
void* library;
jint result = JNI_ERR;
const char* agent = op->arg(0);
const char* absParam = op->arg(1);
const char* options = op->arg(2);
bool is_absolute_path = (absParam != NULL) && (strcmp(absParam,"true")==0);
if (is_absolute_path) {
library = os::dll_load(agent, ebuf, sizeof ebuf);
} else {
// Try to load the agent from the standard dll directory
os::dll_build_name(buffer, sizeof(buffer), Arguments::get_dll_dir(), agent);
library = os::dll_load(buffer, ebuf, sizeof ebuf);
if (library == NULL) {
// not found - try local path
char ns[1] = {0};
os::dll_build_name(buffer, sizeof(buffer), ns, agent);
library = os::dll_load(buffer, ebuf, sizeof ebuf);
}
}
if (library != NULL) {
// Lookup the Agent_OnAttach function
OnAttachEntry_t on_attach_entry = NULL;
const char *on_attach_symbols[] = AGENT_ONATTACH_SYMBOLS;
for (uint symbol_index = 0; symbol_index < ARRAY_SIZE(on_attach_symbols); symbol_index++) {
on_attach_entry =
CAST_TO_FN_PTR(OnAttachEntry_t, os::dll_lookup(library, on_attach_symbols[symbol_index]));
if (on_attach_entry != NULL) break;
}
if (on_attach_entry == NULL) {
// Agent_OnAttach missing - unload library
os::dll_unload(library);
} else {
// Invoke the Agent_OnAttach function
JavaThread* THREAD = JavaThread::current();
{
extern struct JavaVM_ main_vm;
JvmtiThreadEventMark jem(THREAD);
JvmtiJavaThreadEventTransition jet(THREAD);
result = (*on_attach_entry)(&main_vm, (char*)options, NULL);
}
if (HAS_PENDING_EXCEPTION) {
CLEAR_PENDING_EXCEPTION;
}
if (result == JNI_OK) {
Arguments::add_loaded_agent(agent, (char*)options, is_absolute_path, library);
}
// Agent_OnAttach executed so completion status is JNI_OK
st->print_cr("%d", result);
result = JNI_OK;
}
}
return result;
}
#define AGENT_ONATTACH_SYMBOLS {"Agent_OnAttach"}
3 執(zhí)行 Instrument 的 Agent on attach
加載instrument的動態(tài)庫亲雪,并且調(diào)用方法instrument動態(tài)庫中的Agent_OnAttach
方法:
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM* vm, char *args, void * reserved) {
.....
initerror = createNewJPLISAgent(vm, &agent);
if ( initerror == JPLIS_INIT_ERROR_NONE ) {
......
if (parseArgumentTail(args, &jarfile, &options) != 0) {
return JNI_ENOMEM;
}
attributes = readAttributes( jarfile );
if (attributes == NULL) {
fprintf(stderr, "Error opening zip file or JAR manifest missing: %s\n", jarfile);
free(jarfile);
if (options != NULL) free(options);
return AGENT_ERROR_BADJAR;
}
agentClass = getAttribute(attributes, "Agent-Class");
if (agentClass == NULL) {
fprintf(stderr, "Failed to find Agent-Class manifest attribute from %s\n",
jarfile);
free(jarfile);
if (options != NULL) free(options);
freeAttributes(attributes);
return AGENT_ERROR_BADJAR;
}
if (appendClassPath(agent, jarfile)) {
fprintf(stderr, "Unable to add %s to system class path "
"- not supported by system class loader or configuration error!\n",
jarfile);
free(jarfile);
if (options != NULL) free(options);
freeAttributes(attributes);
return AGENT_ERROR_NOTONCP;
}
oldLen = strlen(agentClass);
newLen = modifiedUtf8LengthOfUtf8(agentClass, oldLen);
if (newLen == oldLen) {
agentClass = strdup(agentClass);
} else {
char* str = (char*)malloc( newLen+1 );
if (str != NULL) {
convertUtf8ToModifiedUtf8(agentClass, oldLen, str, newLen);
}
agentClass = str;
}
if (agentClass == NULL) {
free(jarfile);
if (options != NULL) free(options);
freeAttributes(attributes);
return JNI_ENOMEM;
}
bootClassPath = getAttribute(attributes, "Boot-Class-Path");
if (bootClassPath != NULL) {
appendBootClassPath(agent, jarfile, bootClassPath);
}
convertCapabilityAtrributes(attributes, agent);
success = createInstrumentationImpl(jni_env, agent);
jplis_assert(success);
/*
* Turn on the ClassFileLoadHook.
*/
if (success) {
success = setLivePhaseEventHandlers(agent);
jplis_assert(success);
}
if (success) {
success = startJavaAgent(agent,
jni_env,
agentClass,
options,
agent->mAgentmainCaller);
}
if (!success) {
fprintf(stderr, "Agent failed to start!\n");
result = AGENT_ERROR_STARTFAIL;
}
if (options != NULL) free(options);
free(agentClass);
freeAttributes(attributes);
}
return result;
}
上面代碼里一開始的createNewJPLISAgent
和on_load
是一樣的注冊了一些鉤子函數(shù),具體詳情可參考:《JVMTI Agent 工作原理及核心源碼分析》疚膊。
在上面的Agent_OnAttach
代碼中我們也看到了义辕,讀取加載的jar中MANIFEST Agent-Class的配置:
agentClass = getAttribute(attributes, "Agent-Class");
創(chuàng)建生成sun.instrument.InstrumentationImpl對象:
success = createInstrumentationImpl(jni_env, agent);
通過InstrumentationImpl對象中的loadClassAndCallAgentmain
方法去初始化在Agent-Class中的類,并調(diào)用class里的agentmain
的方法:
success = startJavaAgent(agent, jni_env, agentClass, options, agent->mAgentmainCaller);
也就是說定義的on_attach
的class里需要有agentmain
的方法實(shí)現(xiàn):
public class MyTransformer {
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, NotFoundException, CannotCompileException, IOException{
....
}
}