UNIX - Lettura G - Libero...

50
UNIX - Lettura G Comunicazione tra processi Nella lettura F sono stati presentati argomenti inerenti la gestione dei processi in UNIX ed è stata verificata la possibilità di progettare e realizzare applicazioni costituite da più processi che concorrono nel perseguire gli obiettivi prefissati dalle corrispondenti specifiche. Naturalmente ciò richiede che i processi abbiano la pos- sibilità di scambiarsi informazioni in modo coordinato e di utilizzare in modo altret- tanto coordinato le risorse messe a disposizione dal sistema. Pertanto è indispen- sabile che il sistema operativo fornisca gli strumenti mediante i quali far comuni- care i processi e, laddove necessario, realizzare opportuni algoritmi di allocazione delle risorse, fatta esclusione della risorsa principale, la CPU, alla cui allocazione provvede il nucleo in modo sostanzialmente trasparente al programmatore delle applicazioni. Le versioni più recenti di UNIX forniscono un ventaglio abbastanza ampio di stru- menti di questo genere: ciascuno di essi viene incontro a specifiche esigenze che sor- gono nella programmazione in forma "concorrente" delle applicazioni. La maggior parte di questi strumenti sono stati presentati in generale nel capitolo riservato alla teoria e le specifiche funzionalità messe a disposizione da UNIX spesso amplia- no le possibilità d'uso rispetto a quelle viste in generale. Ad esempio i semafori sono strutture di dati più complesse di quelle descritte come semplice semaforo binario o numerico, mentre la memoria condivisa è uno strumento la cui necessità conse- gue dal modello di generazione padre-figlio, che non prevede aree di dati comuni. Altri strumenti sono strettamente legati alla architettura orientata al file, tipica di UNIX. La recente disponibilità di macchine collegabili in rete e la conseguente necessità di rendere portabili ed interagenti fra loro applicazioni in esecuzione su macchine diverse, ha indotto precise standardizzazioni a tutti i livelli, ma soprattutto nelle interfacce tra moduli di libreria ed applicazioni, e nell'ambito dei protocolli di comu- nicazione. Si è quindi aperta una nuova problematica riguardante la programma- zione di applicazioni "distribuite" per le quali l'obiettivo globale è perseguito con la collaborazione di processi che hanno sede su macchine diverse. Si può ad esempio pensare alla tipica architettura costituita da una macchina principale (server), che mette a disposizione un certa varietà di servizi, e da un certo numero di macchine secondarie (client o satelliti), collegate almeno alla prima, che realizzano un'inter-

Transcript of UNIX - Lettura G - Libero...

UNIX - Lettura G

Comunicazione tra processi

Nella lettura F sono stati presentati argomenti inerenti la gestione dei processiin UNIX ed è stata verificata la possibilità di progettare e realizzare applicazionicostituite da più processi che concorrono nel perseguire gli obiettivi prefissati dallecorrispondenti specifiche. Naturalmente ciò richiede che i processi abbiano la pos-sibilità di scambiarsi informazioni in modo coordinato e di utilizzare in modo altret-tanto coordinato le risorse messe a disposizione dal sistema. Pertanto è indispen-sabile che il sistema operativo fornisca gli strumenti mediante i quali far comuni-care i processi e, laddove necessario, realizzare opportuni algoritmi di allocazionedelle risorse, fatta esclusione della risorsa principale, la CPU, alla cui allocazioneprovvede il nucleo in modo sostanzialmente trasparente al programmatore delleapplicazioni.

Le versioni più recenti di UNIX forniscono un ventaglio abbastanza ampio di stru-menti di questo genere: ciascuno di essi viene incontro a specifiche esigenze che sor-gono nella programmazione in forma "concorrente" delle applicazioni. La maggiorparte di questi strumenti sono stati presentati in generale nel capitolo riservatoalla teoria e le specifiche funzionalità messe a disposizione da UNIX spesso amplia-no le possibilità d'uso rispetto a quelle viste in generale. Ad esempio i semafori sonostrutture di dati più complesse di quelle descritte come semplice semaforo binarioo numerico, mentre la memoria condivisa è uno strumento la cui necessità conse-gue dal modello di generazione padre-figlio, che non prevede aree di dati comuni.Altri strumenti sono strettamente legati alla architettura orientata al file, tipica diUNIX.

La recente disponibilità di macchine collegabili in rete e la conseguente necessitàdi rendere portabili ed interagenti fra loro applicazioni in esecuzione su macchinediverse, ha indotto precise standardizzazioni a tutti i livelli, ma soprattutto nelleinterfacce tra moduli di libreria ed applicazioni, e nell'ambito dei protocolli di comu-nicazione. Si è quindi aperta una nuova problematica riguardante la programma-zione di applicazioni "distribuite" per le quali l'obiettivo globale è perseguito con lacollaborazione di processi che hanno sede su macchine diverse. Si può ad esempiopensare alla tipica architettura costituita da una macchina principale (server), chemette a disposizione un certa varietà di servizi, e da un certo numero di macchinesecondarie (client o satelliti), collegate almeno alla prima, che realizzano un'inter-

G.2 L E T T U R A G

faccia distribuita verso ipotetici utenti di quei servizi.

In questa lettura saranno presenti molti esempi di programmazione in linguaggio C di cuiè, quindi, necessaria la conoscenza.

Al termine sarà possibile:

· costruire un'applicazione con il modello produttore-consumatore che si sincronizzamediante pipe;

· gestire in un'applicazione i segnali, in particolare quelli generati dal sistema;· far comunicare due o più processi mediante code di messaggi, sfruttando eventual-

mente il meccanismo di selezione basato sulla codifica di tipo del messaggio;· costruire meccanismi di sincronizzazione anche non banali con l'uso di gruppi di sema-

fori, ad esempio regioni critiche, modelli lettori-scrittori, semafori privati, ecc.;· conoscere il significato del concetto di file locking;· far comunicare processi via memoria condivisa, aggiungendovi le necessarie sincroniz-

zazioni;· costruire e attivare processi daemon;· conoscere come colloquiano i processi su rete e qualche cenno di uso del meccanismo dei

socket.

G.1 I processi UNIX e la programmazione concorrente

Il fatto stesso che più processi possano essere creati dal medesimo utente e che nonsiano scorrelati tra loro, solleva un problema di comunicazione e conseguente sincronizza-zione. Una estensione a questo problema può coinvolgere utenti distinti, collegati allamedesima macchina o su macchine distinte in qualche modo connesse (tipicamente attra-verso una rete locale o più ampia). Questi meccanismi introducono la possibilità di pensa-re le applicazioni in termini di processi concorrenti. Anche se UNIX mette oggi a disposi-zione un set abbastanza vasto di primitive per la programmazione concorrente, alcuneanche abbastanza sofisticate, un ausilio alla programmazione concorrente ad alto livello èdelegato ai linguaggi realizzabili "al di sopra" di UNIX (ADA, Concurrent Pascal), i cuicompilatori e librerie devono consentire l'utilizzazione di costrutti più sicuri rispetto aquelli di base forniti dal sistema operativo.

