Comunicazione tra processi - Segnali

UN signalè una notifica a un processo che indica il verificarsi di un evento. Viene anche chiamato il segnalesoftware interrupt e non è prevedibile conoscerne l'occorrenza, quindi è anche chiamato un asynchronous event.

Il segnale può essere specificato con un numero o un nome, di solito i nomi dei segnali iniziano con SIG. I segnali disponibili possono essere controllati con il comando kill –l (l per Elenco dei nomi dei segnali), che è il seguente:

Ogni volta che viene generato un segnale (sia a livello di codice o segnale generato dal sistema), viene eseguita un'azione predefinita. Cosa succede se non si desidera eseguire l'azione predefinita ma si desidera eseguire le proprie azioni alla ricezione del segnale? È possibile per tutti i segnali? Sì, è possibile gestire il segnale ma non per tutti i segnali. E se vuoi ignorare i segnali, è possibile? Sì, è possibile ignorare il segnale. Ignorare il segnale non implica né l'esecuzione dell'azione predefinita né la gestione del segnale. È possibile ignorare o gestire quasi tutti i segnali. I segnali che non possono essere ignorati o gestiti / catturati sono SIGSTOP e SIGKILL.

In sintesi, le azioni eseguite per i segnali sono le seguenti:

  • Azione predefinita
  • Gestisci il segnale
  • Ignora il segnale

Come discusso, il segnale può essere gestito alterando l'esecuzione dell'azione predefinita. La gestione del segnale può essere eseguita in uno dei due modi, ovvero tramite chiamate di sistema, signal () e sigaction ().

#include <signal.h>

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

La chiamata di sistema signal () chiamerebbe il gestore registrato dopo la generazione del segnale come menzionato in signum. Il gestore può essere uno dei SIG_IGN (Ignorare il segnale), SIG_DFL (Ripristino del segnale sul meccanismo predefinito) o un gestore di segnali o indirizzo funzione definito dall'utente.

Questa chiamata di sistema in caso di successo restituisce l'indirizzo di una funzione che accetta un argomento intero e non ha alcun valore di ritorno. Questa chiamata restituisce SIG_ERR in caso di errore.

Sebbene con signal () sia possibile chiamare il rispettivo gestore di segnali registrato dall'utente, non è possibile eseguire una regolazione fine come mascherare i segnali che dovrebbero essere bloccati, modificare il comportamento di un segnale e altre funzionalità. Ciò è possibile utilizzando la chiamata di sistema sigaction ().

#include <signal.h>

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

Questa chiamata di sistema viene utilizzata per esaminare o modificare un'azione del segnale. Se l'atto non è nullo, la nuova azione per signal signum viene installata dall'atto. Se oldact non è nullo, l'azione precedente viene salvata in oldact.

La struttura sigaction contiene i seguenti campi:

Field 1 - Gestore menzionato in sa_handler o sa_sigaction.

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

Il gestore per sa_handler specifica l'azione da eseguire in base al signum e con SIG_DFL che indica l'azione predefinita o SIG_IGN per ignorare il segnale o il puntatore a una funzione di gestione del segnale.

Il gestore di sa_sigaction specifica il numero del segnale come primo argomento, il puntatore alla struttura siginfo_t come secondo argomento e il puntatore al contesto dell'utente (controllare getcontext () o setcontext () per ulteriori dettagli) come terzo argomento.

La struttura siginfo_t contiene informazioni sul segnale come il numero del segnale da fornire, il valore del segnale, l'ID del processo, l'ID utente reale del processo di invio, ecc.

Field 2 - Set di segnali da bloccare.

int sa_mask;

Questa variabile specifica la maschera dei segnali che dovrebbero essere bloccati durante l'esecuzione del signal handler.

Field 3 - Bandiere speciali.

int sa_flags;

Questo campo specifica una serie di flag che modificano il comportamento del segnale.

Field 4 - Ripristina gestore.

void (*sa_restorer) (void);

Questa chiamata di sistema restituisce 0 in caso di successo e -1 in caso di fallimento.

Consideriamo alcuni programmi di esempio.

Innanzitutto, iniziamo con un programma di esempio, che genera un'eccezione. In questo programma, stiamo cercando di eseguire un'operazione di divisione per zero, che fa sì che il sistema generi un'eccezione.

