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.

Definizione

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 ad 1, che viene eseguito con l'utenza root e il gruppo root, 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 di pid (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 processo init (PID pari ad 1) 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 ed init. 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.

Definizione

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 imposta errno a ESRCH, allora il processo non esiste;
  • Se la funzione restituisce -1 e imposta errno a EPERM, 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:

  1. 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;
  2. 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:

  1. Il PID del processo bersaglio;
  2. 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.