Inter-Process Communication

Pagina creata da Mattia Lorenzi
 
CONTINUA A LEGGERE
Inter-Process Communication∗
                                       C. Baroglio
                                     a.a. 2002-2003

1     Introduzione
In Unix i processi possono essere sincronizzati utilizzando strutture dati speciali,
appartenti al pacchetto IPC (inter-process communication). Queste strutture
consentono inoltre lo scambio di dati fra processi. IPC contiene tre tipi di
strutture: semafori, code di messaggi e memoria condivisa. Lo schema di utilizzo
delle tre è identico:
    1. uno dei processi alloca la struttura di interesse;
    2. tutti i processi che devono essere sincronizzati tramite la struttura, uti-
       lizzano una chiave comune, decisa a tempo di programmazione, per fare
       riferimento alla struttura allocata. Tale chiave è usata anche dal processo
       allocatore;
    3. le strutture di IPC possono essere utilizzate solo per mezzo di system call
       predefinite;
    4. una volta terminato il compito previsto, le strutture di IPC devono essere
       esplicitamente disallocate da uno dei processi in questione (non necessari-
       amente il processo che ha effettuato l’allocazione).
Per ciascuno dei tre tipi di strutture IPC sono definite una system call -get e
una system call -ctl: la prima consente alternativamente di allocare un semaforo,
una coda o un’area di memoria condivisa, oppure di ottenere l’identificatore di
accesso a una struttura allocata da un altro processo. La seconda system call
consente di eseguire operazioni diverse, specificate da nomi in codice, fra i quali
la disallocazione della struttura condivisa (operazione: IPC RMID). Infine per
ciascuna struttura sono previste system call specifiche che consentono, nel caso
dei semafori di eseguire le operazioni di sincronizzazione up e down, nel caso
delle code di messaggi di effettuare send e receive e, per la memoria condivisa,
di agganciare l’area in questione allo spazio degli indirizzi di un processo.
   ∗ Queste note contengono una descrizione ad alto livello delle system call viste a lezione ed

integrano i lucidi. Per i dettagli si consulti il manuale on-line.

                                               1
2    Chiavi ed identificatori interni
Una struttura di IPC è in un certo senso esterna a qualsiasi processo, anche
al processo allocatore, ed è gestita dal sistema operativo. Il numero di strut-
ture allocabili in un sistema è finito e ciascuna struttura è rappresentata da
una struttura dati contenente tutte le informazioni che la riguardano. Queste
strutture dati variano a seconda del tipo di oggetto IPC che consideriamo.
    Per allocare una struttura di IPC vengono utilizzate le system call -get:
semget (semafori), msgget (code di messaggi) e shmget (aree di memoria condi-
visa). In tutti e tre i casi il primo argomento è un numero intero detto chiave
e il risultato restituito dalla system call è un altro numero intero detto identi-
ficatore. In un certo senso anche la chiave è un identificatore della struttura in
questione, il suo uso è però assai diverso da quello del valore restituito da una
-get.
    Siano prog1.c e prog2.c i codici sorgenti di due programmi che generano due
processi P1 e P2 da sincronizzare. Il primo richiede l’allocazione di un semaforo,
che sarà utilizzato anche dal processo generato dal secondo programma. P1
eseguirà una semget, richiedendo al sistema operativo di allocare un semaforo.
Ottiene come risultato un identificatore che gli consente di accedere alla specifica
struttura semaforo allocata. P2 dovrà accedere, affinché le cose funzionino come
desideriamo, alla stessa struttura semaforo allocata da P1. P2 però non può
sapere quale struttura è stata allocata dal sistema operativo e quindi quale
identificatore è stato restituito a P1, a meno che P1 e P2 non abbiano definito
un nome convenzionale con il quale identificano la struttura di IPC in questione;
tale nome convenzionale è la chiave. Per fare un’analogia, facciamo finta che
invece di semafori si parli di numeri di telefono: un numero di telefono identifica
(in senso lato) un utente. Tale utente avrà però anche un nome: per telefonare a
una persona posso chiedere a un servizio automatico di darmi il numero associato
al nome che mi interessa. Ebbene, per tornare ai semafori, il nome è l’analogo
della chiave, il numero di telefono lo è dell’identificatore restituito da semget
e infine il servizio automatico è il sistema operativo. Non posso telefonare a
una persona se conosco solo il suo nome (chiave), devo avere il suo numero
(identificatore).

