Un modello di sistema a divisione di tempo
Questa sezione descrive un modello di sistema time-sharing che ritengo particolarmente adatto a realizzare la comunicazione tra processi. La struttura di base di questo modello di sistema time-sharing non è originale [5][9].
Il modello di sistema time-sharing consta di due parti: il monitor e i processi. Il monitor svolge diverse funzioni, tra cui trasferire il controllo da un processo all'altro secondo le necessità (per esempio quando un processo ha consumato un tempo «sufficiente» o quando si verifica un interrupt), gestire la memoria centrale e il supporto di swap, controllare il passaggio del controllo da un processo all'altro (ossia i meccanismi di protezione), creare processi, prendersi cura dei processi dormienti, ecc.
I processi svolgono la maggior parte delle funzioni normalmente considerate funzioni di supervisore in un sistema time-sharing (processi di sistema) nonché le normali funzioni utente (processi utente). Un tipico processo di sistema è il gestore del disco o il file system. Per ragioni di efficienza può essere utile pensare ai processi di sistema come bloccati in memoria centrale.
Un processo può chiedere al monitor di svolgere diverse funzioni: avviare un altro processo uguale e autonomo (ossia caricare un programma, oppure trovare da qualche parte una copia di un programma che possa essere condivisa, avviarlo e passargli alcuni parametri iniziali); fermare il processo in esecuzione; porre il processo corrente in stato di attesa fino al verificarsi di un evento specificato; inviare un messaggio a un processo specificato; rendersi disponibile a ricevere un messaggio da un processo specificato; rendersi disponibile a ricevere un messaggio da qualunque processo; inviare un messaggio a un processo capace di ricevere da qualunque processo; e richiedere un numero unico. Non vi è dubbio che dovrebbero esistere anche altre funzioni del monitor. Si lascia al lettore, come esercizio, convincersi che il monitor di cui è costretto a servirsi può fornire queste funzioni: la maggior parte può farlo.
Qui non mi occuperò delle considerazioni sulla protezione, ma presumerò invece che tutti i processi siano processi «buoni», che non commettono mai errori. Se il lettore ha bisogno di una struttura di protezione da tenere presente mentre legge questa nota, il sistema di capability descritto in [5][6][7][8] dovrebbe essere soddisfacente.
Esaminiamo ora un po' più da vicino le otto operazioni elencate sopra che un processo può chiedere al monitor di eseguire.
START. Questa operazione avvia un altro processo. Ha due parametri: una sorta di identificazione del programma da caricare e una lista di parametri per quel programma. Una volta caricato, il programma viene avviato al punto di ingresso assegnatogli e la sua lista di parametri gli viene passata in qualche modo ben noto. Il processo continuerà a esistere finché non ferma se stesso.
HALT. Questa operazione pone in stato di attesa il processo attualmente in esecuzione, in attesa del completamento di qualche evento. L'operazione ha un parametro: l'evento da attendere. Esempi di eventi sono l'arrivo di un interrupt hardware, l'arrivo di un messaggio da un altro processo, ecc. Il processo viene ripreso dall'istruzione successiva al comando SLEEP. Il monitor non pone mai unilateralmente un processo in stato di attesa, salvo quando il processo esaurisce il proprio quanto di tempo.
RECEIVE. Questa operazione permette a un altro processo di inviare un messaggio a questo processo. L'operazione ha quattro parametri: la porta (definita più sotto) in attesa del messaggio, la porta da cui un messaggio sarà accettato, una specifica del buffer disponibile per ricevere il messaggio e un indirizzo di trasferimento quando la trasmissione è completata. [In altre parole, un indirizzo di interrupt. Qualunque porta di messaggi può essere usata per consentire interrupt, canali di eventi, ecc. L'utente programma ciò che vuole.]
SEND. Questa operazione invia un messaggio a qualche altro processo. [Suppongo che un processo potrebbe anche inviare un messaggio a se stesso.] Ha quattro parametri: una porta a cui inviare il messaggio, la porta da cui il messaggio viene inviato, il messaggio e un indirizzo di trasferimento quando la trasmissione è completata.
RECEIVE ANY. Questa operazione permette a qualunque processo di inviare un messaggio a questo processo. L'operazione ha quattro parametri: la porta in attesa del messaggio, il buffer disponibile per ricevere il messaggio, un indirizzo di trasferimento quando il messaggio è ricevuto e un indirizzo dove può essere annotata la porta che ha inviato il messaggio.
SEND FROM ANY. Questa operazione permette a un processo di inviare un messaggio a un processo capace di ricevere un messaggio da qualunque processo. Ha gli stessi quattro parametri di SEND. La necessità di questa operazione sarà discussa più avanti.
UNIQUE. Questa operazione ottiene un numero unico dal monitor.
Una porta è un particolare percorso di dati verso un processo o proveniente da un processo. Tutte le porte hanno associato un numero unico che serve a identificare la porta. Le porte sono usate per trasmettere messaggi da un processo a un altro nel modo seguente. Si considerino due processi, A e B, desiderosi di comunicare. Il processo A esegue un RECEIVE sulla porta N dalla porta M.
Il processo B esegue un SEND sulla porta N dalla porta M. Il monitor abbina i numeri di porta e trasferisce il messaggio dal processo B al processo A. Non appena il buffer è stato interamente trasmesso fuori dal processo B, il processo B viene ripreso nella posizione specificata nell'operazione SEND. Non appena il messaggio è interamente ricevuto presso il processo A, il processo A viene ripreso nella posizione specificata nell'operazione RECEIVE. Come i processi si procurino i numeri di porta corretti con cui comunicare con altri processi non riguarda il monitor: questo problema è lasciato ai processi.
Un esempio. Supponiamo che il nostro modello di sistema time-sharing sia inizializzato con diversi processi sempre in esecuzione. Inoltre, questi processi permanenti hanno alcune porte universalmente note e assegnate in modo permanente. [Oppure esiste forse una sola porta permanentemente nota, appartenente a un processo-direttorio che tiene una tabella delle associazioni tra processi permanenti e porte ben note.] Supponiamo che due dei processi in esecuzione permanente siano il processo di registrazione e il processo di scansione del telescrivente. Quando il processo di scansione del telescrivente inizia a essere eseguito, si pone in attesa di un interrupt dallo scanner hardware del telescrivente. Il processo di registrazione si pone inizialmente in attesa di un messaggio dal processo di scansione del telescrivente, attraverso porte SEND e RECEIVE ben note e permanenti. Il processo di scansione del telescrivente tiene una tabella indicizzata per numero di telescrivente, in cui ogni voce contiene una porta a cui inviare i caratteri di quel telescrivente e una porta su cui ricevere i caratteri destinati a quel telescrivente. Se arriva un carattere (risvegliando il processo di scansione del telescrivente) e il processo non ha alcuna voce per quel telescrivente, ottiene dal monitor una coppia di numeri unici (tramite UNIQUE) e invia un messaggio contenente tale coppia di numeri al processo di registrazione, usando le porte su cui sa che il processo di registrazione ha un RECEIVE in attesa. [In realtà, il processo di scansione potrebbe usare sempre la stessa coppia di numeri di porta per un dato telescrivente, purché siano passati a una sola copia dell'executive alla volta.] Il processo di scansione inserisce anche la coppia di numeri nella tabella dei telescriventi e invia i caratteri, e tutti i caratteri futuri di quel telescrivente, dalla porta con il secondo numero alla porta con il primo numero. Il processo di scansione probabilmente passa anche una seconda coppia di numeri unici al processo di registrazione perché li usi per l'output del telescrivente, ed esegue un RECEIVE usando questi numeri. Il processo di registrazione, quando riceve il messaggio dal processo di scansione, avvia una copia di quello che gli utenti del SDS 940 TSS [12] chiamano executive (quel programma che stampa gli elenchi dei file, dice chi si trova sugli altri telescriventi, esegue sottosistemi, ecc.) e passa a questa copia dell'executive i numeri di porta, affinché anche questo processo executive possa fare i suoi ingressi e le sue uscite verso il telescrivente usando queste porte. Se il processo di registrazione vuole ottenere un numero di job e una password dall'utente, può usare temporaneamente questi numeri di porta per comunicare con l'utente prima di passarli all'executive.
I numeri di porta sono spesso scambiati tra processi. Più raramente una porta viene trasferita a un altro processo. È cruciale che, una volta che un processo ha trasferito una porta a un altro processo, il primo non usi più quella porta. Potremmo aggiungere un meccanismo che imponga questa regola. Il sistema di oggetti protetti di [8] è uno di questi meccanismi. [Naturalmente, se il sistema di oggetti protetti ci è disponibile, non c'è davvero più bisogno di specificare due numeri di porta prima che una trasmissione possa avvenire. Il fatto che un processo conosca un numero di porta RECEIVE esistente è prova prima facie del diritto di quel processo di inviare a quella porta. La differenza tra le porte RECEIVE e RECEIVE ANY dipende allora soltanto dal numero di copie di un particolare numero di porta che sono state distribuite. Un sistema basato su questo approccio sarebbe chiaramente preferibile a quello qui descritto se fosse possibile assumere che tutti i sistemi time-sharing autonomi di una rete adottassero questo meccanismo di protezione. Se questa ipotesi non può essere fatta, sembra più pratico richiedere entrambi i numeri di porta.]
Si noti che da qualche parte nel monitor deve esistere una tabella dei numeri di porta associati ai processi e alle posizioni di ripresa. Le voci della tabella vengono cancellate dopo ogni abbinamento SEND/RECEIVE. Si noti inoltre che, se un processo è in esecuzione (magari in attesa) e ha un RECEIVE ANY in sospeso, allora qualunque processo che conosca il numero della porta di ricezione può parlare con esso senza passare attraverso processi di registrazione o simili. Ciò è ovviamente essenziale all'interno di un sistema time-sharing locale e sembra molto utile in una rete più generale se si vuole raggiungere l'ideale della condivisione di risorse.
Quando viene eseguito un SEND, non accade nulla finché non viene eseguito un RECEIVE corrispondente. Se non viene eseguito un RECEIVE appropriato per un certo tempo, il SEND va in timeout dopo un po' e il processo mittente viene avvisato. Se viene eseguito un RECEIVE ma il SEND corrispondente non si verifica per molto tempo, il RECEIVE va in timeout e il processo ricevente viene avvisato.
Un RECEIVE ANY non va mai in timeout, ma può essere ritirato. Un messaggio SEND FROM ANY viene sempre inviato immediatamente e sarà scartato se non esiste un ricevente appropriato. Non viene restituito alcun messaggio di errore e la conferma, se ve n'è una, è compito dei processi. Se la tabella in cui SEND e RECEIVE vengono abbinati dovesse traboccare, un processo che origina un ulteriore SEND o RECEIVE viene avvisato esattamente come se il SEND o il RECEIVE fosse andato in timeout.
In generale, le porte ben note e assegnate in modo permanente sono usate tramite RECEIVE ANY e SEND FROM ANY. Le porte permanenti serviranno molto spesso ad avviare processi e di conseguenza attraverso di esse passeranno pochi dati.
Ancora un esempio, questa volta una dimostrazione dell'uso del compilatore FORTRAN. Abbiamo già spiegato come un utente si sieda al suo telescrivente e venga collegato a un executive. Procediamo da lì. L'utente sta effettuando ingressi e uscite verso l'executive, che esegue SEND e RECEIVE. Alla fine l'utente digita RUN FORTRAN e l'executive chiede al monitor di avviare una copia del compilatore FORTRAN e passa a FORTRAN, come parametri di avvio, le due porte che l'executive stava usando per parlare con il telescrivente. FORTRAN si aspetta naturalmente questi parametri ed esegue SEND e RECEIVE su queste porte per scoprire quali file di ingresso e di uscita l'utente vuole usare. FORTRAN stampa INPUT FILE? all'utente, che risponde F001. FORTRAN invia allora un messaggio al processo del file system, che dorme in attesa di qualcosa da fare. Il messaggio è inviato tramite porte ben note e chiede al file system di aprire F001 per l'ingresso. Il messaggio contiene anche una coppia di porte che il processo del file system può usare per inviare la sua risposta. Il file system cerca F001, lo apre per l'ingresso, inserisce alcune voci nelle sue tabelle dei file aperti e invia a FORTRAN un messaggio contenente le porte che FORTRAN può usare per leggere il file. La stessa procedura è seguita per il file di uscita. Quando la compilazione è completa, FORTRAN restituisce i numeri di porta del telescrivente all'executive, che dormiva in attesa di un messaggio da FORTRAN, e poi FORTRAN ferma se stesso. Il processo del file system torna a dormire quando non ha altro da fare.
[Il lettore avrà ormai notato che non mi piace pensare che un nuovo processo (costituito da una nuova copia concettuale di un programma) venga avviato ogni volta che un altro utente desidera usare quel programma. Preferisco invece pensare al programma come a un unico processo che sa di essere usato simultaneamente da molti altri processi e che multiplexa consapevolmente tra gli utenti, oppure ritarda il servizio agli utenti finché non riesce a occuparsene.]
Inoltre, il processo del file system può mantenere un piccolo insieme di numeri di porta che riusa continuamente, se riesce a far sì che gli utenti del file system restituiscano i numeri di porta quando hanno finito con essi. Naturalmente, quando questo insieme di numeri di porta si sarà alla fine disperso, il file system potrà ottenere dal monitor alcuni nuovi numeri unici.
Si noti che, quando due processi desiderano comunicare, stabiliscono da soli la connessione e sono liberi di farlo in modo reciprocamente comodo. Per esempio, possono scambiarsi i numeri di porta, oppure un processo può scegliere tutti i numeri di porta e indicare all'altro quali usare. Naturalmente, in una particolare implementazione di un sistema time-sharing, i costruttori del sistema potrebbero scegliere di limitare l'esecuzione di SEND e RECEIVE da parte dei processi e di vietare il passaggio arbitrario dei numeri di porta, richiedendo invece che venga chiamato il monitor (o qualche altro programma speciale) per svolgere queste funzioni.
Il controllo di flusso è fornito in questo sistema con il metodo semplice di non iniziare mai un SEND da un processo prima che sia eseguito un RECEIVE da parte del ricevente. Naturalmente possono essere scambiati messaggi tra processi per suggerire a un processo di smettere di inviare o che venga allocato dello spazio, ecc.