Comunicación entre procesos: señales

UN signales una notificación a un proceso que indica la ocurrencia de un evento. La señal también se llamasoftware interrupt y no es predecible para conocer su ocurrencia, por lo que también se llama un asynchronous event.

La señal se puede especificar con un número o un nombre, normalmente los nombres de las señales comienzan con SIG. Las señales disponibles se pueden verificar con el comando kill –l (l para enumerar nombres de señales), que es el siguiente:

Siempre que se genera una señal (ya sea mediante programación o señal generada por el sistema), se realiza una acción predeterminada. ¿Qué sucede si no desea realizar la acción predeterminada pero desea realizar sus propias acciones al recibir la señal? ¿Es esto posible para todas las señales? Sí, es posible manejar la señal pero no para todas las señales. ¿Qué pasa si quieres ignorar las señales? ¿Es esto posible? Sí, es posible ignorar la señal. Ignorar la señal implica no realizar la acción predeterminada ni manejar la señal. Es posible ignorar o manejar casi todas las señales. Las señales que no se pueden ignorar ni manejar / capturar son SIGSTOP y SIGKILL.

En resumen, las acciones realizadas para las señales son las siguientes:

  • Acción por defecto
  • Manejar la señal
  • Ignora la señal

Como se discutió, la señal se puede manejar alterando la ejecución de la acción predeterminada. El manejo de la señal se puede realizar de dos formas, es decir, a través de llamadas al sistema, signal () y sigaction ().

#include <signal.h>

typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t handler);

La señal de llamada del sistema () llamaría al manejador registrado al generar la señal como se menciona en signum. El manejador puede ser SIG_IGN (Ignorar la señal), SIG_DFL (Regresar la señal al mecanismo predeterminado) o el manejador de señal definido por el usuario o la dirección de función.

Esta llamada al sistema en caso de éxito devuelve la dirección de una función que toma un argumento entero y no tiene valor de retorno. Esta llamada devuelve SIG_ERR en caso de error.

Aunque con signal () se puede llamar al manejador de señales respectivo registrado por el usuario, no es posible realizar ajustes finos como enmascarar las señales que deben bloquearse, modificar el comportamiento de una señal y otras funcionalidades. Esto es posible usando la llamada al sistema sigaction ().

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)

Esta llamada al sistema se utiliza para examinar o cambiar una acción de señal. Si el acto no es nulo, la nueva acción de señal signum se instala desde el acto. Si oldact no es nulo, la acción anterior se guarda en oldact.

La estructura sigaction contiene los siguientes campos:

Field 1 - Handler mencionado en sa_handler o sa_sigaction.

void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);

El manejador de sa_handler especifica la acción a realizar en base al signum y con SIG_DFL indicando acción predeterminada o SIG_IGN para ignorar la señal o puntero a una función de manejo de señal.

El controlador de sa_sigaction especifica el número de señal como primer argumento, el puntero a la estructura siginfo_t como segundo argumento y el puntero al contexto del usuario (consulte getcontext () o setcontext () para obtener más detalles) como tercer argumento.

La estructura siginfo_t contiene información de la señal, como el número de la señal a entregar, el valor de la señal, la identificación del proceso, la identificación del usuario real del proceso de envío, etc.

Field 2 - Conjunto de señales a bloquear.

int sa_mask;

Esta variable especifica la máscara de señales que deben bloquearse durante la ejecución del manejador de señales.

Field 3 - Banderas especiales.

int sa_flags;

Este campo especifica un conjunto de banderas que modifican el comportamiento de la señal.

Field 4 - Restaurar controlador.

void (*sa_restorer) (void);

Esta llamada al sistema devuelve 0 en caso de éxito y -1 en caso de error.

Consideremos algunos programas de muestra.

Primero, comencemos con un programa de muestra, que genera una excepción. En este programa, estamos tratando de realizar una operación de división por cero, lo que hace que el sistema genere una excepción.

/* signal_fpe.c */
#include<stdio.h>

int main() {
   int result;
   int v1, v2;
   v1 = 121;
   v2 = 0;
   result = v1/v2;
   printf("Result of Divide by Zero is %d\n", result);
   return 0;
}

Pasos de compilación y ejecución

Floating point exception (core dumped)

Por lo tanto, cuando intentamos realizar una operación aritmética, el sistema ha generado una excepción de punto flotante con el volcado de núcleo, que es la acción predeterminada de la señal.

Ahora, modifiquemos el código para manejar esta señal en particular usando la llamada al sistema signal ().

