ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

JVM中的监听信号的线程以及Unix域套接字通信的线程

2021-09-12 11:34:11  阅读:139  来源: 互联网

标签:socket 域套 描述符 6617 JVM 进程 线程


【实验】

package com.infuq.tmp;

public class Main {
    public static void main(String args[]) {
        for (;;) {
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

以上代码中,让JVM不退出,我们对它做点手脚,看一下JVM中的两个线程.

编译之后运行它.

通过jps查看进程号=6617
在这里插入图片描述

查看进程6617的线程
ps -Lf 6617
在这里插入图片描述

共计20个轻量级进程(LWP),即线程.

也可以通过/proc/6617/task查看进程6617下有多少个任务(即线程), 也是20个线程,如下.
在这里插入图片描述

我们再看一下这个进程6617打开的文件描述符,如下

ls -l /proc/6617/fd
在这里插入图片描述
共计6个文件描述符, 0,1,2分别是标准输入,标准输出和标准错误输出. 3,4,5描述符表示打开的3个jar.

总结一下,此时的JVM里面,共计20个线程,进程打开了6个文件描述符.

面试题: 如何知道JVM中的线程个数,有哪些方法?

接下来,我们在/tmp目录下创建一个.attach_pid6617文件,如下

在这里插入图片描述

接下来,我们使用kill命令向进程发送退出信号.
在这里插入图片描述

信号机制是进程间通信的一种方式

再观察下线程的信息
在这里插入图片描述在这里插入图片描述

多了一个6666的线程.

再看下进程6617打开的文件描述符
在这里插入图片描述
会发现多了一个文件描述6,而且还是个socket文件描述符.

总结一下,使用kill命令向JVM进程发送一个退出信号, 结果JVM多了1个线程,还多了1个sokcet文件描述符.

进程间通信的方式有很多,其中信号就是其中一种方式. 关于进程间的通信可以阅读它 . 向JVM发送一个信号之后,那么JVM必然有一个线程来处理信号,而这个线程就是Signal Dispatcher线程. 我相信,读者朋友,通过jstack命令查看线程栈的时候,一定能看到这个线程.

Signal Dispatcher线程在JVM启动的时候就创建了. 关于JVM的启动,我们先在这里简单说一下.

在jdk/src/share/bin/main.c文件中,有个main方法,它是一切的源头,JVM就是从这里开始它的人生之旅的,经过一路小跑,会创建main线程,也会创建JVM. 还会创建Signal Dispatcher线程,Signal Dispatcher线程会阻塞等待接收外部的信号. 比如上文中,我们使用kill向指定的进程6617发送的3号退出信号,就是由进程6617中的Signal Dispatcher线程来处理的. Signal Dispatcher线程在收到并处理3号退出信号的时候,它会创建Attach Listener线程,也会创建一个socket文件描述符,这个socket文件描述符就是上文中看到的那个6号文件描述符,那么这个socket文件描述符能干啥用呢?

除了信号可以用于进程间通信, Unix Domain Socket也可以用于进程间通信. 这种socket有别于网络socket. Unix Domain Socket仅用于本地进程间通信, 而网络socket用于网络间的进程间通信. 而通过Unix Domain Socket创建出来的6号文件描述符,它就是由Attach Listener这个线程来使用的. 这个Attach Listener线程作为服务端,监听客户端的请求. 比如像jstack命令,阿里的Arthas(阿尔萨斯)等工具,它们底层都是通过这个socket文件描述符连接到目标JVM,从而实现通信.

在这里插入图片描述

我们通过JDK自带的bin目录下的工具jvisualvm,通过图形化的方式,再次查看下进程6617中的线程.

在这里插入图片描述
看看你公司的服务器是否有这两个线程呢?

接下来我们通过3种方式获取进程6617的线程栈信息.

面试题: 如何得到一个进程的线程栈信息?

第一种方式就是通过jstack命令,或者JDK体系的其他命令.

在这里插入图片描述第二种方式,通过Java代码的方式

import com.sun.tools.attach.VirtualMachine;
import sun.tools.attach.HotSpotVirtualMachine;
import java.io.InputStream;

public class Attach {
    public static void main(String[] args)throws Exception {
        // attach底层就是发送了一个kill -3 6617的命令给目标JVM
        VirtualMachine virtualMachine = VirtualMachine.attach("6617");
        HotSpotVirtualMachine hotSpotVirtualMachine = (HotSpotVirtualMachine)virtualMachine;
        // 发送threaddump命令给目标JVM
        InputStream inputStream = hotSpotVirtualMachine.remoteDataDump(new String[]{});

        byte[] buff = new byte[256];
        int len;
        do {
        	// 接收目标JVM返回的数据
            len = inputStream.read(buff);
            if (len > 0) {
                String respone = new String(buff, 0, len, "UTF-8");
                System.out.print(respone);
            }
        } while(len > 0);

        inputStream.close();
        virtualMachine.detach();
    }
}

编译并运行这个Java程序,依然可以得到进程6617的线程栈信息
在这里插入图片描述
第三种方式,通过C语言的方式, 之所以通过C语言的方式,旨在说明一点,不管我们使用的是jstack命令,还是上面的Java程序,或者阿里开源的Arthas(阿尔萨斯)工具,在它们的底层,都是通过同一种方式与目标JVM进行通信的, 而通过C语言,能更好的把它展现给我们看.

个人理解,如果真想把JVM或者JDK学透了,C语言是要熟悉的. JVM的底层都是C语言,包括与操作系统的一些交互,都是C语言. 包括进程间的通信等, 如果不懂C语言,不懂一些操作系统的知识,那么很难学透JVM或者JDK. 之所以要学习JVM等底层知识, 个人理解,主要是让我们的知识体系健全,不至于知识碎片化.

代码如下

// threaddump.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <errno.h>
#include <stddef.h>
#include <unistd.h>

#define BUFFER_SIZE 8192
const char *filename = "/tmp/.java_pid6617";

int main(int argc, char **argv)
{
        struct sockaddr_un  un;
        int             fd;
        char            buffer[BUFFER_SIZE];
        char            *cmd = "1\0threaddump\0\0\0\0"; // 长度16

        un.sun_family = AF_UNIX;
        strcpy(un.sun_path, filename);

        fd = socket(PF_UNIX, SOCK_STREAM, 0);
        connect(fd, (struct sockaddr *) &un, sizeof(un));

        // 方式一
        send(fd, cmd, 16, 0);
        recv(fd, buffer, BUFFER_SIZE, 0);

        // 方式二
        //write(fd, cmd, 16);
        //read(fd, buffer, BUFFER_SIZE);

        printf("\n%s\n", buffer);
        close(fd);
        return 0;
}

编译
在这里插入图片描述运行
在这里插入图片描述
上面我们可以看到,线程栈信息正常打印出来了. 那么它是如何做到的呢?
首先,在代码中定义了一个 const char *filename = “/tmp/.java_pid6617”; 文件名, 我们看下这个文件.
在这里插入图片描述6617就是进程ID. 当我们通过kill命令向JVM发送3号退出信号的时候, Signal Dispatcher线程就会把Attach Listener线程创建出来, Attach Listener线程就会根据进程ID创建一个/tmp/.java_pid<PID>的文件. 如果是网络socket通信,是基于IP和端口,而如果是Unix Domain Socket通信,就是基于文件的,而此时创建了一个/tmp/.java_pid<PID>的文件, Attach Listener线程就会创建一个服务端的socket, 那么客户端就可以根据这个/tmp/.java_pid<PID>的文件创建一个客户端,然后与服务端进行通信了. 那么如何创建客户端的socket呢?
在我们的C语言代码里

// 创建Unix Domain Socket用于本机进程间通信
fd = socket(PF_UNIX, SOCK_STREAM, 0);
// 连接服务器. 服务器也是通过Unix Domain Socket创建的.
connect(fd, (struct sockaddr *) &un, sizeof(un));

通过以上两句,创建了客户端的socket, 并与服务端(也就是目标JVM)建立了连接, 然后就是发送命令了.
代码中我们发送了一个threaddump的命令,如下

char            *cmd = "1\0threaddump\0\0\0\0"; // 长度16

一切皆协议, 客户端和服务端约定好了, 服务端接收什么样子的命令格式才表示需要dump线程栈, 于是乎,客户端就构造这样的命令, 然后把它发送给目标JVM. 目标JVM的Attach Listener线程收到命令之后,进行处理,然后把处理结果返回给客户端, 于是乎客户端就拿到了目标JVM的线程栈.

在这里插入图片描述

本篇啰嗦这么多,主要就是在表达,如何与目标JVM进行通信,以及涉及的一些线程和知识点.


个人站点
语雀

公众号

在这里插入图片描述

标签:socket,域套,描述符,6617,JVM,进程,线程
来源: https://blog.csdn.net/qq_45859054/article/details/120231094

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有