Un primo meccanismo di sincronizzazione già presentato riguarda i pipe. In particolaregli unnamed pipe consentono le comunicazioni tra processi strettamente correlati, ovveropadre-figlio o con un grado di parentela meno "diretto", mentre il tipo named può essereutilizzato per le comunicazioni tra processi non correlati (al limite anche tra utenti diver-si, fatte salve le protezioni all'accesso al pipe).

Nel caso del pipe la sincronizzazione è del tipo produttore-consumatore su coda FIFO dicaratteri. Il consumatore (lettore) attende se il pipe è vuoto, il produttore attende se il pipeè pieno. Nel caso di un named pipe, la sincronizzazione è un pochino più complessa eriguarda anche l'operazione di apertura. Infatti un processo che apra un FIFO (questa è lanomenclatura che si trova sui manuali) in lettura (scrittura) attende che vi sia un proces-so che apre il FIFO in scrittura (lettura). Questa quindi può essere vista come una formadi sincronizzazione doppia tra i due processi (quella che viene anche chiamata rendez-vousstretto). In alternativa è possibile impostare il flag O_NDELAY nella chiamata open, nelqual caso la sicronizzazione con il processo partner non è richiesta, l'apertura in lettura si

completa subito, mentre quella in scrittura restituisce un errore se un processo nonha già aperto in lettura (vedi la pagina di manuale di open). Il flag influenza poianche la sincronizzazione nelle operazioni di lettura-scrittura (vedi pagine dimanuale di read e write).

Esempio 1: Questo programma esegue nel processo figlio un filtraggio del fileinviato dal padre e produce una versione in cui gli spazi multipli sono ridotti a spa-zio unico. La comunicazione avviene attraverso un pipe unnamed creato dal padre.#include <stdio.h>#include <fcntl.h>#include <unistd.h>#include <sys/types.h>#include <sys/wait.h>#define PINPUT 0 /* indice pipe input */#define POUTPUT 1 /* indice pipe input */#define MYEOF '\004'/* carattere EOF nel pipe */#define BUFLEN 256 /* lunghezza buffer */

void main(){int fdp[2];/* doppio file descriptor per il pipe unnamed */int ret;char c, buf[BUFSIZ];

if (pipe(fdp)) {perror("fallito pipe");exit(1);

}switch(fork()) {

case -1:perror("fork");exit(2);

case 0: /* figlio */fprintf(stderr, "Figlio, inizia operazione\n");for(;;) {

/* legge carattere */if (read(fdp[PINPUT], &c, 1) == -1) {

perror("figlio read pipe");exit(1);

}if (c == MYEOF) { /* EOF */

fprintf(stderr, "Figlio, termina\n");exit(0);

}write(1, &c, 1);/* output del carattere, qualsiasi esso sia */

ret = 0;while (c == ' ')

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.3

/* ignora spazi successivi */if ((ret = read(fdp[PINPUT], &c, 1)) == -1){

perror("figlio read pipe");exit(1);

}if (c == MYEOF) { /* EOF */

fprintf(stderr, "Figlio, termina\n");exit(0);

}if (ret != 0)

/* output carattere che segue la sequenza di spazi */write(1, &c, 1);

}default: /* padre */

fprintf(stderr, "Padre, inizia\n");while ((ret = read(0, &buf, BUFLEN)) != 0)

write(fdp[POUTPUT], &buf, ret);/* copia buffer in pipe */

/* EOF */buf[0] = MYEOF;write(fdp[POUTPUT], &buf, 1);wait(&ret); /* attende terminazione del figlio */fprintf(stderr, "Padre, finito\n");

}}

Osservazioni: Per notificare la terminazione dell'operazione al figlio, il padreinvia il carattere speciale MYEOF che si assume corrispondere ad una codifica nonutilizzata per il testo trasmesso (è stato scelto il codice di CTRL-D). Il padre e ilfiglio, dopo la chiamata a fork, possono chiudere il pipe "da un lato", quello che cia-scuno di loro non usa, mediante uno dei due file descriptor che, come è noto, sonoduplicati per i due processi. Collaudare l'esempio con il sistema in uso.

G.2 I segnali

Per segnale (signal) si intende una richiesta di interruzione che viene inviataad un insieme specificato di processi; può essere generato dal sistema o da un altroprocesso. In alcuni casi è l'immagine "interna" di un segnale fisico proveniente dal-l'esterno (ad esempio l'impostazione di una combinazione di tasti particolare oppu-re il verificarsi di una condizione eccezionale). Costituisce uno strumento di sincro-nizzazione elementare non cumulabile (un segnale, non "intercettato" prima che nearrivi uno analogo, viene perso); porta con sé l'essenza dell'informazione trasmes-sa, cioè la segnalazione del verificarsi di un evento. Si tratta inoltre di una sincro-nizzazione di tipo asincrono diretto: pur essendo diretta a un set specifico di pro-cessi, questi non sono in generale in attesa dell'evento: ciascuno di essi può mette-

G.4 L E T T U R A G

re in precedenza a disposizione una "routine di servizio" che viene attivata dal siste-ma quando il corrispondente segnale è stato intercettato e il processo passa in ese-cuzione. Se la routine non è stata definita, viene intrapresa un'azione di default checonsiste, in molti casi, nella terminazione forzata del processo. Virtualmente la"routine" o l'azione di default "interrompono" il processo in un punto non prefissa-bile a priori. In alternativa alla esecuzione di una routine di servizio, il processo puòanche decidere di ignorare il segnale, talché l'interruzione non si verificherà affat-to (nota comunque che alcuni segnali non sono ignorabili).

La primitiva signal serve per definire una routine di servizio dei segnali, mentrela primitiva kill genera un segnale da parte di un processo verso un altro proces-so oppure verso tutti i processi del proprio gruppo o di altri gruppi o quelli propri diun certo utente. Una chiamata a sistema connessa ai segnali è anche pause il cuiscopo è sospendere il processo chiamante in attesa della ricezione di un segnale ditipo non ignorato (cioè per il quale vi è una azione definita dall'utente o dal siste-ma). Naturalmente se l'azione causa la terminazione del processo, pause non ritor-na il controllo al processo. Due segnali che possono essere usati per generiche sin-cronizzazioni tra processi sono SIGUSR1 e SIGUSR2.

Singoli segnali o gruppi di segnali possono essere mascherati (blocked) e riabilitatidal processo durante la sua esecuzione: ciò è talvolta necessario per rendere inin-terrompibili da questi segnali sezioni critiche di codice che interferiscono con le cor-rispondenti routine di servizio (analogamente a quanto si fa rispetto interrupthardware, a seconda del processore disabilitando le interruzioni o elevando la prio-rità del processore). Il mascheramento e successiva riabilitazione si ottengonorispettivamente con le chiamate sigblock e sigsetmask.

I segnali sono una delle fonti di generazione di eventi che possono modificare lasequenza di esecuzione di un processo. Vengono memorizzati in una sequenza di bitassociata al processo destinatario (è contenuta nella struttura proc): ciascun bit èindicizzato mediante il numero associato al corrispondente segnale. La routine(interna) di sistema psig ha la responsabilità di valutare se un segnale è penden-te ed attivare la corrispondente azione di servizio. Un processo swapped non ricevei segnali, nel senso che non è possibile attivare la loro routine di servizio finché ilprocesso non viene ricaricato in memoria. Inoltre un segnale può essere sentito omeno anche in dipendenza della priorità del processo: alcune priorità, quelle piùalte, dovute all'attesa di eventi più prioritari, non consentono l'interrompibilità daparte dei segnali. Ad esempio la routine sleep può rimuovere un processo appenainserito da lei stessa nella coda di attesa, se la priorità lo consente e un segnale èstato nel frattempo inviato al processo. Altrimenti la valutazione dei segnali verràintrapresa dopo la chiamata alla routine swtch, e quindi solo alla riattivazione delprocesso per effetto di una chiamata wakeup. Riassumendo il nucleo valuta l'even-tuale presenza di segnali pendenti per un processo nel passaggio di quest'ultimo damodo kernel a user e quando entra ed esce dallo stato di sleeping ad un livello dipriorità sufficientemente basso. L'attivazione della routine di servizio definita dalprocesso avviene esclusivamente con un ritorno in modo user.

Esempio 2: Il seguente programma usa SIGTERM in due modi diversi: il primo conuna routine esplicita, il secondo ripristinando l'azione precedente, cioè la termina-zione, che rappresenta il default.

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.5

#include <stdio.h>#include <signal.h>#include <unistd.h>void (*oldservice)();void myserv(int sig) {/* routine di servizio del segnale */

printf("myserv, sig = %d (SIGTERM=%d)\n", sig, SIGTERM);oldservice = signal(SIGTERM, oldservice);

/* ripristina vecchio servizio */printf(

"myserv, ripristinato servizio oldserv=0x%lx myserv=0x%lx\n",(long)oldservice, (long) myserv);

}

void main() {int i, pid, ret;char s[40];

oldservice = signal(SIGTERM, myserv);/* il padre predispone il servizio, per se'

e per il figlio */switch(pid = fork()) {

case -1:perror("fork");exit(1);

case 0: /* figlio */printf("Figlio, oldserv=0x%lx\n", (long)oldservice);for (i=1; i++;) {

pause();printf("Figlio, dopo pause ciclo = %d\n", i);

}exit(0);

default: /* padre */printf("Padre, battere return per primo segnale:\n");gets(s);kill(pid, SIGTERM);

printf("Padre, battere return per secondo segnale:\n");gets(s);kill(pid, SIGTERM);pid = wait(&ret);printf("Padre, figlio terminato, esco\n");exit(0);

}}

Il processo figlio attende un segnale sulla chiamata pause. Il primo segnale SIG-TERM viene gestito dalla routine myserv ed inoltre sblocca il processo figlio che rici-cla; il secondo segnale produce l'azione di default che abortisce il processo figlio.

G.6 L E T T U R A G

G.3 Meccanismi di comunicazione (IPC)

IPC (InterProcess Communication) è un acronimo riferito ad un insieme ditool di comunicazione presenti in UNIX per arricchire le possibilità di comunica-zione tra processi, in aggiunta ai segnali e ai pipe. Essi sono:· le code di messaggi (equivalgono ai mailbox);· i semafori (si tratta di semafori numerici multipli);· la memoria condivisa (che consente il superamento della indipendenza tra gli

spazi di indirizzamento dei processi).

Gli IPC sono tutti risorse di sistema che vengono allocate ai processi su richiesta diquesti, ma rimangono risorse centralizzate: devono essere restituite al sistemaesplicitamente per poter essere riassegnate ad altri processi. La allocazione di unarisorsa IPC (operazione chiamata creazione) richiede che si specifichi una chiave(una specie di nome) che è un valore di tipo key_t: il sistema restituisce un hand-le che consente il successivo uso della risorsa allocata. Tutti i processi che conosco-no la chiave di una risorsa già creata possono ottenere dal sistema il corrisponden-te handle per poterla usare. Le chiavi sono specifiche di un tipo di IPC: ad esempiopossono coesistere un semaforo e una coda di messaggi con la stessa chiave, vistoche le primitive sono specifiche di ciascun tipo di IPC; sono quindi anche in gradodi controllare se un handle è relativo ad una risorsa IPC del tipo richiesto dalla pri-mitiva stessa. Un IPC ha associati permessi d'uso, come nel caso dei file (quelli diesecuzione sono non significativi), hanno owner, user, e group ID e anche un crea-tor user ID che ha il privilegio della rimozione della risorsa.

G.4 Code di messaggi

Come già accennato, una coda di messaggi è un IPC che corrisponde al concettodi mailbox. I messaggi sono in questo caso record di n byte con n arbitrario (salvoun massimo configurabile): quindi è responsabilità delle applicazioni strutturare imessaggi e far sì che inviante e ricevente diano la stessa interpretazione alla strut-tura del messaggio.

Come tutti gli IPC una coda deve essere creata prima di poter essere usata, con laprimitiva specifica msgget e con gli appositi flag. Tra le informazioni di stato dellacoda, prelevabili con msgctl e flag IPC_STAT, vi sono il numero dei messaggi incoda e il numero utile di byte dei messaggi in coda. Quando la memoria per la buf-ferizzazione è insufficiente per un nuovo invio, il processo inviante si sospende inattesa che qualche ricevente liberi memoria sufficiente, leggendo un messaggio.L'area utente che contiene il record da inviare e che viene copiata poi in un bufferdel nucleo all'invio, grazie a questa copia, è immediatamente riutilizzabile al ritor-no della msgsnd. Al contrario un ricevente attende se non vi sono messaggi in coda.Inviante e ricevente possono richiedere la non sospensione con il flag IPC_NOWAIT.Il valore di ritorno della primitiva di invio o ricezione dice, in questo caso, se l'ope-razione è effettivamente avvenuta o meno. Un processo inviante o ricevente vienesbloccato anche nel caso riceva un segnale (non ignorato): dopo l'esecuzione della

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.7

routine di servizio, il processo esce dalla primitiva su cui si era sospeso con unopportuno codice di errore, che consente al processo di essere notificato dell'inter-ruzione ed eventualmente di ripetere l'operazione sulla coda. I messaggi, oltre alrecord di dati, contengono necessariamente un campo tipo (type): questo può essereutilizzato nella primitiva msgrcv come meccanismo di selezione. L'interfaccia dellaprimitiva prevede un parametro type che funge da selettore: se positivo, estrae ilprimo messaggio che ha tale valore come tipo; se nullo, estrae il primo messaggioindipendentemente dal suo tipo; se negativo, seleziona il primo messaggio che ha ilproprio tipo (positivo) minimo tra quelli che in coda che hanno tipo minore o ugua-le al modulo del valore negativo fornito. Un tipo nonnullo può far estrarre dallacoda un messaggio che non è il primo in assoluto (il più "vecchio", temporalmenteparlando). L'esempio che segue nel testo (server per un gruppo di file condivisi) èsicuramente interessante anche se non di immediata lettura. Si suggerisce di esa-minarlo dopo aver fatto un po' di sperimentazione con qualche esempio più sempli-ce.

Esempio 3: Il seguente programma invia le righe prelevate da stdin al processofiglio suddivise in record da massimo 20 caratteri. Il messaggio contiene la lun-ghezza effettiva del record.#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#include <unistd.h>#define MAXSTR 20 /* massima lunghezza record */#define MAXSIZ (MAXSTR+sizeof(int))

/* massima lunghezza parte dati del messaggio *//* struttura messaggio, istanza unica come buffer */struct mymsg {

long type;int siz;char s[MAXSTR];} buf = {{1}}; /* campo tipo non utilizzato */

void main() {struct msqid_ds mst;int mid, ret, siz;char s[20*MAXSTR], *sp;

if ((mid = msgget(IPC_PRIVATE, 0600)) == -1) {perror("creazione coda messaggi");exit(1);

}if (msgctl(mid, IPC_STAT, &mst) != -1)

fprintf(stderr, "OK coda qbytes=%d\n",mst.msg_qbytes);

switch(fork()) {case -1:

G.8 L E T T U R A G

perror("fork");exit(2);

case 0: /* il figlio eredita l'indice della coda */fprintf(stderr, "Figlio, entra nel ciclo\n");for(;;) {

if (msgrcv(mid, &buf, MAXSIZ, 0L, 0) != -1) {/* ricevuto messaggio */if (buf.s[0] == EOF)

/* fine file */exit(0);

if (buf.siz == MAXSTR) {/* campo intermedio della riga, manca EOS */

strncpy(s, buf.s, MAXSTR);s[MAXSTR] = '\0';fputs(s, stdout);

}else

/* fine riga */fputs(buf.s, stdout);

}else {

perror("msgrecv del figlio");exit(3);

}} /* for */

default: /* padre, spezza le righe */for (;;) {

if (fgets(s, 20*MAXSTR, stdin) == NULL) {/* EOF */buf.s[0] = EOF;buf.siz = 1;

if (msgsnd(mid, &buf, sizeof(int)+1, 0) == -1){

perror("padre, invio EOF");exit(4);

}break;

/* esce dal ciclo */}/* spezzamento riga */sp = s;for (siz=strlen(s); siz>=0; sp+=MAXSTR,

siz-=MAXSTR) {/* copia fino a MAXSTR byte */strncpy(buf.s, sp, MAXSTR);buf.siz = (strlen(sp) < MAXSTR) ?

strlen(sp)+1 : MAXSTR;if (msgsnd(mid, &buf, sizeof(int)+buf.siz,

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.9

0) == -1) {perror("send record");continue;

/* in alternativa, si puo' uscire e fareil kill del figlio */

}} /* for */

}fprintf(stderr, "Padre, attende terminazione figlio\n");

wait(&ret);if (msgctl(mid, IPC_RMID, 0) == -1)

perror("rimozione coda");fprintf(stderr, "Padre, finito ret figlio=%d\n", ret);

}}

L'esempio fa vedere anche l'utilizzazione della primitiva msgctl con il flagIPC_STAT per ottenere il numero di byte riservati alla coda dal sistema.

G.5 I semafori

Ciascun semaforo di UNIX è in realtà un insieme (gruppo) di semafori nume-rici. Esso ha un proprio handle, ottenuto al solito con una primitiva di creazione(semget), e gli n semafori all'interno del gruppo sono indicizzati nel range 0..n-1come se il gruppo fosse un vettore di singoli semafori.

Come è un multiplo un semaforo, così sono multiple le operazioni che si possonoeseguire sul semaforo-gruppo. La questione è un po' delicata e merita una rifles-sione attenta. Una chiamata semop consente di specificare un set di m operazioni(m non prefissato) chiamato lista di operazioni. Una singola operazione è rap-presentata da 3 valori: l'indice del semaforo, la modifica o test (valore semadj) edun flag. Se semadj è un valore positivo, il corrispondente semaforo viene incre-mentato del valore specificato; se il valore è nullo, viene eseguito un test di zero sulsemaforo e il processo attende se il test dà esito negativo e finché risulterà positivo(un'operazione che vedrai essere utile solo se in connessione con altre); se negativo,esegue il decremento purché il valore attuale del semaforo sia maggiore o uguale alvalore di decremento, altrimenti il processo attende che ciò si verifichi (in sostanza,si rende impossibile la condizione che il semaforo diventi negativo). L'attesa puòessere evitata se si fornisce per l'operazione il flag IPC_NOWAIT.

L'aspetto delicato è connesso al concetto di indivisibilità, qualora le operazioni spe-cificate siano più di una. La lista va concepita in senso unitario. Se una delle ope-razioni risulta bloccante, il processo attende, ma è come se non avesse eseguito leprecedenti nella lista. Pertanto, allo sblocco del processo, la lista delle operazioniverrà di nuovo eseguita da capo (in modo indivisibile). Solo se tutte le operazionirichieste danno esito positivo, il processo non si blocca e sappiamo che le operazio-ni sono state sicuramente eseguite come un'unica operazione indivisibile. In estre-

G.10 L E T T U R A G

ma sintesi, le operazioni di una lista vengono eseguite come un'unica operazioneindivisibile e solo se tutte possono essere contestualmente "soddisfatte".

Esempio 4: Supponendo un semaforo-gruppo con due semafori, inizialmente postinell'ordine ad 1 e a 0, ed un processo che esegue una lista di due operazioni di decre-mento unitario, una per ciascun semaforo, il processo si blocca sul secondo semafo-ro e il decremento sul primo semaforo non viene effettivamente eseguito. Quandoun altro processo incrementa il secondo semaforo, entrambe le operazioni vengonodi nuovo provate dal primo processo che potrebbe di nuovo bloccarsi qualora, duran-te la sua precedente attesa, un altro processo avesse decrementato il primo sema-foro. Si può collaudare quanto affermato con il seguente semplice programma.#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <signal.h>#include <errno.h>#include <unistd.h>#define P_OP (-1) /* operazione P su semaforo */#define V_OP (1) /* operazione V su semaforo */#define MYCODE 53 /* codice per ftok */

void main(){key_t key;int npro,sid, val, pid;char s[60];union semun arg;struct sembuf sops[2];

if ((key = ftok(".", MYCODE)) == -1) {perror("ftok");exit(1);

}if ((sid = semget(key, 2, 0600 | IPC_CREAT | IPC_EXCL))

== -1) {perror("creazione semaforo");exit(1);

}arg.val = 1;if (semctl(sid, 0, SETVAL, &arg) == -1) {

perror("set value sem 0");exit(1);

}arg.val = 0;if (semctl(sid, 1, SETVAL, &arg) == -1) {

perror("set value sem 1");exit(2);

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.11

}switch(pid = fork()) {

case -1:perror("fork");exit(2);

case 0: /* il figlio eredita l'indice del semaforo */printf("Figlio, attende su semaforo\n");sops[0].sem_num = 0;sops[0].sem_op = P_OP;sops[0].sem_flg = 0;sops[1].sem_num = 1;sops[1].sem_op = P_OP;sops[1].sem_flg = 0;if (semop(sid, sops, 2) == -1) {

perror("figlio semop");exit(3);

}printf("Figlio sganciato, termina\n");exit(0);

default: /* padre, osserva e sgancia il semaforo */printf("Padre, battere return per vedere sem:\n");gets(s);if ((val = semctl(sid, 0, GETVAL, NULL)) == -1) {

perror("get value sem 0");exit(4);

}if ((npro = semctl(sid, 0, GETNCNT, NULL)) == -1) {

perror("get npro sem 0");exit(4);

}printf("Semaforo 0 valore = %d npro = %d\n", val, npro);

if ((val = semctl(sid, 1, GETVAL, NULL)) == -1) {perror("get value sem 1");exit(5);

}if ((npro = semctl(sid, 1, GETNCNT, NULL)) == -1) {

perror("get npro sem 0");exit(4);

}printf("Semaforo 1 valore = %d npro = %d\n", val, npro);printf("Padre, battere return per sequenza P-V su 0\n");

gets(s);sops[0].sem_num = 0;sops[0].sem_op = P_OP;sops[0].sem_flg = 0;if (semop(sid, sops, 1) == -1) {

perror("padre semop");exit(3);

G.12 L E T T U R A G

}sops[0].sem_num = 0;sops[0].sem_op = V_OP;sops[0].sem_flg = 0;if (semop(sid, sops, 1) == -1) {

perror("padre semop");exit(3);

}printf("Padre, battere return per vedere sem:\n");gets(s);if ((val = semctl(sid, 0, GETVAL, NULL)) == -1) {

perror("get value sem 0");exit(4);

}if ((npro = semctl(sid, 0, GETNCNT, NULL)) == -1) {

perror("get npro sem 0");exit(4);

}printf("Semaforo 0 valore = %d npro = %d\n", val, npro);

if ((val = semctl(sid, 1, GETVAL, NULL)) == -1) {perror("get value sem 1");exit(5);

}if ((npro = semctl(sid, 1, GETNCNT, NULL)) == -1) {

perror("get npro sem 0");exit(4);

}printf("Semaforo 1 valore = %d npro = %d\n", val, npro);

printf("Padre, battere return per sganciare:\n");gets(s);sops[0].sem_num = 1;sops[0].sem_op = V_OP;sops[0].sem_flg = 0;if (semop(sid, sops, 1) == -1) {

perror("padre semop");exit(3);

}printf("Padre, battere return per vedere sem:\n");gets(s);if ((val = semctl(sid, 0, GETVAL, NULL)) == -1) {

perror("get value sem 0");exit(4);

}if ((npro = semctl(sid, 0, GETNCNT, NULL)) == -1) {

perror("get npro sem 0");exit(4);

}printf("Semaforo 0 valore = %d npro = %d\n", val, npro);

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.13

if ((val = semctl(sid, 1, GETVAL, NULL)) == -1) {perror("get value sem 1");exit(5);

}if ((npro = semctl(sid, 1, GETNCNT, NULL)) == -1) {

perror("get npro sem 0");exit(4);

}printf("Semaforo 1 valore = %d npro = %d\n", val, npro);

printf("Padre attende term. figlio\n");wait(&val);if (semctl(sid, 0, IPC_RMID, 0) == -1)

perror("rimozione sem");printf("Padre finito\n");

}}

L'aggiunta del flag IPC_NOWAIT non modifica la regola di indivisibilità ma sempli-cemente restituisce il controllo al processo con un codice d'errore anziché bloccarsialla corrispondente operazione. Invece il flag SEM_UNDO fa sì che, se un processoesegue alcune modifiche su un semaforo con tale flag, queste vengono rimosse (com-pensate) se il processo termina per qualche ragione, onde evitare possibili condi-zioni di blocco critico.

Esempio 5: Il problema dei 5 filosofi è un noto problema di allocazione di risorse.5 commensali alternano una fase di "riflessione" ad una di "alimentazione".Operano su una tavola circolare con 5 forchette in modo che ogni filosofo, per man-giare, necessita di entrambe le forchette ai suoi lati (probabile versione occidentaledella coppia di bastoncini di orientale memoria), solo che ogni forchetta è in comu-ne a due filosofi adiacenti. Per evitare la situazione di blocco critico che si creereb-be se ogni filosofo si appropriasse di una sola forchetta, nell'ipotesi che l'acquisizio-ne avvenisse separatamente per le due forchette (questa è una tipica situazione di"impegno" circolare), è necessario imporre che ogni filosofo richieda ad un ipoteticoallocatore entrambe le forchette contestualmente. Questo problema si risolve facil-mente con un unico set di 5 semafori. Il filosofo i-esimo (i: 0..4) richiede un decre-mento unitario sui semafori che rappresentano le proprie forchette, ovvero quelli diindice i e (i+1) mod 5. Il nucleo del processo che rappresenta un singolo filosofo èdato dalla seguente funzione phil:#define P_OP (-1) /* operazione P su semaforo */#define V_OP (1) /* operazione V su semaforo */#define MAXEAT 6 /* ciclo */#define RANDFAC (0x80000000L / 10L) /* range secondi 0..9 */#define FORKOPS 2 /* due operazioni */#define NUMPHIL 5static int sid; /* set di NUMPHIL semafori */void phil(int me)/* filosofo di indice me: 0..NUMPHIL-1 */{int i, delay;

G.14 L E T T U R A G

long now;struct sembuf getfork[] = {

0, P_OP, 0,0, P_OP, 0};/* test d'ingresso forchette adiacenti */

struct sembuf putfork[] = {0, V_OP, 0,0, V_OP, 0};/* rilascio d'uscita *//* init parte variabile, forchette adiacenti */getfork[0].sem_num = putfork[0].sem_num = me;

getfork[1].sem_num = putfork[1].sem_num = (me+1) % NUMPHIL;/* init random */srand(me);for(i=1; i<=MAXEAT; i++) {

/* pensa */delay = (int)(rand() / RANDFAC)+1;now = time(0);printf("Filosofo me=%d pensa per %d sec alle %s",

me, delay, ctime(&now));sleep(delay);/* vuole mangiare */now = time(0);printf(" Filosofo me=%d tenta di mangiare alle %s",

me, ctime(&now));if (semop(sid, getfork, FORKOPS) == -1) {

perror("filosofo getfork");exit(1);

}/* mangia */delay = (int)(rand() / RANDFAC)+1;now = time(0);

printf(" Filosofo me=%d ora mangia per %d sec alle %s",me, delay, ctime(&now));

sleep(delay);now = time(0);

printf(" Filosofo me=%d ha finito di mangiare %s",me, ctime(&now));

if (semop(sid, putfork, FORKOPS) == -1) {perror("filosofo putfork");exit(3);

}}printf("Filosofo me=%d esce alle %s",

me, ctime(&now));}

La funzione comprende la generazione di ritardi casuali ed alcune visualizzazioni

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.15

di istanti temporali per seguire meglio l'esecuzione del programma. La logica diindivisibilità garantisce l'acquisizione contemporanea delle forchette. Nota però chel'operazione di restituzione potrebbe essere anche eseguita con 2 semop separati,uno per ogni forchetta. Completare l'esempio creando 5 processi "fratelli" che ese-guono la funzione phil passando il proprio indice i.

Per completezza si cita un meccanismo di sincronizzazione su file che assomiglia aquello dei semafori e che consente di realizzare in modo piuttosto semplice sezionicritiche ad accesso controllato (tipicamente esclusivo) su una determinata porzionedi un file. Il meccanismo di protezione è denominato lock (file locking). Mediantela chiamata a sistema lockf è possibile organizzare regioni di un file come sema-fori (sono detti advisory locks) oppure come zone ad accesso esclusivo di chi possie-de il lock (si parla di enforcement mode record locks). Se un altro processo tenta diaccedere alla risorsa protetta da un lock, viene sospeso in attesa che il possessoredel lock lo rimuova (esplicitamente o perché termina) oppure gli viene tornato uncodice di errore. In maggior dettaglio le funzioni attivabili con la primitiva lockfsono: unlock , lock con attesa in caso di risorsa già impegnata, lock con ritorno dierrore in caso di risorsa impegnata, test che valuta se su una regione un altro pro-cesso ha già imposto un lock. Maggiori dettagli si trovano nelle pagine di manuale.

G.6 Memoria condivisa

Come già detto più volte, lo spazio di indirizzamento utile dei processi nonammette aree fisiche comuni a più processi. Pertanto è reso disponibile un mecca-nismo IPC che consente di allocare (dinamicamente) segmenti condivisi che pos-sono essere singolarmente "attaccati" (attach) all'interno dello spazio di indirizza-mento di ciascuno dei processi che ne fanno richiesta, purché autorizzati in base adeterminati permessi. Ogni processo, dopo l’"incollaggio", può usare un range diindirizzi utili propri per accedere al segmento condiviso, che ha invece ovviamenteindirizzi fisici prefissati (può essere composto di più pagine se c'è un meccanismo dipaginazione). Di conseguenza, per usare un segmento condiviso non basta conosce-re il suo handle, come nel caso degli altri IPC, ma bisogna prima "attaccarlo" alproprio spazio di indirizzamento. Quando un processo ha terminato di usare il seg-mento, lo può "scollegare" (detach) ed infine, se ne è il creatore, restituire al siste-ma analogamente a quanto fatto per gli altri IPC (flag IPC_RMID).

Le operazioni di "incollaggio" possibili sono due. La prima è la più comune: è il siste-ma a decidere ove collocare il segmento nello spazio del processo, valutando l'am-piezza del segmento e il range di indirizzi già occupato dal processo per le aree ditesto e dati. L'altra opzione consente al processo di precisare l'indirizzo di attach:questo può risultare utile se il segmento condiviso dovrà contenere puntatori (asso-luti) a locazioni del segmento stesso. A questo punto è evidente che è assolutamen-te normale che due processi distinti possano accedere al medesimo segmento con-diviso con indirizzi (base) virtuali diversi, anche se non è obbligatorio: ad esempioun processo figlio creato con fork eredita l'indirizzo di attach se l'operazione è stataeseguita dal padre. Sarà il sistema di gestione della memoria in generale a map-pare indirizzi logici (corrispondenti) usati da processi diversi nello stesso indirizzo

G.16 L E T T U R A G

fisico.

È noto che in generale l'accesso a memoria comune è fonte di interferenza e quindipuò essere necessario aggiungere qualche meccanismo di sincronizzazione: i sema-fori sono stati introdotti anche a questo scopo. Un ulteriore grado di protezione puòessere ottenuto sfruttando il meccanismo dei permessi. Un processo può avererispetto ad un segmento condiviso il solo permesso di lettura e non quello di scrit-tura; questa protezione non elimina comunque la possibilità di interferenza perchéun lettore può esaminare una struttura di dati complessa solo parzialmente aggior-nata da uno scrittore e che si trova quindi in uno stato inconsistente.

Esempio 6: Il programma che segue utilizza un piccolo segmento condiviso per tra-smettere ad un processo figlio l'informazione di quanti secondi dopo terminare. Ilfiglio valuta periodicamente la variabile che rappresenta tale informazione.Ovviamente non c'è alcun problema di interferenza.#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/shm.h>#include <unistd.h>#define p2int(p) (*((int *)(p)))#define MYKEY 0x12345L

void main(){int sid, att, pid, cnt = 1;char *pseg;char str[80];

/* creazione shared memory */if ((sid = shmget(MYKEY, sizeof(int), IPC_CREAT |

IPC_EXCL | 0600) ) == -1) {perror("padre creazione shm");exit(1);

}/* attach del padre, anche per il figlio */if ((pseg = (char *)shmat(sid, (char *)NULL, 0)) ==

(char *) (-1L)) {perror("padre shmat");exit(2);

}p2int(pseg)= 0; /* valore iniziale */printf("Padre sid=%d pseg=0x%lx\n", sid, (long)pseg);switch(pid = fork()) {

case -1:perror("fork");exit(3);

case 0: /* figlio */

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.17

while ((att = p2int(pseg)) == 0) {printf("Figlio, ciclo %d\n", cnt++);sleep(3);

}printf("Figlio terminera` tra %d secondi\n",

att);sleep(att);if (shmdt(pseg) == -1)

perror("figlio detach");exit(0);

default: /* padre, fornisce valore attesa */do {

printf("Padre, battere secondi term. figlio:");gets(str);/* esce quando != 0 */

} while ((att = atoi(str)) == 0);p2int(pseg) = att;printf("Padre attende term. figlio\n");pid = wait(&att);printf("Padre, terminato figlio, rimuove shm\n");if (shmdt(pseg) == -1) /* detach */

perror("padre detach");if (shmctl(sid, IPC_RMID, 0) == -1)

/* rimozione segmento */perror("rimozione shm");

printf("Padre finito\n");exit(0);

}}

G.7 Programmazione concorrente

Con gli esempi visti in questa lettura, utilizzanti i vari tipi di IPC, segnali e pipe,e con esempi anche delle unità precedenti, si può affermare che sia già stato affron-tato un certo grado di programmazione concorrente. Gli strumenti UNIX per lesincronizzazioni e le comunicazioni (pipe, segnali, semafori, code, memoria condivi-sa) sono strumenti abbastanza sofisticati ma pur sempre ad un livello d'utilizzazio-ne piuttosto basso. Vi sono infatti scarse garanzie su un loro uso appropriato: sem-plici errori o dimenticanze possono produrre seri malfunzionamenti, difficili daidentificare (deadlock, scorrettezze nella sincronizzazione, ecc.). In ambiente UNIXsono per altro disponibili compilatori per linguaggi concorrenti ad alto livello (ADA,Concurrent C e Pascal e altri più recenti) con annesso supporto run-time per la rea-lizzazione del o dei modelli di concorrenza che ciascuno di essi realizza (ad esempiola chiamata a procedura remota o rendez-vous esteso di ADA oppure i monitor delConcurrent Pascal).

G.18 L E T T U R A G

Limitandosi comunque a tool più direttamente disponibili in UNIX, vengono pre-sentate due entità tipiche per la programmazione concorrente: le coroutine e idaemon.

Strettamente parlando le coroutine sono programmi accoppiati in grado di attivar-si reciprocamente in forma alterna: mediante una primitiva, genericamente indica-ta come transfer, il controllo passa da una coroutine all'altra che prosegue l'esecu-zione dal punto in cui si era lei stessa interrotta precedentemente con un transferverso la prima. Rispetto ad una subroutine, un'istanza della quale può certo essereattivata da un'altra subroutine, ma perde la sua validità quando le restituisce ilcontrollo con un return, una coroutine mantiene la propria identità attraverso il sal-vataggio del proprio contesto all'atto del transfer. In UNIX le coroutine sono in veri-tà due processi: il contesto è quindi salvato dal sistema operativo, che è anche effet-tivamente responsabile dell'attivazione dei processi. Il funzionamento come corou-tine va visto nel senso che i due processi collaborano, attraverso una esecuzione inalternanza, ad un comune scopo.

La seconda entità considerata è il daemon. Con questo termine si intende un pro-cesso-server, un processo cioè non strettamente correlato ad alcun altro (è figlio diinit, quindi del tutto generale), non ha normalmente associato il controllo di unterminale, non muore finché il servizio va mantenuto, il servizio è generalmentecostituito dalla gestione di una risorsa globale. Esempi di questo genere sono lp(gestore di stampa in spooling), cron (gestore di servizi a tempo), getty (gestore diun terminale per le operazioni di login). Molti utenti in genere possono richiederein istanti diversi servizi ad un daemon. I daemon "standard" possono essere creatidirettamente da init (vedi inittab), oppure con il meccanismo di fork ricordan-do che, se un padre termina, i figli possono venire "adottati" da init. Se il padre èil leader di un gruppo, il segnale SIGHUP è inviato a tutti i figli e nipoti, ma puòessere da questi ignorato; non viene generato se il padre non è leader. Un daemonabbandona il controllo del terminale con la chiamata setpgrp. Una caratteristicadei daemon è quella di comunicare attraverso uno dei meccanismi esaminati in que-sta lettura, tipicamente un named pipe.

G.8 Cenni su networking e socket

L'introduzione delle reti di calcolatori ha permesso l'integrazione di macchine edi servizi di tipo distribuito, anche nel caso di contesti operativi ed architetturehardware disomogenei. L'integrazione è consentita dalla realizzazione di interfaccee protocolli standard che regolano ed armonizzano la comunicazione tra le variemacchine, con un sufficiente grado di indipendenza dalle loro caratteristiche speci-fiche. È ad esempio il caso del protocollo TCP/IP che è diventato uno standard defacto, per i sistemi UNIX in particolare, e anche per altri. I servizi di più alto livel-lo messi a disposizione da questo protocollo consentono la comunicazione affidabiletra processi su macchine remote: la suddivisione dei blocchi di dati da spedire inuna sequenza di pacchetti, il loro recupero ordinato, la gestione degli errori conritrasmissione dei pacchetti non correttamente ricevuti, sono a carico del protocol-lo e quindi del tutto trasparenti alle applicazioni. Tra le applicazioni di uso gene-

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.19

rale supportate da TCP/IP vi sono telnet, emulatore di terminale, e ftp (filetransfer protocol), che consente il trasferimento di file nelle due direzioni tra mac-chine remote.

Altri servizi su rete sono l'estensione fuori della macchina di partenza della postaelettronica: il comando mail accetta nomi di login seguiti dall'indicazione dellamacchina (indirizzo di posta) dove l'utente riconosciuto con quel nome riceverà laposta a lui diretta. L'indirizzo è un'informazione simbolica, quindi di tipo logico,dalla quale tutte le macchine coinvolte nel percorso da quella di partenza a quelladi arrivo riescono a ricavare le necessarie informazioni per l'instradamento: questeoperazioni sono a carico di uno o più daemon incaricati di questo servizio, che com-prende anche l'accumulo di messaggi qualora il proseguimento nella prossima trat-ta d'instradamento sia al presente impossibile.

Un altro sistema standard di comunicazione in UNIX, che viene citato per comple-tezza, è UUCP (UNIX to UNIX copy) che consente la trasmissione dati in back-ground tra 2 sistemi UNIX in qualche modo connessi (via linea seriale con modeme, più di recente, anche via rete locale). Il sistema può supportare la trasmissionedella posta e l'emulazione di terminali.

La disponibilità di protocolli standard quali TCP/IP consente anche di fornire altritipi di servizi anche sofisticati. Vi sono ad esempio una serie di comandi che rap-presentano la versione remota di alcuni utili comandi generali: rlogin (remotelogin), rcp (remote copy), rsh (remote shell) e altri, sostanzialmente indipendentidal protocollo sottostante. Un altro servizio di rete molto diffuso è lo NFS (networkfile system) che consente di trattare i filesystem in modo indipendente dalla locali-tà fisica degli stessi: da una qualsiasi macchina supportata dal protocollo NFS, unfilesystem su un disco di una qualche macchina in rete è visto, una volta montato,come sottoalbero della gerarchia dei directory visibili. È anche possibile una gestio-ne distribuita ma unica di tutti gli utenti di un gruppo di macchine. NFS si appog-gia su TCP/IP.

Le versioni BSD (Berkeley Software Distribution) di UNIX, assieme al protocolloTCP/IP, hanno anche introdotto il concetto di socket. Socket è la concatenazione diun indirizzo di macchina nella rete e un indirizzo di porta che definisce un punto diconnessione verso un processo di quella macchina. All'incirca quindi il socket è l'e-stensione su rete del concetto di port per le connessioni "dirette" tra processi; un'al-tra interpretazione del socket è come indirizzo di rete di un processo. Il collega-mento tra processo e socket è chiamato binding. Con una apposita primitiva è pos-sibile aprire una connessione tra 2 socket e consentire da quel momento tra i duecorrispondenti processi di iniziare una sessione di scambio di dati. La comunicazio-ne può avvenire attraverso i servizi TCP (che corrispondono a servizi del 4° livelloISO-OSI, cioè livello di trasporto). IP è il protocollo (di livello 3) che provvede al cor-retto instradamento e alla suddivisione dei blocchi di dati in pacchetti chiamatidatagram. ftp, telnet, NFS sono stati realizzati sull'interfaccia dei socket.

UNIX System V ha basato la sua architettura di rete sul concetto di stream: unostream rappresenta interfacce tra i livelli di protocollo e il kernel di UNIX. Questeinterfacce introducono livelli di standardizzazione tali da consentire che i protocol-li vedano le medesime interfacce. Uno stream è costituito da tre parti: la testa, che

G.20 L E T T U R A G

costituisce l'interfaccia verso i processi utente; uno o più moduli opzionali di elabo-razione dei dati; ed un driver che comanda un dispositivo per servizi di comunica-zione o di tipo software (interno) come pseudo-dispositivo. Il sistema RFS (RemoteFile System) di System V, progettato per interconnettere nodi UNIX identici su unarete affidabile, è stato costruito utilizzando l'interfaccia stream.

Esempio 7: Si mostra come si possa costruire una coppia di processi server-clientall'interno della stessa macchina, usando i socket. Un processo crea un socket con lachiamata omonima che prevede l'indicazione di un dominio di comunicazione (quel-li supportati sono generalmente il dominio UNIX per processi all'interno della stes-sa macchine e il dominio Internet per processi su macchine in rete con i protocolliDARPA a cui TCP e IP appartengono). Si deve poi precisare il tipo di comunicazio-ne (a circuito virtuale o stream socket, cioè con un protocollo affidabile, o a data-gram, di tipo non affidabile ma più efficiente) ed eventualmente un particolare pro-tocollo. Ottenuto il socket, con una chiamata bind il processo server assegna al soc-ket un indirizzo, la cui semantica dipende dal dominio e dal protocollo precisati inprecedenza: questo indirizzo identifica univocamente il port di cui il socket è l'im-magine e quindi fornisce l'associazione effettiva tra socket e processo server. Tutti iclient che vorranno aprire una connessione con il server, dovranno precisare l'indi-rizzo del suo socket. Nel caso della modalità a circuito virtuale, il server deve poiprecisare con la primitiva listen l'ampiezza della coda che memorizza le richiestedi connessione, provenienti dai client, che sono ancora pendenti. Dopodiché si dis-pone ad accettare una richiesta di connessione con la primitiva accept, da cui rice-ve come risposta fornita dal sistema l'indirizzo del processo client che ha fatto per-venire la richiesta. Per consentire che un server possa generare più processi di ser-vizio che gestiscono varie connessioni, mano a mano che pervengono le richieste, laprimitiva accept ritorna un nuovo socket descriptor, distinto da quello ottenutooriginariamente con socket. Questo descriptor può essere interpretato comedescrittore della connessione stabilita con il client e può essere utilizzato da un pro-cesso figlio per colloquiare con il client. Nel frattempo il padre-server può disporsiad accettare una successiva richiesta. Da parte sua il client, ottenuto un socketdescriptor con la chiamata socket, richiede una connessione con una chiamataconnect a cui passa come parametro l'indirizzo del socket del server (lo stesso dellachiamata bind prima descritta). Primitive di comunicazione sono la coppia read-write oppure la coppia send-recv per stream socket, la coppia sendo-recvfromnel caso datagram. Una connessione a socket può essere chiusa con la chiamatashutdown mentre il relativo descriptor può essere rilasciato al solito con la chia-mata close.

L'esempio che viene presentato è stato collaudato in ambiente SunOS e Linux, ovel'indirizzo di uno stream socket è un pathname fornito nella seguente struttura:struct sockaddr {

short sa_family; /* posto sempre AF_UNIX */ushort sa_data[14];

};

La lunghezza da fornire nella chiamata bind è pari a sizeof(short) + strlen(path)ove strlen(path) è la lunghezza, non includente il terminatore, del nome copiato nelcampo sa_data. Il sistema crea un file che ha il pathname fornito e che deve esse-

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.21

re rimosso con una chiamata unlink se si vuole riutilizzate lo stesso pathname insessioni successive.#include <stdio.h>#include <sys/types.h>#include <sys/socket.h>

server(){int sod, conn; /* socket e connection descriptor */char buf[256];struct sockaddr mysad; /* address del socket */int i; int flen; /* lunghezza address ricevuto in accept */

mysad.sa_family = AF_UNIX;strcpy(mysad.sa_data, "miost");if ((sod = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {

perror("server socket");exit(0);

}/* bind nome, senza EOS */if(bind(sod, &mysad, sizeof(short)+strlen(mysad.sa_data))

== -1) {perror("server bind");exit(0);

}printf("Server socket=%d bind \"miost\", ora listen\n", sod);

if (listen(sod, 3) == -1) {shutdown(sod, 2);perror("server listen");exit(0);

}/* lunghezza coda richieste unitaria */for(i=1; i<=5; i++) {

printf("Server, ciclo %d esegue accept\n", i);if ((conn = accept(sod, &mysad, &flen)) == -1) {

/* ascolta richieste di connessione: la chiamatarestituisce l'indirizzo del client */

shutdown(sod, 2);perror("server accept");exit(0);

}printf("Server, arrivata richiesta\n");if (fork() == 0) {

/* il figlio esegue il servizio */close(sod); /* non serve al figlio */read(conn, buf, 256);printf("Server buf <%s>\n", buf);printf("Server, servizio terminato\n");

G.22 L E T T U R A G

exit(0);}/* padre */close(conn); /* al padre non server */sleep(5);/* il padre si può far terminare con SIGINT */

} /* for *//* distruzione sockaddr come file */printf("*** Server finito, rimuove socket file\n");unlink("miost");

}

client(int cnt) {int sod; /* socket descriptor */struct sockaddr mysad; /* address del socket */char buf[256];

mysad.sa_family = AF_UNIX;strcpy(mysad.sa_data, "miost");printf("Client %d chiama socket\n", cnt);if ((sod = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {

perror("client socket");exit(0);

}printf("Client %d sock = %d chiama connect\n", cnt, sod);/* connect fornendo nome, senza EOS */if (connect(sod, &mysad,

sizeof(short)+strlen(mysad.sa_data))== -1) {perror("connect");exit(0);

}sprintf(buf, "Sono client %d", cnt);write (sod, buf, strlen(buf)+1);printf("Client %d ha inviato, muore\n", cnt);shutdown(sod, 2);close(sod);exit(0);

}

main() {int cnt = 1;char s[20];

printf("Main, crea server\n");if (fork()==0) {

fclose(stdin); /* non server al server */server();exit(0); /* comunque il server non torna */

}

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.23

for (cnt=1; cnt<=5; cnt++) {printf("Main ciclo %d, battere return per nuovo client\n",

cnt);gets(s);if (fork() == 0) {

/* il figlio fa comunque exit */fclose(stdin); /* non server al client */client(cnt);

}}printf("++++ main finito\n");

}

G.9 Esercizi sulla comunicazione tra processi

G.9.1 ESERCIZI SUL PARAGRAFO G.1

Esercizio 1: Modificare il programma presentato nell'esempio 1 in modo da utiliz-zare le primitive di stream I/O anziché quelle di base (read e write).

R: In sostanza si tratta di sfruttare le caratteristiche della funzione fdopen cheapre uno stream file su un file già aperto e restituisce il puntatore a FILE che con-sente di adoperare le funzioni per l'I/O bufferizzato.#include <stdio.h>#include <fcntl.h>#include <unistd.h>/*#include <sys/types.h>#include <sys/wait.h>*/#define PINPUT 0 /* indice pipe input */#define POUTPUT 1 /* indice pipe input */#define MYEOF '\004'/* carattere EOF nel pipe */#define BUFLEN 256 /* lunghezza buffer */

void main(){int fdp[2];/* doppio file descriptor per il pipe unnamed */int ret;FILE *fp;char c;

if (pipe(fdp)) {perror("fallito pipe");exit(1);

}

G.24 L E T T U R A G

switch(fork()) {case -1:

perror("fork");exit(2);

case 0: /* figlio */fprintf(stderr, "Figlio, inizia operazione\n");/* apre "stream" pipe in input */if ((fp = fdopen(fdp[PINPUT], "r")) == NULL) {

perror("apertura stream pipe");exit(3);

}for(;;) {

/* legge carattere */if ((c = getc(fp)) == MYEOF) {

/* EOF */fprintf(stderr, "Figlio, termina\n");exit(0);

}putchar (c);/* output del carattere, qualsiasi esso sia */

ret = 0;while (c == ' ') {

ret = 1;if ( (c = getc(fp)) == MYEOF) {

/* EOF */fprintf(stderr, "Figlio, termina\n");exit(0);

}}if (ret != 0)

/* output carattere che segue la sequenza di spazi */putchar(c);

}default: /* padre */

fprintf(stderr, "Padre, inizia\n");/* apre "stream" pipe in output */if ((fp = fdopen(fdp[POUTPUT], "w")) == NULL) {

perror("apertura stream pipe");exit(3);

}while ((c = getchar()) != EOF)

putc(c, fp);/* copia buffer in pipe */

/* EOF */putc(MYEOF, fp);fclose(fp);

/* flush di output */wait(&ret); /* attende terminazione del figlio */

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.25

fprintf(stderr, "Padre, finito\n");}

}

Esercizio 2: Sfruttando la particolare sincronizzazione doppia tra un "lettore" eduno "scrittore" all'atto dell'apertura di un pipe, predisporre e collaudare un pro-gramma rendez che sincronizza nel modo detto due processi padre (lettore) e figlio(scrittore) dopo che ciascuno ha atteso un certo tempo, come indicato, in secondi,sulla linea di comando.

R: L'esercizio è abbastanza semplice: si possono sfruttare per la sincronizzazionele funzioni di stream I/O (fopen e fclose) dopo aver assicurato la creazione delpipe con mknod.#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>

void main(int argc, char **argv) {FILE *fp;int att, pid;

if (argc < 3) {fprintf(stderr, "** uso: rendez attesapadre attesafiglio\n");

exit(1);}if (mknod("rendezp", S_IFIFO | 0600, 0) == -1) {

perror("mknod");exit(2);

}if ((pid = fork()) == 0) {

att = atoi(argv[2]);printf("Figlio, attende %d secondi\n", att);sleep(att);

printf("Figlio, open pipe write <=> sincronizzazione\n");if((fp = fopen("rendezp", "w")) == NULL) {

perror("figlio open pipe write");exit(3);

}printf("Figlio, eseguita sincronizzazione, termina\n");

fclose(fp);exit(0);

} /* figlio */else if (pid == -1) {

perror("fork");}else {

att = atoi(argv[1]);printf("Padre, attende %d secondi\n", att);sleep(att);

G.26 L E T T U R A G

printf("Padre, open pipe read <=> sincronizzazione\n");if((fp = fopen("rendezp", "r")) == NULL) {

perror("padre open pipe read");exit(4);

}printf("Padre, eseguita sincronizzazione\n");fclose(fp);printf("Padre, attende terminazione figlio\n");wait(&att);

}printf("Padre, rimuove pipe ed esce\n");if (unlink("rendezp") == -1) {

perror("rimozione pipe personale");exit(5);

}exit(0);

}

G.9.2 ESERCIZI SUL PARAGRAFO G.2

Esercizio 3: Che cos'è un segnale? Cosa fa un processo per inviarlo e per intercet-tarlo? Quali sono i tipi di azione specificabili?

R: Un segnale è l'equivalente di un'interruzione asincrona inviata ad un proces-so da parte del sistema o di altri processi. I segnali sono numerati e quindi identi-ficabili. L'invio di un segnale da parte di un processo avviene con una chiamata allaprimitiva kill che consente di specificare il processo o gruppo di processi destina-tari del segnale e il particolare segnale inviato. Un processo può specificare,mediante una chiamata alla primitiva signal che tipo di azione intraprendere afronte di uno specifico segnale, scelta tra 3 possibili: l'esecuzione di esplicita routi-ne di cui si fornisce l'indirizzo; ignorare; una azione di default decisa dal sistema inbase al tipo del segnale.

Esercizio 4: Modificare il programma verprg (si veda l’esercizio 3 della lettura F)in verprg3 in modo da definire una routine di servizio del segnale SIGINT (com-binazione di tasti CTRL-C) che visualizza su stdout un messaggio con il numero disegnale pervenuto e fa terminare il programma. Aggiungere anche un'attesa di con-ferma prima della chiamata a setpgrp. Verificare nel collaudo che accade batten-do la sequenza CTRL-C in ciascuno dei due punti di attesa da tastiera, attivando ilprogramma con lo shell a disposizione, e darne una interpretazione.

R: Nella parte iniziale del programma occorre definire la routine di servizio delsegnale SIGINT ed i necessari include file.#include <sys/termios.h>#include <signal.h>#include <fcntl.h>#include <unistd.h>

void mysig(int sig)

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.27

{printf("** SIGINT (%d) processo %d **\n", sig, getpid());exit(10);

}

Altre modifiche da apportare riguardano l'attivazione della routine di servizio conla chiamata signal e la richiesta di conferma:signal(SIGINT, mysig);printf("Battere return:");gets(s);printf("PID=%d process group ID orig.= %d PID shell=%s\n",

Supponendo di disporre dello shell sh, l'attivazione in foreground con il comando:$ verprg3 $$

fa sì che un SIGINT inviato alla prima attesa, cioè prima che venga chiamatasetpgrp, pervenga al processo e venga gestito dalla routine mysig; inviato invecein occasione della seconda attesa dopo tale chiamata, non interrompe il processo, aconferma del significato che hai studiato relativo alla funzione setpgrp. Se attiva-to in background con il comando:$ verprg3 $$ &

il processo viene interamente eseguito poiché la sconnessione con il terminale,dovuta all'operatore & fa fallire le attese da quest'ultimo.Con lo shell csh si può osservare che l'attivazione in foreground, nonostante la chia-mata a setpgrp, fa arrivare il segnale SIGINT al processo in entrambi i punti diattesa. Nel caso background, la prima attesa fa andare il processo nello stato distopped. La cosa interessante è che, eliminando dal codice questo primo punto diattesa, l'esecuzione background ammette la seconda attesa dopo la chiamata asetpgrp, anche se un SIGINT non perviene al processo.Piccole differenze con lo shell in uso potrebbero produrre effetti leggermente diver-si da quelli qui illustrati, anche se sostanzialmente analoghi.

Esercizio 5: Usare il segnale SIGUSR1 gestito da un processo padre affinché que-st'ultimo visualizzi ogni 10 secondi su stdout il numero di RETURN battuti al termi-nale dopo l'ultima visualizzazione e "letti" dal figlio. Una riga che inizia con 'x' faterminare il programma, mediante kill del padre.

R:#include <stdio.h>#include <signal.h>#include <unistd.h>static int cnt = -1; /* variabile di conteggio */

void count(int sig) {/* routine di servizio per SIGUSR1 (passato come parametro)*/

cnt++;if (cnt != 0) /* salvo prima chiamata diretta */

fprintf(stderr, "arrivato ret\n");signal(sig, count);

}

G.28 L E T T U R A G

void main(int argc, char **argv){char s[40];int prot;

prot = ((argc > 1) && (argv[1][0] == 'p'));switch(fork()) {

case -1:perror("fork");exit(1);

case 0: /* figlio */printf("Figlio (%d), battere piu` “\

”RETURN (x RETURN per terminare)\n",getpid());

for(;;) {gets(s);if (s[0] == 'x') {

printf("Kill del padre\n");kill(getppid(), SIGKILL);exit(0);

}/* raccoglie return */kill(getppid(), SIGUSR1);

}/* esce a causa del kill del padre */

default: /* padre */printf(

"Padre (%d), attiva routine di servizio %s protezione\n",getpid(), prot ? "con" : "senza");

count(SIGUSR1);printf("Padre, entra nel ciclo di attesa a tempo\n");for(;;) {

int oldmask;sleep(10);if (prot) {

oldmask = sigblock(sigmask(SIGUSR1));printf(

"Padre, num. RETURN %d oldmask=0x%x, attende 2 sec\n", cnt,oldmask);

}else

printf("Padre, num. RETURN %d, attende 2 sec\n", cnt);

sleep(2); /* <A> */printf("Padre, ricicla attesa 10 sec\n");cnt=0;if (prot)

sigsetmask(oldmask);}

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.29

/* il padre esce per kill dal figlio */}

}In questo esempio viene evidenziata una possibile interferenza tra processo e rou-tine di servizio del segnale. Puoi notare infatti che è possibile che qualche RETURNnon venga conteggiato se l'interruzione inviata con il segnale dal figlio capita pro-prio dopo che il padre ha visualizzato il valore corrente della variabile cnt e primadel suo azzeramento (punto <A> del programma). Per renderlo valutabile median-te collaudo, è stata predisposta una soluzione che ammette un parametro aggiunti-vo (un lettera p) sulla linea di comando. Se presente questo parametro, viene crea-ta, mediante le funzione sigblock e sigsetmask, una regione critica compren-dente le due operazioni di lettura-visualizzazione e modifica, in modo da renderlein questo caso un'unica operazione indivisibile. Si verifica bene invece la perdita diconteggio se, attivando il programma senza il citato parametro, si batte qualcheRETURN mentre il processo attende 2 secondi nel punto <A>.

G.9.3 ESERCIZI SUL PARAGRAFO G.3

Esercizio 6: Che cos'è un IPC? Che cosa significa creare una risorsa IPC? Chi hadiritto ad usare un IPC? Ed a rimuoverlo (cioè a distruggerlo)?

R: Un IPC è uno strumento di comunicazione e sincronizzazione tra processianche scorrelati. Gli IPC si distinguono in code di messaggi, semafori e memoriacondivisa. Quando un processo crea una risorsa IPC, fornisce una chiave: l'opera-zione riserva una risorsa, prendendola tra quelle libere, e la rende usabile dai pro-cessi. Il creatore ottiene dal sistema un handle associato univocamente alla risorsaallocata e impone alla risorsa opportuni permessi. Da questo momento tutti i pro-cessi che verranno a conoscenza dell'handle, creatore compreso, e che sono compa-tibili con i permessi associati allo IPC, lo possono utilizzare. Per conoscere l'handleè anche possibile farne apposita richiesta al sistema conoscendone la chiave. Solo ilcreatore ha diritto di rimuovere la risorsa, restituendola al pool di risorse libere.

Esercizio 7: Fornire l'analogia esistente tra tipiche operazioni su file e quelle suIPC.

R: Vedere la seguente tabella:Operazione IPC Operazione file Commentoipcget creat + open crea IPC assegnando

permessi e ID variipcget senza IPC_CREAT open fornisce l'handle di un IPC

già creatoipcctl con IPC_RMID unlink rimuove l'IPC (se ne ha

diritto)ipcctl con IPC_STAT stat interrogazione sulle

caratteristiche dell'IPCipcctl con IPC_SET chown e chmod modifica permessi e ID vari

(se ne ha diritto)

G.30 L E T T U R A G

G.9.4 ESERCIZI SUL PARAGRAFO G.4

Esercizio 8: Che cos'è una coda di messaggi? In che cosa consiste la sincronizza-zione? Per quali motivi un ricevente in attesa si sblocca?

R: Una coda di messaggi è un IPC che consente la bufferizzazione in una codaFIFO di record di byte di lunghezza arbitraria. La sincronizzazione è del tipo pro-duttore-consumatore: il ricevente attende se non vi sono messaggi in coda, l'invian-te attende solo se la memoria per la bufferizzazione del messaggio è insufficiente.L'attesa può comunque essere evitata con il flag IPC_NOWAIT. Il ricevente si sbloc-ca quando un messaggio, di tipo compatibile a quello fornito nella primitiva di rice-zione, viene inviato alla coda da un altro processo, oppure se al ricevente vieneinviato un segnale (non ignorato). Il ricevente nemmeno si blocca se la primitiva diricezione viene chiamata con il flag IPC_NOWAIT oppure se un messaggio adegua-to è già presente in coda al momento della chiamata.

Esercizio 9: Che significato hanno i parametri size e type nella chiamata msgrcv?

R: size rappresenta il numero massimo di byte che l'applicazione consente ven-gano copiati nell'area fornita nella chiamata. Se il messaggio ricevuto è più lungodel valore di size, la copia avviene solo se nella chiamata è precisato il flagMSG_NOERROR, ma in questo caso il ricevente non può sapere quale fosse la lun-ghezza originaria a meno che l'inviante non l'abbia scritta nei primi byte del mes-saggio stesso. Altrimenti la copia non avviene e al ricevente viene passato un codi-ce di errore. Ovviamente è opportuno che i processi dell'applicazione si mettanod'accordo in modo da usare aree di ampiezza comunque sufficiente. Il tipo consen-te invece la specificazione di un selettore: un valore nullo disabilita la selezione, unvalore positivo seleziona messaggi con quel valore di tipo, un valore negativo è l'op-posto del massimo valore di tipo ammesso per un messaggio da estrarre.Nell'ultimo caso, se più messaggi verificano tale condizione, viene preso quello ditipo minimo.

Esercizio 10: Scrivere il segmento di codice che consente ad un processo di riceve-re messaggi con priorità, classificate in A, B, C in ordine crescente. Fare in modoche venga in ogni istante prelevato il messaggio di priorità più elevata.

R: Si tratta di assegnare il tipo 1 ai messaggi di priorità C, 2 ai B e 3 agli A. Unsegmento di codice secondo richiesta è del seguente tipo:if ( (msgrcv (qid, &msgbuf, msgsiz, -3L, IPC_NOWAIT) != -1))

&& (errno == ENOMSG) )/* se non ci sono già messaggi, di cui verrebbe prelevato ilpiù prioritario, attendi il primo messaggio che arriverà */

msgrcv (qid, &msgbuf, msgsiz, 0L, 0);

Esercizio 11: Predisporre e collaudare un programma con 2 processi, un padre eun figlio, tale che il padre trasferisce le righe (si sanno lunghe al massimo 60 carat-teri) da stdin al figlio attraverso una coda di messaggi. Il tipo del messaggio preve-de un valore numerico rappresentato dalla cifra 1..9 che eventualmente compare inprima posizione in ciascuna riga, o il valore 10 se il primo carattere non è nel rangeprevisto. Il figlio estrae ogni 10 secondi tutti i messaggi nella coda con tipo nelrange 1..9, visualizzandoli in ordine di tipo crescente: allo scopo si cerchi di sfrut-

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.31

tare il meccanismo di selezione indotto con un valore di tipo negativo per la chia-mata msgrcv Visualizza anche il numero di messaggi non estratti. La terminazio-ne del figlio deve avvenire mediante un segnale SIGTERM.

R: La soluzione è analoga a quella dell'esempio 3, solo che le righe non vengonospezzate. Il campo del buffer ha pertanto lunghezza 61, al fine di contenere la rigamassima; il tipo converte come cifra numerica il primo carattere della riga. Il nume-ro di messaggi non estratti è ricavato con una chiamata a msgctl e flag IPC_STAT.Occorre anche predisporre una routine di servizio per il figlio associata al segnaleSIGTERM.#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/msg.h>#include <signal.h>#include <errno.h>#include <unistd.h>#define MAXSTR 60 /* massima lunghezza riga */#define MYCODE 59 /* codice ftok *//* struttura messaggio, una istanza come buffer */struct mymsg {

long type;char s[MAXSTR+1];} buf = {{0}};

int stop = 0; /* variabile per la terminazione del figlio */

/* routine di servizio segnale SIGTERM */void mysig (int sig) {

fprintf(stderr, "figlio, ricevuto SIG=%d\n", sig);stop = 1;

/* exit(0); */}

void main() {struct msqid_ds mst;key_t key;int mid, ret, pid;char s[20*MAXSTR];

if ((key = ftok(".", MYCODE)) == -1) {perror("ftok");exit(1);

}if ((mid = msgget(key, 0600 | IPC_CREAT | IPC_EXCL))

== -1) {perror("creazione coda messaggi");exit(2);

G.32 L E T T U R A G

}switch(pid = fork()) {

case -1:perror("fork");exit(2);

case 0: /* il figlio eredita l'indice della coda */signal (SIGTERM, mysig);

/* per la terminazione del figlio */for(;;) {

fprintf(stderr, "figlio, riceve messaggi\n");while(msgrcv(mid, &buf, MAXSTR+1, -9L, IPC_NOWAIT)

!= -1)/* ricevuto messaggio */puts(buf.s);

if (msgctl(mid, IPC_STAT, &mst) != -1)fprintf(stderr,

"Figlio, messaggi non previsti rimasti in coda = %d\n",mst.msg_qnum);

if (errno == EINTR)/* segnale di term. mentre in attesa */exit(0);

if (errno != ENOMSG) {/* errore */perror("figlio msgrcv");exit(3);

}fprintf(stderr, "figlio, attende 10 sec\n");sleep(10);if (stop)

/* segnale di term. mentre in attesa */exit(0);

}default: /* padre, invia le righe */

for (;;) {if (gets(s) == NULL) {

/* EOF */kill(pid, SIGTERM); /* uccide il figlio */fprintf(stderr,

"Padre, attende terminazione figlio\n");wait(&ret);if (msgctl(mid, IPC_RMID, 0) == -1)

perror("rimozione coda");fprintf(stderr, "Padre, finito, ret figlio=%d\n",

ret);exit(0);

}/* invia riga */strncpy(buf.s, s, MAXSTR);

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.33

buf.s[MAXSTR] = '\0';buf.type = (s[0]>='1') && (s[0]<='9') ?

(long)(s[0]-'0') : 10;if (msgsnd(mid, &buf, (strlen(s)>MAXSTR) ?

MAXSTR+1: strlen(s)+1, 0) == -1) {perror("padre invio riga");exit(2);

}}

}}

Si può notare la particolare gestione relativa alla ricezione del segnale di termina-zione, nei vari casi in cui può pervenire al processo. Per metterla in maggior evi-denza, è stata evitata la chiamata exit direttamente nella routine di servizio e siè preferito inserirla dopo la chiamata sleep qualora il flag stop denoti la ricezio-ne del segnale.

G.9.5 ESERCIZI SUL PARAGRAFO G.5

Esercizio 12: Cos'è un semaforo in UNIX? Quali operazioni si possono eseguire sudi esso? Qual è la logica di indivisibilità?

R: Un semaforo UNIX è in realtà un gruppo di semafori numerici, cioè possonoassumere ciascuno un valore intero >= 0. Le operazioni sono incrementi e decre-menti multipli e test di zero. Il test e il decremento possono produrre il blocco delprocesso. Le operazioni possono essere raggruppate in una lista. La logica di indi-visibilità stabilisce che tutte le operazioni di una lista vengano eseguite in unicasoluzione, purché nessuna di esse produca il blocco del processo, altrimenti non ven-gono eseguite e il processo si blocca se non è stato definito il flag IPC_NOWAIT perl'operazione bloccante. Al risveglio la lista viene di nuovo valutata nella sua inte-rezza.

Esercizio 13: In base alla logica di indivisibilità, cosa si può dire per il test di zeropresente in una lista? Come si può esprimere la stessa cosa con il formalismo dellereti di Petri?

R: La logica di indivisibilità garantisce che i semafori in cui il test di zero ha datoesito positivo rimangano a zero per tutta la durata delle operazioni di modifica inse-rite nella lista ed effettivamente eseguite. La cosa si può rappresentare con una sin-gola transizione che ha tanti archi disabilitanti quanti i corrispondenti test di zero:se la transizione risulta abilitata, lo scatto è indivisibile e le operazioni di modifica,rappresentate da archi pesati entranti (decrementi) e/o uscenti (incrementi), ven-gono eseguite senza che altre operazioni sui semafori possano interferire.

Esercizio 14: Simulare il funzionamento di un buffer circolare di MAXSIZ byte suun file mediante due funzioni inser e extr che rispettivamente consentono l'inse-rimento e l'estrazione di un byte. Organizzare una coppia di processi padre-figliosecondo il modello produttore-consumatore e con 2 semafori (numerici) che control-lano l'accesso al buffer simulato come area di comunicazione.

G.34 L E T T U R A G

R: Si può utilizzare un unico set di 2 semafori, uno inizializzato a 0 (elementipieni) e uno a MAXSIZ (elementi vuoti). Il padre apre il file in scrittura e genera ilfiglio, che inizialmente eredita il file aperto ma poi lo riapre con un descrittoredistinto in modo che le operazioni di lettura e scrittura vengano eseguite su ele-menti distinti del file, eliminando qualsiasi problema di mutua esclusione. Il pro-gramma in sostanza fa una copia da stdin a stdout.#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <fcntl.h>#include <unistd.h>#define P_OP (-1) /* operazione P su semaforo */#define V_OP (1) /* operazione V su semaforo */#define MYCODE 53 /* codice per ftok */#define MAXBUFSIZ 256 /* size buffer */#define SCRISEM 0 /* indice sem. elementi scritti */#define DISPSEM 1 /* indice sem. elementi disponibili */#define MYEOF '\004' /* carattere di EOF nel buffer */

void inser(int fd, char c) {/* inserisce nel buffer fd */

if ( (tell(fd) % (long) MAXBUFSIZ) == 0L) /* overflow */lseek(fd, 0L, SEEK_SET);

write(fd, &c, 1);}

char extr(int fd) {/* estrae da buffer fd */char c;

if ( (tell(fd) % (long) MAXBUFSIZ) == 0L) /* overflow */lseek(fd, 0L, SEEK_SET);

read(fd, &c, 1);return c;

}

void main() {key_t key;static int sid; /* due semafori, disponibili e scritti */int fdp, fdc, pid, pos;char c, str[128];union semun arg;/* operazioni sui semafori */struct sembuf getdisp[] = {

DISPSEM, P_OP, 0};struct sembuf getscri[] = {

SCRISEM, P_OP, 0};

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.35

struct sembuf putdisp[] = {DISPSEM, V_OP, 0};

struct sembuf putscri[] = {SCRISEM, V_OP, 0};

if ((fdp = open("bufc", O_WRONLY | O_CREAT, 0600)) == -1) {perror("apertura buffer");exit(1);

}if ((key = ftok(".", MYCODE)) == -1) {

perror("ftok");exit(2);

}if ((sid = semget(key, 2,

0600 | IPC_CREAT | IPC_EXCL)) == -1) {perror("creazione semaforo");exit(3);

}/* inizializza semaforo elementi disponibili */arg.val = MAXBUFSIZ;if (semctl(sid, DISPSEM, SETVAL, &arg) == -1) {

perror("set value sem ELDISP");exit(4);

}/* inizializza semaforo elementi scritti */arg.val = 0;if (semctl(sid, SCRISEM, SETVAL, &arg) == -1) {

perror("set value sem ELSCRI");exit(5);

}/* fa partire figlio consumatore */switch(pid = fork()) {

case -1:perror("fork");exit(5);

case 0: /* figlio consumatore, attende su ELSCRI */close(fdp);if ((fdc = open("bufc", O_RDONLY, 0600)) == -1) {

perror("consumatore, apertura buffer");exit(6);

}for (;;) {

if (semop(sid, getscri, 1) == -1) {perror("getscri consumatore");exit(7);

}if ((c = extr(fdc)) == MYEOF)

/* fine file, figlio termina */exit(0);

G.36 L E T T U R A G

putchar(c);if (semop(sid, putdisp, 1) == -1) {

perror("putdisp consumatore");exit(8);

}}

default: /* padre, produttore */for (;;) {

if (fgets(str, 128, stdin) == NULL)str[0] = MYEOF;

pos = 0;do {

if (semop(sid, getdisp, 1) == -1) {perror("getdisp produttore");exit(9);

}inser(fdp, str[pos++]);if (semop(sid, putscri, 1) == -1) {

perror("putscri produttore");exit(10);

}} while ((str[pos-1] != '\n') &&

(str[pos-1] != MYEOF));if (str[pos-1] == MYEOF)

break;} /* for */

}fprintf(stderr, "Padre attende term. figlio\n");pid = wait(&pos);fprintf(stderr, "Terminato figlio, rimuove sem\n");if (semctl(sid, 0, IPC_RMID, 0) == -1)

perror("rimozione sem");fprintf(stderr, "Padre finito\n");exit(0);

}

G.9.6 ESERCIZI SUL PARAGRAFO G.6

Esercizio 15: Che cos'è la memoria condivisa? Si tratta di memoria fisica o vir-tuale? Si può allocare memoria di questo tipo ma privata di un singolo processo?

R: Per memoria condivisa si intende un meccanismo di allocazione dinamica disegmenti di ampiezza fissata all'atto della creazione, che ciascun processo (con idovuti permessi) può collocare nel proprio spazio di indirizzamento. Vista dal pro-cesso è sempre memoria virtuale e sarà responsabilità dello strato di gestione dellamemoria eseguire il mapping tra il range di indirizzi virtuali in cui è collocato unsegmento e gli indirizzi fisici corrispondenti. Pur non essendo vietata la creazionedi memoria condivisa privata di singoli utenti (basta chiudere i permessi di lettura

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.37

e scrittura per tutti gli altri utenti), non si può in generale evitare che altri proces-si creati dallo stesso utente possano "attaccarsi" al segmento. Comunque questeoperazioni non avrebbero senso visto che il consueto allocatore (malloc) fa egre-giamente allo scopo poiché opera nello spazio virtuale privato del processo.

Esercizio 16: Se dopo l'uso tutti i processi che hanno fatto riferimento ad un seg-mento condiviso lo scollegano (shmdt), compreso il creatore, il segmento scompare?

R: No, rimane congelato con il suo contenuto di quel momento. Processi successi-vamente possono eseguire un attach al segmento e recuperare i valori memoriz-zati perché al solito ne conoscono l'handle.

Esercizio 17: Ampliare la soluzione dei lettori scrittori, vista nella sezione suisemafori, aggiungendo come area di scambio un vettore-dizionario costituito da nelementi. ogni elemento è costituito da una stringa alfabetica di 6 caratteri e da unvalore intero associato di 2 byte. I lettori forniscono la stringa da ricercare e rice-vono in risposta il valore associato. Gli scrittori possono modificare il valore asso-ciato ad una stringa esistente oppure aggiungere una nuova coppia stringa-valore.La realizzazione è di tipo semplice per ciò che attiene la gestione del dizionario: nonordina le stringhe e consente un numero massimo prefissato di inserzioni.

R: La soluzione si commenta da sola.#include <stdio.h>#include <string.h>#include <ctype.h>#include <sys/types.h>#include <sys/ipc.h>#include <sys/sem.h>#include <sys/shm.h>#include <unistd.h>#define MAXSTRSIZ 6#define MAXELEM 8 /* per provare overflow, aumentarlo */#define maxel (&(table->tab[MAXELEM]))#define MYKEY 0x4567Ltypedef struct elemmodel {

char str[MAXSTRSIZ]; /* simbolo */short val; /* valore */} elem;

/* tipo elementi tabella */struct tabletype {

elem *last; /* ultima posizione scritta */elem tab[MAXELEM]; /* tabella vera e propria */

} *table;/* puntatore alla tabella in shared memory */

elem *find(char *str) {/* ricerca simbolo str nella tabella e ritorna puntatore

all'elemento se trovato */elem *p = table->tab;

if (p > table->last)

G.38 L E T T U R A G

/* tabella vuota */return (elem *)NULL;

while (strncmp(p->str, str, MAXSTRSIZ) != 0)if (p == table->last)

/* simbolo non trovato */return (elem *) NULL;

elsep++;

return p;}

elem *ins(elem *pos, char *str, short val) {/* sostituisce valore val all'elemento puntato da pos oppurese pos==NULL, inserisce nuovo elemento str-val alla finedella tabella */

if (pos == NULL) {/* inserisce alla fine */if (table->last == maxel) {

/* overflow */printf("** overflow tabella **\n");return (elem *)NULL;

}printf("inserisce <%s> con valore %d\n", str, val);strncpy((++(table->last))->str, str, MAXSTRSIZ);table->last->val = val;return table->last;

}/* pos != NULL */printf("sostituisce <%s> con valore %d\n", str, val);pos->val = val;return pos;

}

/* processo lettore */void lettore(void) {int sid, shid; /* ID sem e shm */char str[80];struct sembuf lock_shared[] = { /* lock condiviso */

1, 0, 0, 0, 1, SEM_UNDO};struct sembuf unlock_shared[] = { /* unlock condiviso */

0, -1, SEM_UNDO};elem *pos;

printf("lettore, init risorse\n");if ((sid = semget(MYKEY, 2, 0600 | IPC_CREAT))

== -1) {perror("lettore creazione semaforo");exit(1);

}

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.39

if ((shid = shmget(MYKEY, sizeof(struct tabletype),IPC_CREAT | 0600) ) == -1) {

perror("lettore creazione shm");exit(2);

}if ((table = (struct tabletype *)shmat(shid, (char *)

NULL, 0)) == (struct tabletype *) (-1L)) {perror("lettore shmat");exit(3);

}printf("lettore acquisisce simboli da stdin\n");while(gets(str) != NULL) { /* acquisisce simbolo da stdin ed esegue lettura dalla

tabella */printf("**** lettore entra zona critica\n");

if (semop(sid, lock_shared, 2) == -1) {perror("lettore lock shared");exit(4);

}printf("**** lettore entra effettua ricerca <%s>\n", str);

if ((pos = find(str)) != NULL) {/* il rilascio avviene dopo la visualizzazione del

messaggio che contiene un accesso alla tabella */printf("simbolo <%.*s> trovato, valore %d\n",

MAXSTRSIZ, str, pos->val);if (semop(sid, unlock_shared, 1) == -1) {

perror("lettore unlock shared");exit(5);

}}else {

/* non accede piu` alla tabella, la rilascia primadella visualizzazione del messaggio */

if (semop(sid, unlock_shared, 1) == -1) {perror("lettore unlock shared");exit(6);

}printf("simbolo <%.*s> NON trovato\n",

MAXSTRSIZ, str);}

}if (shmdt(table) == -1)

perror("lettore detach");if (shmctl(shid, IPC_RMID, 0) == -1)

perror("lettore rimozione shm");if (semctl(sid, 0, IPC_RMID, 0) == -1)

perror("lettore rimozione sem");}

G.40 L E T T U R A G

/* processo scrittore */void scrittore(void) {char *p;int sid, shid;elem *pos;char str[80];struct sembuf lock_excl[] = { /* lock esclusivo */

0, 0, 0, 1, 0, 0, 1, 1, SEM_UNDO};struct sembuf unlock_excl[] = { /* unlock esclusivo */

1, -1, SEM_UNDO};printf("scrittore, init risorse\n");if ((sid = semget(MYKEY, 2, 0600 | IPC_CREAT ))

== -1) {perror("scrittore creazione semaforo");exit(1);

}if ((shid = shmget(MYKEY, sizeof(struct tabletype),

IPC_CREAT | 0600) ) == -1) {perror("scrittore creazione shm");exit(2);

}if ((table = (struct tabletype *)shmat(shid, (char *)

NULL, 0)) == (struct tabletype *) (-1L)) {perror("scrittore shmat");exit(3);

}/* init tabella */table->last = (table->tab)-1;

/* elemento virtualmente precedente al primo */printf("scrittore acquisisce simboli-valori da stdin\n");while(gets(str) != NULL) {

/* acquisisce stringa e valore da stdin ed inseriscenella tabella */

p = str;while (!isspace(*p)) p++;

/* salta stringa */*p++ = '\0';

/* isola stringa */while (isspace(*p)) p++;

/* salta spazi successivi, p punta a valore */printf("lettore accede a sezione critica\n");if (semop(sid, lock_excl, 3) == -1) {

perror("scrittore lock excl");exit(4);

}printf("scrittore, accede tabella\n");pos = ins(find(str), str, atoi(p));

/* tenta inserimento simbolo nella tabella:

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.41

puo` uscire subito dalla regione critica */printf(

"scrittore esce da regione critica, battere return\n");{char s[10]; gets(s); };

if (semop(sid, unlock_excl, 1) == -1) {perror("scrittore unlock excl");exit(5);

}if (pos != NULL)

printf("simbolo <%.*s> inserito con valore %d\n",MAXSTRSIZ, str, atoi(p));

elseprintf("simbolo <%.*s> modificato con valore %d\n",

MAXSTRSIZ, str, atoi(p));}

}

void main(){int pid;

printf("Lettori scrittori: crea processi\n");if ((pid = fork()) == 0) {

/* scrittore */scrittore();exit(0);

}if ((pid = fork()) == 0) {

/* lettore */lettore();exit(0);

}printf("generatore, attende due terminazioni\n");wait(&pid);printf("generatore, attende seconda terminazione\n");wait(&pid);printf("generatore, esce\n");exit(0);

}

G.9.7 ESERCIZI SUL PARAGRAFO G.7

Esercizio 18: Che cosa si intende per coroutine e per daemon?R: In ambiente UNIX, una coppia di coroutine è una coppia di processi che con-

corrono, generalmente attraverso una esecuzione alternata e coordinata, al conse-guimento di uno scopo complessivo. Tipicamente l'evoluzione di una delle coroutinecorrisponde all'attesa dell'altra su qualche condizione bloccante, ad esempio I/O suun dispositivo. Un daemon è invece un processo di tipo server, figlio di init, non

G.42 L E T T U R A G

collegato ad un terminale e che è in grado di fornire servizi ad un insieme di uten-ti diversi. La richiesta dei servizi e le conseguenti risposte possono trasferirsi attra-verso appositi canali di comunicazione (IPC, pipe ecc.).

Esercizio 19: Costruire e collaudare un daemon che consenta l'interrogazione diun data base costituito da un certo numero di elementi stringa-valore intero.

R: La soluzione comprende un programma di interfaccia e un daemon. La strin-ga di richiesta viene inviata nel pipe di nome FIFO, le risposte in un pipe con nomecreato dal programma di interfaccia e comunicato al daemon insieme alla stringa.Il programma d'interfaccia prevede che si forniscano nome pipe e stringa comeparametri della linea di comando./* procedura di interfaccia */#include <stdio.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#define MAXSTR 80 /* dim. massima stringa riposta */

void main(int argc, char **argv) {FILE *fp1, *fp2;char str[MAXSTR];int val;

if (argc < 3) {fprintf(stderr,

"** uso: cerca pipename searchstring\n");exit(1);

}if (mknod(argv[1], S_IFIFO | 0600, 0) == -1) {

perror("mknod");exit(2);

}if((fp1 = fopen("FIFO", "w")) == NULL) {

perror("open pipe FIFO");exit(3);

}fputs(argv[1], fp1);

/* comunica al daemon nome pipe */fputs("\n", fp1);fputs(argv[2], fp1);

/* comunica al daemon stringa di ricerca */fputs("\n", fp1);fclose(fp1);

/* chiude pipo FIFO *//* aperto in anticipo il pipe in lettura prima che vi

scriva il daemon */if((fp2 = fopen(argv[1], "r")) == NULL) {

perror("open pipe personale");exit(4);

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.43

}fgets(str, MAXSTR, fp2);if ((val = atoi(str)) == -1)

printf("<%s> non trovata\n", argv[2]);else

printf("<%s> e` associata al valore = %d\n", argv[2],val);

fclose(fp2);if (unlink(argv[1]) == -1) {

perror("rimozione pipe personale");exit(5);

}exit(0);

}

/* daemon */#include <stdio.h>#include <string.h>#include <sys/types.h>#include <sys/stat.h>#include <unistd.h>#include <fcntl.h>#define MAXSTR 80struct tabel { char *s; int v;} table[] = {{"pippo", 3},{"carlo", 5},{"giulio", 3},{"sergio", 10},{"boh", 2},{NULL, 0}};

int find(char *s)/* ricerca nella tabella il valore associato a s*/{struct tabel *p = table;

/* semplice ricerca lineare */while ((p->s) != NULL)

if (strcmp(s, p->s) == 0)return p->v;

elsep++;

return -1;}

void main(){FILE *fp, *fp2;

G.44 L E T T U R A G

int pid, val;char str[MAXSTR];int fd;if ((pid = fork()) == 0) {setpgrp(0, 0);fprintf(stderr, "daemon, crea FIFO pipe\n");

if(mknod("FIFO", S_IFIFO | 0666, 0) == -1) {/* permessi estesi a tutti */perror("daemon creazione pipe");exit(1);

}for(;;) {if((fp = fopen("FIFO", "r")) == NULL) {

perror("daemon apertura pipe");exit(2);

}if ((pid = fork()) == 0) {

/* corpo daemon come figlio */setpgrp();

/* si sconnette dal terminale *//* **** qui ci dovrebbero essere una serie di chiamate

a signal per gestire i segnali: omessa persemplicita` */

for (;;) {/* ciclo infinito */fgets(str, MAXSTR, fp);str[strlen(str)-1] = '\0';

/* elimina newline */if ((fp2 = fopen(str, "w")) == NULL)

/* in caso di errore, ignora e ricicla */continue;

fgets(str, MAXSTR, fp);str[strlen(str)-1] = '\0';

/* elimina newline */sprintf(str, "%d", find(str));fputs(str, fp2);fputs("\n", fp2);fclose(fp2);fclose(fp);

} /* for */}else if (pid == -1) {

perror("daemon, creazione figlio");exit(3);

}/* padre esce subito */

}

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.45

G.10 Test sulla Lettura G

Test 1: I pipe e la memoria condivisa sono meccanismi di comunicazione:a) solo per processi padre-figlio;b) per processi che possono trovarsi su macchine distinte collegate in rete;c) tra due soli processi;d) tra più processi.