/* signal_fpe_handler.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_dividebyzero(int signum);

int main() {
   int result;
   int v1, v2;
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGFPE, handler_dividebyzero);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   v1 = 121;
   v2 = 0;
   result = v1/v2;
   printf("Result of Divide by Zero is %d\n", result);
   return 0;
}

void handler_dividebyzero(int signum) {
   if (signum == SIGFPE) {
      printf("Received SIGFPE, Divide by Zero Exception\n");
      exit (0);
   } 
   else
      printf("Received %d Signal\n", signum);
      return;
}

Pasos de compilación y ejecución

Received SIGFPE, Divide by Zero Exception

Como se discutió, las señales son generadas por el sistema (al realizar ciertas operaciones como dividir por cero, etc.) o el usuario también puede generar la señal mediante programación. Si desea generar una señal mediante programación, utilice la función de biblioteca raise ().

Ahora, tomemos otro programa para demostrar cómo manejar e ignorar la señal.

Supongamos que hemos emitido una señal usando raise (), ¿qué sucede entonces? Después de subir la señal, se detiene la ejecución del proceso actual. Entonces, ¿qué pasa con el proceso detenido? Puede haber dos escenarios: primero, continúe la ejecución cuando sea necesario. En segundo lugar, finalice (con el comando kill) el proceso.

Para continuar la ejecución del proceso detenido, envíe SIGCONT a ese proceso en particular. También puede emitir comandos fg (primer plano) o bg (fondo) para continuar la ejecución. Aquí, los comandos solo reiniciarían la ejecución del último proceso. Si se detiene más de un proceso, solo se reanuda el último proceso. Si desea reanudar el proceso detenido anteriormente, reanude los trabajos (usando fg / bg) junto con el número de trabajo.

El siguiente programa se usa para elevar la señal SIGSTOP usando la función raise (). La señal SIGSTOP también se puede generar presionando la tecla CTRL + Z (Control + Z) por parte del usuario. Después de emitir esta señal, el programa dejará de ejecutarse. Envía la señal (SIGCONT) para continuar la ejecución.

En el siguiente ejemplo, reanudamos el proceso detenido con el comando fg.

/* signal_raising.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

int main() {
   printf("Testing SIGSTOP\n");
   raise(SIGSTOP);
   return 0;
}

Pasos de compilación y ejecución

Testing SIGSTOP
[1]+ Stopped ./a.out
./a.out

Ahora, mejore el programa anterior para continuar la ejecución del proceso detenido emitiendo SIGCONT desde otro terminal.

/* signal_stop_continue.c */
#include<stdio.h>
#include<signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler_sigtstp(int signum);

int main() {
   pid_t pid;
   printf("Testing SIGSTOP\n");
   pid = getpid();
   printf("Open Another Terminal and issue following command\n");
   printf("kill -SIGCONT %d or kill -CONT %d or kill -18 %d\n", pid, pid, pid);
   raise(SIGSTOP);
   printf("Received signal SIGCONT\n");
   return 0;
}

Pasos de compilación y ejecución

Testing SIGSTOP
Open Another Terminal and issue following command
kill -SIGCONT 30379 or kill -CONT 30379 or kill -18 30379
[1]+ Stopped ./a.out

Received signal SIGCONT
[1]+ Done ./a.out

En otra terminal

kill -SIGCONT 30379

Hasta ahora, hemos visto el programa que maneja la señal generada por el sistema. Ahora, veamos la señal generada a través del programa (usando la función raise () o mediante el comando kill). Este programa genera la señal SIGTSTP (terminal stop), cuya acción por defecto es detener la ejecución. Sin embargo, dado que estamos manejando la señal ahora en lugar de la acción predeterminada, llegará al controlador definido. En este caso, solo estamos imprimiendo el mensaje y saliendo.

/* signal_raising_handling.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_sigtstp(int signum);

int main() {
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGTSTP, handler_sigtstp);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   printf("Testing SIGTSTP\n");
   raise(SIGTSTP);
   return 0;
}

void handler_sigtstp(int signum) {
   if (signum == SIGTSTP) {
      printf("Received SIGTSTP\n");
      exit(0);
   }
   else
      printf("Received %d Signal\n", signum);
      return;
}

Pasos de compilación y ejecución

Testing SIGTSTP
Received SIGTSTP

Hemos visto casos de realizar acciones predeterminadas o manejar la señal. Ahora es el momento de ignorar la señal. Aquí, en este programa de muestra, estamos registrando la señal SIGTSTP para ignorar a través de SIG_IGN y luego estamos elevando la señal SIGTSTP (terminal stop). Cuando se genere la señal SIGTSTP, se ignorará.

/* signal_raising_ignoring.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_sigtstp(int signum);

int main() {
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGTSTP, SIG_IGN);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   printf("Testing SIGTSTP\n");
   raise(SIGTSTP);
   printf("Signal SIGTSTP is ignored\n");
   return 0;
}

Pasos de compilación y ejecución

Testing SIGTSTP
Signal SIGTSTP is ignored

Hasta ahora, hemos observado que tenemos un manejador de señales para manejar una señal. ¿Podemos tener un solo controlador para manejar múltiples señales? La respuesta es sí. Consideremos esto con un programa.

El siguiente programa hace lo siguiente:

Step 1 - Registra un controlador (handleSignals) para captar o manejar señales SIGINT (CTRL + C) o SIGQUIT (CTRL + \)

Step 2 - Si el usuario genera la señal SIGQUIT (ya sea mediante el comando kill o el control del teclado con CTRL + \), el controlador simplemente imprime el mensaje como retorno.

Step 3 - Si el usuario genera la señal SIGINT (ya sea mediante el comando kill o el control del teclado con CTRL + C) la primera vez, entonces modifica la señal para realizar la acción predeterminada (con SIG_DFL) la próxima vez.

Step 4 - Si el usuario genera la señal SIGINT por segunda vez, realiza una acción predeterminada, que es la terminación del programa.

/* Filename: sigHandler.c */
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handleSignals(int signum);

