17 - Capturando sinais
Na aula de hoje iremos criar programas que reagem a sinais recebidos. Já vimos que o sistema tem uma série de comportamentos padrão para cada sinal, então hoje aprenderemos a customizar esse comportamento.
Warning
Apesar de muitos recursos mostrarem o uso da chamada signal para a captura de sinais, ela é considerada obsoleta e o recomendado é usar sigaction, que é um pouco mais complicada de usar mas permite maior flexibilidade ao definir o comportamento do processo.
Capturando sinais - a chamada sigaction
O programa trecho apresentado abaixo cria um struct sigaction e o seta para executar um handler quando o processo receber SIGINT (Ctrl+C).
// Fora da main, criamos a função que será nosso handler
void sig_handler(int num) {
// faz algo aqui
}
int main() {
....
/* Dentro da main, uma das primeiras coisas que fazemos é
registrar nosso handler */
struct sigaction s;
s.sa_handler = sig_handler; // aqui vai a função a ser executada
sigemptyset(&s.sa_mask);
s.sa_flags = 0;
sigaction(SIGINT, &s, NULL);
....
}
Example
Usando como exemplo o código acima, modifique o arquivo sinal1.c para que o programa só termine após apertar Ctrl+C três vezes. Você pode usar exit para sair na terceira vez. Não se esqueça de consultar man sigaction para verificar quais includes devem ser usados.
Clique para ver uma proposta de solução do exercício!
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
int contador = 0;
void sig_handler(int num) {
contador++;
printf("Chamou Ctrl+C\n");
if (contador == 3) {
exit(0);
}
}
int main() {
struct sigaction handler;
handler.sa_handler = sig_handler;
handler.sa_flags = 0;
sigemptyset(&handler.sa_mask);
sigaction(SIGINT, &handler, NULL);
printf("Meu pid: %d\n", getpid());
while(1) {
sleep(1);
}
return 0;
}
Provavelmente sua solução para o exercício acima funciona, mas seu término não é condizente com a ação do usuário. Ao sair com exit o processo pai (no caso o shell) não consegue saber que o programa foi interrompido pelo usuário e acha que ele terminou normalmente. Podemos resetar o comportamento padrão de um sinal atribuindo a constante SIG_DFL (signal default) a sigaction.sa_handler.
Example
Restaure o comportamento original no segundo Ctrl+C, fazendo com que o processo realmente termine com o sinal.
Clique para ver uma proposta de solução do exercício!
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/types.h>
int contador = 0;
void sig_handler(int num) {
contador++;
printf("Chamou Ctrl+C\n");
if (contador == 2) {
struct sigaction handler;
handler.sa_handler = SIG_DFL;
handler.sa_flags = 0;
sigemptyset(&handler.sa_mask);
sigaction(SIGINT, &handler, NULL);
}
}
int main() {
struct sigaction handler;
handler.sa_handler = sig_handler;
handler.sa_flags = 0;
sigemptyset(&handler.sa_mask);
sigaction(SIGINT, &handler, NULL);
printf("Meu pid: %d\n", getpid());
while(1) {
sleep(1);
}
return 0;
}
Exercise 1
Resposta
você deve receber no terminal uma string identificando o sinal.
Um dos problemas da solução proposta é que o último CTRL+C fica sem printf de mensagem! Para resolver o problema, vamos alterar a linha if (contador == 3) e fazer o processo enviar um SIGINT para si próprio.
Clique para ver uma proposta de solução do exercício!
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <signal.h>
int contador = 0;
void sig_handler(int num)
{
contador++;
printf("Chamou Ctrl+C\n");
if (contador == 3)
{
struct sigaction handler;
handler.sa_handler = SIG_DFL;
handler.sa_flags = 0;
sigemptyset(&handler.sa_mask);
sigaction(SIGINT, &handler, NULL);
/*
Envia o sinal para si mesmo.
Outra forma: raise(SIGINT);
*/
kill(getpid(), SIGINT);
}
}
int main()
{
struct sigaction handler;
handler.sa_handler = sig_handler;
handler.sa_flags = 0;
sigemptyset(&handler.sa_mask);
sigaction(SIGINT, &handler, NULL);
printf("Meu pid: %d\n", getpid());
while (1)
{
sleep(1);
}
return 0;
}
Sinais e concorrência
Nesta parte vamos trabalhar com o arquivo sinais-concorrentes.c.
Example
Leia o conteúdo do arquivo acima e complete as partes faltantes.
Exercise 2
Resposta
Os handlers foram corretamente chamados. Mas precisamos conferir melhor o valor da variável status.
Vamos agora examinar o que acontece quando trabalhamos com vários sinais sendo recebidos ao mesmo tempo.
Example
Abra dois terminais. No primeiro execute sinais-concorrentes.
Exercise 3
Resposta
Envie kill -s SIGINT <pid>, kill -s SIGTERM <pid> e kill -s SIGINT <pid> e confira o resultado!
Exercise 4
Resposta
Não! Podemos perceber que o valor do status foi modificado sem que o primeiro handler fosse finalizado, tornando inconsistente a nossa tratativa. Isto ocorre porque a chegada de um novo sinal faz com que o handler que está executando seja interrompido e o novo handler do novo sinal passa a executar. Apenas quando este segundo handler finalizar é que o anterior continuará sua execução.
Warning
Valide sua resposta com o professor antes de prosseguir. Se quiser, poderá esperar pela correção do exercício.
Bloqueio de sinais
A principal vantagem de usarmos sigaction é que esta chamada permite configurar sinais a serem bloqueados durante a execução da função sa_handler. Ou seja, se um sinal bloqueado for recebido durante sua execução ele é colocado "em espera" até que sa_handler acabe de rodar!
Exercise 5
Resposta
Sim! Desta forma, garantimos consistência no valor da variável status.
Exercise 6
Resposta
No registro do handler de SIGINT, bloqueamos SIGTERM. No registro do handler de SIGTERM, bloqueamos SIGINT.
Exercise 7
Resposta
Podemos fazer, por exemplo, sigaddset(&handler_sigterm.sa_mask, SIGINT);
Example
Modifique sinais-concorrentes.c para que SIGTERM seja bloqueado enquanto o handler de SIGINT roda. Repita então o experiemento acima e veja que não há mais conflito na variável global compartilhada.
O que fizemos não permite que SIGINT seja interrompido por um SIGTERM, mas permite que um SIGTERM seja interrompido por um SIGINT! Corrija esta situação.
Exercícios para praticar
Exercise 8
Exercise 9
Uma parte importante de sinais em sistemas POSIX é que, ao interromper um processo, eles podem cancelar operações que estavam ocorrendo. Em especial, chamadas de sistema que deixam um processo bloqueado (como wait e sleep) ou que fazem operações de entrada/saída (como read e write).
Exercise 10
Exercise 11
Exercise 13
Exercise 14