Università di Roma Tor Vergata Corso di Laurea triennale in Informatica Sistemi operativi e reti A.A. 2013-14 Pietro Frasca Lezione 22 Martedì 7-1-2014 1
System Call per l'uso dei segnali Un processo che riceve un segnale può gestire l azione di risposta alla ricezione di tale evento utilizzando la system call signal: void (*signal(int sig, void (*handler)()))(int); sig è un intero (o la costante simbolica) che specifica il segnale da gestire; handler è il puntatore alla funzione che implementa il codice da eseguire quando il processo riceve il segnale. Il parametro handler può specificare la funzione di gestione dell'interruzione (handler), oppure assumere il valore SIG_IGN nel caso in cui il segnale debba essere ignorato; SIG_DFL nel caso in cui debba essere eseguita l azione di default. 2
la funzione handler ha un parametro di tipo intero che, al momento della sua attivazione, assumerà il valore dell identificativo del segnale che ha ricevuto. La chiamata sigaction appartenente allo standard POSIX è da preferirsi alla signal. L esempio seguente mostra l uso della system call signal. 3
#inc1ude <signal.h> void gestore (int signum){ } printf( Ricevuto il segnale %d \n", signum); /* In alcune versioni di unix l associazione segnale/gestore non è persistente. In questo caso è necessario rieseguire la signal. */ // signal(sigusr1, gestore); main () ( } signal (SIGUSRl, gestore) ; /* da qui in poi il processo eseguirà la funzione gestore quando riceverà il segnale SIGUSRl */ signal (SIGUSRl, SIG_IGN) ; / * SIGUSRl è da qui ignorato: il processo non eseguirà più la funzione gestore in risposta a SIGUSRl */ 4
Le associazioni tra segnali e azioni sono registrate nella User Structure del processo. Dato che la fork copia la User Area del padre nella User Area del figlio e che padre e figlio condividono lo stesso codice, il figlio eredita dal padre le informazioni relative alla gestione dei segnali e quindi: Le azioni di default dei segnali del figlio sono le stesse del padre; ogni processo figlio ignora i segnali ignorati dal padre; ogni processo figlio gestisce i segnali con le stesse funzioni usate dal padre; Dato che padre e figlio hanno User Structure distinte, eventuali chiamate signal eseguite dal figlio sono indipendenti dalla gestione dei segnali del padre. Inoltre, un processo quando chiama una funzione della famiglia exec non mantiene l'associazione segnale/handler dato che una exec mantiene la User Structure del processo che la chiama, ma non dati e codice e quindi neanche le funzioni di gestione dei segnali. 5
Invio di segnali tra processi I processi possono inviare segnali ad altri processi con la system call kill: #include <signal.h> int kill (int pid, int sig); pid è il pid del processo destinatario del segnale sig. Se pid vale zero, il segnale sig viene inviato a tutti i processi della gerarchia del processo mittente. sig è il segnale da inviare, espresso come numero o come costante simbolica. L esempio seguente mostra l uso di signal e kill. Il programma, genera due processi (padre e figlio). Entrambi i processi gestiscono il segnale SIGUSR1 mediante la funzione gestore: il figlio, infatti, eredita l'impostazione della signal del padre chiamata prima della fork. Una volta attivi entrambi i processi, il padre invia continuamente il segnale SIGUSR1 al figlio. 6
#include <stdio.h> #include <signal.h> void gestore (int signum) { } static cont=0; printf ("Pid %d; ricevuti n. %d segnali %d \n", getpid(), cont++, signum); int main () { int pid; signal(sigusr1, gestore); pid = fork (); if (pid==0) /* figlio */ for (; ;) pause(); else /* padre */ for ( ; ;) { } kill (pid, SIGUSR1); sleep(1); } 7
Oltre alla system call kill, esistono altre chiamate di sistema che automaticamente inviano segnali. Ad esempio la funzione alarm causa l invio del segnale SIGALRM al processo che la chiama dopo un intervallo di tempo specificato nell argomento della funzione. #include <unistd.h> unsigned int alarm (unsigned int seconds) L esempio seguente mostra l uso di alarm e pause. Dopo ns secondi viene inviato un segnale di allarme (SIGALRM) e viene eseguita la function azione specificata in signal. Il tempo di allarme ns viene incrementato dopo ogni chiamata di alarm. La function system manda in esecuzione il programma specificato nell'argomento. Nell esempio viene eseguita la funzione system che consente di mandare in esecuzione un programma specificato nell argomento, in questo caso viene eseguito comando date che visualizza data e ora correnti. 8
#include <stdio.h> #include <stdlib.h> #include <signal.h> int ns=1; // periodo iniziale di allarme (1 secondo) int nmax=10; // valore massimo dell'intervallo di allarme void azione(){ /* questa funzione viene eseguita ogni volta che il processo riceve il segnale SIGALRM, */ printf("segnale di allarme ricevuto...eseguo date \n"); system("date"); // esegue il comando date } /* riassegnamento del periodo di allarme che cancella il precedente periodo assegnato. */ alarm(ns); // ns viene incrementato 9
int main() { int i; signal(sigalrm,azione); alarm(ns); while(ns <= nmax) { printf("processo in pausa\n"); pause(); printf("fine pausa\n"); ns++;// incremento del periodo di allarme } exit(0); } 10
Comunicazione: pipe In Unix, i processi possono comunicare tra loro scambiandosi messaggi. La chiamata di sistema pipe (tubo) implementa l astrazione di un canale per consentire la comunicazione tra processi. La dimensione di una pipe è limitata ed è stabilita dalla costante di sistema BUFSIZ che generalmente è di 4KB. L accodamento dei messaggi nella pipe avviene in modalità FIFO. La comunicazione mediante pipe è unidirezionale dato che si può accedere ad essa in lettura (o ricezione) da un solo estremo e in scrittura (o trasmissione) dall altro estremo. La pipe è un canale di comunicazione di tipo da-molti-amolti, in quanto mediante la stessa pipe più processi possono inviare messaggi e più processi possono riceverli. 11
Le pipe vengono gestite allo stesso modo dei file. In particolare, ogni lato di accesso alla pipe è rappresentato da un file descriptor. I processi utilizzano le funzioni di lettura e scrittura di file read e write rispettivamente per ricevere o inviare messaggi dalla o alla pipe. Per creare una pipe si utilizza la system call: int pipe (int fd[2]); in cui il parametro fd è un vettore di 2 file descriptor, che vengono inizializzati dalla pipe. In caso di successo, l intero fd[0] rappresenta il lato di lettura della pipe e fd[1] il lato di scrittura della pipe. La pipe restituisce zero se viene eseguita con successo o un valore negativo, in caso di fallimento. 12
Ogni lato di accesso alla pipe, quindi è rappresentato da un file descriptor. In particolare, un processo mittente, per inviare messaggi utilizza la system call write sul file descriptor fd[1]; analogamente un processo destinatario può ricevere messaggi mediante la system call read sul file descriptor fd[0]. I processi che possono comunicare attraverso una stessa pipe sono il processo che l ha chiamata e tutti i suoi discendenti. Infatti, poichè la pipe è identificata dalla coppia di file descriptor (fd[0], fd[1] ) appartenenti allo spazio di indirizzamento del processo padre, ogni processo discendente dal padre eredita una copia di (fd[0], fd[1]) e una copia della tabella dei file aperti del processo. 13
La pipe ha capacità limitata: quindi, come nel problema produttore/consumatore è necessario sincronizzare i processi in caso di canale pieno e vuoto. Con la pipe la sincronizzazione è implicitamente fornita dalle funzioni read e write che funzionano in modalità bloccante e prevede che, in caso di pipe vuota, un processo destinatario attenda il prossimo messaggio; analogamente, in caso di pipe piena un processo mittente si sospende in attesa di spazio libero. int fd[2] pipe(fd) fd[0] fd[1] RICEVENTE MITTENTE read (fd[0],mess,dim) write (fd[1],mess,dim) 14
Esempio uso di pipe Un processo crea tramite fork() un processo figlio. I due processi, padre e figlio, comunicano attraverso pipe, usando le write e read. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define DIM 256 #define LEGGI 0 #define SCRIVI 1 int main() { int n, pd[2]; int pid; char messaggio[dim]; if (pipe(pd) < 0) { printf ("errore pipe"); exit(1); } 15
if ( (pid = fork()) < 0) { printf("errore fork"); exit(1); } else if (pid > 0) { /* padre */ close(pd[leggi]); // chiude il canale che non usa write(pd[scrivi], "Ciao, figlio", DIM); } else { /* figlio */ close(pd[scrivi]);// chiude il canale che non usa n = read(pd[leggi], messaggio, DIM); printf("%s \n", messaggio); } } 16
Sospensione di processi La SC pause int pause (void); sospende il processo chiamante fino a quando esso riceve un segnale (che non deve essere ignorato) restituisce -1 in caso di errore La SC sleep unsigned int sleep (unsigned int seconds); sospende il processo chiamante per un intervallo di tempo, espresso in secondi, specificato nell argomento. restituisce il numero di secondi mancanti a soddisfare la richiesta: la sospensione programmata con sleep può essere infatti più breve di quanto richiesto. 17
Ad esempio nel caso il processo sia abilitato a gestire segnali, l intervallo di tempo di attesa programmato con sleep viene annullato e la funzione ritorna la quantità di tempo che mancava alla scadenza. usleep Sospende il processo chiamante per un intervallo di tempo, espresso in microsecondi, specificato nel parametro della funzione. #include <unistd.h> int usleep(unsigned long usec) 18
Gestione degli errori La maggior parte delle system call restituisce il valore -1 in caso di errore ed assegna lo specifico codice di errore alla variabile globale int errno; Se la system call ha successo, errno non viene resettato (mantiene l ultimo codice di errore che si è verificato). 19
Il file header errno.h Il file errno.h contiene la definizione dei nomi simbolici dei codici di errore Esempio: # define EPERM 1 /* Not owner */ # define ENOENT 2 /* No such file or directory */ # define ESRCH 3 /* No such process */ # define EINTR 4 /* Interrupted system call */ # define EIO 5 /* I/O error */... 20
La SC perror void perror (const char *prefisso) converte il codice in errno nel corrispondente messaggio di errore, e lo visualizza anteponendo la stringa prefisso. Esempio:... fd=open( prova.txt", O_RDONLY); if (fd==-1) perror ( errore file");... errore file: No such file or directory 21