Test 2: Un segnale in generale:a) interrompe immediatamente un processo swapped;b) sblocca sicuramente un processo che è in attesa su una chiamata pause;c) può sbloccare un processo che è in attesa di qualche evento in dipendenza del-

l'evento stesso e delle predisposizioni precedenti del processo;d) non può essere inviato ad un singolo processo da parte di un altro processo che

non sia il padre del primo.

Test 3: Se un processo A ha eseguito la seguente chiamata:signal(SIGUSR1, f);

e successivamente la chiamata:kill(0, SIGUSR1);

allora:a) un processo B dello stesso gruppo di A eseguirà come conseguenza la funzione f;b) il processo swapper (PID = 0) riceverà il segnale;c) nessun processo riceverà il segnale ma dal codice di ritorna dalla chiamata kill

A può stabilire se la definizione di f come routine di servizio è valida;d) il processo A eseguirà la funzione f.

Test 4: Un IPC:a) è un meccanismo di comunicazione che ciascun singolo processo può allocare

dichiarando una variabile di un tipo di dati completamente definito in un inclu-de file di libreria;

b) è utilizzabile da un qualsiasi processo che disponga del relativo handle;c) può essere utilizzato anche dopo che è terminato il processo che l'ha creato;d) viene deallocato quando termina il processo che l'ha creato.

