mysql进程kill不掉_由 JVM Attach API 看跨进程通信中的信号和 Unix 域套接字

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

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

你将获得下面这些相关的知识

  • 信号是什么
  • 如何写一个不能被“轻易”杀死的程序
  • Unix 域套接字的用法
  • 利用神器 strace 来查看黑盒应用的内部调用过程
  • JVM Attach API 的使用和过程详解

信号是什么

信号是某事件发生时对进程的通知机制,也被称为“软件中断”。信号可以看做是一种非常轻量级的进程间通信,信号由一个进程发送给另外一个进程,只不过是经由内核作为一个中间人发出,信号最初的目的是用来指定杀死进程的不同方式。

每个信号都一个名字,以 "SIG" 开头,最熟知的信号应该是 SIGINT,我们在终端执行某个应用程序的过程中按下 Ctrl+C 一般会终止正在执行的进程,正是因为按下 Ctrl+C 会发送 SIGINT 信号给目标程序。

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

293b77cf4194ee29df9ca572c76c12d9.png

在 Linux 中,一个前台进程可以使用 Ctrl+C 进行终止,对于后台进程需要使用 kill 加进程号的方式来终止,kill 命令是通过发送信号给目标进程来实现终止进程的功能。默认情况下,kill 命令发送的是编号为 15 的 SIGTERM 信号,这个信号可以被进程捕获,选择忽略或正常退出。目标进程如何没有自定义处理这个信号,就会被终止。对于那些忽略 SIGTERM 信号的进程,则需要编号为 9 的 SIGKILL 信号强行杀死进程,SIGKILL 信号不能被忽略也不能被捕获和自定义处理。

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

8128e812ec49376e2237d2fcb4c3ee83.png

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

e1966f985a59bb561105afaf0e9fbe82.png

这种情况下,在终端中Ctrl+C,kill -3,kill -15都没有办法杀掉这个进程,只能用kill -9

56d2769b61238f54e529905490e5b4c8.png

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

5e99e12e889158a00b7666634a9a8ff9.png

Unix 域套接字(Unix Domain Socket)

使用 TCP 和 UDP 进行 socket 通信是一种广为人知的 socket 使用方式,除了这种方式还有一种称为 Unix 域套接字的方式,可以实现同一主机上的进程间通信。虽然使用 127.0.01 环回地址也可以通过网络实现同一主机的进程间通信,但 Unix 域套接字更可靠、效率更高。Docker 守护进程(Docker daemon)使用了 Unix 域套接字,容器中的进程可以通过它与Docker 守护进程进行通信。MySQL 同样提供了域套接字进行访问的方式。

Unix 域套接字是什么?

Unix 域套接字是一个文件,通过 ls 命令可以看到

9c6e508e0430be1ef6d211678f0822f8.png

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

与普通套接字的区别是什么?

  • Unix 域套接字更加高效,Unix 套接字不用进行协议处理,不需要计算序列号,也不需要发送确认报文,只需要复制数据即可
  • Unix 域套接字是可靠的,不会丢失报文,普通套接字是为不可靠通信设计的
  • Unix 域套接字的代码可以非常简单的修改转为普通套接字

域套接字代码示例

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

代码结构如下:

567ec62e8143ff34a6901d3c8f7b747c.png

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

b700cbcfa34b8bc6ea813c0bbce5d035.png

客户端的代码如下:

582751e27821836fc5178e9584133802.png

在命令行中进行编译和执行

0c2bd7a0f953f0795ef4662add63a5df.png

启动两个终端,一个启动 server 端,一个启动 client 端

./server./client

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

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

在 client 输入 hello,在 server 的终端就可以看到

./serverreceive 6 bytes: hello

JVM Attach API

JVM Attach API 基本使用

下面以一个实际的例子来演示动态 Attach API 的使用,代码中有一个 main 方法,每个 3s 输出 foo 方法的返回值 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 的 agent 与通过 JVM 启动 javaagent 参数指定的 agent jar 包的方式有所不同,动态 Attach 的 agent 会执行 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 程序,这个 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 . MyTestMain100100100agentmain calledReloading: MyTestMain505050

JVM Attach API 的原理分析

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

java -cp /path/to/your/tools.jar:. MyAttachMain 1234Exception in thread "main" java.io.IOException: No such processat 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 的默认行为是 dump 当前的线程堆栈,那为什么调用 VirtualMachine.attach 没有输出调用栈堆栈呢?

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

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

这个文件是一个 UNIX 域套接字文件,由 Attach 成功以后的目标 JVM 进程生成。如果这个文件存在,说明正在 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 文件以后发送 SIGQUIT 信号给目标 JVM。然后每隔 200ms 检查一次 socket 文件是否已经生成,5s 以后还没有生成则退出,如果有生成则进行 socket 通信

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

  • 目标 JVM 如果发现 .attach_pid12345 不存在,则认为这不是一个 attach 操作,执行默认行为,输出当前所有线程的堆栈
  • 目标 JVM 如果发现 .attach_pid12345 存在,则认为这是一个 attach 操作,会启动 Attach Listener 线程,负责处理 Attach 请求,同时创建名为 .java_pid12345 的 socket 文件,监听 socket。

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

#define SIGBREAK SIGQUITstatic 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 套接字文件,同时监听此套接字,准备 Attach 端发送数据。

那 Attach 端和目标进程用 socket 传递了什么信息呢?可以通过 strace 的方式看到 Attach 端究竟往 socket 里面写了什么:

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) = 55842 [pid 3869] connect(5, {sa_family=AF_LOCAL, sun_path="/tmp/.java_pid12345"}, 110) = 05843 [pid 3869] write(5, "1
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值