Sistemi Operativi II Massimo Bernaschi Istituto per le Applicazioni del Calcolo Mauro Picone Consiglio Nazionale delle Ricerche Viale del Policlinico, 137-00161 Rome - Italy http://www.iac.cnr.it/ e-mail: m.bernaschi@iac.cnr.it
Inter Process Communication (IPC) I meccanismi di IPC permettono ad un processo di comunicare con un altro senza richiedere la condivisione di parti dello spazio di indirizzamento. Un utilizzo classico è quello della comunicazione serializzata con cui lo standad output di un primo processo diventa lo standard input di un secondo processo (pipe). Le pipe nella loro forma più elementare (anonymous) sono half-duplex Le named pipe sono molto più potenti: full-duplex; comunicazione in rete;... 2
Anonymous pipe in Win32 Per creare un anonymous pipe (unidirezionale): BOOL CreatePipe( PHANDLE hreadpipe, PHANDLE hwritepipe, LPSECURITY_ATTRIBUTES lppipeattributes, DWORD nsize); La pipe ha due handle: uno per la lettura ed uno per la scrittura. L argomento nsize indica la dimensione della pipe ma non è necessario specificare un effettivo valore. Si può usare il valore di default specificando 0. 3
Anonymous pipe in Win32 4
Se un processo intende comunicare con un altro processo dopo averlo creato, il meccanismo più semplice è definire come input handle del processo creato l handle hreadpipe. Le operazioni di lettura da un pipe handle sono bloccanti se la pipe è vuota. Il numero di byte letti è il minimo tra quello disponibile nella pipe e quello specificato nella ReadFile Un operazione di scrittura su una pipe è bloccante se la pipe è piena. esempio: pipe.c in chaptr11.c 5
Named pipe in Win32 Le named pipe sono un meccanismo molto più sofisticato di IPC. Le named pipe sono message-oriented, in altre parole un processo può leggere messaggi di lunghezza variabile nello stesso modo in cui sono stati inviati dal processo mittente. Le named pipe sono bidirezionali. Possono esserci più istanze indipendenti di una named pipe. Ad esempio più client possono comunicare con un singolo server usando la stessa pipe ed il server può rispondere ad un client usando la stessa istanza. Il nome della pipe può essere acceduto da sistemi in rete sebbene per questo tipo di comunicazione sia consigliabile l uso dei socket. 6
Per creare la prima istanza di una named pipe si utilizza la funzione: HANDLE CreateNamedPipe( LPCTSTR lpname, DWORD dwopenmode, DWORD dwpipemode, DWORD nmaxinstances, DWORD noutbuffersize, DWORD ninbuffersize, DWORD ndefaulttimeout, LPSECURITY_ATTRIBUTES lpsecurityattributes); solo i sistemi Windows 2000/2003/NT/XP possono invocare la CreateNamedPipe lpname deve essere nella forma \\.\pipe\pipename pipename può includere qualsiasi carattere escluso il backslash; 7
dwpipemode specifica se la scrittura è message-oriented o byte-oriented ; se la lettura è per messaggi o per blocchi; se le operazioni di lettura sono bloccanti; nmaxinstances specifica il numero massimo di istanze; ndefaulttimeout specifica il tempo di timeout (in millisecondi) per la WaitNamedPipe. È l unico caso in cui una funzione createxx specifica un timeout per un altra funzione; il processo invocante viene in genere indicato come server; i processi client (che, come detto, possono essere su altri sistemi) aprono la pipe con la CreateFile; cancellare l ultima istanza di una named pipe cancella la pipe stessa. 8
Un client si collega alla named pipe invocando CreateFile sul nome della pipe. se il client è locale il nome è esattamente lo stesso usato per lpname \\.\pipe\pipename se il server è su un altro sistema, il nome da utilizzare è: \\servername\pipe\pipename 9
10
Le funzioni GetNamedPipeHandleState e SetNamedPipeHandleState permettono, rispettivamente, di ottenere e definire le caratteristiche di una named pipe. 11
Il processo che crea una named pipe può rimanere in attesa della connessione sulla pipe da parte di un client con la funzione: BOOL ConnectNamedPipe( HANDLE hnamedpipe, LPOVERLAPPED lpoverlapped); Se lpoverlapped è NULL, la funziona ritorna non appena c è la connessione da parte di un client. il valore di ritorno dipende dal momento in cui è arrivata la connessione rispetto al momento dell invocazione della funzione. Le operazioni di lettura e scrittura avvengono con le normali ReadFile e WriteFile. La funzione BOOL WaitNamedPipe( LPCTSTR lpnamedpipename, DWORD ntimeout); 12
è invece utilizzata da un client per sincronizzarsi con il server. La funzione ritorna non appena il server invoca la ConnectNamedPipe. Notare che la funzione fallisce se il server non ha ancora creato la pipe. 13
Funzioni avanzate per le named pipe La comune sequenza WriteFile, ReadFile può essere vista come una singola transazione e Win32 fornisce una funzione specifica per questo scopo: BOOL TransactNamedPipe( HANDLE hnamedpipe, LPVOID lpinbuffer, DWORD ninbuffersize, LPVOID lpoutbuffer, DWORD noutbuffersize, LPDWORD lpbytesread, LPOVERLAPPED lpoverlapped); notare come venga ritornato il numero di byte letti in lpbytesread. Questa funzione offre anche un miglioramento delle prestazioni rispetto all utilizzo della coppia WriteFile, ReadFile. 14
Per ottenere una forma ancora più compatta può essere utilizzata la BOOL CallNamedPipe( LPCTSTR lpnamedpipename, LPVOID lpinbuffer, DWORD ninbuffersize, LPVOID lpoutbuffer, DWORD noutbuffersize, LPDWORD lpbytesread, DWORD ntimeout); Questa funzione è sincrona mentre la precedente offre la possibilità di effettuare I/O asincrono (usando la struttura lpoverlapped). 15
La funzione BOOL PeekNamedPipe( HANDLE hnamedpipe, LPVOID lpbuffer, DWORD nbuffersize, LPDWORD lpbytesread, LPDWORD lptotalbytesavail, LPDWORD lpbytesleftthismessage); Legge byte o messaggi (a seconda della modalità) dalla pipe senza rimuovere i dati. Una successiva lettura con ReadFile trova gli stessi dati. questa funzione non è bloccante la disponibilità di dati nella pipe è segnalata da un valore diverso da zero per *lptotalbytesavail lpbytesleftthismessage indica, come valore di ritorno, il numero di byte in un messaggio che non possono essere letti 16
perché il valore nbuffersize è troppo piccolo. Questo valore è zero per una pipe in byte mode. I tipici attributi di sicurezza di una named pipe sono GENERIC READ, GENERIC WRITE e SYNCHRONIZE che permette ad un thread di rimanere in attesa sulla pipe. 17
Pipe in Unix/Linux Le pipe sono state la prima forma di IPC in Unix. half-duplex; possono essere utilizzate solo tra processi che hanno un antenato comune. 1. il processo A crea una pipe; 2. il processo A chiama fork e crea il processo B; 3. la pipe è utilizzata per la comunicazione tra A e B. Per creare una pipe: #include <unistd.h> int pipe(int filedes[2]); filedes[0] è aperto in lettura mentre filedes[1] è aperto in scrittura. 18
Se il flusso dei dati è da A verso B, A chiude filedes[0] e B chiude filedes[1]. Se il flusso dei dati è da B verso A, A chiude filedes[1] e B chiude filedes[0]. esempi: apue/ipc/pipe1.c e apue/ipc/pipe2.c. La pipe è riconosciuta come un tipo di file speciale (FIFO). Nel caso Unix/Linux l associazione tra un estremo della pipe e lo standard input o output di un processo deve essere fatta dal processo stesso. La read da una pipe il cui estremo di scrittura è stato chiuso ritorna 0. Se si cerca di scrivere su una pipe il cui estremo di lettura è stato chiuso, viene inviato un SIGPIPE al processo invocante. 19
Funzioni popen e pclose La libreria di I/O standard in Unix/Linux offre la possibilità di combinare la creazione di un processo, la creazione di una pipe per comunicare con il nuovo processo e l esecuzione di un programma da parte di questo nuovo processo #include <stdio.h> FILE *popen(const char *command, const char *type); popen invoca fork ed exec per eseguire command se type è uguale a r, il file pointer ritornato è connesso allo standard output di command. se type è uguale a w, il file pointer ritornato è connesso allo standard input di command. Notare come questa funzione a differenza di fopen non è parte 20
della libreria C standard. Notare inoltre che command è eseguito come sh -c command questo permette, ad esempio, l espansione di caratteri speciali (*). La funzione int pclose(file *stream); chiude il file stream, attende la terminazione del comando e ritorna lo stato di terminazione della shell che ha eseguito il comando. Una possibile implementazione di popen è in apue/lib.rhlin/popen.c esempio: filtro per la conversione maiuscole/minuscole: apue/ipc/popen1.c 21
Coprocessi e named pipe in Unix/Linux Utilizzando due pipe semplici è possibile collegare lo standard input e lo standard output di un processo ad un altro. esempio: apue/ipc/pipe4.c e apue/ipc/add2.c È necessario fare attenzione ai meccanismi di buffering della libreria di I/O standard. esempio: apue/ipc/add2stdio.c Come in Win32, è possibile utilizzare in Unix/Linux delle named pipe o FIFO Le FIFO permettono la comunicazione anche tra processi del tutto indipendenti Creare una FIFO è simile a creare un file: #include <sys/types.h> 22
#include <sys/stat.h> int mkfifo (const char *pathname, mode_t mode); i valori possibili per mode sono gli stessi della open; si applicano anche gli stessi concetti di ownership. Una volta che è stata creata con mkfifo, la FIFO deve essere acceduta con le funzioni di I/O open, close, read, write, unlink,... Il comportamento quando si cerca di scrivere ma nessun processo tiene aperta la FIFO in lettura oppure quando l ultimo processo scrittore chiude la FIFO e si cerca di leggere è lo stesso delle pipe semplici. Le FIFO possono essere usate da: comandi di shell per passare dati da una pipeline ad un altra senza creare file temporanei; 23
consideriamo il caso in cui sia necessario processare un certo flusso con due diversi filtri: mkfifo fifo1 prog3 < fifo1 & prog1 < input_file tee fifo1 prog2 24
in un applicazione client-server. più client possono contattare un server su una singola well known FIFO; il server è però costretto ad utilizzare una FIFO per ogni client per inviare le risposte. 25
Altre tecniche di IPC Esistono altre tecniche di IPC sia in Win32 che in Unix/Linux In Win32 ricordiamo le mailslot meccanismo di broadcast; comunicazione unidirezionale; possono essere distribuite in rete; un messaggio inviato da un client può essere letto da tutti i server. Tutti i server ricevono lo stesso messaggio. In Unix/Linux ricordiamo le message queue lista linkata di messaggi mantenuta nel kernel; è possibile accedere i messaggi in qualsiasi ordine (non necessariamente FIFO). 26
Semafori in Unix/Linux Un semaforo è un contatore usato per permettere l accesso a dati condivisi da più processi. Per ottenere l accesso un processo: 1. controlla il valore del semaforo che controlla la risorsa condivisa; 2. se il valore del semaforo è positivo, l accesso è possibile. Il processo decrementa il valore del semaforo per indicare che sta usando un unità della risorsa; 3. se il valore del semaforo è zero, il processo va dormire fino a quando il valore del semaforo non diventa più grande di zero. Quando il processo viene svegliato, ritorna al punto 1; ovviamente il test sul valore del semaforo ed il decremento devono essere eseguiti come un operazione atomica. 27
4. quando il processo ha finito di utilizzare la risorsa, incrementa il valore del semaforo di 1. Un tipo molto utilizzato di semaforo è quello binario (controlla una singola risorsa ed è inizializzato ad uno) In Unix i semafori, originariamente implementati nel System V, sono piuttosto macchinosi da utilizzare: #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg); int semctl(int semid, int semnum, int cmd, union semun arg); la key è un tipo definito in <sys/types.h> (un int in Linux, vedi /usr/include/bits/types.h); per condividere un semaforo è possibile utilizzare diverse tecniche: 28
un processo crea il semaforo (semget) con la chiave IPC PRIVATE e rende poi disponibile l identificatore ritornato; i processi concordano una chiave (esplicitamente oppure usando la funzione ftok); semflg assolve lo stesso compito (e può assumere gli stessi valori) dell argomento flag nella open; semun è descritto in /usr/include/bits/sem.h ma deve essere definito dall applicazione! l argomento cmd della semctl può assumere 10 valori!; la semctl è tipicamente utilizzata per inizializzare il semaforo (comando SETVAL). Per le operazioni sul semaforo viene utilizzata la: int semop(int semid, struct sembuf *sops, unsigned nsops); la struct sembuf è definita in /usr/include/linux/sem.h 29
esempio: apue/lib.rhlin/semaph.c il campo sem op della struttura sembuf assume valori: positivi per rilasciare risorse. Il valore di sem op è aggiunto al valore del semaforo; negativi per ottenere risorse. Se il valore del semaforo è più grande o uguale al valore assoluto di sem op, questo viene sottratto al valore del semaforo. Se il valore del semaforo è più piccolo, il processo è messo a dormire (a meno che non venga specificato il flag IPC NOWAIT). se sem op è zero, il processo rimane in attesa fino a quando il valore del semaforo non diventa 0. Da notare un serio problema di principio nell API per i semafori: la creazione di un semaforo è indipendente dalla sua inizializzazione. Non è possibile creare atomicamente un semaforo ed inizializzarlo 30
ad un valore dato. 31
Shared Memory IPC in Unix/Linux Oltre al meccanismo di condivisione offerto dalla funzione mmap, in molti sistemi Unix (soprattutto quelli derivanti dal System V) ed in Linux è possibile per più processi condividere un segmento dello spazio di indirizzamento in modo che faccia riferimento allo stesso blocco di memoria. La prima funzione da invocare per utilizzare questo meccanismo è #include <sys/types.h> #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, int size, int shmflg); il significato del parametro key è lo stesso descritto in precedenza per il caso dei semafori; size indica la dimensione minima del segmento. Se si fa 32
riferimento ad un segmento esistente è possibile specificare come valore 0. Normalmente la dimensione del segmento è arrotondata ad un multiplo del PAGE SIZE; flag ha lo stesso utilizzo che nel caso dei semafori (permessi...). Esistono limiti sia sulla dimensione che sul numero di segmenti che possono essere creati con questa primitiva (SHMALL, SHMMAX, SHMMNI,..., vedi /usr/include/linux/shm.h.) Un processo può attaccare un segmento al proprio spazio di indirizzamento con la funzione: #include <sys/types.h> #include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg) Come nel caso della mmap è consigliabile specificare 0 come 33
valore per shmaddr, cioè per l indirizzo al quale verrà attaccato il segmento. In questo caso è il sistema operativo a scegliere un indirizzo opportuno. Sempre in analogia alla mmap, il valore di ritorno in caso di fallimento è 1. Per rimuovere dallo spazio di indirizzamento di un processo un segmento si utilizza la int shmdt(const void *shmaddr). La funzione: #include <sys/types.h> #include <sys/types.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); permette di effettuare un certo numero di operazioni sul segmento a seconda del valore di cmd. In particolare è possibile sapere quanti processi hanno il segmento attaccato nel proprio 34
spazio di indirizzamento usando il valore IPC STAT per cmd. La struttura shmid ds è definita in /usr/include/bits/shm.h. esempio: apue/ipc/tshm.c 35
36