Test 5: Quando nel creare un IPC un processo A usa la chiave (valore di tipokey_t) pari a IPC_PRIVATE:a) se un altro processo ha creato lo stesso tipo di IPC con la stessa chiave, allora la

primitiva di creazione fallisce;b) solo il processo A può normalmente "autorizzare" altri processi ad utilizzare

l'IPC;c) solo il processo A può usare l'IPC e non altri processi;d) potrebbe essere restituito l'handle di un analogo IPC solo però se creato in pre-

cedenza dallo stesso processo A.

Test 6: Se un processo A esegue una chiamata:msgrcv(id, &m, 200, -3L, IPC_NOWAIT);

a) se in coda vi è in ordine di arrivo un messaggio di tipo 4 e uno di tipo 3, vieneestratto quello di tipo 3;

G.46 L E T T U R A G

b) se in coda vi è in ordine di arrivo un messaggio di tipo 4 e uno di tipo 3, vieneestratto quello di tipo 4;

c) non vengono estratti messaggi e il processo attende;d) non vengono estratti messaggi e il processo non attende e la chiamata restitui-

sce un codice di errore ENOMSG.

Test 7: Un semaforo UNIX è in generale:a) un gruppo di semafori binari del tutto indipendenti tra loro;b) un gruppo di semafori numerici identificati da un unico handle;c) un singolo semaforo numerico ma sul quale si possono eseguire nello stesso

