I segnali costituiscono una forma (molto semplice) di comunicazione tra processi: Tecnicamente sono interruzioni software causate da svariati eventi:
- Generati da terminale. Ad esempio
ctrl-c
(SIGINT
); - Eccezioni dovute ad errori in esecuzione: es. divisione per 0, riferimento “sbagliato” in memoria, ecc…
- Segnali esplicitamente inviati da un processo all’altro;
- Eventi asincroni che vengono notificati ai processi: esempio
SIGALARM
.
Cosa possiamo fare quando arriva un segnale?
- Ignorarlo
- Gestirlo
- Lasciare il compito al gestore di sistema
Vediamo alcuni segnali POSIX (Portable Operating System Interface) supportati in Linux. La lista qui sotto è contenuta in man 7 signal
First the signals described in the original POSIX.1-1990 standard.
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.
Descriviamo brevemente alcuni dei segnali:
SIGHUP
viene inviato a un processo quando il terminale a cui è associato viene chiusoSIGINT
viene inviato a un processo digitandoctrl-C
per interromperloSIGQUIT
, meno conosciuto del precedente, viene inviato per far terminare un processo tramite la sequenzactrl-\
SIGILL
,SIGABRT
,SIGFPE
eSIGSEGV
corrispondono ad azioni del processo stesso: istruzione illegale, invocazione diabort
, errore aritmetico, riferimento a memoria illegaleSIGKILL
,SIGTERM
sono esempi di segnale inviati da un processo a un altro, che vengono utilizzati per terminare un processo. Questi segnali si possono inviare dalla shell tramite i comandokill -9 pid
oppurekill pid
. Si noti cheSIGKILL
termina sempre il processo che riceve il segnale mentreSIGTERM
può essere gestito o ignorato dal processo (come discuteremo tra poco)SIGPIPE
viene inviato a un processo che prova a scrivere su una pipe che non ha lettori (vedremo in dettaglio nella prossima lezione)SIGALRM
è un timer che “sveglia” un processo, simile al timer usato dallo scheduler per implementare il time-sharingSIGCHLD
viene inviato a un processo quando un suo processo figlio termina (il segnale viene utilizzato per svegliare un processo che ha invocato lawait
, altrimenti viene ingnorato)
Azioni possibili quando un processo riceve un segnale:
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. Il default handler termina il programma (serve per dare un timeout). La shell analizza lo stato di uscita del programma e stampa il motivo della terminazione (“alarm clock”).
Vediamo un semplice esempio di programma che setta l’allarme dopo 3 secondi e poi va in loop infinito:
#include <unistd.h> int main() { alarm(3); while(1){} }
Il programma “punta una sveglia” dopo 3 secondi e attende in un ciclo infinito
Dopo 3 secondi il processo termina (viene anche scritto a terminale il motivo della terminazione in quanto la shell legge e interpreta il valore di ritorno del programma).
$ ./alarm
<... dopo 3 secondi....>
Alarm clock
$
Impostare il gestore dei segnali tramite signal
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. Vediamo un semplice esempio:
#include <stdio.h> #include <signal.h> #include <unistd.h> 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){} }
Dopo tre secondi arriva il segnale SIGALRM
. Viene invocato alarmHandler
che stampa a video una frase e reimposta l’allarme dopo 3 secondi. Il controllo ritorna al punto in cui il programma è stato interrotto (nel ciclo while quindi) e il programma attende altri 3 secondi che arrivi il successivo segnale:
questo me lo gestisco io! <=== dopo 3 secondi
questo me lo gestisco io! <=== dopo 3 secondi
questo me lo gestisco io! <=== dopo 3 secondi
........
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:
- che il segnale va ignorato
- che l’handler è quello di default di sistema
Il valore di ritorno di signal
è
SIG_ERR
in caso di errore- l’handler precedente, in caso di successo
NOTA: sigaction
rimpiazza signal
con un’implementazione più stabile nelle varie versioni UNIX. Viene raccomandata se si vuole portabilità. Ad esempio, la signal
originale UNIX, e la sua versione System V, fa il reset del gestore a SIG_DFL
ogni volta che viene ricevuto il segnale. In Linux, si ottiene questo comportamento particolare compilando con l’opzione --ansi
. Per l’uso di sigaction
si faccia riferimento al manuale.
Esempio: Proteggersi da ctrl-c
Vediamo ora un altro esempio di gestione dei segnali. Se modifichiamo il gestore del segnale SIGINT
possiamo evitare che un programma venga interrotto tramite ctrl-c
da terminale.
#include <signal.h> #include <stdio.h> #include <unistd.h> 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. La signal, quando va a buon fine, ritorna il gestore precedente del segnale, che salviamo nella variabile old
. Quando vogliamo reimpostare tale gestore è sufficiente passare old
come secondo parametro a signal
.
Se eseguiamo il programma possiamo osservare che per 3 secondi ctrl-c
non ha alcun effetto. Appena viene reimpostato il vecchio gestore, invece, ctrl-c
interrompe il programma.
$ ./ctrlc
Sono protetto!
<ctrl-c>
<ctrl-c>
<ctrl-c> <==== nessun effetto
Non sono più protetto!
<ctrl-c> <==== esce!
Sospensione e ripristino di processi tramite kill
La chiamata a sistema kill
manda un segnale a un processo.
In questo esempio mostriamo come la chiamata a sistema kill
possa essere utilizzata per sospendere, ripristinare e terminare un processo.
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> 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 }
Quando eseguiamo il programma notiamo che il primo figlio viene sospeso per 5 secondi e che alla fine entrambi i processi figli sono terminati:
$ ./kill
6720 è vivo !
6721 è vivo !
6720 è vivo !
6721 è vivo !
6720 è vivo !
6721 è vivo ! <==== sospende 6720
6721 è vivo !
6721 è vivo !
6721 è vivo !
6721 è vivo !
6720 è vivo ! <==== risveglia 6720 6721 è vivo ! 6720 è vivo ! 6721 è vivo ! >
Mascherare i segnali
A volte risulta utile bloccare temporaneamente la ricezione dei segnali per poi riattivarli. Tali segnali non sono ignorati ma solamente ‘posticipati’.
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.
La chiamata a sistema sigprocmask(int action, sigset_t *newmask, sigset_t *oldmask)
compie azioni differenti a seconda del valore del primo parametro action
:
SIG_BLOCK
: l’insieme dei segnalinewmask
viene unito all’insieme dei segnali attualmente bloccati, che sono restituiti inoldmask
;SIG_UNBLOCK
: l’insieme dei segnalinewmask
viene sottratto dai segnali attualmente bloccati, sempre restituiti inoldmask
;SIG_SETMASK
: l’insieme dei segnalinewmask
sostituisce quello dei segnali attualmente bloccati (oldmask
).
Per gestire gli insiemi di segnali (di tipo sigset_t
) si utilizzano:
sigemptyset(sigset_t *set)
che inizializza l’insiemeset
all’insieme vuotosigaddset(sigset_t *set, int signum)
che aggiunge il segnalesignum
all’insieme set
L’esempio mostra come bloccare SIGINT
e poi ripristinarlo.
#include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> 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. Non stiamo quindi modificando il gestore del segnale ma solo sospendendone la ricezione per un periodo di tempo.
$ a.out
Sono protetto!
<ctrl-c>
<ctrl-c>
<ctrl-c> <==== per 3 secondi nessun effetto
<==== esce appena la maschera viene tolta (senza dare ulteriori ctrl-c)!
Tramite sigpending
è possibile ottenere l’insieme dei segnali “pendenti” (vedere il manuale per maggiori informazioni)
NOTA: sigprocmask
non aderisce allo standard ANSI (ISO C99) ma solamente allo standard POSIX. La gestione dei segnali da parte di ANSI-C è molto povera e si riduce solamente a signal
, raise(sig)
, che equivale a kill(getpid(),sig)
, e abort()
, cioè “abnormal termination”. ANSI-C non prevede multi-processing e quindi non prevede l’invio di segnali ad altri processi (sezione 7.14.2.1).
Attendere un segnale tramite pause
Negli esempi abbiamo sempre usato while(1){}
per attendere un segnale (busy-waiting). La system call pause()
attende un segnale senza consumare tempo di CPU. Vediamo un esempio:
#include <signal.h> #include <stdio.h> #include <unistd.h> void alarmHandler() { printf("questo me lo gestisco io!\n"); } int main() { signal(SIGALRM, alarmHandler); alarm(3); pause(); printf("termino!\n"); }
Interferenze e funzioni ‘safe’ POSIX
L’uso della printf
nell’handler è rischioso perché usa strutture dati condivise dai diversi processi: Se anche il programma interrotto stava facendo I/O i due potrebbero interferire! printf non è “safe”.
Facendo man 7 signal
(in sistemi Linux recenti), troviamo la lista di funzioni safe che sicuramente non creano problemi di interferenza:
_Exit(), _exit(), abort(), accept(), access(), aio_error(), aio-
_return(), aio_suspend(), alarm(), bind(), cfgetispeed(), cfget-
ospeed(), cfsetispeed(), cfsetospeed(), chdir(), chmod(), chown(),
clock_gettime(), close(), connect(), creat(), dup(), dup2(), execle(),
execve(), fchmod(), fchown(), fcntl(), fdatasync(), fork(), fpath-
conf(), fstat(), fsync(), ftruncate(), getegid(), geteuid(), getgid(),
getgroups(), getpeername(), getpgrp(), getpid(), getppid(), get-
sockname(), getsockopt(), getuid(), kill(), link(), listen(), lseek(),
lstat(), mkdir(), mkfifo(), open(), pathconf(), pause(), pipe(),
poll(), posix_trace_event(), pselect(), raise(), read(), readlink(),
recv(), recvfrom(), recvmsg(), rename(), rmdir(), select(), sem_post(),
send(), sendmsg(), sendto(), setgid(), setpgid(), setsid(), setsock-
opt(), setuid(), shutdown(), sigaction(), sigaddset(), sigdelset(),
sigemptyset(), sigfillset(), sigismember(), signal(), sigpause(), sig-
pending(), sigprocmask(), sigqueue(), sigset(), sigsuspend(), sleep(),
socket(), socketpair(), stat(), symlink(), sysconf(), tcdrain(),
tcflow(), tcflush(), tcgetattr(), tcgetpgrp(), tcsendbreak(), tcset-
attr(), tcsetpgrp(), time(), timer_getoverrun(), timer_gettime(),
timer_settime(), times(), umask(), uname(), unlink(), utime(), wait(),
waitpid(), write().
Il seguente esempio cerca di far interferire le printf
aggiungendo nel ciclo while la stampa di una stringa. Se il programma viene interrotto proprio durante la stampa, la printf
del gestore potrebbe interferire con quella del programma.
#include <signal.h> #include <stdio.h> #include <unistd.h> 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 }
Tipicamente le stampe sono troncate o mischiate. In alcuni casi si possono addirittura perdere alcune printf
(e/o alcuni a-capo) perché eseguite a metà di un’altra printf
. In casi estremi può accadere che il programma vada in errore e si blocchi.
NOTA è necessario eseguire il comando con in coda “ | grep io
” per evitare di osservare tutte le stampe “prova”. Il comando 'grep'
stampa solo le linee contenenti la stringa “io” (che compare in “questo me lo gestisco io”). la pipe ‘|’ fa si che l’output del comando venga reindirizzato al comando successivo come input (è un modo per creare ‘al volo’ un canale di comunicazione message passing tra due processi, che approfondiremo la prossima volta).
$ ./safe | grep io
questo me lo gestisco io 0!
questo me lo gestisco io 2!
questo me lo gestisco io 4!
questo me lo gestisco io 6!
questo me lo gestisco io 7!
questo me lo gestisco io 8!
questo me lo gestisco io 12!
questo me lo gestisco io 14!
questo me lo gestisco io 15!
questo me lo gestisco io 16!
questo me lo gestisco io 17!
provaquesto me lo gestisco io 18!
provaquesto me lo gestisco io 19!