Figura 1: Envio de sinal a processo
Introdução
Veremos nesse artigo como efetuar o tratamento de sinais no Linux usando Java. Porém, antes, faz-se necessário um pequeno overview da teoria dos sinais.
Teoria de Sinais em C no Linux
Para certos tipos de aplicações, pode ser necessário o tratamento de eventos especiais, disparados a partir do acionamento de combinação de teclas, exceções, alarmes, etc.
Alguns exemplos:
- Batches que processam arquivos em lote deveriam não ter sua execução interrompida ao pressionar a combinação de teclas CTRL+Z / CTRL+C.
- Tratamento de exceção gerada por divisão por zero, acesso ilegal a memória, etc.
Esses eventos podem ser gerados a qualquer momento durante a execução do programa e por isso são considerados assíncronos.
Quando um evento desses ocorre, é enviada para a aplicação um sinal, que é uma notificação de software, e caso esse sinal possua um manipulador associado, o mesmo será executado. Se não houver manipulador associado, o tratamento default fornecido pelo kernel será processado.
Por exemplo, o tratamento default para certas combinações de teclas:
- CTRL+C = Processo termina imediatamente
- CTRL+Z = Processo suspende a execução
- CTRL+\ = Processo termina imediatamente, com a exceção sendo escrita num arquivo core ou no console.
Listagem 1: Programa loop.c sem tratamento de sinal
int main(void)
{
for( ; ; ) {
// Loop infinito
}
}
Compilando: gcc -o loop loop.c
Executando: ./loop
Como o programa entra em loop infinito, só podemos interrompê-lo a partir do pressionamento do CTRL+C, CTRL+\ ou enviando um sinal SIGKILL (kill -9) para o programa.
Podemos definir um manipulador de sinal, por exemplo, para impedir que o CTRL+C termine o programa. Para isso, temos:
Listagem 2: Programa loop.c com tratamento de CTRL+C
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler_SIGINT(int sig)
{
printf("\nCTRL+C pressionado\n");
}
int main(void)
{
if(signal(SIGINT, handler_SIGINT) == SIG_ERR) {
fprintf(stderr, "Não foi possível capturar sinal\n");
}
for( ; ; ) {
// Loop infinito
}
}
Nesse caso, estamos substituindo o handler (tratador) default do CTRL+C, por um que apenas imprime uma mensagem e não finaliza a aplicação.
Cada sinal/evento é identificado por uma constante numérica. Por exemplo, SIGINT que se refere ao CTRL+C tem valor 2, SIGKILL valor 9, etc. Para ver todos os valores e seus significados, acesse http://www.yolinux.com/TUTORIALS/C++Signals.html.
A função signal, cuja declaração é: void (*signal(int sig, void (*func)(int)))(int); indica no seu primeiro argumento o sinal a ser tratado, e no segundo a função que irá ser executada quando o sinal for recebido.
Em caso de erro, a macro SIG_ERR será retornada pela função.
Poderíamos reescrever a aplicação acima, pois existe um handler pré-definido SIG_IGN, que é usado para ignorar determinado sinal.
Listagem 3: Programa loop.c com tratamento de CTRL+C usando SIG_IGN
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
int main(void)
{
if(signal(SIGINT, SIG_IGN) == SIG_ERR) {
fprintf(stderr, "Não foi possível capturar sinal\n");
}
for( ; ; ) {
// Loop infinito
}
}
Comando KILL
Existe outra forma de se enviar o CTRL+C para o programa, sem usar a combinação de teclas, através do comando kill. Apesar do nome, ele é usado para enviar qualquer sinal para um processo em execução:
kill -<signal> <pid>
onde <signal> é o valor numérico do sinal e <pid> é o id do processo em execução.
Por exemplo, podemos enviar um sinal de ABORT (SIGABRT = 6) para o programa abaixo, que irá tratá-lo.
Listagem 4: Programa loop.c com tratamento de ABORT
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
if(sig == SIGABRT) {
printf("Sinal SIGABRT recebido\n");
}
}
int main(void)
{
if(signal(SIGABRT, handler) == SIG_ERR) {
fprintf(stderr, "Não foi possível capturar sinal\n");
}
for( ; ; ) {
// Loop infinito
}
}
Para enviar o sinal SIGABRT, utilize o kill: kill -6 <PID>.
Já o famoso sinal SIGKILL (kill -9), utilizado para encerrar uma aplicação, NÃO pode ser capturado ou ignorado, e ele sempre irá finalizar o processo.
Função atexit()
Apesar de não podermos registrar nenhum evento para ser executado quando o sinal SIGKILL é enviado, quando a aplicação termina através da função exit() ou via return na função main(), podemos determinar uma função de callback a ser invocada antes do término do programa, através da função atexit():
int atexit(void (*function)(void));
A função atexit recebe como argumento um ponteiro de função, que será invocado quando a aplicação finalizar (via exit ou return no método main). O retorno será 0 se a função atexit teve sucesso ao registrar o callback, ou diferente de 0 indicando erro.
Listagem 5: exitTest.c: Registrando evento de finalização com atexit()
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(void)
{
printf("Função chamada antes do programa terminar\n");
}
int main(void)
{
atexit(handler);
sleep(5);
return 1; // ou exit(1)
}
Compilando: gcc -o exitTest exitTest.c
Executando: ./exitTest
Saída: Função chamada antes do programa terminar
O programa registra a função handler, que será invocada após a passagem de 5 segundos (função sleep).
A título de curiosidade, se criássemos um loop infinito dentro da função handler, o programa nunca terminaria, a não ser usando kill -9 (isso deve ser um dos fortes motivos para não ser possível ignorar ou sobrescrever o tratador default do sinal SIGKILL...).
Com isso finalizamos essa breve introdução sobre sinais em Linux.
Nota: Para uma visão mais aprofundada do tratamento de sinais em Linux em C, sugiro a leitura dos links em referência. Há vários outros tipos de funcionalidades interessantes envolvidas no tratamento de sinais.
Tratamento de sinais usando Java
1) Usando classe Runtime
A classe Runtime possui um método chamado addShutdownHook, que permite registrar um “gancho”, no caso uma ou mais threads, que serão inicializadas quando a JVM estiver para ser encerrada.
Segundo a documentação do método, a thread será invocada quando:
- O programa finalizar normalmente, isto é, todas as threads da aplicação terminarem suas execuções (mais precisamente, todas as threads não-daemons do programa forem finalizadas), ou quando o método exit() da classe System for invocado.
- Quando o usuário pressionar CTRL+C ou quando o usuário efetuar log-off ou solicitar shutdown do sistema operacional.
O método addShutdownHook() é mais abrangente que o atexit(), pois ele é responsivo a mais tipos de eventos geradores, como o CTRL+C.
Listagem 6: Classe TestShutdownHook1
public class TestShutdownHook1 {
private static final int SECONDS = 10;
public static void main(String args[]) {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("Programa sendo finalizado");
}
});
System.out.println("Aguarda " + SECONDS + " segundo(s)");
try {
Thread.sleep(SECONDS*1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("Antes de exit");
System.exit(0);
System.out.println("Depois de exit");
}
}
A classe TestShutdownHook1 registra uma thread, que simplesmente imprime uma mensagem no console. O programa aguarda n segundos e então executa System.exit(), que fará com que a thread seja executada, e o último System.out não seja impresso.
Se comentarmos o System.exit(), ainda assim a thread será executada.
Da mesma forma, se pressionarmos o CTRL+C durante a execução do programa, a thread será executada e depois o programa será finalizado.
Podemos registrar também mais de uma thread:
Listagem 7: Classe TestShutdownHook2
public class TestShutdownHook2 {
private static final int SECONDS = 10;
public static void main(String args[]) {
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("Programa sendo finalizado 1");
}
});
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
System.out.println("Programa sendo finalizado 2");
}
});
System.out.println("Aguarda " + SECONDS + " segundo(s)");
try {
Thread.sleep(SECONDS*1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
System.out.println("Antes do FIM");
}
}
Cuja saída será:
Listagem 8: Saída
Aguarda 10 segundo(s)
Antes do FIM
Programa sendo finalizado 1
Programa sendo finalizado 2
Não há um limite para a quantidade de threads que podemos registrar, porém a JVM não garante a ordem em que elas serão executadas, portanto não dependa dessa característica para implementar algum tipo de lógica sequencial. E uma vez iniciadas, as threads irão rodar concorrentemente, o que resulta em aplicar os mesmos cuidados típicos de aplicações concorrentes, como proteger acesso a recursos compartilhados, evitar deadlocks, etc.
Como curiosidade, no caso de adicionarmos um loop infinito em uma das threads "gancho", a aplicação não será finalizada, mesmo usando CTRL+C ou System.exit(), sendo necessário enviar um sinal SIGKILL (kill -9).
Quando um comando SIGKILL é enviado, a JVM é interrompida imediatamente e os ganchos nunca são executados.
Agora, aumente o quantidade de segundos para 360 e registre mais uma thread:
Listagem 9: Registrando nova Thread
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
final File f = new File("/home/senaga/teste.txt");
if(f.exists()) {
f.delete();
}
f.createNewFile();
} catch (IOException ex) {
ex.printStackTrace();
}
}
});
Durante a execução do programa, efetue shutdown ou logoff do sistema operacional. Note que as threads registradas serão executadas, sendo que essa nova thread irá criar um arquivo teste.txt, para demonstrar que ela realmente foi invocada.
Por fim, quando o S.O. está em processo de shutdown, ele possui um tempo limite em que pode esperar a aplicação encerrar de forma normal. O caso do loop infinito não iria travar o processo de shutdown, justamente por causa desse tempo onde o S.O. irá esperar, e depois, se a aplicação ainda estiver rodando, irá finalizá-la sumariamente. Portanto, evite escrever ganchos que consumam muito tempo. O correto é eles serem curtos e de rápida execução, além de thread-safe.
2) Usando classes do pacote sun.misc
Podemos usar as classes Signal e SignalHandler do pacote sun.misc para registrar manipuladores de sinais.
Listagem 10: Tratamento do sinal SIGINT ou INT pelo Java
import sun.misc.Signal;
import sun.misc.SignalHandler;
public class SignalTest {
private static final int SECONDS = 15;
public static void main(String[] args) {
Signal.handle(new Signal(("INT")), new SignalHandler() {
@Override
public void handle(Signal signal) {
System.out.println("Capturando CTRL+C");
}
});
try {
Thread.sleep(SECONDS*1000);
} catch(Exception e) {
e.printStackTrace();
}
}
}
Ao pressionar o CTRL+C durante a execução do programa, a mensagem será impressa e o programa não irá finalizar, pois sobrescrevemos o manipulador default.
Para registrar um manipulador, basta utilizar o método handle de Signal, passando como argumentos:
- O sinal a ser capturado, através da classe Signal (no construtor definimos o sinal)
- O manipulador do evento, que é um objeto cuja classe implementa a interface SignalHandler, através do seu método handle.
Para sistemas Unix/Linux, os sinais que podemos interceptar são: SEGV, ILL, FPE, BUS, SYS, CPU, FSZ, ABRT, INT, TERM, HUP, USR1, QUIT, BREAK, TRAP, PIPE.
Para Windows: SEGV, ILL, FPE, ABRT, INT, TERM, BREAK.
Nota: O risco de se usar as classes do pacote sun.misc é a não-garantia de que essas classes possam estar presentes em versões futuras do JDK da Oracle, por serem proprietárias, e até mesmo que sejam compatíveis de uma versão para outra.
3) Utilizando acesso a código nativo via JNI/JNA
Podemos acessar a função nativa signal em C diretamente, usando JNA / JNI. Por exemplo, em JNA podemos definir uma interface que representa a função signal em C, e depois usá-la, junto com outras classes de suporte do JNA para definir manipuladores para os sinais.
Listagem 11: Classe ClibraryFunctions representando a função signal, usando JNA
package br.com.devmedia.jna;
import com.sun.jna.Callback;
import com.sun.jna.Library;
public interface CLibrary extends Library {
public interface SignalFunction extends Callback {
void invoke(int signal);
}
SignalFunction signal(int signal, SignalFunction func);
}
Listagem 12: Classe CLibraryFunctions
package br.com.devmedia.jna;
import com.sun.jna.Native;
public final class CLibraryFunctions {
private CLibrary cLibraryInstance;
public static int SIGTSTP = 20;
public CLibraryFunctions() {
cLibraryInstance = (CLibrary)Native.loadLibrary("c", CLibrary.class);
}
public void signal(int signal, CLibrary.SignalFunction sf) {
cLibraryInstance.signal(signal, sf);
}
}
Listagem 13: Classe de Teste
package br.com.devmedia.jna;
public class JNAHelloWorld {
public static void main(String[] args) {
final CLibraryFunctions cLib = new CLibraryFunctions();
// Registrando "listener" para o CTRL+Z
cLib.signal(CLibraryFunctions.SIGTSTP, new CLibrary.SignalFunction() {
@Override
public void invoke(int signal) {
cLib.printf("CTRL+Z pressionado");
System.exit(1);
}
});
// Loop infinito
while (true) {}
}
}
A API JNA foge do escopo desse artigo. Para uma visão mais aprofundada do JNA, sugiro a leitura do artigo, Acesso ao código nativo usando JNA (Java Native Access), onde é explicado detalhadamente o funcionamento dessas três classes além da própria API do JNA.
Conclusão
Foram abordados três formas diferentes de se efetuar tratamento de sinais em Java: através de Runtime, classes do pacote sun.misc.* e JNA, além de uma introdução rápida da teoria dos sinais. Espero que esse artigo tenha útil para que os desenvolvedores possam tratar esses eventos especiais em suas aplicações, quando isso for necessário.
Obrigado e até a próxima!