Segnali e Comunicazione tra Processi in Linux

Fino a qualche anno fa i problemi sulla comunicazione e sincronizzazione tra processi, IPC ( Inter Process Communication ), erano grandi e complessi. L’avvento dei sistemi operativi realmente multitasking per i personal computer ha cambiato le cose. In questo articolo vedremo i tipi di comunicazione tra processi soffermandoci sull’invio dei segnali in ambiente Linux.
Esistono due modi di comunicazione tra processi, basati su due principi diversi ma ugualmente potenti:

  • scambio di messaggi – due o più processi stabiliscono una comunicazione con l’invio e la ricezione di sequenze arbitrarie di byte, i messaggi appunto. I messaggi sono organizzati in una o più code e sono proprio i processi a decidere la coda da utilizzare per mettersi in ascolto. Spesso è anche possibile contrassegnare un messaggio con un numero particolare che ne identifichi il tipo e la priorità. Normalmente si utilizza la FIFO ( First In First Out ), cioè il primo messaggio inserito sarà quello ad essere letto per primo.
  • memoria condivisa – usando questo metodo è possibile leggere o scrivere una locazione arbitraria di memoria che più processi possono gestire, magari mappata in un indirizzo differente per ogni processo. Se più processi leggono o scrivono su una porzione di memoria comune, è importante stabilire un protocollo di sincronizzazione che garantisca la coerenza dei dati.

Grazie alla lunghissima storia di UNIX che parte dalla fine degli anni ’60 esistono molti sistemi IPC, ogni meccanismo però può essere ricondotto all’invio di messaggi o scambi di porzioni di memoria. Una delle possibilità più grezze e meno versatili è quella dei segnali che consentono l’invio di messaggi ad un processo con un numero identificativo che rappresenta il comando. Esistono poi le pipe, poi vennero i socket ( diversi da quelli INET che servono per comunicazioni di rete, qui servono per la comunicazione tra processi della stessa macchina ), i semafori per la sincronizzazione, la memoria condivisa e le code di messaggi. In questo articolo vedremo i segnali, facendo al termine un piccolo esempio di programmazione.
Un segnale è un evento contraddistinto da un numero che viene recapitato ad un processo che può gestirlo in vari modi:

  • ignorandolo e proseguendo l’elaborazione come se nulla fosse accaduto
  • terminando l’esecuzione
  • richiamando una certa funzione

Il meccanismo dei segnali può essere grossolanamente classificato in uno scambio di messaggi, ma talmente primitivo che non c’è modo di conoscere chi ha inviato il segnale. All’inizio i segnali furono inventati per dare al kernel la possibilità di indicare qualcosa ad un processo. Il segnale ha un numero identificativo che ne indica il comando da effettuare, vediamo i principali:

  • SIGHUP ( 1 ) – Hangup – Il processo riceve questo segnale quando il terminale a cui era associato viene chiuso o scollegato, spesso molti server rileggono i propri file di configurazione all’arrivo di questo segnale.
  • SIGINT ( 2 ) – Interrupt – Ricevuto da un processo quando l’utente preme la combinazione di tasti di interrupt, di solito Ctrl+C.
  • SIGQUIT ( 3 ) – Quit – Simile al precedente, ma in più il processo genera un core dump, ovvero un file che contiene lo stato della memoria nel momento in cui il segnale è stato ricevuto, di norma la combinazione di tasti che lo genera è Ctrl+\.
  • SIGILL ( 4 ) – Illegal instruction – Il processo ha tentato di eseguire un’istruzione proibita o inesistente.
  • SIGKILL ( 9 ) – Kill – Questo segnale non può essere intercettato dal processo ricevente che non può fare altro che terminare. Il modo più sicuro e brutale di uccidere un processo.
  • SIGSEGV ( 11 ) – Segmentation violation – Generato quando il processo tenta di accedere ad un indirizzo di memoria al di fuori del proprio spazio.
  • SIGTERM ( 15 ) – Termination – Inviato ad un processo per chiedergli educatamente di terminare. Possibile ignorare questo segnale da parte del processo.
  • SIGUSR1, SIGUSR2 ( 10, 12 ) – User defined – Non hanno un significato preciso e possono essere utilizzati dai processi utente per implementare un rudimentale protocollo di comunicazione.
  • SIGCHLD ( 17 ) – Child death – Inviato ad un processo quando uno dei suoi figli termina.