3    Allocazione e accesso
Ciascuna system call -get ha diversi usi e comportamenti che dipendono dai
valori del primo e dell’ultimo suo argomento. Il primo argomento, lo abbiamo
visto, è la chiave. L’ultimo è detto normalmente “flag” e specifica sia i diritti
di accesso da associare alla struttura condivisa sia alcune opzioni relative alla
creazione. Vediamo i casi principali.
    Supponiamo che un processo voglia allocare una nuova struttura dati con-
divisa. Può procedere in due modi. Il più semplice è utilizzare la specifica
convenzionale IPC PRIVATE come chiave. IPC PRIVATE forza l’allocazione
di un nuovo oggetto IPC, se vi è ancora spazio disponibile, il cui identifica-

                                         2
tore viene restituito al processo chiamante. Questa soluzione può apparire in
contrasto con quanto spiegato sopra, in quanto IPC PRIVATE non è una vera
chiave (è solo l’indicazione di creare forzatamente un nuovo oggetto), non può
quindi essere usata per accedere all’identificatore di uno specifico semaforo, a
una specifica coda o a una specifica area di memoria condivisa. Questa soluzione
è però molto comoda quando un processo alloca delle strutture i cui identifi-
catori verranno passati in eredità ai processi figli. Supponiamo che il codice
contenga l’istruzione:
                     semid = semget(IPC PRIVATE, . . . , . . . )
Il valore restituito da semget viene salvato in una variabile di tipo intero.
Quando il processo eseguirà una fork, il figlio erediterà una copia della vari-
abile semid e del suo valore. Erediterà quindi l’identificatore del semaforo allo-
cato, restituito dal sistema operativo e potrà usarlo per eseguire operazioni sul
semaforo, senza dover eseguire una seconda semget.
    La soluzione precedente funziona se i processi da sincronizzare sono in re-
lazione padre–figli. Non sempre questo è il caso. Più in generale i processi da
sincronizzare saranno avviati da comandi differenti all’interno di shell differenti.
Occorre quindi specificare chiavi effettive affinché possano usare le stesse strut-
ture IPC. Supponiamo di voler usare la chiave 1234 e che un processo esegua
semget(1234, . . . , . . . ). Ci sono due possibilità: o il semaforo richiesto non è an-
cora stato allocato oppure è già stato allocato da qualche processo (in generale
un processo diverso dal nostro). Nel secondo caso la semget restituisce semplice-
mente l’identificatore del semaforo allocato. Se invece non esiste alcun semaforo
con quella chiave, il sistema operativo può ancora eseguire due operazioni di-
verse: l’allocazione oppure la restituzione di un errore. Il comportamento tenuto
dipende dal valore dei flag. Entrambe le operazioni sono lecite: a seconda dello
schema di sincronizzazione che si intende implementare può darsi che il nostro
processo debba effettivamente interpretare il ruolo dell’allocatore della struttura
oppure può darsi che questo ruolo competa ad un altro processo. Nel secondo
caso, se il nostro processo scopre che la struttura non è ancora stata allocata,
deve fermarsi perché probabilmente qualcosa è andato storto.
    Supponiamo che non esista alcuna struttura IPC del tipo richiesto, con la
