Inter-Process Communication
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
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