int main(void) {
   void (*sigHandlerInterrupt)(int);
   void (*sigHandlerQuit)(int);
   void (*sigHandlerReturn)(int);
   sigHandlerInterrupt = sigHandlerQuit = handleSignals;
   sigHandlerReturn = signal(SIGINT, sigHandlerInterrupt);
   if (sigHandlerReturn == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   sigHandlerReturn = signal(SIGQUIT, sigHandlerQuit);
   
   if (sigHandlerReturn == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   while (1) {
      printf("\nTo terminate this program, perform the following: \n");
      printf("1. Open another terminal\n");
      printf("2. Issue command: kill %d or issue CTRL+C 2 times (second time it terminates)\n", getpid());
      sleep(10);
   }
   return 0;
}

void handleSignals(int signum) {
   switch(signum) {
      case SIGINT:
      printf("\nYou pressed CTRL+C \n");
      printf("Now reverting SIGINT signal to default action\n");
      signal(SIGINT, SIG_DFL);
      break;
      case SIGQUIT:
      printf("\nYou pressed CTRL+\\ \n");
      break;
      default:
      printf("\nReceived signal number %d\n", signum);
      break;
   }
   return;
}

Pasos de compilación y ejecución

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates)
^C
You pressed CTRL+C
Now reverting SIGINT signal to default action
          
To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates)
^\You pressed CTRL+\
To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 120
Terminated

Otra terminal

kill 71

Segundo método

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates)
^C
You pressed CTRL+C
Now reverting SIGINT signal to default action

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates)
^C

Sabemos que para manejar una señal, tenemos dos llamadas al sistema, es decir, señal () o sigaction (). Hasta ahora hemos visto con la llamada al sistema signal (), ahora es el momento de llamar al sistema sigaction (). Modifiquemos el programa anterior para que funcione con sigaction () de la siguiente manera:

/* Filename: sigHandlerSigAction.c */
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handleSignals(int signum);

int main(void) {
   void (*sigHandlerReturn)(int);
   struct sigaction mysigaction;
   mysigaction.sa_handler = handleSignals;
   sigemptyset(&mysigaction.sa_mask);
   mysigaction.sa_flags = 0;
   sigaction(SIGINT, &mysigaction, NULL);
   
   if (mysigaction.sa_handler == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   mysigaction.sa_handler = handleSignals;
   sigemptyset(&mysigaction.sa_mask);
   mysigaction.sa_flags = 0;
   sigaction(SIGQUIT, &mysigaction, NULL);
   
   if (mysigaction.sa_handler == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   while (-1) {
      printf("\nTo terminate this program, perform either of the following: \n");
      printf("1. Open another terminal and issue command: kill %d\n", getpid());
      printf("2. Issue CTRL+C 2 times (second time it terminates)\n");
      sleep(10);
   }
   return 0;
}

void handleSignals(int signum) {
   switch(signum) {
      case SIGINT:
      printf("\nYou have entered CTRL+C \n");
      printf("Now reverting SIGINT signal to perform default action\n");
      signal(SIGINT, SIG_DFL);
      break;
      case SIGQUIT:
      printf("\nYou have entered CTRL+\\ \n");
      break;
      default:
      printf("\nReceived signal number %d\n", signum);
      break;
   }
   return;
}

Veamos el proceso de compilación y ejecución. En el proceso de ejecución, veamos emitir CTRL + C dos veces, comprobaciones / formas restantes (como arriba) que también puede probar para este programa.

Pasos de compilación y ejecución

To terminate this program, perform either of the following:
1. Open another terminal and issue command: kill 3199
2. Issue CTRL+C 2 times (second time it terminates)
^C
You have entered CTRL+C
Now reverting SIGINT signal to perform default action
To terminate this program, perform either of the following:
1. Open another terminal and issue command: kill 3199
2. Issue CTRL+C 2 times (second time it terminates)
^C