7.1 Errori in C
Durante l’esecuzione di un programma ci possono essere diversi tipi di errori: system calls che falliscono, divisioni per zero, problemi di memoria etc…
Alcuni di questi errori non fatali, come una system call che fallisce, possono essere indagati attraverso la variabile errno. Questa variabile globale contiene l’ultimo codice di errore generato dal sistema.
Per convertire il codice di errore in una stringa comprensibile si può usare la funzione char *strerror(int errnum).
In alternativa, la funzione void perror(const char *str) che stampa su stderr la stringa passatagli come argomento, concatenata tramite ‘: ‘ con strerror(errno).
Esempio 1: errore apertura file
Dato il seguente script in C chiamato "errFile.c"
#include <stdio.h>
#include <errno.h>
#include <string.h> //errFile.c
extern int errno; // declare external global variable
int main(void)
{
FILE * pf;
pf = fopen ("nonExistingFile.boh", "rb"); // Prova ad aprire un file in modalità lettura binaria
if (pf == NULL) // Dato che il file non esiste la funzione fopen restituirà NULL ed il valore della variabile globale errno verrà impostato per indicare l’errore che si è verificato
{
fprintf(stderr, "errno = %d\n", errno); // Stampa il valore di errno
perror("Error printed by perror"); // Accetta come argomento una stringa che viene stampata sullo standard error, seguita da una descrizione dell'errore corrente che è basata sul valore corrente della variabile globale errno
fprintf(stderr,"Strerror: %s\n", strerror(errno)); // Si utilizza la funzione strerror per ottenere una stringa di errore che descrive l'errore che si è verificato e poi la si stampa
}
else
{
fclose (pf);
}
}
L'output è il seguente:
errno = 2
Error printed by perror: No such file or directory
Strerror: No such file or directory
Ricordiamo che stderr alla fine è un file, nello specifico corrisponde al file descriptor 2. Per questo invochiamo la funzione fprintf() specificando il canale stderr. Se avessimo specificato stdout allora sarebbe stata equivalente ad un printf().
Esempio 2: errore processo non esistente
Dato il seguente script in C chiamato "errFSig.c"
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h> //errSig.c
extern int errno; // declare external global variable
int main(void)
{
int sys = kill(3443,SIGUSR1); // Invia un segnale SIGUSR1 al processo con ID 3443, ma tale processo non esiste
if (sys == -1) // Dato che il processo non esiste la funzione kill restituisce -1 e la variabile globale errno verrà impostata per indicare l'errore che si è verificato
{
fprintf(stderr, "errno = %d\n", errno);
perror("Error printed by perror");
fprintf(stderr,"Strerror: %s\n", strerror(errno));
}
else
{
printf("Signal sent\n");
}
}
L'output è il seguente:
errno = 3
Error printed by perror: No such process
Strerror: No such process
7.2 Process groups
Gestione dei processi in Unix
All’interno di Unix i processi vengono raggruppati secondi vari criteri, dando vita a sessioni, gruppi e threads.
Perchè i gruppi
I process groups consentono una migliore gestione dei segnali e della comunicazione tra i processi.
Un processo, per l’appunto, può:
- Aspettare che tutti i processi figli appartenenti ad un determinato gruppo terminino;
- Mandare un segnale a tutti i processi appartenenti ad un determinato gruppo.
Esempio
waitpid(-33,NULL,0); // Attende che termini uno qualsiasi dei processi figli del gruppo di processi con ID 33 (|-33|). Null indica che non siamo interessati allo stato di uscita del processo figlio. 0 indica che non ci sono opzioni specificate
kill(-45,SIGTERM); // Invia il segnale SIGTERM a tutti i figli del gruppo con ID 45 per richiedere la terminazione di un processo in modo pulito
Gruppi in Unix
Mentre, generalmente, una sessione è collegata ad un terminale, i processi vengono raggruppati nel seguente modo:
- In bash, processi concatenati tramite pipes appartengono allo stesso gruppo: cat /tmp/ciao.txt | wc -l | grep ‘2’ (questo comando conta il numero di righe nel file e verifica se il risultato contiene il carattere 2)
- Alla loro creazione, i figli di un processo ereditano il gruppo del padre
- Inizialmente, tutti i processi appartengono al gruppo di ‘init’, ed ogni processo può cambiare il suo gruppo in qualunque momento.
Il processo il cui PID è uguale al proprio GID è detto process group leader.
Group system calls
La funzione setpgid
viene utilizzata per modificare il gruppo di processo di un processo specificato, per creare nuovi gruppi di processo o per spostare un processo in un gruppo di processo esistente. La firma della funzione è la seguente:
int setpgid(pid_t pid, pid_t pgid); //set GID of proc. (0=self)
La funzione getpgid
viene utilizzata per ottenere l'ID del gruppo di processo di un processo specificato. Restituisce l'ID del gruppo di processo o restituisce -1 in caso di errore.
La firma della funzione è la seguente:
pid_t getpgid(pid_t pid);
esempio 1.
In questo esempio viene creata una gerarchia di 5 processi utilizzando la chiamata di sistema fork()
, viene modificato il gruppo di processo dei processi e stampate le informazioni relative all'ID del processo, all'ID del processo padre e all'ID del gruppo di processo.
Dato il seguente script in C chiamato "setpgid.c"
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> //setpgid.c
int main(void)
{
int isChild = !fork(); // Crea un processo figlio, restituendo true (1) per il figlio ed false (0) per il padre
if(isChild)
printf("Figlio: PID: %d - PPID: %d - GID %d\n",getpid(),getppid(),getpgid(0)); // Sia il processo padre che il processo figlio stampano il loro PID (ID del processo), PPID (ID del processo padre), GID (ID del gruppo di processi)
else
printf("Padre: PID: %d - PPID: %d - GID %d\n",getpid(),getppid(),getpgid(0)); // Sia il processo padre che il processo figlio stampano il loro PID (ID del processo), PPID (ID del processo padre), GID (ID del gruppo di processi)
if(isChild) // Solo il processo figlio esegue l'if
{
isChild = !fork(); // Il processo figlio crea un altro processo figlio, restituendo true (1) per il processo figlio del processo figlio iniziale, mentre il valore di ritorno per il processo figlio iniziale è false (0)
if(!isChild) // Entra il figlio generato dalla fork iniziale
{
setpgid(0,0); // Il processo chiamante imposta il come GID il proprio PID e diventa il leader del suo gruppo
}
sleep(1); // Il processo figlio iniziale ed il processo figlio del processo figlio si mettono in pausa per un secondo prima di continuare l’esecuzione del codice
fork(); // Entrambi i processi figli generano un loro processo figlio
printf("PID: %d - PPID: %d - GID %d\n",getpid(),getppid(),getpgid(0));
}; while(wait(NULL)>0); // Il processo padre si mette in attesa che tutti i processi figli terminino e poi termina anche lui
}
L'output è simile al seguente:
Padre: PID: 12334 - PPID: 6834 - GID 12334
Figlio: PID: 12335 - PPID: 12334 - GID 12334 #Figlio 1
PID: 12336 - PPID: 12335 - GID 12334 #Figlio 1 di Figlio 1
PID: 12335 - PPID: 12334 - GID 12335 #Figlio 1
PID: 12340 - PPID: 12336 - GID 12334 #Figlio 1 di Figlio 1 di Figlio 1
PID: 12339 - PPID: 12335 - GID 12335 #Figlio 2 di Figlio 1
Esempio 2.
Vediamo ora un esempio di invio di segnali ai gruppi. In particolare:
- Processo ‘ancestor’ crea un figlio
- Il figlio cambia il proprio gruppo e genera 3 figli (Gruppo1)
- I 4 processi aspettano fino all’arrivo di un segnale
- Processo ‘ancestor’ crea un secondo figlio
- Il figlio cambia il proprio gruppo e genera 3 figli (Gruppo2)
- I 4 processi aspettano fino all’arrivo di un segnale
- Processo ‘ancestor’ manda due segnali diversi ai due gruppi
Dato il seguente script in C chiamato "gsignal.c"
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>
#include <stdlib.h>//gsignal.c
void handler(int signo) // Viene chiamata quando un processo riceve SIGUSR1 O SIGUSR2
{
printf("[%d,%d] sig%d received\n",getpid(),getpgid(0),signo); // Stampa l’ID del processo, l'ID del gruppo di processi e il numero del segnale ricevuto
sleep(1); exit(0); // Aspetta un secondo e poi termina il processo
}
int main(void)
{
signal(SIGUSR1,handler);
signal(SIGUSR2,handler);
int ancestor = getpid(); // Assegna ad ancestor l'ID del processo principale
int group1 = fork(); // A group1 viene assegnato il valore restituito dalla funzione. Se il processo corrente è il processo padre allora conterrà l'ID del processo figlio, invece se il processo corrente è il processo figlio allora conterrà 0
int group2;
if(getpid()!=ancestor )// Se è il processo figlio esegue l'if
{
setpgid(0,getpid()); // Diventa leader del gruppo impostando il proprio ID come GID
fork();fork(); // Vengono generati 3 processi figli: la prima fork crea un nuovo processo figlio, mentre la seconda fork viene eseguita sia dal primo processo figlio sia da quello generato nella fork precedente.Tutti e 3 assumono il GID del processo padre (figlio del padre iniziale nel main)
}
else // Parte eseguita solo dal processo padre
{
group2 = fork(); // Il processo padre crea un secondo processo figlio
if(getpid()!=ancestor) // Parte eseguita dal processo figlio appena creato
{
setpgid(0,getpid()); // Diventa leader del gruppo impostando il proprio ID come GID
fork();fork(); // Vengono generati 3 processi figli: la prima fork crea un nuovo processo figlio, mentre la seconda fork viene eseguita sia dal primo processo figlio sia da quello generato nella fork precedente.Tutti e 3 assumono il GID del processo padre (figlio del padre iniziale nel main)
}
}
if(getpid()==ancestor) // Parte eseguita solo dal processo padre
{
printf("[%d]Ancestor and I'll send signals\n",getpid()); // Stampa un messaggio che indica che è l'antenato
sleep(1); // Attende un secondo
kill(-group2,SIGUSR2); //Send SIGUSR2 to group2 (il meno indica che è un gruppo con quell'ID e non un processo singolo)
kill(-group1,SIGUSR1); //Send SIGUSR1 to group1
}
else // Parte eseguita dai figli
{
printf("[%d,%d]chld waiting signal\n", getpid(),getpgid(0));
while(1); // Attende di ricevere un segnale dal padre. Apppena viene ricevuto, che in questo caso è kill, il gestore dei segnali termina il processo figlio
}
while(wait(NULL)>0); // Attende che tutti i figli terminino
printf("All children terminated\n");
}
L'output è simile al seguente:
[12624]Ancestor and I'll send signals
[12629,12627]child waiting signal
[12627,12627]child waiting signal
[12631,12627]child waiting signal
[12632,12627]child waiting signal
[12628,12628]child waiting signal
[12630,12628]child waiting signal
[12634,12628]child waiting signal
[12633,12628]child waiting signal
[12634,12628] sig31 received
[12630,12628] sig31 received
[12628,12628] sig31 received
[12631,12627] sig30 received
[12632,12627] sig30 received
[12633,12628] sig31 received
[12629,12627] sig30 received
[12627,12627] sig30 received
All children terminated
Esempio 3.
Vediamo ora un esempio di wait di figli di un determinato gruppo. In particolare:
- Processo ‘ancestor’ crea un figlio
- Il figlio cambia il proprio gruppo e genera 3 figli (Gruppo1)
- I 4 processi aspettano 2 secondi e terminano
- Processo ‘ancestor’ crea un secondo figlio
- Il figlio cambia il proprio gruppo e genera 3 figli (Gruppo2)
- I 4 processi aspettano 4 secondi e terminano
- Processo ‘ancestor’ aspetta la terminazione dei figli del gruppo1
- Processo ‘ancestor’ aspetta la terminazione dei figli del gruppo2
Dato il seguente script in C chiamato "waitgroup.c"
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h> //waitgroup.c
int main(void)
{
int group1 = fork(); // A group1 viene assegnato il valore restituito dalla funzione. Se il processo corrente è il processo padre allora conterrà l'ID del processo figlio, invece se il processo corrente è il processo figlio allora conterrà 0
int group2;
if(group1 == 0) // If eseguito dal processo figlio
{
setpgid(0,getpid()); // Diventa leader del gruppo impostando il proprio ID come GID
fork();fork(); // Generated 3 children in new group
sleep(2); return 0; // Wait 2 sec and exit
}
else // Parte eseguita dal processo padre
{
group2 = fork();
if(group2 == 0) // If eseguito dal processo figlio
{
setpgid(0,getpid()); // Become group leader
fork();fork(); // Generated 3 children
sleep(4); return 0; // Wait 4 sec and exit
}
}
sleep(1); // Dopo la creazione dei due gruppi di processi, il processo padre attende per 1 secondo per assicurarsi che i figli abbiano cambiato il loro gruppo
while(waitpid(-group1,NULL,0)>0); // Il processo padre attende la terminazione di tutti i processi nel gruppo1
printf("Children in %d terminated\n",group1);
while(waitpid(-group2,NULL,0)>0); // Il processo padre attende la terminazione di tutti i processi nel gruppo2
printf("Children in %d terminated\n",group2);
}
L'output è simile al seguente:
Children in 12678 terminated
Children in 12679 terminated
Conclusioni
L’organizzazione dei processi in gruppi consente di organizzare meglio la comunicazione e di coordinare le operazioni avendo in particolare la possibilità di inviare dei segnali ai gruppi nel loro complesso.