chiave specificata, e che il nostro processo debba fungere da allocatore. In
questo caso occorre specificare come flag della system call -get un or bit a bit,
i cui operandi sono la parola chiave IPC CREAT (senza ‘e’ finale) e i diritti di
accesso che si intende assegnare all’oggetto allocato; per esempio IPC CREAT
| 0600. IPC CREAT è una costante predefinita nel sistema, 0600 i diritti di
accesso di cui parleremo più avanti. Per esempio:
                 semid = semget(1234, . . . , IPC CREAT | 0600)
Si osservi che, nel caso in cui dovesse esistere una struttura di IPC con chiave
identica a quella specificata, la -get non eseguirebbe alcuna allocazione e si
limiterebbe a restituire l’identificatore trovato.
    Se invece vogliamo che il processo non esegua alcuna allocazione e si limiti
ad utilizzare una struttura IPC già allocata, restituendo errore nel caso che non

                                            3
la trovi, è importante non specificare IPC CREAT nei flag. Spesso si utilizza
in questo caso come flag il valore 0. Per esempio:
                          semid = semget(1234, . . . , 0)

4     Parametri specifici delle varie system call -get
Nelle sezioni precedenti sono state illustrate le caratteristiche comuni delle varie
system call -get. Ciascuna di esse ha però anche alcuni parametri specifici, che
catturano caratteristica della struttura di IPC considerata.
    La system call semget, che consente di allocare semafori, ha tre parametri.
Oltre ai due già descritti occorre specificare anche il numero di semafori da
allocare. La questione è che in Unix i semafori vengono allocati a pool: ogni
struttura IPC mantenuta dal sistema operativo contiene infatti un puntatore a
un vettore di semafori in senso tradizionale, ai quali è possibile accedere sin-
golarmente. Supponiamo, per esempio, che un processo padre debba allocare i
semafori privati dei propri 10 figli. In Unix può eseguire un’unica operazione di
allocazione, nella quale richiede di riservare un vettore di 10 semafori. Cias-
cun figlio potrà accedere al proprio semaforo privato specificandone l’indice
nell’array.
    La system call msgget, che consente di allocare una coda di messaggi ha solo
due parametri: la chiave e i flag, dei quali abbiamo già parlato.
    Infine, la system call shmget, per le aree di memoria condivisa, ha tre
parametri: i due visti più un numero che corrisponde alla dimensione in byte
dell’area da riservare.

5     System call -ctl
Le system call di questo tipo consentono di effettuare operazioni dette “di con-
trollo”. Ciascuna di esse consente di effettuare una certa varietà di operazioni
predefinite. I loro parametri sono: le informazioni necessarie ad identificare la
struttura su cui agire, una keyword che codifica l’operazione da svolgere, un
eventuale parametro dell’operazione.
    1. semctl(semid, semnum, op, param): alcune delle operazioni che possono
       essere svolte sui semafori tramite semctl agiscono sull’intero pool di se-
       mafori allocato, altre su di un elemento del vettore di semafori. Il primo
       parametro è l’identificatore del pool di semafori restituito da semget; il
       secondo è l’eventuale indice di una specifica componente del vettore. Il
       terzo è l’operazione da eseguire. Le più frequenti sono: SETVAL (assegna
       un valore alla componente di indice semnum del pool semid), GETVAL
       (restituisce il valore della componente di indice semnum del pool semid),
       IPC RMID (rimuove l’intero pool di semafori).
    2. msgctl(msqid, op, param): msqid è l’identificatore interno della coda di
       messaggi su cui si vuole operare, op è l’operazione da svolgere, ve ne sono

                                         4
diverse, fra di esse la più utilizzata è IPC RMID, che consente di rimuove
      la coda. Il terzo argomento è un parametro utile ad alcune delle operazioni
      possibili.
    3. shmctl(shmid, op, param): analoga alla precedente, per le aree di memoria
       condivisa.
Il tipo del terzo parametro varia a seconda della system call -ctl considerata.
Nel caso dei semafori si tratta di una union semun, nel caso delle code si tratta
di un puntatore a struct msqid ds (un tipo di dato predefinito), infine per le aree
condivise si tratta di un puntatore a struct shmid ds.
    La union semun non è un tipo predefinito quindi nel definire variabili di
