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());
        
    }
}

arthas怎么结合docker arthas attach_ide

 

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}文件。

 

arthas怎么结合docker arthas attach_arthas怎么结合docker_02

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流程