最新公告
  • 欢迎您光临欧资源网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入我们
  • JVMAttachAPI的实现原理你知道吗?(组图)

    在JDK5中,开发者只能在JVM启动时指定一个javaagent来操作premain中的字节码,而Instrumentation仅限于执行前的main函数,有一定的局限性。从 JDK6 开始就引入了动态 Attach Agent 方案。除了在命令行中指定 javaagent 之外,现在还可以通过 Attach API 远程加载它。我们常用的jstack、arthas等工具都是通过Attach机制实现的。

    本文将结合跨进程通信和Unix域套接字中的信号来看JVM Attach API的实现原理

    您将获得以下相关知识

    什么是信号

    信号是事件发生时对进程的通知机制,也称为“软件中断”。信号可以看作是一种非常轻量级的进程间通信形式。信号从一个进程发送到另一个进程linux的通信方式套接字,但通过内核作为中间人。信号的最初目的是指定终止进程的不同方法。.

    每个信号都有一个名称,以“SIG”开头。最著名的信号应该是 SIGINT。当我们在终端执行应用程序的过程中按下Ctrl+C时,通常会终止正在执行的进程。正是因为按下 Ctrl+C 会向目标程序发送一个 SIGINT 信号。

    每个信号量都有一个唯一的数字标识,从 1 开始。下面是常见信号量的列表:

    在Linux中,可以通过Ctrl+C来终止一个前台进程,而后台进程则需要通过添加进程号来终止。kill 命令用于通过向目标进程发送信号来终止进程。默认情况下,kill 命令发送一个 15 号 SIGTERM 信号,该信号可以被进程捕获并忽略或正常退出。目标进程如何不自定义处理这个信号,它将被终止。对于那些忽略 SIGTERM 信号的进程,需要 SIGKILL 信号 9 号来强制终止该进程。SIGKILL 信号不能被忽略或自行捕获和处理。

    下面我写了一段C代码,自定义处理SIGQUIT、SIGINT、SIGTERM信号

    编译运行上面的signal.c文件

    这种情况下,在终端中,Ctrl+C,kill -3,kill -15 不能杀进程,只能用kill -9

    JVM 对 SIGQUIT 的默认行为是打印所有正在运行的线程的堆栈信息。在类 Unix 系统上,可以使用命令 kill -3 pid 发送 SIGQUIT 信号。运行上面的MyTestMain,使用jps查找整个JVM的进程id,执行kill -3 pid,可以看到终端打印的所有线程的调用栈信息:

    Unix 域套接字

    使用 TCP 和 UDP 进行套接字通信是众所周知的使用套接字的方式,除此之外还有一种方式称为 Unix 域套接字,它可以实现同一主机上的进程间通信。Unix 域套接字更加可靠和高效,尽管使用 127.0.01 环回地址也可以通过网络在同一主机上实现进程间通信。Docker 守护进程使用 Unix 域套接字,容器中的进程可以通过该套接字与 Docker 守护进程通信。MySQL 还提供对域套接字的访问。

    什么是 Unix 域套接字?

    Unix 域套接字是可以使用 ls 命令查看的文件

    通过读写这个文件,两个进程实现了进程之间的信息传递。文件的所有者和权限决定了谁可以读取和写入套接字。

    与普通插座有什么区别?

    域套接字代码示例

    下面是域套接字的简单 C 实现示例。注意:为了简化代码,文中代码省略了错误处理。包含异常错误处理的完整代码见:github.com/arthur-zhan…

    代码结构如下:

    server.c 充当 Unix 域套接字服务器。启动后,在当前目录下会生成一个名为 tmp.sock 的 Unix 域套接字文件。它读取客户端写入的内容并输出。

    客户端代码如下:

    从命令行编译并执行

    启动两个终端,一个启动服务器端,一个启动客户端

    ./server
    ./client
    

    可以看到当前目录下生成了一个“tmp.sock”文件

    ls -l
    srwxrwxr-x. 1 ya ya 0 9月 8 00:08 tmp.sock
    

    在客户端输入hello,就可以在服务端的终端看到了

    linux的通信方式套接字_中国古代通信所有方式_linux命名管道通信

    ./server
    receive 6 bytes: hello
    

    JVM 附加 API

    JVM Attach API 的基本用法

    下面是一个实际的例子来演示动态 Attach API 的使用。代码中有一个main方法,foo方法的返回值是每3s输出100。接下来,动态Attach上的MyTestMain进程,修改foo的字节码,让foo方法返回50。

    public class MyTestMain {
     public static void main(String[] args) throws InterruptedException {
     while (true) {
     System.out.println(foo());
     TimeUnit.SECONDS.sleep(3);
     }
     }
     public static int foo() {
     return 100; // 修改后 return 50;
     }
    }
    

    进行如下操作:

    1、编写 Attach Agent 并注入 foo 方法。完整代码见:github.com/arthur-zhan…

    动态Attach的代理与通过JVM启动javaagent参数指定的代理jar包的方式不同。动态 Attach 的代理会执行 agentmain 方法而不是 premain 方法。

    public class AgentMain {
     public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
     System.out.println("agentmain called");
     inst.addTransformer(new MyClassFileTransformer(), true);
     Class classes[] = inst.getAllLoadedClasses();
     for (int i = 0; i < classes.length; i++) {
     if (classes[i].getName().equals("MyTestMain")) {
     System.out.println("Reloading: " + classes[i].getName());
     inst.retransformClasses(classes[i]);
     break;
     }
     }
     }
    }
    

    2、由于是跨进程通信,Attach的发起者是一个独立的java程序,它会调用VirtualMachine.attach方法开始与目标JVM的跨进程通信。

    public class MyAttachMain {
     public static void main(String[] args) throws Exception {
     VirtualMachine vm = VirtualMachine.attach(args[0]);
     try {
     vm.loadAgent("/path/to/agent.jar");
     } finally {
     vm.detach();
     }
     }
    }
    

    使用jps查询MyTestMain的进程id,

    java -cp /path/to/your/tools.jar:. MyAttachMain pid
    复制代码
    

    您可以看到 MyTestMain 输出中的 foo 方法返回了 50。

    java -cp . MyTestMain
    100
    100
    100
    agentmain called
    Reloading: MyTestMain
    50
    50
    50
    

    JVM Attach API原理分析

    在执行MyAttachMain时,当指定了一个不存在的JVM进程时,会出现如下错误:

    java -cp /path/to/your/tools.jar:. MyAttachMain 1234
    Exception in thread "main" java.io.IOException: No such process
    	at sun.tools.attach.LinuxVirtualMachine.sendQuitTo(Native Method)
    	at sun.tools.attach.LinuxVirtualMachine.(LinuxVirtualMachine.java:91)
    	at sun.tools.attach.LinuxAttachProvider.attachVirtualMachine(LinuxAttachProvider.java:63)
    	at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208)
    	at MyAttachMain.main(MyAttachMain.java:8)
    

    可以看出,VirtualMachine.attach最终调用了sendQuitTo方法,该方法是一个native方法。底层是将SIGQUIT号发送给目标JVM进程。

    正如我们在前面的信号部分中提到的,JVM 对 SIGQUIT 的默认行为是转储当前线程堆栈,那么为什么对 VirtualMachine.attach 的调用没有输出调用堆栈呢?

    对于Attach的发起者,假设目标进程为12345,这部分的详细流程如下:

    1、附加端检查临时文件目录下是否有.java_pid12345文件

    该文件是目标JVM进程Attach成功后生成的UNIX域socket文件。如果这个文件存在,就说明它在Attach,这个socket可以用来做进一步的通信。如果此文件不存在,请创建一个 .attach_pid12345 文件。这部分的伪代码如下:

    String tmpdir = "/tmp";
    File socketFile = new File(tmpdir, ".java_pid" + pid);
    if (socketFile.exists()) {
     File attachFile = new File(tmpdir, ".attach_pid" + pid);
     createAttachFile(attachFile.getPath());
    }
    

    2、Attach端检查是否没有.java_pid12345文件,创建.attach_pid12345文件后向目标JVM发送SIGQUIT信号。然后每隔200ms检查socket文件是否生成,5s后没有生成则退出,生成则进行socket通信

    3、对于目标JVM进程,其Signal Dispatcher线程收到SIGQUIT信号后,会检查.attach_pid12345文件是否存在。

    /hotspot/src/share/vm/runtime/os.cpp部分源码中的逻辑如下:

    #define SIGBREAK SIGQUIT
    static void signal_thread_entry(JavaThread* thread, TRAPS) {
     while (true) {
     int sig;
     {
     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
     }
    }
    

    AttachListener的is_init_trigger会在.attach_pid12345文件存在时新建一个.java_pid12345 socket文件,同时监听这个socket,为Attach端发送数据做准备。

    Attach端和目标进程通过socket传递什么信息?您可以通过 strace 看到 Attach 端写入套接字的内容:

    sudo strace -f java -cp /usr/local/jdk/lib/tools.jar:. MyAttachMain 12345 2> strace.out
    ...
    5841 [pid 3869] socket(AF_LOCAL, SOCK_STREAM, 0) = 5
    5842 [pid 3869] connect(5, {sa_family=AF_LOCAL, sun_path="/tmp/.java_pid12345"}, 110) = 0
    5843 [pid 3869] write(5, "1", 1) = 1
    5844 [pid 3869] write(5, "", 1) = 1
    5845 [pid 3869] write(5, "load", 4) = 4
    5846 [pid 3869] write(5, "", 1) = 1
    5847 [pid 3869] write(5, "instrument", 10) = 10
    5848 [pid 3869] write(5, "", 1) = 1
    5849 [pid 3869] write(5, "false", 5) = 5
    5850 [pid 3869] write(5, "", 1) = 1
    5855 [pid 3869] write(5, "/home/ya/agent.jar"..., 18 
    

    可以看到写入socket的内容如下:

    1
    
    load
    
    instrument
    
    false
    
    /home/ya/agent.jar
    
    

    数据由 字符分隔。第一行中的 1 代表协议版本。接下来就是向目标JVM发送命令“load instrument false /home/ya/agent.jar”,目标JVM收到数据后就可以加载数据了。对应的agent jar包用于重写字节码。

    从socket来看,VirtualMachine.attach方法相当于三次握手建立连接,VirtualMachine.loadAgent是握手成功后发送数据linux的通信方式套接字,VirtualMachine.detach相当于四次挥手断开连接。

    该过程如下图所示:

    概括

    本文介绍了在同一主机上的进程之间进行通信的两种方式,信号和 Unix 域套接字。JVM 的 Attach 机制充分利用了信号和域套接字提供的功能。首先,创建一个临时文件来表明这是一个附加操作,然后向目标进程发送 SIGQUIT 信号。如果目标进程发现有attach临时文件,则创建一个监听Unix域socket文件,attach发起者可以通过socket API读写数据。

    站内大部分资源收集于网络,若侵犯了您的合法权益,请联系我们删除!
    欧资源网 » JVMAttachAPI的实现原理你知道吗?(组图)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    欧资源网
    一个高级程序员模板开发平台

    发表评论