istante (cioè in modo indivisibile) una lista di operazioni;d) sinonimo di vettore intero in memoria condivisa.

Test 8: Il seguente codice in linguaggio C:struct sembuf sops[2] = { 0, 0, 0, 1, -1, SEM_UNDO};semop(sid, sops, 2);

a) esegue dapprima un test di zero sul primo dei due semafori e, se non fallito, ese-gue certamente il decremento sul secondo semaforo e poi ritorna;

b) come a) solo che il decremento avviene esclusivamente se il valore del secondosemaforo è > 0, altrimenti, a causa del flag indicato, la chiamata termina e resti-tuisce un codice di errore;

c) come b) solo che se il secondo semaforo è = 0, allora il processo attende e la chia-mata si completa certamente quando un altro processo effettua un'operazione diincremento del semaforo stesso;

d) come c) ma non è detto che la chiamata si completi quando un altro processoeffettua un'operazione di incremento del semaforo stesso.

Test 9: Un semaforo UNIX:a) consente che un processo attenda il verificarsi contemporaneo di 3 eventi;b) non consente con un'operazione indivisibile, di verificare l'avvenuta generazio-

ne di più di 2 eventi (o segnalazioni);c) consente che un processo attenda che si siano verificati 3 eventi prodotti cia-

scuno da uno di 3 altri processi;d) non consente di realizzare l'equivalente di una transizione di una rete di Petri

