Envio de sinal a processo

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!

Referências