/* 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;
}

Fasi di compilazione ed esecuzione

Floating point exception (core dumped)

Pertanto, quando stiamo cercando di eseguire un'operazione aritmetica, il sistema ha generato un'eccezione in virgola mobile con core dump, che è l'azione predefinita del segnale.

Ora, modifichiamo il codice per gestire questo particolare segnale usando la chiamata di 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;
}

Fasi di compilazione ed esecuzione

Received SIGFPE, Divide by Zero Exception

Come discusso, i segnali vengono generati dal sistema (eseguendo determinate operazioni come la divisione per zero, ecc.) Oppure l'utente può anche generare il segnale in modo programmatico. Se vuoi generare il segnale in modo programmatico, usa la funzione di libreria raise ().

Ora, prendiamo un altro programma per dimostrare come maneggiare e ignorare il segnale.

Supponiamo di aver alzato un segnale usando raise (), cosa succede allora? Dopo aver alzato il segnale, l'esecuzione del processo in corso viene interrotta. Allora cosa succede al processo interrotto? Ci possono essere due scenari: in primo luogo, continuare l'esecuzione quando necessario. Secondo, termina (con il comando kill) il processo.

Per continuare l'esecuzione del processo interrotto, inviare SIGCONT a quel particolare processo. Puoi anche emettere i comandi fg (in primo piano) o bg (in background) per continuare l'esecuzione. Qui, i comandi riavvierebbero solo l'esecuzione dell'ultimo processo. Se più di un processo viene interrotto, viene ripreso solo l'ultimo processo. Se si desidera riprendere il processo precedentemente interrotto, riprendere i lavori (utilizzando fg / bg) insieme al numero del lavoro.

Il seguente programma viene utilizzato per aumentare il segnale SIGSTOP utilizzando la funzione raise (). Il segnale SIGSTOP può essere generato anche dalla pressione dell'utente del tasto CTRL + Z (Control + Z). Dopo aver emesso questo segnale, il programma interromperà l'esecuzione. Inviare il segnale (SIGCONT) per continuare l'esecuzione.

Nell'esempio seguente, riprendiamo il processo interrotto con il comando fg.

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

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

Fasi di compilazione ed esecuzione

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

Ora, migliora il programma precedente per continuare l'esecuzione del processo interrotto emettendo SIGCONT da un altro terminale.

/* 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;
}

Fasi di compilazione ed esecuzione

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

In un altro terminal

kill -SIGCONT 30379

Finora abbiamo visto il programma che gestisce il segnale generato dal sistema. Vediamo ora il segnale generato tramite il programma (usando la funzione raise () o tramite il comando kill). Questo programma genera il segnale SIGTSTP (terminal stop), la cui azione predefinita è di interrompere l'esecuzione. Tuttavia, poiché ora stiamo gestendo il segnale invece dell'azione predefinita, arriverà al gestore definito. In questo caso, stiamo solo stampando il messaggio e uscendo.

/* 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;
}

Fasi di compilazione ed esecuzione

Testing SIGTSTP
Received SIGTSTP

Abbiamo visto casi di esecuzione di un'azione predefinita o di gestione del segnale. Ora è il momento di ignorare il segnale. Qui, in questo programma di esempio, stiamo registrando il segnale SIGTSTP da ignorare tramite SIG_IGN e quindi stiamo alzando il segnale SIGTSTP (terminal stop). Quando viene generato il segnale SIGTSTP, questo verrà ignorato.

/* 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;
}

Fasi di compilazione ed esecuzione

Testing SIGTSTP
Signal SIGTSTP is ignored

Finora, abbiamo osservato che abbiamo un gestore di segnali per gestire un segnale. Possiamo avere un unico gestore per gestire più segnali? La risposta è si. Consideriamolo con un programma.

Il seguente programma esegue le seguenti operazioni:

Step 1 - Registra un gestore (handleSignals) per catturare o gestire i segnali SIGINT (CTRL + C) o SIGQUIT (CTRL + \)

Step 2 - Se l'utente genera il segnale SIGQUIT (tramite il comando kill o il controllo da tastiera con CTRL + \), il gestore stampa semplicemente il messaggio come return.

Step 3 - Se l'utente genera il segnale SIGINT (tramite comando kill o controllo da tastiera con CTRL + C) la prima volta, allora modifica il segnale per eseguire l'azione predefinita (con SIG_DFL) dalla volta successiva.

Step 4 - Se l'utente genera il segnale SIGINT una seconda volta, esegue un'azione predefinita, che è la fine del programma.

/* 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;
}

Fasi di compilazione ed esecuzione

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

Un altro terminale

kill 71

Secondo metodo

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

Sappiamo che per gestire un segnale, abbiamo due chiamate di sistema, ovvero signal () o sigaction (). Finora abbiamo visto con la chiamata di sistema signal (), ora è il momento per la chiamata di sistema sigaction (). Modifichiamo il programma sopra per eseguire utilizzando sigaction () come segue:

/* 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;
}

Vediamo il processo di compilazione ed esecuzione. Nel processo di esecuzione, vediamo il problema CTRL + C due volte, i controlli / modi rimanenti (come sopra) puoi provare anche per questo programma.

Fasi di compilazione ed esecuzione

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