Inviare Segnali ad un Processo tramite kill in Linux
La system call kill
di Linux rappresenta il modo più semplice per inviare segnali ad un processo.
In questa lezione vedremo come si utilizza per inviare segnali e anche come possa essere utilizzata per verificare l'esistenza di un processo.
Funzione kill
per inviare segnali ad un processo
Un processo Linux può inviare segnali ad un altro processo attraverso la system call kill()
. Questa funzione è l'analogo del comando kill
da terminale.
Il nome kill deriva dal fatto che la gestione di default della maggior parte dei segnali originariamente presenti in UNIX era quella di terminare il processo bersaglio.
La firma della funzione è la seguente:
#include <signal.h>
int kill(pid_t pid, int sig);
La funzione richiede due argomenti:
pid
: il PID del processo bersaglio;sig
: il segnale da inviare.
In generale, pid
rappresenta il process id (PID) del processo bersaglio. Tuttavia, se il suo valore è minore o uguale a zero si comporta in modo differente. Nella sezione in basso vedremo come cambia il comportamento.
Per quanto riguarda, invece, il valore di ritorno, la funzione restituisce 0 in caso di successo. In caso di errore, kill
restituisce -1
e imposta la variabile errno
con il valore ESRCH
se il processo bersaglio non esiste o EPERM
se il processo chiamante non ha i permessi necessari per inviare il segnale.
System Call kill
La System Call kill
di Linux permette di inviare un segnale da un processo ad un altro di cui se ne conosce il PID.
#include <signal.h>
int kill(pid_t pid, int sig);
pid
: il PID del processo bersaglio;sig
: il segnale da inviare.
Il valore di ritorno è pari a 0 se il segnale è stato inviato correttamente. In caso di errore, la funzione restituisce -1
e imposta la variabile errno
con il valore ESRCH
se il processo bersaglio non esiste o EPERM
se il processo chiamante non ha i permessi necessari per inviare il segnale.
Esempio
Vediamo un esempio di utilizzo della funzione kill
.
In questo esempio realizzeremo due programmi: un programma bersaglio ed un programma che invia il segnale.
Il processo bersaglio, semplicemente, è un processo che, all'infinito, stampa un messaggio a schermo ogni secondo.
/* bersaglio.c */
#include <stdio.h>
#include <unistd.h>
int main() {
/* Stampa il proprio PID */
printf("PID: %d\n", getpid());
while (1) {
printf("Processo bersaglio\n");
sleep(1);
}
return 0;
}
In questo caso abbiamo usato la funzione sleep
per addormentare il bersaglio per un secondo ad ogni iterazione.
Inoltre, il programma stampa il proprio PID a schermo all'avvio. Questa informazione ci servirà poi per inviare il segnale.
Il programma che invia il segnale è il seguente:
/* invia_segnale.c */
#include <stdio.h>
#include <signal.h>
int main() {
/* PID del processo bersaglio */
pid_t pid;
/* Richiede il PID all'utente */
printf("Inserisci il PID del processo bersaglio: ");
scanf("%d", &pid);
/* Invia il segnale SIGTERM al processo bersaglio */
kill(pid, SIGTERM);
return 0;
}
In questo caso, il programma chiede all'utente di inserire il PID del processo bersaglio. Successivamente, invia il segnale SIGTERM
al processo bersaglio.
Per eseguire il programma, apriamo due terminali. Nel primo eseguiamo il processo bersaglio:
$ ./bersaglio
L'output del programma sarà simile a questo:
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
A questo punto ci segniamo il PID del processo bersaglio (nel nostro caso 1234
) e lo inseriamo nel secondo terminale quando il programma che invia il segnale lo richiede.
$ ./invia_segnale
Inserisci il PID del processo bersaglio: 1234
Quando il programma invia_segnale
invia il segnale SIGTERM
nella shell in cui abbiamo lanciato il processo bersaglio vedremo che il processo terminerà.
$ ./bersaglio
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
Processo bersaglio
Terminated
Funzione kill
e permessi
Quando un processo vuole inviare un segnale ad un altro attraverso la funzione kill
, esso necessita dei permessi appropriati. Per motivi di sicurezza, infatti, non tutti i processi possono inviare segnali a tutti gli altri processi. Se fosse possibile, un processo qualsiasi potrebbe inviare segnali a processi sensibili e causare malfunzionamenti o danni.
Le regole per inviare segnali sono le seguenti:
-
Un processo con i privilegi di
root
può inviare segnali a qualsiasi processo; -
Il processo
init
, con PID pari ad1
, che viene eseguito con l'utenzaroot
e il grupporoot
, rappresenta un caso speciale.Può ricevere solo i segnali per cui ha installato un handler. Tutti i segnali per cui non ha un handler sono automaticamente scartati. Ciò evita che per errore possa essere terminato da un segnale non gestito. Del resto il processo
init
è fondamentale per il sistema. -
Processi lanciati con utenze non privilegiate, quindi lanciati non come utenti
root
, possono inviare segnali solo a:- Se l'utenza reale o effettiva corrisponde a quella reale del processo bersaglio;
- Se l'utenza reale o effettiva corrisponde al set-user-id memorizzato del processo bersaglio.
-
Il segnale
SIGCONT
viene trattato in maniera speciale. Un processo non privilegiato, quindi lanciato con un utenza qualsiasi, può inviare questo segnale a chiunque durante la stessa sessione. Questa regola permette alle shell di riprendere l'esecuzione di processi fermati, anche se i processi dei comandi sono stati lanciati con utenze diverse o le hanno cambiate in fase di esecuzione.
Comportamento di kill
con pid
minore o uguale a zero
Abbiamo visto che, quando il parametro pid
è maggiore di zero, la funzione kill
invia il segnale al processo con il PID specificato.
In caso contrario, il parametro viene interpretato in maniera differente:
- Se
pid
è uguale a zero: il segnale viene inviato a tutti i processi del gruppo del processo chiamante, incluso il processo chiamante stesso; - Se
pid
è minore di -1: il segnale viene inviato a tutti i processi appartenenti al gruppo di processi con ID pari al valore assoluto dipid
(ammesso che il processo che invia il segnale abbia i permessi); Questa funzionalità ha particolare utilità specialmente nella gestione dei job; -
Se
pid
è uguale a -1: il segnale viene inviato a tutti i processi per cui il processo chiamante ha i permessi di inviare segnali, eccetto il processoinit
(PID pari ad1
) e se stesso.Ovviamente, se il processo che invia il segnale è lanciato con utenza
root
, allora può inviare segnali a tutti i processi in questo modo, esclusi se stesso edinit
. Per tal motivo si parla di segnali broadcast.
Controllare l'esistenza di un processo
La system call kill
ha anche un'altra importante applicazione.
Se viene inviato il segnale 0
, chiamato anche segnale nullo, la funzione non invia alcun segnale. Invece, la funzione effettua un controllo di errore per verificare se un segnale può essere inviato al processo destinatario.
In altre parole, ciò significa che possiamo usare la funzione kill
per verificare se un PID corrisponde effettivamente ad un processo in esecuzione.
Infatti, se la funzione fallisce e restituisce -1
e imposta errno
al valore ESRCH
, allora il processo non esiste.
Viceversa, se la funzione ha successo e restituisce 0
, oppure fallisce con EPERM
, allora il processo esiste. Infatti, nel primo caso il processo esiste e possiamo inviargli un segnale. Nel secondo caso, il processo esiste ma non abbiamo i permessi per inviare segnali. In entrambe i casi, il processo esiste.
Verifica di esistenza di un processo
Specificando come parametro sig
il valore 0
, la funzione kill
non invia alcun segnale ma controlla se il processo con PID pid
esiste.
#include <signal.h>
int kill(pid_t pid, int sig);
- Se la funzione restituisce
0
, allora il processo esiste; - Se la funzione restituisce
-1
e impostaerrno
aESRCH
, allora il processo non esiste; - Se la funzione restituisce
-1
e impostaerrno
aEPERM
, allora il processo esiste ma non abbiamo i permessi per inviare segnali.
Verificare, tuttavia, che un PID esiste non garantisce che un processo sia effettivamente in esecuzione. Questo per due motivi:
- I PID dei processi vengono riciclati dal sistema operativo. Se un processo termina, il suo PID può essere riutilizzato per un nuovo processo. Quindi, un PID, che in passato corrispondeva ad un processo A, potrebbe ora corrispondere ad un processo B;
- Un processo potrebbe essere nello stato zombie. In questo caso, il processo è terminato ma il suo PID è ancora presente nella tabella dei processi. In questo caso, inviare un segnale al processo zombie non ha effetto.
Esistono altre tecniche più sofisticate per verificare se un processo è effettivamente in esecuzione, ma le vedremo nelle prossime lezioni.
Semplice re-implementazione del comando kill
Adesso, come esempio finale, proviamo a scrivere una semplice versione del comando kill
. La versione che andremo a scrivere prende in ingresso da riga di comando due parametri:
- Il PID del processo bersaglio;
- Il segnale da inviare.
Il programma invierà il segnale al processo bersaglio. Inoltre, aggiungiamo anche la funzionalità di verifica dell'esistenza di un processo.
/* mykill.c */
#define _POSIX_C_SOURCE 200809L
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[]) {
/* Controllo argomenti */
if (argc != 3) {
fprintf(stderr, "Utilizzo: %s <PID> <segnale>\n", argv[0]);
exit(EXIT_FAILURE);
}
/* PID del processo bersaglio */
pid_t pid = atoi(argv[1]);
/* Segnale da inviare */
int sig = atoi(argv[2]);
/*
* Se il valore di sig è maggiore di zero
* invia il segnale al processo bersaglio
*/
if (sig > 0 && sig < 32) {
if (kill(pid, sig) == -1) {
perror("kill");
exit(EXIT_FAILURE);
}
else {
printf("Segnale [SIG%s] inviato al processo %d\n",
sigabbrev_np(sig), pid);
}
}
else if (sig == 0) {
/*
* Se il segnale è pari a zero
* verifica l'esistenza del processo
*/
if (kill(pid, 0) == -1) {
/* Controlla errno */
if (errno == ESRCH) {
printf("Il processo %d non esiste\n", pid);
}
else if (errno == EPERM) {
printf("Il processo %d esiste ma non è possibile inviare segnali\n", pid);
}
else {
perror("kill");
exit(EXIT_FAILURE);
}
}
else {
printf("Il processo %d esiste\n", pid);
}
}
else {
fprintf(stderr, "Il segnale deve essere compreso tra 1 e 31\n");
exit(EXIT_FAILURE);
}
return 0;
}
Proviamo ad adoperare la nostra versione di kill
, mykill
, prima sul processo bersaglio dell'esempio di sopra. Lanciamo, in un terminale, il processo bersaglio:
$ ./bersaglio
Il suo possibile output sarà:
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
Nel secondo terminale, lanciamo il nostro programma mykill
, prima con segnale pari a 0
per verificare l'esistenza del processo bersaglio:
$ ./mykill 1234 0
Il processo 1234 esiste
Successivamente, inviamo un segnale al processo bersaglio:
$ ./mykill 1234 15
Segnale [SIGTERM] inviato al processo 1234
In questo caso il segnale 15
corrisponde al segnale SIGTERM
. Il processo bersaglio terminerà.
$ ./bersaglio
PID: 1234
Processo bersaglio
Processo bersaglio
Processo bersaglio
...
Processo bersaglio
Terminated
Ora, proviamo a lanciare mykill
per verificare l'esistenza del processo 1
, ossia init
:
$ ./mykill 1 0
Il processo 1 esiste ma non è possibile inviare segnali
Come previsto, il processo init
esiste ma non possiamo inviargli segnali in quanto non ne abbiamo il permesso.
In Sintesi
In questa lezione abbiamo analizzato la system call kill
per inviare segnali ad un processo in Linux.
Questa funzione prende in ingresso il PID del processo bersaglio e il segnale da inviare. A seconda del valore del parametro PID, la funzione si comporta in maniera differente ed è in grado di inviare anche i segnali a tutti i processi di un gruppo in modalità broadcast.
Abbiamo visto come sia possibile utilizzare la funzione kill
per verificare l'esistenza di un processo. Infine, abbiamo realizzato una semplice versione del comando kill
in C.
Nella prossima lezione, vedremo come cambiare la gestione di un segnale attraverso la funzione signal
.