che abbia un arco in ingresso da un posto P e un arco in uscita verso lo stessoposto P.

Test 10: Dopo che un processo A ha creato un segmento di memoria condivisa diampiezza pari a 1000 byte:a) un processo B creato da un altro utente può scrivere il 35o byte del segmento con-

diviso chiamando l'opportuna primitiva di sistema con l'ID del segmento, comu-nicatogli da A;

b) A può leggere il 35o byte pur di disporre del puntatore al segmento ottenutomediante l'opportuna primitiva di sistema;

c) un processo B può accedere al segmento solo se è figlio di A;d) un processo B non può accedere al segmento se A ha eseguito una primitiva di

detach sul segmento stesso.

Test 11: La chiamata al sistema, eseguita da un processo A:p=shmat(id, (char *)200, SHM_RDONLY);

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.47

a) consente al processo A di eseguire:x = p[pos];

con pos qualsiasi;b) come a) ma con pos < 200;c) non consente di eseguire correttamente l'istruzione:

*p = '3';d) non consente di eseguire correttamente l'istruzione:

x = *p;

Test 12: Se si vuole che un processo A acceda (in modo indivisibile) ad un vettoredi 2 elementi floating point in memoria condivisa per scambiarli:a) si concorda con gli altri processi che vogliono accedere al vettore, di utilizzare un

