arthas启动-attach深入理解
上篇文章我们描述了arthas attach的流程,最后遗留了三个问题,attach过程中获取VirtualMachineDescriptor,VirtualMachine,以及loadAgent过程中两个JVM进程之间如何进行交互的。我们就依次对这三个问题展开进行描述,最后在给出一下上篇文章中描述的两种情况的原因
三个问题详解
VirtualMachine.list的实现
java层面我们可以进行debug, 跟踪下来我们会发现,获取VirtualMachineDescriptor的实现就是通过调用AttachProvider#listVirtualMachines实现, 我们核心关注下HotSpotAttachProvider的实现,此处我们会看到之前提到的/tmp/hsperfdata_{user}的用途
入口函数的实现
如果我们点开VirtualMachine#list的实现时,会发现第一步其实时通过spi机制(有兴趣可以了解下,java原生的spi,dubbo框架中也实现了一套类似的spi机制)获取到所有的AttachProvider的实现类, 遍历调用AttachProvider的listVirtualMachines。 我们关注下HotSpotAttachProvider的实现逻辑,其实现流程如下分为两个阶段:
1、通过MonitoredHost#activeVms获取所有的java进程id列表
2、遍历所有的进程id立标,检测jvm进程是否可以attach,可以attach的情况下将其组装成HotSpotVirtualMachineDescriptor的对象。
public List<VirtualMachineDescriptor> listVirtualMachines() {
//简化了下代码的
MonitoredHost host = MonitoredHost.getMonitoredHost(new HostIdentifier((String)null));
// 获取java进程的id列表
Set vms = host.activeVms();
Iterator var20 = vms.iterator();
while(var20.hasNext()) {
Integer vmid = (Integer)var20.next();
String pid = vmid.toString();
String name = pid;
boolean isAttachable = false;
MonitoredVm mvm = null;
try {
// 此处创建MonitoredVm对象,内部会通过native的方式生成的一个bytebuffer对象。
mvm = host.getMonitoredVm(new VmIdentifier(pid));
try {
isAttachable = MonitoredVmUtil.isAttachable(mvm);
name = MonitoredVmUtil.commandLine(mvm);
} catch (Exception var16) {
}
if (isAttachable) {
result.add(new HotSpotAttachProvider.HotSpotVirtualMachineDescriptor(this, pid, name));
}
} catch (Throwable var18) {
} finally {}
}
}
获取java进程列表
通过MonitoredHost#activeVms,我们可以获取到java进程的id的列表。 我们追踪该方法的实现可以追踪到LocalVmManager#activeVms方法, 在这个类中我们可以看到为什么需要/tmp/hsperfdata_{user}目录。
构造函数我们可以看到几个关键的变量:
tmpdirs: 存放PerfData信息的目录, 该目录在linux中默认链接到了/tmp目录, mac中在其它位置,最终获取通过XXX, 可以配置参数调整
userPattern: 我们可以看到userPattern的正则表达为 hsperfdata_\\S*。 代表hsperfdata_+n个非空字符的目录。 此处可以看到/tmp/hsperfdata_{user}的原因
userFilter: userFilter就是实现的FilenameFilter,方便对目录中的文件进行过滤, 在linux中默认指的就是/tmp目录, 通过遍历/tmp目录就可以
filePattern: 正则表达式为^[0-9]+$,文件名式以进程的id的方式存储的,所以文件名的匹配就是匹配全数字的文件名
fileFilter: fileFilter和userFilter的作用类似,只不过是用于遍历确定的hsperfdata_{user}目录下的文件
public LocalVmManager(String user) {
this.userName = user;
if (this.userName == null) {
// linux 中此目录链接到了/tmp目录
this.tmpdirs = PerfDataFile.getTempDirectories((String)null, 0);
// 此处的匹配逻辑匹配包含hsperfdata_+n个非空字符的目录
this.userPattern = Pattern.compile("hsperfdata_\\S*");
this.userMatcher = this.userPattern.matcher("");
this.userFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
LocalVmManager.this.userMatcher.reset(name);
return LocalVmManager.this.userMatcher.lookingAt();
}
};
} else {
this.tmpdirs = PerfDataFile.getTempDirectories(this.userName, 0);
}
// filePattern是用于匹配进程id的,由于进程id时数字,所以此处匹配为^[0-9]+$
this.filePattern = Pattern.compile("^[0-9]+$");
this.fileMatcher = this.filePattern.matcher("");
this.fileFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
LocalVmManager.this.fileMatcher.reset(name);
return LocalVmManager.this.fileMatcher.matches();
}
};
this.tmpFilePattern = Pattern.compile("^hsperfdata_[0-9]+(_[1-2]+)?$");
this.tmpFileMatcher = this.tmpFilePattern.matcher("");
this.tmpFileFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
LocalVmManager.this.tmpFileMatcher.reset(name);
return LocalVmManager.this.tmpFileMatcher.matches();
}
};
}
从MonitoredHost的几个关键属性的含义大致可以明白activeVms的实现应该就是遍历tmpdirs的目录,通过user_Filter和fileFilter的对目录和文件过滤,获取到符合条件的文件。此处如果用户名传入情况下就不需要使用user_Filter对tmpdirs过滤了。 其核心的代码如下,基本上就是前面说的逻辑,只不过增加了文件可读性的限制。
if (this.userName != null) {
files = tmpdir.listFiles(this.fileFilter);
if (files != null) {
for(j = 0; j < files.length; ++j) {
if (files[j].isFile() && files[j].canRead()) {
vmid = PerfDataFile.getLocalVmId(files[j]);
if (vmid != -1) {
jvmSet.add(vmid);
}
}
}
}
} else {
files = tmpdir.listFiles(this.userFilter);
for(j = 0; j < files.length; ++j) {
if (files[j].isDirectory()) {
File[] files = files[j].listFiles(this.fileFilter);
if (files != null) {
for(int j = 0; j < files.length; ++j) {
if (files[j].isFile() && files[j].canRead()) {
int vmid = PerfDataFile.getLocalVmId(files[j]);
if (vmid != -1) {
jvmSet.add(vmid);
}
}
}
}
}
}
}
遍历获取的进程id列表
上一步骤获取到进程id列表,进程id列表仅仅是通过遍历文件获取的,针对这些文件对应的进程是否正常, 需要进行响应的check, 此处的逻辑就是:
1、通过MonitoredHost#getMonitoredVm 获取到MonitoredVm对象。 debug跟踪下去,最后会调用到Perf#attach函数,此函数是个native函数, 参数分别user,pid, 以及mod类型
2、通过MonitoredVm对象判断是否可以attach,此处其实不代表进程存在,仅仅是通过文件中的信息获取的
3、如果可以attach的情况下将,AttachProvider对象, pid, 执行的命令组装成VirtualMachineDescriptor的对象
for (Integer vmid: vms) {
String pid = vmid.toString();
String name = pid; // default to pid if name not available
boolean isAttachable = false;
MonitoredVm mvm = null;
try {
mvm = host.getMonitoredVm(new VmIdentifier(pid));
try {
isAttachable = MonitoredVmUtil.isAttachable(mvm);
name = MonitoredVmUtil.commandLine(mvm);
} catch (Exception e) {
}
if (isAttachable) {
result.add(new HotSpotVirtualMachineDescriptor(this, pid, name));
}
} catch (Throwable t) {
if (t instanceof ThreadDeath) {
throw (ThreadDeath)t;
}
} finally {
if (mvm != null) {
mvm.detach();
}
}
}
接下来我们看看Perf#attach的实现是什么样的。
查看perf.cpp中定义attch的实现,主要包含如下两步骤
1: 通过调用PerfMemory::attach获取到数据的地址以及大小容量信息,即代码中的address变量和capacity。 主要看Linux系统上的实现,最终实现是在perfMemory_linux.cpp中实现。
2: 通过JNIEnv创建NewDirectByteBuffer,Perf#attach返回ByteBuffer对象最终是一个DirectByteBuffer对象
PERF_ENTRY(jobject, Perf_Attach(JNIEnv *env, jobject unused, jstring user, int vmid, int mode))
PerfWrapper("Perf_Attach");
char* address = 0;
size_t capacity = 0;
const char* user_utf = NULL;
ResourceMark rm;
{
ThreadToNativeFromVM ttnfv(thread);
user_utf = user == NULL ? NULL : jstr_to_utf(env, user, CHECK_NULL);
}
if (mode != PerfMemory::PERF_MODE_RO &&
mode != PerfMemory::PERF_MODE_RW) {
THROW_0(vmSymbols::java_lang_IllegalArgumentException());
}
// attach to the PerfData memory region for the specified VM
PerfMemory::attach(user_utf, vmid, (PerfMemory::PerfMemoryMode) mode,
&address, &capacity, CHECK_NULL);
{
ThreadToNativeFromVM ttnfv(thread);
return env->NewDirectByteBuffer(address, (jlong)capacity);
}
PERF_END
查看perfMemory_linux.cpp中的attach的实现
1、如果传入的vmid和当前进程id一致的情况下从进程的信息中可以直接获取。我们可以想象到,每个java进程都会维护一个PerfData, 如果进程一致,直接从进程信息中就可以获取到起始地址以及大小了。
2、如果不一致的情况下通过mmap_attach_shared的方式获取,从名称可以大致看出获取的逻辑, 应该是通过mmap文件的方式实现,接下来我们实际看下具体的实现方式,看看是不是通过mmap的方式。
void PerfMemory::attach(const char* user, int vmid, PerfMemoryMode mode, char** addrp, size_t* sizep, TRAPS) {
if (vmid == 0 || vmid == os::current_process_id()) {
*addrp = start();
*sizep = capacity();
return;
}
mmap_attach_shared(user, vmid, mode, addrp, sizep, CHECK);
}
查看mmap_attach_shared实现,整体逻辑就是找到对应的文件,与java层面获取vmid的文件是同一个文件,之后通过mmap(内存映射文件的方法)的方式处理,基本步骤如下
1、获取用户名信息, 如果用户名为空,需要先通过vmid获取到用户名, 由于hsperfdata_需要用到用户名,所以需要优先获取到用户名信息
2、获取到文件, 与之前java的逻辑类似,获取tmp 目录。 组装完整的逻辑
3、通过mmap的方式获取到文件信息, sizep默认是传入0, 最终sizep的值会设置为文件的大小
static void mmap_attach_shared(const char* user, int vmid, PerfMemory::PerfMemoryMode mode, char** addr, size_t* sizep, TRAPS) {
char* mapAddress;
int result;
int fd;
size_t size = 0;
const char* luser = NULL;
int mmap_prot;
int file_flags;
ResourceMark rm;
// 用户未传入的情况下 通过进程id获取用户
if (user == NULL || strlen(user) == 0) {
luser = get_user_name(vmid, CHECK);
} else {
luser = user;
}
char* dirname = get_user_tmp_dir(luser);
if (!is_directory_secure(dirname)) {
FREE_C_HEAP_ARRAY(char, dirname, mtInternal);
THROW_MSG(vmSymbols::java_lang_IllegalArgumentException(),
"Process not found");
}
char* filename = get_sharedmem_filename(dirname, vmid);
char* rfilename = NEW_RESOURCE_ARRAY(char, strlen(filename) + 1);
strcpy(rfilename, filename);
//打开文件,获取文件描述符
fd = open_sharedmem_file(rfilename, file_flags, CHECK);
if (*sizep == 0) {
size = sharedmem_filesize(fd, CHECK);
} else {
size = *sizep;
}
// 调用mmap 方式获取到address地址
mapAddress = (char*)::mmap((char*)0, size, mmap_prot, MAP_SHARED, fd, 0);
result = ::close(fd);
assert(result != OS_ERR, "could not close file");
MemTracker::record_virtual_memory_reserve((address)mapAddress, size, mtInternal, CURRENT_PC);
*addr = mapAddress;
*sizep = size;
}
回到java层面, 创建完MonitoredVm之后,使用MonitoredVmUtil获取了部分信息。这部分逻辑我们就不做过多的解释,核心就是通过文件中的内容,我们可以自己写个demo,手工读取一个hsperfdata_{user}下的文件,下面的代码逻辑就相当于调用的isAttachable,以及commandLine方法。 其中文件是我个人获取的一个java进程的hsperfdata文件。
import sun.jvmstat.monitor.MonitorException;
import sun.jvmstat.monitor.StringMonitor;
import sun.jvmstat.perfdata.monitor.v2_0.PerfDataBuffer;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
public class TestHsperfdata {
public static void main(String[] args) throws IOException, MonitorException {
MappedByteBuffer mappedByteBuffer = new RandomAccessFile("/Users/dingym.hz/demo/29767", "r")
.getChannel()
.map(FileChannel.MapMode.READ_ONLY, 0, new File("/Users/dingym.hz/demo/29767").length());
PerfDataBuffer perfDataBuffer = new PerfDataBuffer(mappedByteBuffer, 29767);
StringMonitor attachAble = (StringMonitor)perfDataBuffer.findByName("sun.rt.jvmCapabilities");
System.out.println(attachAble == null ? null : attachAble.stringValue().charAt(0) == '1');
StringMonitor command = (StringMonitor)perfDataBuffer.findByName("sun.rt.javaCommand");
System.out.println(command == null ? "Unknown" : command.stringValue());
}
}
VirtualMachine#attach
通过调用VirtualMachine的attach方法我们可以获取到一个VirtualMachine对象, 这个类核心处理的一个逻辑就是与目标的jvm建立通信,通信的实际实现方式就是socket通信, 我们逐步看看具体的实现逻辑。
AttachProviderImpl#attachVirtualMachine
我们从VirtualMachine#attach入口函数查找实现逻辑,最终我们可以跳转到AttachProviderImpl的attachVirtualMachine方法,这个方法大的实现上就分了三个步骤:
1、通过SecurityManager进行检测,是否具有attach的权限
2、测试是否可以attach,此处的实现最终的实现还是使用MonitoredVmUtil#isAttachable进行check
3、创建VirtualMachineImpl对象,此处的构造函数是attach的核心实现
public VirtualMachine attachVirtualMachine(String vmid) throws AttachNotSupportedException, IOException {
this.checkAttachPermission();
this.testAttachable(vmid);
return new VirtualMachineImpl(this, vmid);
}
VirtualMachineImpl的构造函数
构造函数的实现中有几个关键的步骤:
- 1、获取socket file, 文件位于tmpdir下, 文件名格式为.java_pid{pid}的格式
- 2、不存在该文件时会使用调用sendQuitTo函数, 之后轮训等待是否socket file是否存在
- 3、 创建socket,获取socket的fd
- 4、 调用connect函数连接fd
此处可以看到一个关键的文件.java_pid{pid}。 如果有看过该文件的话可以发现该文件在linux上的文件类型为socket文件格式。此处可以猜测到两个jvm进程之间通信时通过该socket文件进行通信的。 同时大家应该注意到如果这个socket文件不存在时,通过sendQuitTo函数后java层面就是轮训等待,那么sendQuitTo函数做的是什么,就可以生成一个socket文件。
LinuxVirtualMachine(AttachProvider provider, String vmid) throws AttachNotSupportedException, IOException {
super(provider, vmid);
int pid = Integer.parseInt(vmid);
path = findSocketFile(pid);
if (path == null) {
File f = createAttachFile(pid);
try {
if (isLinuxThreads) {
int mpid = getLinuxThreadsManager(pid);
sendQuitToChildrenOf(mpid);
} else {
// 由于linux jvm使用的轻量级线程处理,所以走此处的分支
sendQuitTo(pid);
}
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();
}
}
checkPermissions(path);
int s = socket();
try {
connect(s, path);
} finally {
close(s);
}
}
LinuxVirtualMachineImpl#sendQuitTo
我们主要关注Linux平台上的实现。我们发现此函数是native函数, 同样的我们可以从源码中查找LinuxVirtualMachine.c文件,对应的native实现为Java_sun_tools_attach_LinuxVirtualMachine_sendQuitTo函数, 该函数的实现上就一行kill((pid_t)pid, SIGQUIT),就是向目标进程发送SIGQUIT信号。下图是我手工在linux机器上执行模拟sendQuitTo的流程,可以发现发送kill -3 pid后确实会生成.java_pid{pid}文件。 测试的时候注意,需要先创建一个/tmp/.attach_pid{pid}文件。
LinuxVirtualMachineImpl#checkPermissions
之前提到了一个,执行arthas的用户要求和目标jvm的用户一致,其中做的主要判断的位置在checkPermissions。 从上面的实验中我们可以看到,文件其实是由目标jvm进程创建的,文件的属主属组是目标进程的用户信息。我们可以简单的看下实现,看它是怎么实现的。具体的也就分两步:
1、获取socket path 就是.java_pid{pid}文件的属性, 以及通过geteuid(), getegid获取的当前进程信息
2、对比属组,属主是否一致同时判断文件的读写权限,不一致情况下抛出异常,此处就是需要启动arthas的用户和目标jvm用户一致的原因
JNIEXPORT void JNICALL Java_sun_tools_attach_LinuxVirtualMachine_checkPermissions (JNIEnv *env, jclass cls, jstring path)
{
jboolean isCopy;
const char* p = GetStringPlatformChars(env, path, &isCopy);
if (p != NULL) {
struct stat64 sb;
uid_t uid, gid;
int res;
uid = geteuid();
gid = getegid();
res = stat64(p, &sb);
if (res != 0) {
res = errno;
}
if (isCopy) {
JNU_ReleaseStringPlatformChars(env, path, p);
}
if (res == 0) {
if ( (sb.st_uid != uid) || (sb.st_gid != gid) ||
((sb.st_mode & (S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)) != 0) ) {
JNU_ThrowIOException(env, "well-known file is not secure");
}
} else {
char* msg = strdup(strerror(res));
JNU_ThrowIOException(env, msg);
if (msg != NULL) {
free(msg);
}
}
}
}
LinuxVirtualMachineImpl#socket
同样我们可以看到在native实现中此函数的实现同样比较简单,就是创建一个socket对象,获取到socket的文件描述符。
JNIEXPORT jint JNICALL Java_sun_tools_attach_LinuxVirtualMachine_socket (JNIEnv *env, jclass cls)
{
int fd = socket(PF_UNIX, SOCK_STREAM, 0);
if (fd == -1) {
JNU_ThrowIOExceptionWithLastError(env, "socket");
}
return (jint)fd;
}
LinuxVirtualMachineImpl#connet
上一步创建了socket对象,这一步骤从名称上就可以看到,是连接的步骤。 之前我们接触的到的可能都连接的步骤使用的都是ip + 端口。 此处的实现可能跟我们常见的不同, 有兴趣的可以查询资料看看有哪些通信实现。
此处的实现中 connect的sockaddr的实现使用的sockaddr_un这个对象。本地通信的结构体,sun_family使用的AF_UNIX, sun_path使用的就是我们之前通过sendQuitTo生成的socket文件。
JNIEXPORT void JNICALL Java_sun_tools_attach_LinuxVirtualMachine_connect (JNIEnv *env, jclass cls, jint fd, jstring path) {
jboolean isCopy;
const char* p = GetStringPlatformChars(env, path, &isCopy);
if (p != NULL) {
struct sockaddr_un addr;
int err = 0;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, p);
if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
err = errno;
}
if (isCopy) {
JNU_ReleaseStringPlatformChars(env, path, p);
}
if (err != 0) {
if (err == ENOENT) {
JNU_ThrowByName(env, "java/io/FileNotFoundException", NULL);
} else {
char* msg = strdup(strerror(err));
JNU_ThrowIOException(env, msg);
if (msg != NULL) {
free(msg);
}
}
}
}
}
VirtualMachine#loadAgent
前面介绍了获取VirtualMachineDescriptor的列表以及VirtualMachine对象,其实从上面的步骤我们已经可以基本猜测到loadAgent应该就是通过socket向目标jvm发送数据,之后读取结果,查找具体实现中我们可以定位到HotSpotVirtualMachine的loadAgent方法。
loadAgent的实现基本步骤如下,和普通的网络通信一样,发送信息,获取回应信息:
1、调用execute方法,获取到输入流
2、输入流中读取数据,判断returen code值是否为0
private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options) throws AgentLoadException, AgentInitializationException, IOException {
if (agentLibrary == null) {
throw new NullPointerException("agentLibrary cannot be null");
} else {
String msgPrefix = "return code: ";
InputStream in = this.execute("load", agentLibrary, isAbsolute ? "true" : "false", options);
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
try {
String result = reader.readLine();
if (result == null) {
throw new AgentLoadException("Target VM did not respond");
}
if (!result.startsWith(msgPrefix)) {
throw new AgentLoadException(result);
}
int retCode = Integer.parseInt(result.substring(msgPrefix.length()));
if (retCode != 0) {
throw new AgentInitializationException("Agent_OnAttach failed", retCode);
}
} catch (Throwable var10) {
try {
reader.close();
} catch (Throwable var9) {
var10.addSuppressed(var9);
}
throw var10;
}
reader.close();
}
}
execute方法
基本流程就是创建一个连接,顺序将cmd, args参数列表写到socket中。 从socket中读取一个整数数值, 如果completionStatus != 0代表读取执行信息失败
1、写信息的格式就是先写一个字符串1, 接着写入cmd(当前loadAgent,cmd就是load),之后依次写入参数信息。 writeString的实现中就是先写入字符串的信息,紧接着写入的一个值为0的byte字节信息
2、读取整数数值的时候,其实就是依次尝试从socket中读取一个byte的数据,如果对应的数据是\n的情况下,代表读取结束。
InputStream execute(String cmd, Object... args) throws AgentLoadException, IOException {
assert args.length <= 3;
synchronized(this) {
if (this.socket_path == null) {
throw new IOException("Detached from target VM");
}
}
int s = socket();
try {
connect(s, this.socket_path);
} catch (IOException var9) {
close(s);
throw var9;
}
IOException ioe = null;
try {
this.writeString(s, "1");
this.writeString(s, cmd);
for(int i = 0; i < 3; ++i) {
if (i < args.length && args[i] != null) {
this.writeString(s, (String)args[i]);
} else {
this.writeString(s, "");
}
}
} catch (IOException var11) {
ioe = var11;
}
VirtualMachineImpl.SocketInputStream sis = new VirtualMachineImpl.SocketInputStream(s);
int completionStatus;
try {
completionStatus = this.readInt(sis);
} catch (IOException var10) {
sis.close();
if (ioe != null) {
throw ioe;
}
throw var10;
}
if (completionStatus != 0) {
String message = this.readErrorMessage(sis);
sis.close();
if (completionStatus == 101) {
throw new IOException("Protocol mismatch with target VM");
} else if (cmd.equals("load")) {
String msg = "Failed to load agent library";
if (!message.isEmpty()) {
msg = msg + ": " + message;
}
throw new AgentLoadException(msg);
} else {
if (message.isEmpty()) {
message = "Command failed in target VM";
}
throw new AttachOperationFailedException(message);
}
} else {
return sis;
}
}
两种限制的根因
问:限制一定要存在hsperfdata_{user} 目录?
答:arthas的实现中需要通过hsperfdata_{user} 目录查找到所有的jvm进程, 如果不存在此目录,1、jps无法查询到进程列表,2、arthas在获取虚拟机描述符列表的时候会返回空的,导致后续attach失败
问:为什么启动arthas的用户和目标jvm的用户需要一致?
答:在tools.jar的实现中,在与socket文件通信前会检查socket文件的权限是否与启动用户的权限是否一致。同样在本地socket通信的时候也存在权限的限制的
问: 是否只能通过java 进行loadAgent?
答: 从上面的实现中我们可以看到,最终loadAgent的实现中就是通过socket与目标进程之间通信,所以我们也可以使用其它语言来实现。
问: tools.jar 是否是必须的?
答: 从上面的attach的过程中使用到tools.jar的内容, 但是最终loadAgent的过程中是与目标进程通过socket通信,我们可以手工模拟loadAgent的流程,这种情况下tools.jar是非必须的。可以使用其它语言模拟执行loadAgent的操作,下面是我使用go语言的实现方式:执行go run file.go pid即可
package main
import (
"net"
"fmt"
"io"
"os"
"syscall"
"time"
"strconv"
)
func main() {
id:=os.Args[1]
fmt.Println(id)
attach := "/tmp/.attach_pid"+id
_, err := os.Stat(attach)
if err != nil {
os.Create(attach)
}
pid,_ :=strconv.Atoi(id)
err =syscall.Kill(pid, syscall.SIGQUIT)
if err != nil {
fmt.Println(err)
fmt.Println("error to kill pid")
return
}
time.Sleep(10000000)
socket_path := "/tmp/.java_pid"+id
_, err1 := os.Stat(socket_path)
if err1 != nil {
fmt.Println("error to create socket file")
return
}
addr, err2 := net.ResolveUnixAddr("unix", socket_path)
if err2 != nil {
return
}
c, _ := net.DialUnix("unix", nil, addr)
c.Write([]byte("1"))
c.Write([]byte("\x00"))
c.Write([]byte("load"))
c.Write([]byte("\x00"))
c.Write([]byte("instrument"))
c.Write([]byte("\x00"))
c.Write([]byte("false"))
c.Write([]byte("\x00"))
c.Write([]byte("/home/demo/.arthas/lib/3.2.0/arthas/arthas-agent.jar"))
c.Write([]byte("\x00"))
return_str := ""
for {
buf := make([]byte, 1024)
read, err4 := c.Read(buf)
if err4 != nil {
if err == io.EOF {
fmt.Println(read)
break
}
}
if read != 0 {
return_str += string(buf[0 : read - 1])
} else {
break
}
}
}
下一篇
arthas启动-attach流程