Segnali

Segnali

Forma molto semplice di comunicazione tra processi.

Tecnicamente sono interruzioni software causate da svariati eventi:

Gestione dei segnali

Cosa possiamo fare quando arriva un segnale?

Segnali POSIX

Segnali POSIX (Portable Operating System Interface) da man 7 signal

Signal     Value     Action   Comment
-----------------------------------------------------------------------
SIGHUP        1       Term    Hangup detected on controlling terminal
                              or death of controlling process
SIGINT        2       Term    Interrupt from keyboard     <== ctrl-C
SIGQUIT       3       Core    Quit from keyboard          <== ctrl-\
SIGILL        4       Core    Illegal Instruction
SIGABRT       6       Core    Abort signal from abort(3)
SIGFPE        8       Core    Floating point exception
SIGKILL       9       Term    Kill signal                 <== kill -9 (da shell)
SIGSEGV      11       Core    Invalid memory reference
SIGPIPE      13       Term    Broken pipe: write to pipe with no readers
SIGALRM      14       Term    Timer signal from alarm(2)
SIGTERM      15       Term    Termination signal          <== kill (da shell)
SIGUSR1   30,10,16    Term    User-defined signal 1
SIGUSR2   31,12,17    Term    User-defined signal 2
SIGCHLD   20,17,18    Ign     Child stopped or terminated <== gestito da wait()
SIGCONT   19,18,25    Cont    Continue if stopped
SIGSTOP   17,19,23    Stop    Stop process
SIGTSTP   18,20,24    Stop    Stop typed at tty
SIGTTIN   21,21,26    Stop    tty input for background process
SIGTTOU   22,22,27    Stop    tty output for background process

The  signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.

Azioni possibili

The  entries  in  the  "Action"  column of the tables below specify the
default disposition for each signal, as follows:

Term   Default action is to terminate the process.

Ign    Default action is to ignore the signal.

Core   Default action is to terminate the process and dump core (see core(5)).

Stop   Default action is to stop the process.

Cont   Default action is to continue the process if it is currently stopped.

Un esempio semplice: alarm

alarm manda un SIGALRM dopo n secondi (serve per dare un timeout). Il default handler termina il programma.

La shell analizza lo stato di uscita del programma e stampa il motivo della terminazione (“alarm clock”).

#include <unistd.h>
int main()
{
	alarm(3);
	while(1){}
}

Il programma “punta una sveglia” dopo 3 secondi e attende in un ciclo infinito.

Impostare il gestore dei segnali

Tramite la system call signal è possibile cambiare il gestore dei segnali. La system call prende come parametri un segnale e una funzione che da quel momento diventerà il nuovo gestore del segnale.

void alarmHandler()
{
	printf("questo me lo gestisco io!\n");
	alarm(3); // ri-setta il timer a 3 secondi
}
 
int main() { 
	signal(SIGALRM, alarmHandler);
	alarm(3);
	while(1){}
}

Parametri particolari e valore di ritorno

È possibile passare alla signal le costanti SIG_IGN o SIG_DFL al posto della funzione handler per indicare, rispettivamente:

Il valore di ritorno di signal è

Esempio: Proteggersi da ctrl-c

Se modifichiamo il gestore del segnale SIGINT possiamo evitare che un programma venga interrotto tramite ctrl-c

int main() {
	void (*old)(int);
        
	old = signal(SIGINT,SIG_IGN);
	printf("Sono protetto!\n");
	sleep(3);
        
	signal(SIGINT,old);
	printf("Non sono più protetto!\n");
	sleep(3);
}

Notare l’uso del valore di ritorno della signal per reimpostare il gestore originale.

System call kill

La chiamata a sistema kill manda un segnale a un processo.