semaforo ad un componente facendo un test di zero del semaforo e contestual-mente un suo incremento all'inizio della regione critica e un decremento delsemaforo al termine della regione;

b) si usa un puntatore attached al segmento che contiene il vettore con il flagSHM_RDONLY;

c) non occorre far nulla se i processi che vogliono accedere al vettore lo fanno insola lettura;

d) si disabilitano i segnali che possono interrompere il processo prima di entrarenella regione critica.

Test 13: Un daemon è:a) un processo figlio di init che, a causa di un errore, muore ma prima di termi-

nare rimuove qualche IPC possibilmente utilizzato da altri (da cui il suo parti-colare nome);

b) è un processo figlio di init che generalmente non risponde ad un SIGINT invia-to dal terminale di controllo del processo che originariamente l'aveva creato;

c) è un processo che gestisce una rete di elaboratori;d) è sinonimo di spooler.

Test 14: Una coppia di coroutine è tale se si tratta di:a) due processi che comunicano usando un pipe, named o unnamed;b) due processi che collaborano alternandosi nell'esecuzione e comunicando attra-

verso una coda di messaggi;c) due subroutine che si chiamano vicendevolmente (questo è anche chiamato

ricorsione indiretta);d) due programmi autonomi ma che usano due parti distinte dello stesso segmen-

to di memoria condivisa.

Test 15: Un socket è:a) un meccanismo che non consente la comunicazione tra processi sulla stessa

macchina (bisogna per questo usare gli IPC);b) una primitiva richiamabile dai processi per ottenere il file descriptor di un file

di comunicazione distribuito in rete;c) un particolare protocollo di comunicazione;d) una specie di identificatore di un processo, utilizzabile a livello di rete.

G.48 L E T T U R A G

G.11 Risposte ai test

1. d), 2. c), 3. d), 4. c), 5. b), 6. a), 7. b), 8. d), 9. c), 10. b), 11. c), 12. a), 13. b), 14. b),15. d).

UNIX: CO M U N I C A Z I O N E T R A P R O C E S S I G.49

Pagina lasciata intenzionalmente vuota