Questi valori dipendono dal sistema operativo e dall’architettura della macchina, per leggerli occorre aprire il file /usr/include/linux/signal.h che sul mio x86 in Ubuntu 10.04 risiede in /usr/include/asm/signal.h; altro modo di studiare questi valori è dare il comando man 7 signal.
Per mandare un segnale ad un processo si può utilizzare sia un programma eseguibile che una chiamata di sistema, entrambi si chiamano kill, ma la loro sintassi è diversa. La sintassi per il programma eseguibile è:

kill -segnale numProcesso

il primo argomento è il numero del segnale da inviare, basta eliminare SIG dal nome della costante; quindi per inviare un hangup al processo 1500 basta scrivere:

kill -1 1500

o

kill -HUP 1500

per sapere l’identificativo del processo basta dare il comando ps ( vedere l’articolo sulla gestione dei processi in Linux ). Esiste poi la variante killall con la quale si usa il nome del processo da terminare come in questo esempio:

killall -HUP inetd

UNIX autorizza l’invio dei segnali solo ai processi di cui si è proprietari, l’utente root può inviarli a tutti.
I segnali sono gestiti in modo asincrono, cioè si predispone una funzione che viene chiamata quando arriva un certo segnale, ma non si sa a priori quando questa funzione sarà eseguita. Il gestore del segnale è chiamato handler ed è una funzione che riceve un parametro int e non restituisce nulla; il valore dell’argomento è il numero del segnale. Per indicare la funzione handler si utilizza una funzione dichiarata in signal.h che si chiama appunto signal, vediamone il prototipo:

func* signal(int numSegnale, func* gestore)

Al posto di gestore possiamo indicare SIG_IGN che indica che si vuole ignorare il segnale o SIG_DFL che indica che si vuole ripristinare l’azione di default. Vediamo ora un piccolo esempio per mettere in pratica quello che abbiamo appreso in questo articolo:

#include “stdio.h”
#include “signal.h”
#include “unistd.h”
#include “stdlib.h”

void Uscita(int iSegnale)
{
      printf(“Va bene esco perché sei gentile!\n”);
      exit(0);
}

void Seccato(int iSegnale)
{
      printf(“\nEsco, però sei molto sgarbato!\n”);
  &n
bsp;   exit(0);

}

int main(int argc, char** argv)
{
      signal(SIGTERM, Uscita);
      signal(SIGINT, Seccato);

      printf(“Che bello essere in esecuzione con PID %d\n”, getpid());
      scanf(“Per fermare il programma”);

      return 0;
}

Per compilare il programma date il seguente comando:

gcc -Wall -o signal signal.c

ovviamente con il nome del vostro file sorgente. Ora eseguite l’applicazione e terminatela con Ctrl+C, vi verrà scritto un messaggio sul terminale per il segnale SIGINT. Eseguite nuovamente l’applicazione ed aprite un’altra finestra console, qui date il comando:

kill -SIGTERM PID

il PID lo ricevete quando eseguite il programma.
In questo articolo abbiamo parlato dei segnali, forse in futuro proporrò anche altri articoli su pipe, socket o memoria condivisa, ma dovrete essere voi che mi dovete dire se vi piacerebbe approfondire l’argomento sulle comunicazioni tra processi in Linux, potete scrivere un commento all’articolo o iscrivendovi al forum.

Informazioni su Giampaolo Rossi

Sviluppatore di software gestionale da oltre 28 anni.
Questa voce è stata pubblicata in Linux. Contrassegna il permalink.