В JDK5 разработчики могут указать javaagent для работы с байт-кодами в premain только при запуске JVM, а инструментарий также ограничен до выполнения основной функции.Этот метод имеет определенные ограничения. Динамическая схема агента присоединения была представлена начиная с JDK 6. В дополнение к указанию javaagent в командной строке теперь его можно загружать удаленно через API прикрепления. Наши часто используемые инструменты, такие как jstack и arthas, реализуются через механизм Attach.
Это будет сочетаться с сигналами межпроцессного взаимодействия и сокетами домена Unix для реализации принципа JVM Attach API,
Вы приобретете следующие связанные знания
- какой сигнал
- Как написать программу, которую нельзя убить "легко"
- Использование сокетов домена Unix
- Используйте трассировку артефактов для просмотра внутреннего процесса вызова приложения «черный ящик».
- Подробное объяснение использования и процесса JVM Attach API
какой сигнал
Сигнал — это механизм уведомления процесса о возникновении события, также известного как «программное прерывание». Сигналы можно рассматривать как очень легкую форму межпроцессного взаимодействия. Сигналы отправляются от одного процесса к другому, но через ядро в качестве посредника. Первоначальная цель сигналов состояла в том, чтобы указать различные способы завершения процесса.
У каждого сигнала есть имя, начинающееся с "SIG", самый известный сигнал должен бытьSIGINT
, нажимаем во время выполнения приложения в терминалеCtrl+C
Обычно завершает исполняемый процесс именно потому, что нажатиеCtrl+C
пошлетSIGINT
сигнал целевой программе.
Каждый семафор имеет уникальный числовой идентификатор, начинающийся с 1, вот список общих семафоров:
имя сигнала | Нумерация | описывать |
---|---|---|
SIGINT | 2 | Сигнал прерывания клавиатуры (Ctrl+C) |
SIGQUIT | 3 | Сигнал выхода с клавиатуры (Ctrl+/) |
SIGKILL | 9 | «Конечно убить» сигнал, приложение не может игнорировать или поймать, он всегда будет убит |
SIGTERM | 15 | Сигнал завершения |
В Linux процесс переднего плана можно завершить, нажав Ctrl + C. Для фоновых процессов его необходимо завершить, добавив номер процесса для уничтожения.Команда kill используется для завершения процесса, отправив сигнал целевому процессу. По умолчанию команда kill отправляет число 15.SIGTERM
Сигнал, этот сигнал может быть перехвачен процессом, выберите его игнорирование или выход в обычном режиме. Как бы целевой процесс не обработал этот сигнал, он будет завершен. для тех, кто игнорируетSIGTERM
Сигнальный процесс, вам нужен сигнал SIGKILL под номером 9, чтобы принудительно убить процесс, сигнал SIGKILL нельзя игнорировать или захватить и обработать на заказ.
Ниже я написал фрагмент кода C, пользовательскую обработку сигналов SIGQUIT, SIGINT, SIGTERM.
signal.c
static void signal_handler(int signal_no) {
if (signal_no == SIGQUIT) {
printf("quit signal receive: %d\n", signal_no);
} else if (signal_no == SIGTERM) {
printf("term signal receive: %d\n", signal_no);
} else if (signal_no == SIGINT) {
printf("interrupt signal receive: %d\n", signal_no);
}
}
int main() {
signal(SIGQUIT, signal_handler);
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
for (int i = 0;; i++) {
printf("%d\n", i);
sleep(3);
}
}
Скомпилируйте и запустите вышеуказанный файл signal.c
gcc signal.c -o signal
./signal
В этом случае в терминалеCtrl+C
,kill -3
,kill -15
Нет способа убить этот процесс, используйте толькоkill -9
0
^Cinterrupt signal receive: 2 // Ctrl+C
1
2
term signal receive: 15 // kill pid
3
4
5
quit signal receive: 3 // kill -3
6
7
8
[1] 46831 killed ./signal // kill -9 成功杀死进程
По умолчанию JVM для SIGQUIT распечатывает информацию о стеке всех запущенных потоков.В Unix-подобных системах сигнал SIGQUIT можно отправить с помощью команды kill -3 pid. Запустите вышеуказанный MyTestMain, используйте jps, чтобы найти идентификатор процесса всей JVM, выполните kill -3 pid, и вы увидите информацию о стеке вызовов всех потоков, напечатанную в терминале:
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.51-b03 mixed mode):
"Service Thread" #8 daemon prio=9 os_prio=31 tid=0x00007fe060821000 nid=0x4403 runnable [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
...
"Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007fe061008800 nid=0x3403 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"main" #1 prio=5 os_prio=31 tid=0x00007fe060003800 nid=0x1003 waiting on condition [0x000070000d203000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at MyTestMain.main(MyTestMain.java:10)
Сокет домена Unix
Связь через сокеты с использованием TCP и UDP — это хорошо известный способ использования сокетов, в дополнение к этому способу существует способ, называемый сокетами домена Unix, который обеспечивает взаимодействие между процессами на одном хосте. Хотя использование адреса обратной связи 127.0.01 также обеспечивает межпроцессное взаимодействие на одном и том же хосте по сети, доменные сокеты Unix более надежны и эффективны. Демон Docker использует сокет домена Unix, через который процессы в контейнере могут взаимодействовать с демоном Docker. MySQL также предоставляет доступ к сокетам домена.
Что такое сокеты домена Unix?
Сокет домена Unix — это файл, который можно просмотреть с помощью команды ls.
ls -l
srwxrwxr-x. 1 ya ya 0 9月 8 00:26 tmp.sock
Читая и записывая этот файл, два процесса реализуют передачу информации между процессами. Владелец файла и разрешения определяют, кто может читать и записывать сокет.
В чем отличие от обычных розеток?
- Сокеты домена Unix более эффективны: сокетам Unix не нужно выполнять обработку протокола, не нужно вычислять серийные номера и не нужно отправлять подтверждающие сообщения, просто копируются данные.
- Сокеты домена Unix надежны и не теряют пакеты, обычные сокеты предназначены для ненадежной связи.
- Код для сокетов домена Unix можно легко изменить для преобразования в обычные сокеты.
Пример кода сокета домена
Ниже приведен пример простой реализации доменных сокетов на C. Примечание. Чтобы упростить код, код в статье не включает обработку ошибок. Полный код, включая обработку ошибок исключений, см. в следующих разделах:GitHub.com/Артур-Станция…
Структура кода следующая:
.
├── client.c
└── server.c
server.c действует как сервер сокетов домена Unix. После запуска в текущем каталоге создается файл сокета домена Unix с именем tmp.sock.Он считывает содержимое, написанное клиентом, и выводит его.
server.c
int main() {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "tmp.sock");
int ret = bind(fd, (struct sockaddr *) &addr, sizeof(addr));
listen(fd, 5)
int accept_fd;
char buf[100];
while (1) {
accept_fd = accept(fd, NULL, NULL)) == -1);
while ((ret = read(accept_fd, buf, sizeof(buf))) > 0) {
// 输出客户端传过来的数据
printf("receive %u bytes: %s\n", ret, buf);
}
}
Код клиента выглядит следующим образом:
client.c
int main() {
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "tmp.sock");
connect(fd, (struct sockaddr *) &addr, sizeof(addr)) == -1
int rc;
char buf[100];
// 读取终端标准输入的内容,写入到 Unix 域套接字文件中
while ((rc = read(STDIN_FILENO, buf, sizeof(buf))) > 0) {
write(fd, buf, rc);
}
}
Скомпилируйте и выполните из командной строки
gcc server.c -o server
gcc client.c -o client
Запустите два терминала, один запускает серверную часть, а другой запускает клиентскую.
./server
./client
Вы можете видеть, что в текущем каталоге создается файл «tmp.sock».
ls -l
srwxrwxr-x. 1 ya ya 0 9月 8 00:08 tmp.sock
Введите привет в клиенте, вы можете увидеть это в терминале сервера
./server
receive 6 bytes: hello
JVM Attach API
Базовое использование JVM Attach API
Ниже приведен практический пример, демонстрирующий использование динамического API-интерфейса Attach. В коде есть метод main, а возвращаемое значение метода foo равно 100 каждые 3 с. байт-код 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/Артур-Станция…
Динамически присоединенный агент отличается от способа запуска пакета jar агента, заданного параметром javaagent, через JVM.Динамически присоединенный агент будет выполнять метод 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,
java -cp /path/to/your/tools.jar:. MyAttachMain pid
Вы можете видеть, что метод foo в выводе MyTestMain вернул 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.<init>(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, который является нативным методом. Нижний слой - отправить номер SIGQUIT в целевой процесс JVM.
Мы представили более раннюю часть сигнала, JVM, поведение SIGQUIT по умолчанию — дамп текущего стека потока, почему бы не вызвать стек вывода стека вызовов VirtualMachine.attach?
Для инициатора Attach, предполагая, что целевой процесс — 12345, подробный процесс этой части выглядит следующим образом:
1. Сторона прикрепления проверяет наличие файла .java_pid12345 в каталоге временных файлов.
Этот файл представляет собой файл сокета домена UNIX, созданный целевым процессом JVM после успешного подключения. Если этот файл существует, это означает, что он подключается, и этот сокет можно использовать для следующего обмена данными. Если этот файл не существует, создайте файл .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. Сторона подключения проверяет наличие файла .java_pid12345 и отправляет сигнал SIGQUIT на целевую JVM после создания файла .attach_pid12345. Затем проверьте, генерировался ли файл сокета каждые 200 мс, выйдите, если он не был сгенерирован через 5 с, и выполните связь с сокетом, если он сгенерирован.
3. Для целевого процесса JVM после того, как его поток диспетчера сигналов получит сигнал SIGQUIT, он проверит, существует ли файл .attach_pid12345.
- Если целевая JVM обнаруживает, что .attach_pid12345 не существует, она считает, что это не операция присоединения, выполняет поведение по умолчанию и выводит стеки всех текущих потоков.
- Если целевая JVM обнаруживает, что .attach_pid12345 существует, она считает это операцией присоединения, запускает поток прослушивателя присоединения, отвечает за обработку запроса на присоединение и создает файл сокета с именем .java_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
}
}
is_init_trigger для AttachListener создаст новый файл сокета .java_pid12345, когда файл .attach_pid12345 существует, и одновременно отслеживает этот сокет, чтобы подготовить сторону подключения к отправке данных.
Какую информацию сторона Присоединения и целевой процесс передают через сокет? Вы можете увидеть, что сторона Attach записала в сокет с помощью strace:
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, "\0", 1) = 1
5845 [pid 3869] write(5, "load", 4) = 4
5846 [pid 3869] write(5, "\0", 1) = 1
5847 [pid 3869] write(5, "instrument", 10) = 10
5848 [pid 3869] write(5, "\0", 1) = 1
5849 [pid 3869] write(5, "false", 5) = 5
5850 [pid 3869] write(5, "\0", 1) = 1
5855 [pid 3869] write(5, "/home/ya/agent.jar"..., 18 <unfinished ...>
Вы можете видеть, что содержимое, записанное в сокет, выглядит следующим образом:
1
\0
load
\0
instrument
\0
false
\0
/home/ya/agent.jar
\0
между данными\0
Разделение символов, 1 в первой строке представляет версию протокола, за которой следует команда отправки"load instrument false /home/ya/agent.jar"
Для целевой JVM после получения данных целевая JVM может загрузить соответствующий пакет jar агента, чтобы перезаписать байт-код.
С точки зрения сокета метод VirtualMachine.attach эквивалентен трехстороннему рукопожатию для установления соединения, VirtualMachine.loadAgent предназначен для отправки данных после успешного рукопожатия, а VirtualMachine.detach эквивалентен четырем взмахам рук для отключения.
Процесс показан ниже:
резюме
В этой статье представлены два способа взаимодействия между процессами на одном хосте: сигналы и сокеты домена Unix. Механизм подключения JVM в полной мере использует функции, предоставляемые сигналами и сокетами домена. Во-первых, создается временный файл, чтобы указать, что это присоединить операцию, а затем отправить сигнал SIGQUIT целевому процессу.Если целевой процесс обнаруживает, что существует присоединяемый временный файл, он создает прослушивающий файл сокета домена Unix, а инициатор присоединения может записывать и считывать данные через API сокета.
постскриптум
Эта статья не интерпретирует этот механизм JVMTI, есть возможность позже в статье, мы продолжим говорить с случаем.
Вы можете отсканировать QR-код ниже, чтобы подписаться на мой официальный аккаунт: