Joe (lo straniero): Chissà cosa diavolo c'è in quella diligenza.
Silvanito: Facile da sapere, ti ci avvicini e dai una guardatina dentro, se ti sparano addosso vuol dire che c'è oro.
Per la serie “battere il ferro finché è caldo” rimarremo ancora per un po’ sul tema dei Processi già affrontato negli ultimi due articoli (qui e qui) e parleremo di IPC, ossia di Comunicazione tra Processi (e, in particolare, ci soffermeremo sulla POSIX IPC). Questo sarà il primo di una serie, perché il tema è vasto, e merita un buon approfondimento. Dubito che riuscirò a scrivere qualcosa di memorabile come il primo episodio della Trilogia del dollaro citata nel titolo, però è sempre meglio avere delle buone fonti di ispirazione. E se a qualcuno non piacciono questi articoli gli mando a casa Joe (lo straniero) per convincerlo…
![]() |
…come hai detto? Non ti interessa la POSIX IPC?... |
Allora: la comunicazione tra processi si può realizzare in varie maniere, vediamo una lista delle principali disponibili sui sistemi POSIX:
- Message Queue – comunicazione tra due o più processi con capacità full duplex. I processi comunicano tra loro pubblicando un messaggio e recuperandolo dalla coda. Una volta recuperato, il messaggio non è più disponibile nella coda.
- Shared Memory – la comunicazione tra due o più processi viene raggiunta attraverso un pezzo di memoria condiviso tra tutti i processi. La memoria condivisa deve essere protetta dagli accessi simultanei usando meccanismi di sincronizzazione.
- FIFO (Named Pipe) – comunicazione tra due processi non correlati (ottenuti con fork + exec, per esempio). Come spiegato dal nome stesso, il mezzo usato è un file di tipo FIFO. È una comunicazione full duplex, il che significa che il primo processo può comunicare con il secondo processo e viceversa allo stesso tempo. Si può usare anche con processi correlati (ottenuti con fork senza exec), ma non ha molto senso: meglio usare la Anonymous Pipe in questo caso.
- Pipe (Anonymous Pipe) – comunicazione tra due processi correlati (ottenuti con fork senza exec). Il meccanismo è half duplex che significa che il primo processo comunica con il secondo processo. Per ottenere un full duplex, e cioè permettere al secondo processo di comunicare con il primo processo, è necessaria un’altra pipe.
- UNIX domain socket (IPC socket) – comunicazione che usa i socket del Kernel (che non usano TCP/IP)
- Internet domain socket (Network socket) – comunicazione che usa i socket di rete classici basati su TCP/IP.
Possiamo aggiungere qualche altro dettaglio: la POSIX Message Queue e la POSIX Shared Memory hanno degli equivalenti Unix System V: SysV Message Queue e SysV Shared Memory. Le versioni SysV sono concettualmente molto simili a quelle POSIX (che, tra l’altro, sono nate dopo, nel disegno di standardizzazione Single UNIX Specification). E poi esiste, ovviamente, anche la versione Windows di IPC, che in alcuni casi è abbastanza simile a quelle di POSIX e SysV (del resto Windows NT è nato molto tempo dopo UNIX, ed ha attinto molte idee da lì e da openVMS, ma questa è un altra storia…).
In questa serie di articoli ho deciso di analizzare solo le versioni POSIX perché hanno una API un po’ più user-friendly (e anche per non gettare troppa carne al fuoco). Comunque SysV IPC e Windows IPC sono nella lista degli articoli futuri, non vi preoccupate. È solo una questione di tempo…
Cercheremo, quindi, di mostrare, attraverso un codice chiaro e ben commentato, come implementare questi meccanismi di IPC e, al contempo, getteremo un occhio alle prestazioni. Vi preannuncio che in rete c’è molta documentazione sull’argomento, ma è difficile trovare un condensato dei vari metodi con inclusi anche i dettagli delle prestazioni. Quindi penso e spero che questi articoli saranno molto utili!
(…e dopo cotanta introduzione si allontana, sorridendo amabilmente, tra gli applausi del pubblico…)
E vediamo il piano dell’opera: della lista dei 6 punti qua sopra scarteremo il 4 (Pipe) perché l’obiettivo è la comunicazione più difficile, quella tra processi non correlati (amo il rischio). E scarteremo anche la 6 (Internet domain socket) perché tratteremo solo processi in esecuzione sulla stessa macchina, quindi sarebbe un controsenso usare i Network socket disponendo degli IPC socket.
Ho cercato di scrivere il codice delle varie versioni nella maniera più omogenea possibile, in maniera che i vari sorgenti siano quasi sovrapponibili. Inoltre, per poter confrontare i risultati, ho usato gli stessi metodi di scambio dati per tutte le versioni, quindi avremo un processo che scrive e uno che legge, con un tipo di comunicazione simile a quello usato nei classici esempi di Socket Server/Client che sicuramente avrete tutti già visto. Ho usato questo metodo anche per la Shared Memory: so che è un po improprio (il metodo migliore e più semplice è quello usato nell’ultimo articolo) ma si può fare…
Ok, da cosa cominciamo? Io direi dal meccanismo più classico, quello della Fifo (Named Pipe), che ripete il metodo di comunicazione molto usato anche a livello user interface con la Pipe di shell (e bash). E, sulla falsariga dell’articolo precedente, avremo i seguenti listati:
- Il main di un processo padre (processes.c) che crea ed segue due processi figli con fork + exec. I due processi figli si chiameranno writer e reader.
- Il main del processo writer (writer.c).
- Il main del processo reader (reader.c).
- Gli header file del caso e gli eventuali file di libreria.
Ok, cominciamo da processes.c: vai col codice!
- La prima operazione eseguita è la creazione del file FIFO (e cioè della Named Pipe) che sarà usata dai processi figli.
- Il padre non forza più l’uscita dei figli dopo 10 secondi, ma ne attende solo la terminazione: i figli usciranno autonomamente per permetterci il test delle prestazioni.
E ora vediamo l’header file data.h che ci mostra il tipo di dati scambiati e definisce il path della Named Pipe:
E adesso siamo pronti per vedere il codice di writer.c:
Ma si, dai, passiamo al reader!
In definitiva si puo` affermare che la IPC attraverso Named Pipe è veramente semplice da implementare, perchè è pura attività di read/write su un file. Tutto Ok, quindi… e i risultati? Il test l’ho effettuato su una macchina Linux (Kernel 5.4.0) con un i7 (4 core/8 thread) con 8GB di RAM. Visto che è il primo test non voglio anticiparvi se le prestazioni sono buone o cattive (spoiler: sono buone), comunque vediamo:
Allora: il nostro test mostra che, usando la POSIX Named Pipe, abbiamo scambiato due milioni di messaggi tra i due processi in 1.732 secondi (quindi un messaggio ogni 0,87 us). Notare che non ho scritto nessuna ottimizzazione del sistema di lettura/scrittura: anche se i messaggi sono corti (“un-messaggio-di-test:nnnn”) si spediscono ugualmente sizeof(Data) byte (sono ben 1032 byte): ottimizzando per spedire solo il minimo necessario sarebbe ancora più veloce (spoiler: scriverò, prima o poi, un articolo sull’argomento). Nei prossimi post vedremo codice e risultati delle altre POSIX IPC in analisi: vi preannuncio che sarà molto interessante, ma non trattenete il respiro nell’attesa, mi raccomando…
Ciao, e al prossimo post!