questo tipo occorre specificare l’intera struttura, ad esempio: union semun { int
val; struct semid ds *buf; ushort t *array; } arg .

6     System call semop
semop è una system call specifica per i semafori. Consente di effettuare una
serie di operazioni sospensive (up e down) su di un pool di semafori. Ha tre
argomenti: l’identificatore interno del pool di semafori su cui agire, un vettore
di operazioni da eseguire, il numero di tali operazioni. Supponiamo di avere un
pool di 2 semafori e di volere che il nostro processo esegua una up sul primo e
una down sul secondo. Potrà eseguire le due operazioni con una sola chiamata
a semop, nel seguente modo:
          struct sembuf cmd[2];

          semid = semget(KEY, ...);
          . . . /* inizializza cmd[0] */
          . . . /* inizializza cmd[1] */

          semop(semid, cmd, 2);
L’esecuzione di una sola operazione (up o down) è quindi un caso particolare.
     Un’operazione è descritta da una variabile di tipo struct sembuf (tipo pre-
definito nel sistema), avente tre campi: short sem num; short sem op; short
sem flg. Nell’esempio precedente è stato definito un array di due elementi di
questo tipo, esso codificherà quindi due operazioni.
     L’operazione descritta da una variabile di tipo struct sembuf viene applicata
a una specifica componente del pool di semafori in questione. Tale componente
è identificata dal primo campo (sem num). Il secondo campo (sem op) codifica
l’operazione da compiere. Il terzo, che ignoreremo, specifica ulteriori comporta-
menti da tenere in taluni casi particolari; per noi varrà sempre zero.
     Il campo sem op codifica quindi il fatto che la system call debba compiere
una up oppure una down. Questo campo contiene un numero intero (short);
vediamo il comportamento assunto dal sistema operativo a seconda dei possibili

                                           5
valori di questo campo. Chiamando semval il valore corrente del semaforo che
ci interessa, ci sono tre possibilità:
    1. sem op = = 0: particolarità di Unix, attesa del valore zero.
       il processo viene sospeso se il semaforo ha valore maggiore di zero. verrà
       riattivato solo quando il semaforo assume valore zero. Non è un’operazione
       classica dei semafori.
    2. sem op > 0: up.
       semval viene incrementato di sem op unità.
    3. sem op < 0: down.

          • |sem op| ≤ semval: semval viene decrementato di sem op unità, il
            processo continua ad eseguire.
          • |sem op| > semval: sospensione del processo. Il valore del semaforo
            rimane immutato.

7     System call msgsnd e msgrcv
Vediamo ora le system call specifiche per le code di messaggi, che consentono
di effettuare le operazioni di invio (msgsnd) e recezione (msgrcv). Si tratta di
system call molto a basso livello che possono essere utilizzate per implementare
diverse politiche di sincronizzazione, con e senza attesa.
    L’invio di un messaggio è effettuato tramite msgsnd(int msqid, const void
*msgp, size t msgsz, int msgflg). Il primo parametro, msqid, è l’identificatore
interno della coda alla quale si intende inviare il messaggio. L’ultimo parametro,
msgflg, specifica il comportamento da tenere nel caso in cui la coda sia piena. Si
tratta di una situazione particolare, che difficilmente si verificherà in laboratorio
per via delle ridotte dimensioni del progetto. In generale quando la coda è
piena1 e risulta quindi impossibile inserire il messaggio da inviare, il processo
può alternativamente attendere che qualche altro processo consumi un messaggio
in coda, liberando spazio, oppure terminare immediatamente la msgsnd senza
attendere (opzione IPC NOWAIT).
    I due parametri msgp e msgsz riguardano il messaggio da spedire. Un