int main(){
	pid_t pid1,pid2;
	pid1 = fork();
	if ( pid1 < 0 ) {
		perror("errore fork"); exit(EXIT_FAILURE); 
	} else if (pid1 == 0)
		while(1) { // primo figlio
			printf("%d è vivo !\n",getpid());
			sleep(1); }
	pid2 = fork();
	if ( pid2 < 0 ) { 
		perror("errore fork"); exit(EXIT_FAILURE); 
	} else if (pid2 == 0)
		while(1) { // secondo figlio
			printf("%d è vivo !\n",getpid());
			sleep(1); }
	// processo genitore
	sleep(2);
	kill(pid1,SIGSTOP); // sospende il primo figlio
	sleep(5);
	kill(pid1,SIGCONT); // risveglia il primo figlio
	sleep(2);
	kill(pid1,SIGINT); // termina il primo figlio
	kill(pid2,SIGINT); // termina il secondo figlio       
}

Mascherare i segnali

A volte risulta utile bloccare temporaneamente la ricezione dei segnali per poi riattivarli. Tali segnali non sono ignorati ma solamente ‘posticipati’.

sigprocmask(int action, sigset_t *newmask, sigset_t *oldmask), dove action vale:

Gestione delle maschere

Per gestire gli insiemi di segnali (di tipo sigset_t) si utilizzano:

NOTA: POSIX non specifica se più occorrenze dello stesso segnale debbano essere memorizzate (accodate) oppure no. Tipicamente se più segnali uguali vengono generati, solamente uno verrà “recapitato” quando il blocco viene tolto.

Mascherare SIGINT (ctrl-c)

int main() {
	sigset_t newmask,oldmask;

	sigemptyset(&newmask);			// insieme vuoto
	sigaddset(&newmask, SIGINT);	// aggiunge SIGINT alla "maschera"
	// setta la nuova maschera e memorizza la vecchia
	if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0) {
		perror("errore settaggio maschera"); exit(1);
	}
	printf("Sono protetto!\n");
	sleep(3);
        
	// reimposta la vecchia maschera
	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0) {
		perror("errore settaggio maschera"); exit(1); 
	}
	printf("Non sono piu' protetto!\n");
	sleep(3);
}

Se digitiamo ctrl-c mentre il segnale è mascherato esso viene sospeso. Appena la maschera viene rimossa, il segnale è ricevuto dal processo che viene immediatamente interrotto.

NOTA: Non stiamo modificando il gestore del segnale.

Interferenze e funzioni ‘safe’ POSIX

L’uso della printf nell’handler è rischioso perchè usa dati globali: Se anche il programma interrotto stava facendo I/O i due potrebbero interferire!

Facendo man signal-safety (in sistemi Linux recenti), troviamo la lista di funzioni safe:

       Function               Notes
       abort(3)               Added in POSIX.1-2003
       accept(2)
       access(2)
       aio_error(3)
       aio_return(3)
       aio_suspend(3)         See notes below
       alarm(2)
       bind(2)
       ... (molte altre) ...

Usare solo funzioni safe nel handler e assicurarsi che non modifichi variabili globali del programma

oppure

Mascherare i segnali ogni volta che si usa una funzione unsafe nel programma (infattibile in generale)

Esempio di interferenza sulla printf

void alarmHandler();	// gestore
static int i=0;		// contatore globale `volatile'

int main() { 
	signal(SIGALRM, alarmHandler);
	alarm(1);
	while(1){
	        printf("prova\n");
	}
}
void alarmHandler()
{
	printf("questo me lo gestisco io %d!\n",i++);
	alarm(1); 	// ri-setta il timer a 1 secondo
}

NOTA: eseguire il comando con in coda | grep io per evitare di osservare tutte le stampe “prova”.

Esercizi

Esercizio 1

Provare a mascherare il segnale nel main prima e dopo la printf.
Elimina le interferenze ma estremamente poco pratico perché richiede di proteggere tutte le occorrenze di printf nel main

Esercizio 2

Utilizzare la write (safe) per fare la stampa dentro il gestore e verificare che non ci sono interferenze. Questa soluzione non richiede nessuna modifica del main, ma limita le funzioni utilizzabili nei gestori dei segnali solamente a quelle safe

NOTA: è necessario usare uno degli approcci sopra per evitare interferenze, usualmente il secondo perché più pratico!