messaggio è genericamente contenuto in una struttura dati definita dall’utente
a seconda delle proprie esigenze. Supponiamo che si chiami, per esempio,
mio tipo mess. Tale struttura deve però forzatamente contenere due campi,
in quest’ordine: il primo deve essere un numero intero (long, strettamente mag-
giore di zero) indicante il tipo del messaggio, il secondo è il campo dati vero
e proprio e può essere di qualsiasi tipo. Fra i due non deve essere inserito al-
cun altro campo. La struttura mio tipo mess potrebbe, per esempio essere cosı̀
definita:
   1 può esserlo in due modi distinti: o perché contiene il numero massimo di messaggi possibili

o perché occupa le quantità massima di spazio per essa riservato

                                                6
struct mio tipo mess {
        ...
        long tipo;
        struct mio dato dati;
        ...
    }
Come vengono utilizzate queste informazioni? Il tipo del messaggio verrà dis-
cusso quando si parlerà di msgrcv. Cominciamo da msgp e msgsz. L’operazione
di inserzione di un messaggio in coda effettuata dalla msgsnd consiste in un’operazione
di copiatura. Il sistema operativo copia un numero di byte pari al valore di ms-
gsz, consecutivi all’indirizzo specificato da msgp all’interno della coda; msgp
deve essere l’indirizzo del campo tipo della struttura dati contenente il messag-
gio da inviare e dovrebbe essere convertito in un (void *).
    La system call msgrcv viene utilizzata da un processo che desidera ricevere
un messaggio di un tipo specificato e può essere bloccante o non bloccante.
È bloccante quando, qualora la coda non contenga messaggi del tipo di inter-
esse, il processo rimanga sospeso in attesa che giunga un tale messaggio, non
bloccante altrimenti (in questo caso occorre usare il flag IPC NOWAIT). ssize t
msgrcv(int msqid, void *msgp, size t msgsz, long int msgtyp, int msgflg) richiede
come primo argomento l’identificatore della coda in questione; il secondo argo-
mento è l’indirizzo di una variabile all’interno della quale verrà memorizzato il
messaggio ricevuto (void * è un puntatore generico, occorrerà fare un opportuno
cast al tipo di dato del messaggio letto); il terzo argomento è il numero di byte
dei quali è costituito il messaggio; msgtyp è il tipo di messaggio atteso; del flag
abbiamo già parlato.
    Il tipo del messaggio atteso deve essere un numero positivo; nel caso partico-
lare in cui si utilizza il valore zero, verrà estratto dalla coda il primo messaggio
in essa contenuto, indipendentemente dal suo tipo.

8    System call shmat e shmdt
Infine, per quel che riguarda la memoria condivisa, sono disponibili due system
call che consentono a un processo di agganciare un’area di memoria condivisa,
eventualmente allocata da un processo diverso, al proprio spazio degli indirizzi
e di sganciarla nel momento in cui non serve più.
    void *shmat(shmid, addr, flag) vuole come primo argomento l’identificatore
dell’area in questione (restituito da shmget). Come secondo indirizzo utilizzer-
emo sempre il valore zero. In questo modo lasceremo al sistema operativo la
scelta dell’indirizzo attraverso il quale agganciare l’area condivisa al segmento
dati. Il terzo argomento è costituito da eventuali flag di controllo. La system
call restituisce un indirizzo generico, del quale occorrerà effettuare il cast ad
un’opportuno tipo di dato. Se per esempio, in fase di allocazione era stato
richiesto di riservare una quantità di spazio sufficiente a contenere un dato di
tipo mio tipo dato, occorrerà convertire il puntatore generico restituito da shmat
in un mio tipo dato *.

                                        7
shmdt ha un solo argomento, un indirizzo restituito da una shmat. Dopo
aver eseguito una shmdt sull’indirizzo di un’area di memoria condivisa, non sarà
più possibile per il processo accedere all’area in questione, anche se questa non
verrà disallocata.

                                        8
Puoi anche leggere