Kickoff Rocco.
Introduzione alle architetture hw e sw.
Paradigmi di programmazione di applicazioni distribuite.
problemi trasversali come sincronizzazione e mutua esclusione, consistenza.
prova scritta sul paradigma ad oggetti distribuiti (java RMI) e mpi (C ). Orale su tutto.
## Lezione 1 - 24/9/2024
### Introduzione!
Qualsiasi sistema è sempre l'insieme di hardware e software.
Dal punti di vista hardware possiamo dire che un sistema distribuito è un insieme (se possibile scalabile, posso aggiungere hardware senza fare danni ma migliorare le prestazioni) di computer indipendenti (con i loro processori, os) collegato con un sistema di interconnessione (che può essere una semplice rete ethernet o rete dedicata). Dopodichè ci serviranno strati software per far vedere all'utente programmatore quell'insieme di risorse eterogenee come unica macchina molto più grande e potente. Questo tipo di software comunque è abbastanza utopico, ma avere un sistema operativo distribuito è troppo complicato, ci dobbiamo accontentare di avere solamente degli strati software.
Quindi ricapitolando hardware e strati software insieme offrono all'utente la possibilità di programmare applicazioni distribuite, andando a suddividere il lavoro su più unità di elaborazione (processi o programmi che lavorando indipendentemente alla stessa cosa); per fare una cosa del genere bisogna passare dal vedere la risoluzione di un problema in maniera procedurale a risolvere il problema facendo lavorare contemporaneamente più processi (programmi in esecuzione) ognuno con la sua unità di elaborazione e dati; tutto questo con l'obiettivo di risolvere lo stesso problema migliorando le prestazioni.
Spesso però, suddividere il lavoro tra più processi, non li rende automaticamente indipendenti, ma in gran parte dei problemi queste risoluzioni richiedono che le informazioni debbano essere scambiati tra processi, quindi bisogna decidere quando essi debbano fermarsi e sincronizzarsi con gli altri ecc.
### Motivazioni dei sistemi distribuiti
Perchè si è passati da sistemi centralizzati a distribuiti? per i soldi, la condivisione di risorse porta a risparmio, e anche per vincoli tecnologici, ci si è accorti infatti che non si poteva aumentare di troppo la velocità di un singolo processore, nemmeno lavorando sul parallelismo interno allo stesso, quindi per alcune applicazioni nasce l'esigenza di maggiore potenza di calcolo. Pensiamo alle previsioni del tempo, in cui abbiamo tantissimi dati da elaborare che cambiano sempre se si modificano le condizioni iniziali.
Ci sono anche motivi prestazionali, in particolare incremento del tempo di risposta, in particolare si valutano due parametri: speed-up (rapporto tra tempo di esecuzione su una macchina distribuita rispetto alla risoluzione dello stesso problema dello stesso problema su una macchina sequenziale, se si avvicina a N numero di macchine del sistema distribuiti avremo fatto una ottimizzazione eccezzionale di prestazioni), scale-up (riuscire se possibile a risolvere nello stesso tempo, utilizzando un sistema distribuito, un problema più grande); ovviamente l'aspetto prestazionale vale anche per sistemi non solo paralleli ma anche per sistemi aziendali, transazionali, siti web ecc. L'obiettivo prestazionale diventa quindi aumentare il throughput.
Una volta che ho il mio sistema, sicuramente oltre alle prestazioni devo fornire disponibilità e integrità, quindi migliore gestione dei guasti, replicazione delle risorse.
Infine, facilità la modularità dal punto di vista del software.
# Requisiti di un SD
cosa ci aspettiamo da un sistema distribuito come utente programmatore?
che in qualche modo ci venissero risolte problematiche interne di cui non ci vorremmo occupare, ad esempio non mi deve interessare che un computer ha un determinato sistema operativo ecc. Queste cose vorremmo siano risolte dallo strato software, in modo che sia più facile da utilizzare.
Un altra cosa utile sarebbe quelle di non avere risorse che hanno bisogno di parametri di indirizzamento (es. tcp avrò indirizzo ip e numero di porta (identificativo del processo su quella macchina), questa è una cosa che dal punto di vista del codice è da evitare, non devo avere dipendenza del genere che vincolano il codice, verrà fatto dal sistema dinamicamente).
Nasconde eventuali spostamenti di una risorsa, replicazioni di risorse, e cerco di far accedere i dati al processore o macchina più vicino, ovviamente questo non è gratis non solo per le risorse da usare, ma l'operazione che faccio deve propagarsi su tutte le copie del mio archivio, e devo lasciare la risorsa in uno stato condivisio.
C'è anche il problema di accesso alle risorse condivisione, quindi mutua esclusione e sincronizzazione, altrimenti potrei lasciare la risorsa in uno stato non consistenza.
Collegato alla replicazione, ci servono meccanismi di tolleranza ai guasti (sistemi autonomici, che 'da soli', riescono ad accorgersi di un problema e di aggiustarlo). Questo era per quanto riguarda le caratteristiche software.
Dopodichè l'utente deve poter programmare quel sistema per risolvere un problema, e sicuramente i requisiti devono essere:
- Portabilità: prendo il codice sorgente e continua a funzionare se lo porto da una macchina ad un altra, esempio un programma C che utilizza system call, lo porto su un altra macchina e lo ricompilo
- Interoperabilità: far parlare sistemi distribuiti diversi tra di loro. Questo richiede interfacce e protocolli standard.
Per la portabilità sicuramente ci servono delle API (esempio MPI è stato il tentativo di standardizzare delle funzioni per il paradigma a scambi di messaggi, o almeno solo le interfaccie, senza preoccuparsi delle implementazioni), questo serve per poter utilizzare mpi su delle macchine che magari hanno implementazioni diverse su macchine diverse.
Per l'interoperabilità, dobbiamo pensare a come le macchine di parlano, pensiamo ad esempio alle socket, che sono delle funzioni che ci consentono di aprire una connessione di tipo trasporto tra due processi su macchine diverse; le socket possono essere utilizzate con interfacce diverse (C o java, quindi implementazioni e librerie diverse). Tutto questo poi si trasforma in pacchetti relativi al protocollo TCP, e questo dipende dall'interoperabilità, (es. scrivo un client in windows con le socket java, e scrivo uns erver in linux con le socket C, quelle applicazioni sono interpoerabili, perchè possono comunicare utilizzando lo stesso protocollo, che è TCP). ,
Una volta che ho tutti gli strumenti per programmare un sistema distribuito, sicuramente dobbiamo accertarci che funziona (l'applicazione sequenziale trasformata in parallela). Requisito funzionale.
Gli altri requisiti non funzionali sono
- prestazioni
- scalabilità (aggiungo nodi di elaborazione e ottengo migliori prestazioni senza stravolgere il codice). Non è semplice da ottenere, posso incappare in nodi di bottiglia.
- Scalabilità
- Sicurezza
- Affidabilità
- Disponibilita.
Ci sono tecniche per migliorare la scalabilita, non solo per le computazioni ma anche per le comunicazione tra processi diversi .
### Tipi di Sistemi Distribuiti
Di solito i sistemi distribuiti si possono suddividere in tre tipologie:
- Sistemi di calcolo distribuiti ad alte prestazioni, di solito per elaborare applicazioni scientifiche (es. previsioni del tempo). Sono macchine connesse a reti ad alta velocità e costituite da molti nodi, anche l'hardware è orientato alle prestazioni, ad esempio il fantastico supercalcolare Vanvitelli, multicore per ogni nodo, alcuni nodi hanno delle gpu per la computazione parallela, e sono collegati a reti ad alta banda e bassa latenza.
Sempre dal punto di vista del calcolo scientifico, ad un certo punto è nata l'idea di portare nel calcolo distribuito il concetto di distribuzione dell'energia elettrica, nasce quindi il concetto di Grid, in cui le risorse di elaborazione potessero essere usate da chiunque avesse accesso senza sapere chi fornisse la potenza di elaborazione, una sorta di condivisione di risorse di elaborazione, in cui io accedevo a questa rete e usavo le risorse disponibile. Questo concetto però è fallito per via di dover utilizzare un software molto complesso che gestisse la sicurezza ecc, ma anche per il bisogno di aver accesso a risorse in tempi immediati, ma questo non poteva essere garantito sempre (es se sto giocando a elden ring, non sto offrendo risorse alla rete).
- Sistemi Informativi distribuiti: es. sistemi transazionali, sistemi aziendali. Cloud Computing, per ospitare sulla stessa macchina applicazioni isolate tra di loro.
- Sistemi distribuiti pervaisi: sistemi dotati di sensori, con poca capacità di memoria di elaborazione, che raccolgono informazioni dall'ambiente e fanno piccole elaborazioni locali. Es. sistemi domotici, sistemi di assistenza remota, reti di sensori per monitoraggio di qualcosa.
ha finito.
## Lezione 2 - 25/9/2024
### Architetture Hardware
Ricordiamo che ci aspettiamo più unità di elaborazione, in modo da ospitare più processi e implementare in qualche modo del parallelismo.
Dal punto di vista teorico, come classificazione delle architetture hardware, troviamo la tassonomia di flynn.
- SISD: Il modello di von neumann, con un processore che decodifica un istruzione, preleva le istruizioni in memoria, esegue le istruziooni una alla volta e su un singolo dato concettualmente. Quindi il processore è in grado di eseguire una sola istruzione alla volta su un solo dato. Il parallelismo si può ottenere con più core.
- SIMD (single instruction multiple data): processori vettoriali. QUesto modello fa là stessa operazione del modello sisd, ma potenzialmente su più dati. Ho sempre un unico flusso di istruzioni, quindi sempre un programma sequenziale, ma l'array processor o il processore vettoriale consentivano di eseguire la stessa istruzioni su più dati (es. array, matrici), anzichè fare un ciclo for secondo l'approccio sequenziale di von neumann.
- MISD: cpu distinte eseguono differenti istruzioni sugli stessi dati.
- MIMD: modello più famoso. Si abbandona il concetto di algoritmo come sequenza di elaborazione eseguità da una sola unità di elaborazione, ma il programma è composto da più flussi di istruzioni o processi, che vanno in parallelo su unità diverse, e lavorano su più dati. Trasformo un algoritmo sequenziale in un algoritmo parallelo, con più processi che lavorano su più dati.
- 
Quindi questi modelli teorici alternativi a quello di von neumann, hanno portato a due categorie di architetture hardware, in particolare tre:
- il modello simd ha portato a sistemi multiprocessori a memoria condivisa, in cui ho una memoria comune, tante unità di elaborazione che possono accedervi, per esempio ognuno si prende una porzione di un vettore. il codice è lo stesso per tutti.
- il modello mimd invece ha portato alle architetture multicomputer, in cui non c'è memoria condivisa, ogni macchina ha la propria memoria locale e con la propria unità di elaborazione. Quì tutte le difficolta algoritmica del problema è affidata al programmatore, che si deve preoccupare sia di suddivedere il lavoro in varie parti, possibilmente indipendenti, e suddividere anche i dati in varie parti, se però la suddivisione non è completamente indipendente, i processi dovranno scambiarsi informazioni, quindi ci vuole un sistema di interconnessione per lo scambio di dati in maniera efficiente.
- Infine ci sono i sistemi distribuiti, la differenza tra multicomputer e sd sta nell'efficienza del sistema di interconnessione, magari sono macchine simili connesse ad una rete ad alta banda e bassa latenza.
Programmare un multiprocessore o un multicomputer è molto differente, perchè i multiprocessori con la memoria condivisa, ci permette di prendere un codice sequenziale e adatattarlo al multiprocessore, quindi il codice si può adattare facilmente e non devo stravolgere la soluzione, quando c'è un operazione da fare su più dati, distribuisco le iterazioni a ogni nodo di computazione.
Se invece devo usare un multicomputer, devo stravolgere l'algoritmo, nel senso che la soluzione comunque sarà la stessa o simile, ma devo capire quali fasi della computazioni devono essere messe in sequenza, e quali invece possono essere splittate in processi diverse su macchine diverse e dati diverse.

### Multiprocessori
Insieme di nodi di elaborazione che in qualche modo riescono ad accedere 'contemporaneamente' alle locazioni di memoria condivisa. Quindi abbiamo nodi di elaborazione con spazio di memoria condivisa, e i dati stanno tutti in locazioni di memoria accessibili da tutti i nodi, dobbiamo solo occuparci di come essi accedono alla parte di dati che gli viene affidata. Vediamo come sono stati progettati nel tempo. La memoria comune può essere organizzata in modi diversi, e di conseguenza avere tempi di accesso uniformi (per ogni CPU e ogni parola di memoria) (UMA), oppure tempi di accesso non uniformi (NUMA).
- Multiprocessori **UMA**: La prima idea è stata quella di usare il BUS, quindi accesso tradizionale della cpu alla memoria centrale tramite il bus, che però deve essere in questo caso usato in mutua esclusione da più unità di elaborazione. L'accesso in memoria è uniforme dal punto di vista temporale per ogni nodo di computazione. Il problema è che gli accessi devono essere serializzati, ci saranno controlli sul bus, e ciò riduce le prestazioni perchè gli accessi alla memoria devono essere serializzati, e con molte cpu si formano colli di bottiglia.
Come miglioro i colli di bottiglia?
Un miglioramento si ottiene aggiungendo delle memorie cache per ogni nodo di computazione, quindi duplicazione di dati che si trovano in memoria condivisa, e si riduce la contesa ma nascono problemi di coerenza.
Un ultimo miglioramento si ottiene aggiungendo una memoria privata dedicata a variabili locali, questo per un singolo processore. Quindi gli accessi in memoria solo per le strutture dati condivise.

#### UMA basati su crossbar switch
Tra i tentativi di superare il collo di bottiglia del bus dell'accesso alla memoria condivisa, nel tempo sono stati progettati soluzioni diverse da queste, ad esempio una in cui i nodi di elaborazioni non sono più collegati attraverso il bus, ma tramite una rete di switch, se vedi in figura abbiamo le cpu sulle righe e moduli di memoria condivisa sulle colonne.

Abbiamo quindi realizzato comunque un multiprocessore, perchè ogni cpu può accedere a tutti i moduli di memoria. Mi serviranno N^2 switch. se voglio fare una connessone tra una cpu e memoria mi basta chiudere lo switch corrispettivo, l'importante è che non si chiudano due switch su una riga o su una colonna. Posso solo chiudere tutti gli switch sulle diagonali. Bisogna stare attenti e mettere in sequenza le operazioni contemporanee vietate, esempio due cpu che vogliono accedere alla stessa locazione di memoria.
Questa connessione con la rete di switch è costosa, ci serve una rete di N^2 switch, vediamo una soluzione alternativa meno costosa.
#### Uma basati su multistage switching networks

Abbiamo ancora degli switch che consentono due modalità di connessione:
- switch in modalità aperta, i fili che entrano da sopra escono da sopra, e quelli da sotto escono da sotto
- switch in modalità chiusa, i fili si mischiano sopra->sotto e sotto->sopra.
La differenza quì è il numero di switch che ci servono, qui nè bastano n/2 * log n. n/2 sono gli switch per ogni stadio, log2 n sono il numero di stadi.
Questa rappresentazione in bit di cpu e memoria, saprò come aprire o chiudere gli switch sui vari stadi, faccio un confronto bit a bit tra cpu e memoria, se i bit sono diversi lo switch è chiuso, ossia si intreccia il filo di sopra con quello di sotto, e sono uguali lo switch sarà aperto.
Possono nascere dei conflitti, se ho una comunicazione già attiva, devo stare attento alle altre, devo serializzare in qualche modo i conflitti.
- Multiprocessori NUMA: se invece rinunciamo al fatto che ogni processore accede in un tempo uguale per tutti alle locazioni di memoria, parliamo di multiprocessore numa, in cui ogni cpu avrà la sua memoria che ospita una sola parte della memoria globale, quindi lo spazio di indirizzamento è unico per chi scrive il programma, ma i dati in realtà sono distribuiti tra i processori.
Chiaramento l'accesso alla memoria locale è poco costoso, mentre quello ad un altra memoria è molto più costoso.

Come se avessi un multicomputer che simula un multiprocessori, faccio vedere ai processi uno spazio di indirazzamento globale, quando in realtà la memoria condivisa non c'è, a differenza degli esempi di prima.
Dove è arrivata tutta questa evoluziione di multiprocessori? Alle GPGPU (general purpose GPU), tipicamente prodotte da NVIDIA ma non solo, nascono per supporto a compiti di elaborazione grafici, ma attraverso linguaggi specifici si possono vedere come multiprocessori per elaborare programmi paralleli.
Sono costituite da tante unità elaborative semplici, con spazio di memoria condivisa.

Abbiamo un thread processor, raggruppato in un thread processor array, ed essi possono accedere a una memoria condivisa. Poi con l'evoluzione il numero di unità di elaborazione aumentano sempre di più.

Tramite linguaggi specifici, tipo CUDA, quando arriva un pezzo di codice parallelo, viene eseguito sulle gpu ed elaborato da esse, in cui ogni thread processor esegue una singola istruzione su un singolo dato, e con più thread processor avrò proprio il modello simd, quindi stessa istruzione eseguita su più dati.
Ad oggi questi sono i multiprocessori migliori!. Si usano come coprocessori o macchine parallele. I multiprocessori si adattano sempre ad applicazioni basate su modello simd.
### Multicomputer
E' un insieme di macchine autonome ognuno con la propria memoria e processore, sistema operativo. Il tutto si basa sull'avere reti di interconnessioni migliori o peggiori.
L'unicà classificazione che possiamo fare per i multicomputer è:
- omogeneo
- Stessa tecnologia di rete per collegare i singoli computer (tightly-coupled, strettamente
accoppiati).
- Le singole macchine hanno le stesse caratteristiche.
- La scalabilità dipende dal tipo di interconnessione (a Bus o switched).
- Eterogeneo
- Contiene computer indipendenti, in generale, diversi fra toro come caratteristiche (tipo di
processare, memoria, velocità di connessione).
- i computer possono essere interconnessi con reti di tipo diverso (loosely-coupled-
bassamente accoppiati).
#### Multicomputer di tipo omogeneo
Le reti strepamente accoppiate sono delle reti pensate ad hoc ed hanno bisogno di algoritmi di
instradamento. Reti di interconnessione di questo tipo, possono essere:
- Rete a griglia:

- Rete hypercube:

Ogni nodo è un computer.
Nel primo caso ogni nodo è collegato ad altri 4 nodi della rete e quindi può parlare solo con quelli
vicini ed il numero di passi per andare da un nodo all'altro dipende da dove sono disposte le 2 macchine e dall'algoritmo dÌ routing (instradamento); il numero massimo di passi è 2n. Nel sècondo caso ogni nodo è collegato ad altri 4 nodi della rete. Il numero di salti necessari per collegare 2 macchine è pari al numero di bit diversi tra l'identificativo in binario del nodo di partenza e quello del nodo di arrivo. Il numero di passi massimo per
collegare 2 macchine è pari a log2n.
Ad oggi un multicomputer classico è un cluster di elaborazione. Ciò che fa la differenza è la rete ad alta velocità.
Abbiamo capito che hardware abbiamo:
- multiprocessore: macchine a memoria condivisa reale o simulato. Questo porterà all'utilizzo di un paradigma a memoria condivisa per la programmazione.
- multicomputer: non ho memoria condivisa, questo porterà all'utilizzo di un paradigma a scambio di messaggi per la programmazione.
Due paradigmi di programazione: memoria condivisa e scambi di messaggi (esplicito, proprio all'interno del codice metterò delle primite di comunicazione).
Quindi l'hardware è la prima cosa che condiziona la mia scelta progettuale per il progetto di un sistema distribuito.
Ovviamente però, le scelte di progetto dipendono sempre dal tipo di problema, se ho un applicazione che mi richiede computazioni su array, matrici, in generale simd, mi conviene andare sui mutiprocessori.
Andiamo a vedere le architetture software.
### Architetture Software
Il software di gestione di un sistema distribuito è simile a ciò che fa un sistema operativo, quindi gestione delle risorse in maniera efficiente (processore, memoria), e interfacce per nascondere la natura compessa ed eterogenea del sistema finale.
Tutto ciò per far si che utenti e processi possano usare in maniera efficiente le risorse.
Si potrebbe pensare di avere una sorta di sistema operativo distribuito (DOS), che messo su un multicomputer offrisse queste caratteristiche, quindi semplificasse il lavoro del programmatore, e fosse in grado di gestire in maniera efficiente tutti i nodi di elaborazione. Questa sarebbe un architettura software ideale, quindi un multicomputer con nodi connessi in rete e uno strato software che funge da 'sistema operativo sequenziale', che permette al programmatore di vedere il sd come un unica macchina.
Ovviamente questo tipo di strato software è utopico, è troppo complicato sviluppare una cosa del genere.
Dall'altro estremo invece (NOS - Networking Operating Systems) è avere un insieme di nodi di elaborazione ognuno con un proprio SO, l'unica cosa aggiuntiva sono le operazioni di rete da gestire verso altri nodi di elaborazione. I servizi offerti dai NOS sono esecuzione di comandi remoti tramite shell, copie remote di file, utilizzare un file system distribuito.
Un programmatore che utilizza un approccio basato su NOS, può sviluppare le applicazioni distribuiti tramite le API di rete dei NOS, quindi tramite le socket, per scambiare messaggi fra i processi, quindi l'interfaccia di comunicazione non è transparente (es. devo scrivere hardcoded un indirizzo ip o un numero di porta).
Limitiazioni quindi dei nos sono difficoltà di gestione ecc.
Tra questi due estremi, in mezzo troviamo una soluzione con middleware, che è un ulteriore strato di software, che viene installato sopra normali sistemi operativi, che però ci offre dei servizi aggiuntivi e semplifica la programmazione.
Quello con cui lavoreremo noi, in particolare quando vedremo i paradigmi di programmazione, useremo questa architettura.

Cosa offrono i middleware?
- Supporto alla comunicazione: offrire dei servizi per nascondere i dettagli della comunicazione di rete, e quindi utilizzare qualcosa di più semplice da utilizzare delle socker per le connessioni. Dipende dal paradigma di middleware, es. se voglio programmare con scambio di messaggi min serve un middleware che mi consente di mandare un messaggio a un processo, e magari il processo middleware conosce che il processo sta in esecuzione su una determinata macchiana, e ci aiuta nell'instradamento dell'informazione.
- Naming: ogni entità viene individuata da un nome logico piuttosto che un indirizzo (es. url).

Middleware è quello verde, che sta in mezzo alle comunicazioni di rete del sistema operativo e le applicazioni.
Noi avremo a che fare con middleware che rendono le applicazioni indipendenti dallo specifico sistema operativo e processore, ossia un middleware di distribuziione, che consente di implementare un paradigma di programmazione di applicazione distribuita.

Gli altri livelli offrono altri servizi di sicurezza, gestione delle transazioni ecc.

Recap: vorremmo un DOS, dall'altro estremo abbiamo un NOS, difficile da programmare, in mezzo abbiamo NOS+middleware che useremo, con ambienti di programmazione distribuita che ci semplificano la vita.
Chiudiamo il discorso sulle architetture software con la virtualizzazione, che non è strettamente legata ai sistemi distribuiti ma è utile.
### Virtualizzazione
E' la possibilità di avere sopra lìhardware un hypervisor che virtualizza le risorse dell'elaboratore, offrendole a macchine virtuali sulle quali possono girare applicazioni in maniera isolata. Distribuisce le risorse a macchine che possono ospitare anche sistemi operativi diverse.

Lo scopo è l'isolamento delle applicazioni, quindi eventuali guasti non si ripercuotono su altre macchine.
La virtualizzazione è alla base del paradigma cloud computing.
Facilita la portabilità del codice.
Virtualizzazione significa offrire un'interfaccia a quella che lo strato superiore si immagina di avere.

Al livello più alto abbiamo chiamate di libreria, quindi API, ed è un interfaccia di alto livello e consente la portabilità del codice.
Se scendiamo al secondo livello, troviamo l'interfaccia relativa alle system call, quindi chiamate di sistema.
Dopodichè abbiamo la parte tra hardware e software per il processore.
Virtualizzare quindi significa imitare il comportamento di questo interfacce.
Abbiamo due tipi di virtualizzazione:
- di processo: tipo la virtual machine di java, il bytecode prodotto dalla compilazione di java è un interfaccia standard, c'è una macchina virtuale interpete che è in grado di intrpretare le istruzioni del bytecode e di eseguirle su qualsiasi macchina. E' come se avessimo una macchina virtuale dedicata per ogni processo; questo rende java non particolarmente efficiente perchè interpretato, ma ci sono anche vantaggi come sicurezza ecc.
- VMM (Virtual Machine Monitor): virtualizzazione delle risorse hardware, l'hypervisor offre allo strato successivo (il codice del sistema operativo) una virtualizzazione delle risorse. Il sistema operativo non si accorge che sta chiedendo risorse non direttamente all'hardware, ma proprio ad un software. Questo ci dà il vantaggio ad esempio di installare più sistemi operativi su una stessa macchina, e il monitor distribuisce le risorse tra le varie macchine virtuali.

Ovviamente abbiamo il vantaggio dell'isolamento delle applicazioni che girano nelle vm.
Spesso l'hypervisor si può installare sopra il sistema operativo, però più strati software metto e più calano le prestazioni.
la macchina virtuale ed il VMM possono interagire in due
modalità:
– Virtualizzazione completa
– Paravirtualizzazione: hypervisor espone un interfaccia hardware che anche se funzionalmente simile che servono al sistema operativo, ma non è identica, sarà ottimizzata in qualche modo. E' una sorta di virtualizzazione più efficace, però potrebbe richiedere modifiche del so ospitante.
In molti processori si sono sviluppate soluzioni in cui la virtualizzazione viene offerta proprio a livello hardware. Questa si chiama virtualizzazione nativa, dipende proprio dalle istruzioni del processore.
Per quanto riguarda le prestazioni sulla virtualizzazione, le migliori le abbiamo senza virtualizzazione, mentre la completa è la peggiore perchè richiede l'attraversamento dell'hypervisonr, la assistita sarebbe la nativa offerta dai processori.

Per quanto riguarda i container, questi sono dei modi (sempre dei tipi di virtualizzazione) per isolare le applicazioni intese anche come applicazione con tutte le sue dipendenze e strumenti di cui ha bisogno, e i container sono isolati tra loro, ciò ha come effetto la portabilità. Il meccanismo di isolamento o di virtualizzazione però dipendono da uno specifico sistema operativo, ed esempio i container docker, stanno sullo stesso sistema operativo.
Il vantaggio dei container è che i meccanismi di isolamento e virtualizzazioni offerto da un sistema tipo docker sono estramemente più efficienti, quidni i costi prestazionali sono più bassi rispetto a una virtualizzazione classica.
Mettendo insieme le due cosse ottengo una situazione ibrida in cui su una macchina posso avere più macchine virtuali, e su una singola macchina virtuale posso avere dei container. Portabilità e isolamento a costo quasi zero, con dipendenza da uino specifico sistema operativo.
Vediamo come programmare i sistemi distribuiti.
## Modelli di Interazione e di Comunicazione Inteprocesso
Abbiamo capito che lavoreremo se mpre su un multicomputer con un middleware che facilita un determinaton paradigma di programmazione.
Dobbiamo capire anzitutto cos'è un modello di iterazione e comunicazione e come implementarlo col middleware.
Paradigma di iterazione è un modello che ci guida nella distribuzione del lavoro, ad esempio capire dato un problema come distribuire le attività nell'applicazione distribuita, oppure un altro problema è capire come i processi che lavorano insieme per arrivare alla soluzione possono scambiarsi informazioni. E' una sorta di modello di riferimento, che uso quando voglio risolvere un problema in ambito distribuito.
I differenti paradigmi di iterazione si adattano più o meno bene ai vari problemi, quindi il progettista deve scegliere il paradigma giusto.
Dobbiamo quindi capire sia come funzionano i paradigmi e poi quale scegliere.
### Modello Client-Server
E' il paradigmo distribuito più diffuso, che separa in maniera netta chi richiede un determinato servizio da chi lo realizza. Tipicamente il client chiede un servizio offrendo delle informazioni o parametri.

Il server aspetta su un indirizzo ip noto e su un numero di porta noto, dopodichè crea la connessione, produce il risultato e lo restituisce al client.
Questo modello va bene per applicazioni di tipo web o aziendale, ma non va bene per il discorso di parallelizzazione, le due fasi di lavoro non si sovrappongono mai.
Il vantaggio di questo modello è la semplicità, e non c'è bisogno di nessun middleware, ci bastano le socker o comunque protocolli di rete (3 livelli se siamo in lan).

Il problema di questo modello è il primo contatto, perchè il client deve in qualche modo identificare il server, sicuramente serve l'indirizzo ip della macchina su cui gira il server, e dal numero di porta che viene assegnato dal sistema operativo della macchina. Nella realtà probabilmente avrò un client scritto da qualcuno e un server da un altro, ecco che quindi ci sono i numeri di porta standard, cioè sono associati a servizi standard, quindi ci basta sapere il nome (tramite dns) o indirizzo ip della macchina, perchè alcuni servizi sono associati a numeri di porta predefiniti.
Ci vuole poi un servizio di naming che traduce un nome in un indirizzo ip e numero di porta (name server), cosi metto nomi logici nel codice, ed inoltre esso sarà una struttura dinamica, ossia la traduzione tra nome logico e indirizzo di trasporto avviene nel momento in cui serve, quindi posso spostare i servizi da una macchina all'altra, quindi ad esempio cambio ip e porta, e mi basta solo aggiornare il name server.
Nel tempo l'architettura client-server si è complicata, una classica evoluzione è ad esempio l'architettura a 3 livelli:

Il server si 'sdoppia', c'è un primo server frontend che gestisce la richiesta, che a sua volta funge da client ad esempio per fare una ricerca in un database.

Ancora non c'è sovrapposizione.
Esempio è un motore di ricerca:

Il modello può essere espanso a più livelli.
Per migliorare le prestazioni di una applicazione client-server, apparte di una condivisione verticale dei compiti del server in più livelli, l'alternativa è distribuire il lavoro anzichè in modo vertificale ma farlo in orizzontale, ossia ci sarà una duplicazione deis erver, con qualcuno che fa da frontend e distribuisce il lavoro.
Il vantaggio è quello di non usare middleware.
Un evoluzione del modello client-server è il modello RPC, Remote Procedure Call, ed anche gli oggetti distribuiti, che sono l'equivalente delle rpc ma orientate agli oggetti. Sono ancora modelli client-server, ma stavolta il servizio è una chiamata a procedura. Sono comunque modelli di programmazione distribuita.
Quindi si fa una chiamata a una procedura remota, ossia viene richiesta su una macchina remota.
Il modello è client-server perchè c'è ancora un programma chiamante quindi un client, ed il processo chiamante si ferma finchè non ottiene un risultato.
A che serve questo paradigma di programmazione distribuita? Serve a chi è abituato a scrivere un programma sequenziale classico, per usare lo stesso modello di programmazione, quindi continuare a scrivere codice sequenziale, ma può decidere che alcune procedure del codice verranno eseguite su un altra macchina.
Possiamo quindi distribuire l'esecuzione di procedure o metodi di oggetti su macchine diverse.
### RPC
Un processo può chiamare una procedura la cui implementazione è disponibile su una
macchina remota. Il programmatore lato client scrive un normale codice sequenziale, poi alcune di queste, magari una compurazionalmente onerosa, decido che verrà eseguita su un altra macchina (server). Il tutto si chiude come una classica chiusura di procedura, cioè viene attivata e aspettiamo l'esecuzione e il risultato. Non c'è ancora sovrapposizione tra programma chiamante e chiamato.
L'obiettivo delle rpc è quello di replicare la programmazione procedurale in ambito distribuito senza modificare il modo di scrivere l'applicazione. In realtà le cose non sono cosi semplici, ci serve un middleware.
Ricordiamo che localmente quando facciamo una chiamata di procedura, in linguaggio macchina ci sarà un istruzione jump to subroutine, che salva il valore del program counter (prossima istruzione) nello stack, e mette nel PC l'istruzione della procedura; di solito oltre al PC salviamo anche i parametri di ingresso, usando uno spazio di memoria condiviso che è lo stack; dopodichè la procedura viene eseguita, e quando l'elaborazione è compleatata, salva nello stack i parametri di riuscita e fa un ritorno da subroutine, ossia rimette nel PC l'indirizzo della prossima istruzione del programma chiamante. Quindi la chiamata a procedura locale è semplice, è un salto a sottoprogramma, ossia alla prima istruzione di una procedura in memoria, e lo scambio di parametri avviene tramite lo stack condiviso tra programma chiamante e sottoprogramma.
Adesso questa cosa la dobbiamo replicare su due macchine diverse, in una c'è il programma chiamante, e sulla seconda c'è la procedura; quindi abbiamo spazi di memoria diverse.
Adesso quindi non ci basta salvare il valore del PC, ma adesso la chiamata a procedura remota nasconde una connessione con un server con ip e num di porta. Quindi il primo problema riguarda la connessione. Ecco quindi se non vogliamo gestire tutto noi con le socket ci serve un middleware, che ci serve per contattare in qualche modo il server in maniera indipendente al programmatore.
Oltre al problema della connessione, c'è anche il problema dello scambio di parametri, perchè adesso, una volta che ho stabilito la connessione, i dati di ingresso alla procedura devo trasferirli al server, e anche il server poi una volta che ha prodotto il risultato deve spedirlo al programma chiamante.
Quindi i problemi che richiedono la presenza di un middleware sono:
- stabilire una connessione col server
- scambio di parametri.
In realtà vedremo che la chiamata alla procedura remota è una chiamata a una procedura locale fake, con la stessa interfaccia, offerta dal middleware, che prende i parametri, li impacchetta ecc, e li spedisce.
### Oggetti Distribuiti
E' equivalente al paradigma RPC, con la differenza è che anziche usare un linguaggio procedurale (es. C), si utilizza un linguaggio ad oggetti (es. Java), in cui alcuni di questi oggetti o metodi anzichè localmente saranno eseguiti su una macchina remota server.
Noi useremo come middleware le librerie di Java RMI, che consentono di scrivere app. java classiche, ma gli oggetti o alcuni metodi potranno essere eseguiti su un altra macchina.
C'è però qualche complicazione in più rispetto a rpc, i parametri possono essere di tipi primitivo, ed un metodo di un oggetto può avere come parametri di ingressi e uscite saranno oggetti, quindi devo occuparmi di come trasferire un oggetto, quindi una struttura complessa, da una macchina all'altra.
Vedremo che però possiamo trasferire non solo dati ma anche codice con cui gestire quei dati.
### Lezione 4 - 1/10/2024
Paradigma di interazione per scrive app distribuite significa come distribuire il lavoro tra i vari processi, e come i processi cooperano tra di loro, quindi il modello secondo cui progettiamo un app distribuita è legato al paradigma di interazione che si sceglie. Questi paradigmi saranno supportati da un middleware, librerie, ambienti di esecuzione, che li renderà un pò più semplice da utilizzare per sfruttare le caratteristiche hardware.
Il primo che abbiamo visto è client server in cui il lavoro è tutto sul server, e l'interazione avviene da un client che chiede un servizio. Il server può anche scalare orizzontalmente.
Questo modello va bene per applicazioni web e commerciali, ma non va bene in app in cui voglio migliorare le performance, non c'è sovrappossizione di lavoro tra client e server quindi non c'è parallelismo; quindi non si presta ad avere parallelismo e migliorare le prestazioni di un app distribuita.
Questo modello si appoggia su un protocollo trasporto+rete, quindi tcp/ip; quindi scrivere un app client server significa lavorare sul minimo indispensabile, ossia le socket; ha poca trasparenza, dettagli su specificare i tipi di protocollo, indirizzi ip, numeri di porta; il vantaggio è che non ha middleware. Tutto ciò si paga in complessità nello scrivere l'applicazione.
Il modello successivo sono le chiamate a procedura remota e oggetti distribuiti.
RPC ha lo scopo di portare il modo sequenziale di programmazione in cui siamo abitutati a programmare, dando ad esso la decisione di scegliere quali procedure verranno eseguite su un altra macchina. Il codice è lo stesso che scriveremmo su una macchina sequenziale.
Oggetti distribuiti è l'equivalente ad oggetti delle rpc, quindi avrò oggetti e metodi, paradigma oop!. I metodi remoti verranno eseguiti su macchine diverse.
Quì, a fronte della semplificazione della progettazione proprio nella programmazione, dobbiamo risolvere alcuni problemi, dato che sotto abbiamo ancora un modello client-server, la chiamata a sottoprogramma sicuramente richiede una connessione, la procedura remota deve mettersi in attesa, quindi creare socket (ip, porta). U
Un altro problema è legato al fatto che non essendoci uno spazio di memoria condivisa, dobbiamo trasferire fisicamente i parametri di ingresso della procedura e il risultato della stessa, quindi dobbiamo proprio impacchetterli e farli arrivare dal client al server e viceversa; ci sono problemi legati al tipo di formato ecc, che ptoremmo risolvere da soli, ma se ci fosse un middleware a basso livello che ce li risolve lui, il lavoro del programamtore si semplifica; in tal caso i dettagli di come ad esempio il programma chiamante chiama il sottoprogramma e i formati dipendono poi dal middleware.
Poi li vedremo più in dettaglio, in particolare cosa ci offrono i middleware per aiutarci con questo paradigma di interazione.
Ricorda che con rpc e oggetti distribuiti siamo ancora nel paradigma di interazione client-server, ancora non c'è sovrapposizione di lavoro e parallelismo.
Se vogliamo ottenere prestazioni migliori (in termini di speedup e scaleup, in cui lo speedup dal rapporto del tempo di esecuzione in sequenziale e in modo distribuito, se è alto vuol dire che stiamo risolvende lo stesso problema in modo distribuito, es speedup=n dove n sono le macchine, quindi sto distribuendo in maniera perfetta il lavoro sulle macchine, mentre lo scaleup ci dice se il nostro programma parallelo puiò risolvere uno stesso problema sequenziale ma con più dati).
I paradigmi che ci consentono di ottenere prestazioni migliori sono:
- Paradigma di interazione a memoria condivisa, che si sposa bene con i multiprocessore. Consente al programmatore di distribuire le iterazioni di un ciclo ad esempio, che lavorano su una struttura dati, tra più thread o processi; ogni thread si prende una parte dei cicli iterativi, poi sicomme la struttura dati è condivisa tra tutti, se abbiamo fatto bene la divisione, ognuno si prende il proprio pezzo di struttura dati in memoria e ci lavora. La suddivisione si può fare se i cicli di iterazione sono indipendenti l'una tra l'altro, ossia se ho un iterazione i+1 dipende dall'iterazione precedente i, allora non posso più distribuire più il lavoro, perchè chi ha l'iterazione i+1 dovrebbe aspettare il thread che ha l'i-esima, quindi non va bene perche starei ancora creando della sequenzialità. Se quelle iterazioni le vogliamo fare in parallelo su una struttura a memoria condivisa, dobbiamo essere sicuri che le iterazioni siano indipendenti, cosi le posso fare tutte in parallello senza doverle fare in ordine da 0 ad N ad esempio. Di solito di parte da un codice sequenziale che si vuole parallelizzare tramite questo paradigma, e il programmatore deve capire quale parte di codice parallelizzare e come distribuire piùù che il lavoro in realtà i dati ai thread/processi. Questo paradigma lo scegliamo se abbiamo la possibilità di avere un multiprocessore o una gpu (che è un multiprocessore), oppure se l'applicazione che stiamo parallelizzando si presta a questo tipo di distribuzione; questa applicazione si chiama data parallel, che riflette il modello teorico SIMD. Questo modello è più semplice da usare e se riusciamo nel lavoro ci dà buone performance. Applicazioni data parallel sono tipo applicazioni di elaborazione di immagini, quindi non è che questo modello va bene per tutto.
Se ho un modello in cui non ci basta più distribuire i dati per far fare a ogni thread la stessa cosa, quindi non ho più data parallelism, allora avrò il Task parallelism, in cui distribuiamo cose da fare diverse a nodi diversi. Nell'algoritmo quindi bisogna trovare delle istruzioni che possono essere eseguite in parallela perchè non c'è una dipendenza. Nelle applicazioni task parallel devono quindi capire quali sono i task interni e sono parallelizzabili, quindi distribuiamo non solo i dati ma anche i compiti. Tipico di un architettura MIMD. Avrò quindi dei processi che si parlano tra di loro, che implica sicuramente una sincronizzazione, ossia il lavoro fatto su una macchina servirà a qualcun altro a un certo punto, e poi i risultati parziali in quale modo devono essere trasferiti.
- Paradigma di interazione message passing: è il paradigma tipico per le applicazioni task parallel, quindi facciamo diventare l'applicazione un insieme di processi, con codice diverso, istruzioni diverse su dati diversi per ogni processi, però siccome i processi stanno lavorando globalmente sulla stessa cosa devono scambiarsi informazioni, ovviamente non esiste memoria condivisa qui, quindi i dati andranno spediti. Il lavoro più complicato qui è del programmatore, mentre il middleware che supporta questo paradigma ci offre una serie di librerie per sincronizzazione e scambio di dati, in cui la cosa nascosta al programmatore è dove i processi sono in esecuzione, il codice deve essere indipendnete da dove essi sono allocati, ci vogliono delle primitive ad hoc.
Quindi, se vogliamo parallelismo, dobbiamo usare uno di questi due paradigmi. Ovviamente le prestazioni ottime, tipo scaleup=n, ce lo sogniamo perchè pure se riusciamo a distribuire bene il lavoro su processi, quindi nel caso message passing, ci sono anche primite di trasferimento dati che impattano sui costi complessivi, e quindi non possiamo pensare di avere un miglioramento di tempi di elaborazione di n; paghiamo i tempi dei costi di comunicazione. Capiamo quindi che ci serve un sistema di comunciazione efficiente, ossia bassa latenza e ampia banda, proprio per abbattere questi costi aggiuntivi; sono costi che nella versione sequenziale non ci sono!.
### paradigma p2
Vediamo altri paradigmi usati per altre applicazioni:
- Paradigma peer-to-peer: un server è anche client per un altro servizio. I client sono distribuiti e non esiste un vero server. Si usano per file sharing, app di messagistica. Ci sono tanti middlware e ambienti che consentono di implementare app di questo tipo, alcuni di questi sono 'best-effort', basati su nodi che entrano e escono dalla rete in maniera non predefinita, poi ce ne sono altri che hanno un collegamento strutturata con l'architettura di rete sottostante ai peer; il problema è l'associazione dati nodo. Ci sono ad esempio le DHT, che assegnano una chiave ai nodi e ai dati condivisi, identifica quindi qual è il nodo su cui una risorsa è disponibile; ogni nodo ha una visione limitata di tutti gli altri che fanno parte della rete, però conoscendo il numero della risorsa e chiedendo ai nodi vicini, si riesce ad arrivare alla risorsa desiderata.

Esempio nodo 1 se vuole la risorsa 12, deve chiedere ai vicini finchè non arriva, c'è una fanstastica tabella chord!.
Ci può essere anche una struttura in cui ogni nodo possiede uno spazio virtuale cartesiano di dati, ed ogni nodo è responsabile di quella zona (CAN), e l'algoritmo dalle coordinate riesce a contattare dopo tot passi il nodo responsabile di quella risorsa che ci serve.
L'architettura P2P deve reagire dinamicamente a ingresso e uscita dei peer nella rete, ad esempio vedi figura dht chord, se entra il nodo 10, allora il nodo 12 deve lasciarli le risorse 8-9-10, perchè la regola è che le risorse sono assegnate al nodo con id maggiore, però comunque il nodo 10 può entrare o uscire dalla rete, ci vuole una configurazione automatica delle risorse. Lo stesso per CAN, in cui ai nodi sono assegnate le regioni.
Un caso particolare delle reti P2P meno democratico, sono i superpeers, in cui ci sono peers diversi tra di loro, ci sono dei nodi che hanno una funzione di indice, e rendono la comunicazione un pò più semplice e centralizzato.
ABbiamo visto quindi una presentazione generale dei paradigmi di interazione con cui lavoreremo; adesso li vediamo più nel dettaglio-
### Modello client-server, socket
partiamo da Client-server con cui vedremo in particolare come scrivere le socket, siamo senza nessun middlware; usiamo C o Java con le loro librerie.
Abbiamo interoperabilità, posso scrivere un client in C e un server in java ad esempio.

Le socket saranno tcp o udp, che è aperta in attesa di connessioni; tipicamente è su una porta nota sul server, il client deve sapere dove il server si aspetta quella richiesta. I client possono essere tanti, e quando al server arriva una richiesta su quella socket, allora crea una connessione dedicata; quindi quando arriva la connessione, viene creata una nuova socket o connessione dedicata a quello specifico client, con un nuovo numero di porta lato server, quindi la socket inziale è un trucco; questo sistema consente lato server di realizzare un minimo di parallelismo, perchè il server, quando è stato contattato, ha creato un canale di servizio, e può delegare quel servizio a un thread, mentre lui si rimette ad aspettare una nuova connessione, se si mettesse anche a fare lui il lavoro, le richieste successive dovrebbero aspettare che si completino le precedenti. Vedremo un esempio di server multithread.
Le socket saranno tcp e udp, il problema iniziale è sempre conoscere la coppia ip+porta su dove il server sta aspettando le richieste.
Vediamo in C quali sono le primitive o funzioni che ci consentono di gestire le socket.

Questo è un esempio con socket non connessa, ossia che si basa sul protocollo udp, il quale non prevede una fase iniziale di handshake per creare la connessione; ciò non garantisce l'ordine di arrivo dei messaggi come li ho spediti, e anche la sicurezza che essi arrivino, a confronto di tcp. Protocollo connesso invece è tcp, che è un po meno efficiente ma con dei vantaggi legati alla sicurezza e alla connessione iniziale. Il protocollo non connesso udp invece deve avere per ogni messaggio l'indirizzo di trasporto, però non è sicuro che arrivi a destinazione. Dal punto di vista delle librerie, abbiamo funzioni diverse, il server usa una primita socket con dei parametri, è un equivalente della open di un file, in cui dobbiamo specificare se connessa o no, quando creata restituisce un sd ossia un socket descriptor, questo lo facciamo sia lato client che server. Lato server, ci serve la funzione bind per fissare a quella socket un numero di porta fissato.
Una volta creato le due socket (il server ha una porta scelta o fissa, cosi da farsi riconoscere dal client). Il server si mette ad aspettare richieste, mentre il client che fa il primo passo, ha quella primitiva sendto per inviare i dati al server tramite la socket; il messaggio viene acquisito dal server e elabora il risultato, infine risponde al client, conoscendone già il servizio; ogni messaggio deve avere ip+porta del destinatario.
Vediamo la modalità connessa (TCP):

Create le socket, con il bind sul server, si assegna tramite la primitiva listen altri parametri, tipo quante connessioni contemporanemaente quella socket iniziale può gestire senza perderla. Una volta configurata la socket di servizio, il server aspetta una richiesta di connessione. Il client, dopo aver anche lui creato la socket, la primita ora è connect, che crea la connessione tra client e server (3way handshake di tcp). Tramite la funzione accept sul server, questa ci restituisce un nuovo descrittore di socket, quindi ogni client avrà poi la propria socket dedicata. Una volta creata la connessione e la socket dedicata, quella connessione è come se fosse un file, quindi usiamo le primitive write e read sul client per scrivere e leggere dati. La cosa particolare da notare è che il server lavora solo su sc non su sd, sd è la socket iniziale con la porta nota dedicata solo al primo contatto, sc è la socket dedicata a ogni client.

Questo ci consente di scrivere un server multithread, quindi può creare un thread e passargli la socket di servizio di uno specifico client, questo rende concorrente il servizio del singolo client.
Vediamo pure in java.
Il modello client server e i protocolli sono sempre gli stessi. Vediamo che java ci dà delle librerie, oggetti e classi diverse.
java.net E’ il package Java che mette a disposizione una serie di classi chesupportano la programmazione di rete utilizzando i protocolli Internet (IP,TCP,UDP).
La classe InetAddress serve per gestire gli indirizzi IP e i nomi delle macchine, la classe socket si occupa di creare le socket di tipo connesso; stavolta a differenza di C ci sono due classi diverse per le socket.

Vediamo queste classi nel dettagglio:

InetAdress ci permette di scrivere oggetti che rappresentano indirizzi ip (creo un oggetto di tipo InetAddress). Quello che tipicamente si fa è usare quel metodo statico getLocalHost(), che restituisce l'host locale su cui è eseguito il programma, ossiamente ci serve l'ip anche del server, posso usare getHostAddress() o getByName(String host).
Gli indirizzi li usiamo con la classe Socket.

Un costrutture della classe Socket lato client ha come parametri uin oggetto InetAddress e numero di porta, questo però nasconde anche la connect di C, quindi crea la connessione vera e propria; è un oggetto di tipo socket già connesso. Queste operazione possono sempre fallire, vanno in un trycatch sempre. Dopodichè la useremo come stream di ingresso e uscita; associa a questo canale fisico di tipo tcp qualcsoa su cui leggere e scrivere.
Vediamo la socket lato server:

La socket appena creata non è ancora connessa, lo diventa se viene eseguito un metodo accept, in cui ci si mette in attesa della connessione, finchè qualcuno non fa socket() lato client con l'indirizzo del server, e poi restituirà la socket di servizio, sui cui metteremo sempre stream di input e output. Ci sono le socket di servizio, quindi possiamo pensare a server multithread.
Vediamo anche qualcosa per le socket udp:

DatagramSocket: Classe che implementa socket client e server utilizzando UDP
DatagramPacket: Classe per rappresentare un datagram per invio o ricezione
Fine slide socket e lezione4.
faremo esercitatione socket java. poi dopo che abbiamo fatto tutto in java vediamo se possiamo sostituire un pezzo in C e vedere se funziona ancora!.
### Lezione 5 - 2/10/2024
Esercitazione socket.
### Lezione 6 - 2/10/2024
Rpc e oggetti distribuititi rendono la programmazione delle app client-server più semplice e simili all'ambito sequenziale (procedurali come C, o ad oggetti come java).
### RPC
Il modello di base rpc è stato proposto da Birrell e Nelson
nel 1984; L’obiettivo è quello di consentire l’invocazione di
procedure (funzioni) su altre macchine:
- In questo modo la comunicazione interprocesso si
riconduce ad un modello conosciuto e
fondamentalmente sincrono
- E’ un astrazione di livello elevato che nasconde
dettagli di basso livello e rende trasparente la
presenza di una comunicazione attraverso una rete
- Dal punto di vista del programmatore applicativo non
c’è (almeno in teoria) differenza fra chiamate locali e
remote
Quindi continua a scrivere l'applicazione come una serie di procedure, alcune di queste saranno eseguite su un'altra macchina; il programma chiamante ha funzione di client, il chiamato da server.

Nel modello chiamata a procedura remota, abbiamo un processo che sta eseguendo codice su una macchina A che chiama una procedura sulla macchina B, esso quindi si ferma e aspetta il risultato; simile a una chiamata di procedura locale, gli passiamo i parametri, si salta al codice in grado di eseguire la procedura, ma in quel caso la memoria è locale, si fa un jump subroutine, che salva l'indirizzo di ritorno (PC) nello stack e mette nel PC il primo indirizzo della procedura, e anche il passaggio di parametri si risolve con lo stack.

Lo stack si indirizza con lo stack pointer che è un altro registro del processore.
Sempre in ambito locale, il passaggio dei parametri avviene principalmente in due modi:
- passaggio per valore: il parametro è copiato sullo stack, quindi preso dalla memoria e copiato, quindi il sottoprogramma lavora sulla copia e il dato originale non viene alterato. Se però devo lavorare con dati grandi, vettori o matrici, questo metodo non è molto efficiente, dovrei copiare molti dati sullo stack.
- passaggio per riferimento: si mette sullo stack non il dato, ma l'indirizzo di memoria, ad esempio in un vettore quella del primo elemento, e poi con un offset trovo gli altri; questo metodo però è pericoloso perchè si potrebbe modificare il dato reale e non una copia.
Vediamo cosa cambia con rpc, quindi replicare ciò su macchine diverse. La prima posa da considerare è la mancanza di memoria condivisa, ad esempio quello stack. Dobbiamo quindi capire come trasmettere i dati/parametri di ingresso e risultati. Non avrò un jump subroutine stavolta; ci serve che il programma chiamante riesca anzitutto a contattare un processo in esecuzione su una macchina remota, quindi sicuramente ci serve un indirizzo di trasporto; quindi almeno un processo in attesa deve esserci Questo è praticamente un problema di binding, dobbiamo capire ip+porta su cui c'è questo server.
Un altro aspetto da tenere in conto è che a differenza dell'attivazione di procedura locale, quì le cose possono andare in crash, esempio per la connessione, quindi dobbiamo capire se quell'operazione è stata bene una volta sola, o è stata ripetuta più volte a causa di qualche errore. Se la funzione ha idempotenza (la procedura produce lo stesso risultato ogni volta
che è chiamata (anche se avviene più volte)) allora possiamo essere sicuri.
Quindi, problemi principali:
- binding
- trasferimento parametri ingresso e uscita.
Vogliamo che questi problemi siano trasparenti al programmatore, quindi questo paradigma di interazione deve nascondere questi due aspetti (ma sicuramente devono essere implementati, non dal programmatore però). Quì quindi tipicamente viene fornito un middleware, che dovrebbe risolvere queste problematiche.
Qual'è il trucco? Come fa il middleware a risolvere ciò?
Bisogna avere del codice aggiuntivo lato client e server che si occupa del lavoro sporco; questo codice aggiuntivo si chiamano *STUB*, lato client e lato server.

Il programmatore scrive le procedure client e server, e il middleware deve fornire client e server stub.
Di solito c'è un compilatore che scrive questi stub.
Gli stub sono programmi che possono essere
generati in modo automatico a partire da una
descrizione dell’interfaccia della procedura in un
linguaggio IDL (Interface Description Language).
La chiamata a procedura remota sembrerà normale, ma essa in realtà attiverà il client_stub, quindi questo client_stub offre alla prodecura client una funzione che ha la stessa interfaccia della procedura remota, ma che non fa ancora niente, si occupa di creare la connesione; questa prima procedura finta è ancora eseguita localmente quindi, gli basta sapere l'interfaccia, quindi tipi di parametri di ingresso e uscita, perchè deve vedere a chi mandarli e impacchettarli. Il client_stub tutto fà tranne che la procedura che realmente si vuole eseguire in remoto, ma si occupa di risolvere il problema del binding (contattando il server stub).
Quindi il client fa una call locale a una procedura del clientstub che ha stessa interfaccia della remota, poi dopodichè questa tramite librerie sistema operativo e protocollo trasposrto contatta il server_stub e gli manda i parametri; a questo punto il server stub fa anche lui una chiamata locale, questa volta della vera procedura remota, attende il risultato, e lo rispedisce al client stub, che poi farà ritorno da subroutine; quindi il client_stub in qualche modo si può vedere come un jump subroutine, ma è una procedura fake.
Notiamo che client e server stub non sanno come sono fatti client e server, devono sapere solo l'interfaccia della procedura. Quindi al programmatore basta definire l'interfaccia della procedura che vuole eseguire in maniera remota, poi gli stub saranno generati da un compilatore ad esempio.
I passi sono:

Capiamo quindi che RPC lavora con uno scambio di parametri per valore, alla procedura remota non posso passare un indirizzo, hanno spazi di memoria diversi, quindi ci vogliono copie di parametri.
Questo impacchettamentoo di dati nasconde insidie, sopratuttto su un sistema distribuito non omogeneoo nei processori e SO.
Sappiamo che esistono due codifiche per i dati per quanto riguarda i processori, little e big endian.
Quando bisogna trasferire più byte, nel little endian, i dati che hanno rappresentazione su più byte, gestiscono prima la parte meno significativa:

Il byte meno significativo è associato al primo indirizzo.
Nel big endian si fa il contrario, si parte dalla parte più significativa, collacata all'indirizzo più piccolo.
Es. se devo trasferire un intero (4 byte), bisogna mettersi d'accordo su come inviarli e ricevere:

vedi come il secondo ha sbagliato a interpretare quel 5.
Quindi la rappresentazione di dati è insidiosa. Ci serve una regola.
Come risolviamo questo problema? la soluzione più semplice ma meno efficiente è usare una codifica standard di trasferimento, ad esempio indipendetemente dalle codifiche di ricezione e trasmissione, siamo costretti a fare delle trasformazioni:

Non è efficientissimo, se ho due macchine con stessa rappresentazione faccio dei passaggi in più.
Il passaggio di parametri per riferimento non ha molto senso.
Questi problemi in rpc li ritroviamo anche in quello a oggetti distribuiti (ma quello lo approfondiamo di più con le esercitazioni), con rpc vedremo solo cosa ci offre il middleware.
Ricordiamo che con rpc stiamo ancora usando un paradigma client-server, in cui non abbiamo sovrappossizione di lavoro tra programma chiamanete e chiamato, perchè il chiamante si ferma e aspetta.
A volte se il problema lo consente, in certi middleware viene offerta una variante: rpc asincrona, ossia in cui il client avvia tutte le operazioni viste prima col client stub ecc, ma la procedura chiamante avrà subito un feedback dal client stub e non deve mettersi in attesa del risultato, quindi il client può riprendere a eseguire codice senza fermarsi ad aspettare.

questo quindi di sovrappore il lavoro che avviene remotamente con quello del programma chiamante, ovviamente se non ha urgenza di aspettare il risultato. Ovviamente nasce un altro problema, ossia come facciamo a sapere che poi il risultato diventa pronto localmente? di solito queste chiamate asincrone o introducono un meccanismo di modifica, oppure la cosa più semplice sarebbe che, quando il client ha finito di fare le altre cose, rifarà la stessa chiamata a procedura remota, quindi fà in totale due chiamate, la prima non bloccante, e la seconda bloccante.
Vediamo un esempio teorico di utilizzo di un middleware che supporta questo paradigma di interazione con rpc.
DCE (Distributed computing environment), che ci fornisce il codice di client e server stub e risolve il problema del binding; e fornisce anche altri servizi di supporto legati alla sicurezza ecc.
Vediamo cosa deve fare il programmatore (scrivere 3 cose):
- scrivere tramite un linguaggio IDL un interfaccia dei metodi remoti che vuole utilizzare. Lo deve fare tramite IDL perchè questo file deve essere letto da un compilatore IDL, che ne controlla la correttezza.
- Il codice del client e il codice del server in base a quello che deve fare.
Quindi in ambito sequenziale scriverebbe solo client e server, adesso lo sforzo aggiuntivo è che deve scrivere la descrizione dell'interfaccia.
Quell'IDL compiler genera come si chiama la funzione, quali sono i parametri di ingresso uscita e i tipi, e produce tre file:
- client stub, che implementa una funzione fasulla con stessa interfaccia che ho scritto nel file idl
- server stub, in cui c'è il codice per ricevere i parametri della procedura remota da parte del client stub e attivare la rpocedura remota vera e propria.
- Un header, quindi un file .h, in cui ci sono le funzioni che devono essere linkate per ottenere il codice eseguibile.
Sostanzialemte sono dei codici sorgenti, sono per esempio dei file .c e .h, che vengono compilati (i punti .c, ottenengo due file .o).
L'header serve per fare degli include nei codici client e server per fare il link con la funzione che sta nello stub.
In realtà nel .h ci sono altre funzioni a tempo di esecuzione, che risolvono il problema del binding.
Alla fine comunque otteniamo due eseguibili.

vedremo che risolve il binding in maniera dinamico, che sarà un supporto a tempo di esecuzione. Serve il nome logico della funzione e il server deve registrarsi da qualche parte tramite il middleware.
Lo vedremo più avanti!.

Quando scrivo cient e server ho già compilato il .idl.

Il server quando parte registra il nome della macchina/indirizzo ip nella directory machine, e il numero di porta in quel demone che tiene traccia dei servizi eseguiti localmente.
terminata questa fase di registrazione, il client deve fare due ricerce. Il client prima traduce il nome logico in indirizzo ip, poi chiede al deamon il numero di porta, e poi attiva la procedura remota. La doppia traduzione dà più flessibilità al server che può far ripartire il servizio cambiando numero di porta.
Questo è per quanto riguarda il middleware per rendere questo modello di interazione simile a scrivere un applicazione sequenziale.
### Remove Object Invocation
tutto quanto detto per le rpc si riporta al modello di interazione a oggetti distribuiti, o remove object invocation.
quindi consento di attivare degli oggetti (metodi) su una macchina diversa.

Stesso metodo, il client attiva una funzione con stessa interfaccia nel client stub, ma stavolta si chiama proxy anzichè client stub, che fa lo stesso lavoro visto in rpc.
Dall'altro lato il server stub si chiama skeleton, il quale dopo aver ricevuto i parametri attiva un metodo che ha la stessa interfaccia di quello invocato inizialmente.
I problemi sono ancora binding e scambio di parametri. C'è però una prima differenza fondamentale, ossia che i parametri nella programmazione ad oggetti non sono sempre di tipo primitivo, ma possono essere oggetti, quindi io posso passare a una procedura un istanza di un oggetto; inoltre l'oggetto non è solo una struttura dati, ma ci sono anche i metodi (della classe di quell'oggetto).
Questa complicazione può diventare in realtà un opportunità, trasferire dati+codice vedremo che ci consente di inviare non solo i dati su cui lavorare ma dirgli anche come lavorarli.
Un altra caratteristica che si può sfruttare con questo paradigma è che con qualche trucco potremmo fare anche lo scambio per riferimento almeno solo semanticamente, nella pratica sarà sempre una qualche copia, non ho memoria condivisa e gli spazi di indirizzi sono diversi. Per riferimento intendiamo ad esempio che l'originale parametro di ingresso nella memoria del programma chiamante può essere modificato dal programma chiamato, quindi esso riesce a modificare l'oggetto originale.
Anche qui il middleware ci offrirà qualcosa per risolvere il binding e qualcosa più o meno automatica (vedremo che non scriveremo più interfaccia ad esempio, ad esempio in java sarà più semplice).
Un altra differenza sarà il binding, che vedremo può essere esplicito

### Lezione 7 - 8/10/2024
mo fa vedere esempio socket java non connessa con udp; ricordiamo che queste socket richiedono ogni volta di inviare l'indirizzo di trasporto, questo per ogni messaggio; non abbiamo la certezza che i dati arrivino (non c'è un meccanismo di ack), e non siamo sicuri nemmeno arrivino in ordine corretto. con DatagramSocket() posso inviare i messaggi tramite send e receive, mando un DatagramPacket che è un pacchetto di dati, costituiti da ip porta del destinatario, e i dati, che saranno array di byte, ed il numero totale di byte che inviamo.
torniamo a oggetti distribuiti.
Ricordiamo che vogliamo ottenere qualcosa di analogo alle rpc, ma in particolare vogliamo imitare il modo di scrivere o progettare un app distribuita come se la stessimo scrivendo su una macchina sequenziale, in questo caso se vogliamo usare un paradigma di programmazione orientato agli oggetti. Progettiamo il codice come su macchina sequenziale e poi decidiamo che alcuni metodi di oggetti possono essere attivati su una macchina remota; stiamo ancora nel paradigma client server e ancora non c'è sovrapposizione di lavoro.
Le problematiche sono ancora:
- il trasferimento di parametri, col problema del formato; quì questo problema è più accentuato perchè i dati possono essere anche oggetti (dati+codice).
- L'altro problema è quello del binding, ossia capire dove il metodo remoto è in attesa.
Il programmatore si deve preoccupare solo del codice dell'oggetto chiamante e oggetto remoto.
Il trucco di nuovo ce lo offre il middleware, che ci fornisce del codice in più come nel caso rpc, in particolare qui abbiamo:
- un oggetto proxy lato client, che deve offrire un interfaccia del metodo che vogliamo attivare remotamente; quindi il proxy implementa un metodo fake rispetto a quell originale, e tutto fa tranne eseguire le operazioni del metodo remoto vero, ma ha stessi parametri di ingressso e uscita e qualche informazioni per occuparsi del middleware. In particolare si occuperà di contattare lo skeleton e trasferire i parametri di ingresso, mettendosi in attesa del risultato.
- un oggetto skeleton lato server, in attesa di un contatto lato client, quando questo riceve i parametri esso li converte in un formato utile, e poi è lo skeleton che attiva il metodo remoto vero e proprio, che è stato implementato lato server. Quando il server termina la elaborazione, restituisce il risultato allo skeleton, che, cosi come ha fatto il proxy prima, impacchetta il risultato e lo rimanda al proxy, il quale infine lo rimanda al client.
Lo scambio di parametri è ancora per valore, quindi il metodo remoto lavora su copie di dati (interi, float, ma stavolta copie di oggetti anche).
Per quanto riguarda il binding, ci serve qualche servizio del middleware di registry, che sia in grado di registrare servizi/metodi remoti, associando un nome logico a un indirizzo, e tale servizio di registry deve essere pronto a richieste di lookup/ricerca verso il servizio logico da parte del client, cercando di fare le cose in maniera pià transparente possibile.
Per quanto riguarda passaggio di parametri, si chiama:
- marshaling: processo che trasforma un oggetto in
memoria in una espressione serializzabile.
- unmarshaling, prendo i dati dalla rete e li trasfromto di nuovo in oggetti.
Si possono verificare due casi:
- passaggio per riferimento: lo simuliamo, perchè non si può fare realmente, ma diciamo al server in qualche modo di lavorare sul valore originale del parametro, e tutte le modifiche volute o non volute sui dati di ingresso si devono ripercorrere sul dato originale, es se ho un oggetto con campo intero che incremento sul server, anche se quello non è un parametro di uscita, nel client alla fine me lo devo trovare modificato; nel passaggio per valore questo non succede.
- passaggio per valore: se il parametro è un riferimento ad un oggetto
locale (o un tipo primitivo) allora deve essere
copiato per intero nello spazio di memoria
dell’oggetto invocato
### Java RMI
vediamo che ci offre java per implementare questo paradigma.
Java ci offre una libreria, che si chiama java RMI. Sostanzialmente è un insieme di classi messe a disposizione, e poi due servizi aggiuntivi:
- servizio di registry: è praticamente un eseguibile da far partire. Potrebbe stare su una macchina che non è il client o il server, che è associato a un indirizzo che deve essere noto a client e server. Tipicamente fa le operazioni di binding, quindi il server registra il servizio assegnando un nome loogico, il client cerca il nome logico e ottiene l'indirizzo.
- codice extra che sarebbero skeleton e proxy. Questo codice viene prodotto prima a mano (versione vecchia di java, tramite un compilatore usando il .class del server e client, che generavano .class di proxy e skeleton); da una certa versione in poi java ci offre gratis questa operazione, quindi se compilo del codice java con metodi remoti, il compilatore java ci produce in automatico il codice di proxy e skeleton, direttamente nel .class di server e client.
con java si parte dalla definzione di un interfaccia (classe vuota), che comprende i metodi che voglio eseguire remotamente. questo è l'unico pezzo di codice che deve essere condiviso tra client e server.

Non è un interfaccia come tutte le altre, e quindi per renderla utilizzabile remotamente dobbiamo specificare che è un interfaccia remota, infatti questa interfaccia estende l'interfaccia java.rmi.Remote. Ovviamente estendere l'interfaccia vuol dire eredirare i metodi dell'interfaccia remota di java rmi, i metodi ovviamente spesso non ci interessano perchè spesso non li dobbiamo implementare. I metodi che ci serve specificare in questa interfaccia sono i metodi che dobbiamo implementare noi e che vogliamo eseguire remotamente. Bisogna stare attendi che i metodi eseguiti remotamente possono generare eccezioni. Possiamo mettere tutti i metodi remoti che vogliamo.
Vediamo cosa contiene l'interfaccia remota che ci interessa:
in realtà non ci interessa tantissimo.
Il server contiene una classe che
– estende UnicastRemoteObject
– implementa ll’interfaccia
interfaccia remota
– ha il costruttore implementato che genera
RemoteException
• Solo i metodi nell’interfaccia remota sono
disponibili per il client

La classe UnicastRemoteObject sarà quella che attiva veramente il servizio, ossia uso il metodo costruttore che me lo attiva.
Ovviamente lato server ci manca ancora da scrivere un pezzo di codice. stiamo trascurando tutti i problemi di sicurezza che possono esserci quando inviamo codice da eseguire sulla macchina remota!.
Vediamo come si attiva il servizio remoto.
Nel main che abbiamo sul server, dobbiamo creare un oggetto della classe che implementa UnicastRemoteObject, e quell'oggetto rappresenta un servizio, quindi gli viene assegnato un ip e una porta. Cosi però ancora non è utilizzabile da nessuno, ci serve un binding!. Ecco che subentra il ruolo del registry. Se vogliamo che questo servizio venga reso pubblico ai client, lo devo registrare sul servizio di registring, che abbiamo detto è un eseguibile che dobbiamo far partire su una macchina su una porta (noi useremo i metodi bind, rebind, lookup).
Il server usa bind (metodi statici), quindi non dobbiamo creare nessun oggetto della classe Naming. Il metodo bind static richiede due parametri, che sono:
- identificativo del servizio di registry (non del server) + nome logico associato al servizio (quel RMIClass).
- il secondo parametro è il riferimento all'oggetto appena creato, ossia quello prodotto dal costruttore di UnicastRemoteObject.
Se usiamo rebind, se la stringa è già presente semplicemente viene sovrascritta.
Ovviamente se creo un altro oggetto della stessa classe posso duplicare il servizio. ovviamente devo registrarli entrambi con nomi diversi se li voglio rendere utilizzabili.
Il servizio registry è un daemon che quando fatto partire si mette in ascolto sulla porta 1099.

Naming è la classe di javarmi con i suoi metodi statici che ci consente di interagire col registry. unbind ci serve per rimuovere il servizio dal registro.
Vediamo proxy e skeleton. Abbiamo detto ci sono offerti dal compilatore di java, che produce codice extra nei .class; se vogliamo ottenere i .class separati dobbiamo usare il compilatore rmic, altrimenti ci pensa java.
Gli stub e gli skeleton svolgono le operazioni di
marshalling e unmarshalling dei parametri.
Marshalling si basa sul meccanismo di serializzazione, ossia gli oggetti vengono serializzati, e diventano dei byte string che vengono poi inviati. Lo skeleton quando viene contattato riceve questra stringa di byte, e lo deserializza facendo unmarshalling, dopodichè invoca il metodo remoto.

Il ritorno è molto simile.

Lo scambio di parametri è per valore, quindi si lavora su una copia.
Vediamo meglio marshalling:
- processo che trasforma un oggetto in
memoria in una espressione serializzabile
- si applica agli oggetti che vengono passati
al metodo dell’oggetto remoto come
parametro, ed al valore che il metodo
ritorna
- nel caso del passaggio di parametri il
marshalling avviene nello Stub, nel ritorno
dei risultati avviene nello Skeleton
Gli oggetti devono essere di tipo serializzabile, non legati ad una rappresentazione propria del sistema operativo. In java è quindi necessario che gli oggetti parametro
e l’oggetto risultato implementino
l’interfaccia java.io.Serializable
- Non tutte le classi sono serializzabili
– Ad es. la classe Java.awt.Image non è
serializzabile, perché rimanda alla descrizione delle immagini che dipende dai s.o.
IN java rmi possiamo emulare un passaggio per riferimento:
- a volte sarebbe comodo mantenere sempre la
coerenza tra le copie dei parametri del server e del
client.
- occorre però un meccanismo che aggiorni
automaticamente gli oggetti del client quando le
rispettive copie presenti nel server subiscono una
modifica
modifica.
- occorre allora che la copia presente nel server
notifichi alla rispettiva copia nel client ogni
variazione, cioè un meccanismo RMI con client e
server invertiti.
Vogliamo che il server lavori sul dato originale, però il server non può accedere allo spazio di memoria del client; il trucco è far si che il client offra un'interfaccia per accedere ai dati del suo oggetto, ossia per esempio un parametro che vuole essere scambiato per riferimento, deve diventare anche lui un oggetto remoto, ossia deve diventare un oggetto che offre dei servizi per esempio di lettura e scrittura; il server non lavora direttamente su quel dato, ma chiede al client di fare quell'operazione; l'effetto è che sto consentendo al server di fare ciò che vuole sul mio dato di ingresso. Bisogna farlo in maniera consapevole.
Riepilogo:

Lato client è un po più semplice, client e server condividono solo il nome dell'interfaccia. Faccio lookup del servizio e poi faccio cast all'tipo dell'interfaccia che ho definito remota. una volta che ho l'oggetto che implementa l'interfaccia remota posso chiamare il metodo remoto.
Serialization: When you serialize an object, only the member data within that object is written to the byte stream; not the code that actually implements the object.
Marshalling: Term Marshalling is used when we talk about passing Object to remote objects(RMI). In Marshalling Object is serialized(member data is serialized) + Codebase is attached.
So Serialization is a part of Marshalling.
CodeBase is information that tells the receiver of Object where the implementation of this object can be found. Any program that thinks it might ever pass an object to another program that may not have seen it before must set the codebase, so that the receiver can know where to download the code from, if it doesn't have the code available locally. The receiver will, upon deserializing the object, fetch the codebase from it and load the code from that location.
Domani già esercitazione javarmi.
Vediamo un esempio:
partiamo dalla scrittura, consente al client di scrivere una stringa in un file, passando la stringa e il nome del file che il server dovrà creare e scriverci dentro.
sicuramente devo definire l'interfaccia che estende Remote e dentro ci metto i metodi che ci servono, in questo caso un solo metodo scrivifile(string, string). Questo è il .class che ci serve per client e server.
dopodiche creo la classe che avrà il main che sarà eseguito lato server, creo una classe che implementa l'interfaccia remota, ne definisco il metodo scrivi file, e nel main istanzio l'oggetto remoto; facciamo poi un bind. Questo codice non termina perchè il servizio non termina ma rimane in attesa di uno o più attivazioni.
Prima di far partire server e client, devo far partire rmi registry (devo mettero nel path del s.o. in caso).
Infine il client, deve fare lookup e attivare il servizio. creiamo un riferimento all'oggetto di tipo interfaccia comune.
### Lezione 8 - 9/10/2024
Prima di vedere un nuovo paradigma di programmazione openmp, vediamo un altro esempio rmi.
### OpenMP
abbiamo visto socket,rpc e roi, che implementato tutti il paradigma client-server, in cui abbiamo lavoro delegato al server e un client che lo attivava; la sovrapposizione di lavoro non c'è.
Ricordiamo che i due paradigmi distribuiti per applicazioni scientifiche, e ottenere miglioramenti prestazionali sono shared memory e message passing.
OpenMP è (de-facto) una standard API (Application Program Interface) per
scrivere applicazioni parallele a memoria condivisa in C, C++ e Fortran.
Sfrutta:
direttive di compilazione;
variabili d’ambiente;
funzioni a run-time.
I workers che lavorano in parallelo (threads) cooperano attraverso la
memoria condivisa (shared memory):
accessi diretti alla memoria invece di espliciti messaggi;
modello locale di parallelizzazione del codice seriale.
Consente una strategia incrementale di parallelizzazione.
Open specifications for Multi Processing è mantenuto standard dal
OpenMP Architecture Review Board
Adesso vediamo qualcosa per il paradigma a memoria condivisa.
Ricordiamo che in questo paradigma supponiamo di avere un hardware di tipo multiprocessore, quindi ho più nodi di elaborazione e uno spazio di memoria condiviso. Supponiamo quindi la disponibilità di hardware con spazio di memoria condivisa. Per quanto riguarda il problema che vogliamo risolvere con queston paradigma, una parallelizzazione con questo paradigma viene bene se il problema è di tipo data parallel, che signifca che ci sono molti pezzi di lavoro che rientrano nel paradigma SIMD, ossia fare la stessa operazione su più dati; quindi, se abbiamo:
- architettura multiprocessore
- problema di tipo data parallel, faccio stessa operazione su più dati, es. vettori, matrici.
Allora con questo paradigma ottengo le migliori prestazioni col paradigma shared memory.
Notiamo che il lavoro delle unità di elaborazione deve essere indipenente, quindi il lavoro di un thread o da un processo può procedere senza comunicazione o sincronizzazione con gli altri che stanno facendo la stessa cosa su dati diversi.
Un altra cosa da tenere in conto è che con questo paradigma lavoriamo con già del codice sequenziale che risolve quel determinato problema, questo rende semplice di arrivare rapidamente alla parallelizzazione di tipo shared memory facendo poche modifiche.
Paradigma memoria condivisa: trasformare programma sequenziale in sostanzialmente in un applicazione multithread per esempio, se questi thread sono eseguità su unità di elaborazione che lavorano in parallelo, e se queste possono accedere alla memoria condivisa, ecco che abbiamo parallelizato il codice sequenziale.
Sostanzialmente distribuiamo le iterazioni dei cicli.
Questa è l'idea, vediamo che ci offre il middleware OpenMP. Esso ha come target un programmatore che ha disponibilità del codice con un linguaggio procedurale, come C. Conosce cosa fa il codice, quindi quali sono le parti che possono essere parallelizzabili.
OpenMP mette a disposizione un set di direttive di compilazione (che non sono codice o chiamate di procedure), ma sono richieste fatte al compilatore. Quindi il programmatore introduce delle richieste al compilatore per quanto riguarda la parallelizzazione. Le direttive di compilazione non sono chiamate di funzione. QUnado il compilatore vede la direttiva (ha una sintassi nota), farà delle operazioni per contro nostro.

Quindi tramite le direttive di compilazione, openmp ci rende la nostra app multithread.
Oltre a queste direttive, c'è anche una libreria di funzioni per fare altre operazioni.
OpenMP è STANDARD. Il codice openMP quindi è portabile (C+direttive di compilazizone).

Il programmatore deve conoscere il codice e individuare i pezzi di codice che possono essere parallelizzabili, ovviamente è la parte più delicata.
Negli anni 90 si voleva anche esonarare il programmatore da fare ciò.
Vedisamo il modello di programmazione.
Abbiamo un parallelismo basato su threads ma a pezzi.

inizialmente abbiamo un unico thread che inizia ad eseguire le istruzione del main, poi, secondo le nostre direttive di compilazione, e poi vengono fatti partire altri thread.
Bisogna individuare le regioni parallele che possono essere eseguite da più thread. Quando inizia una regione parallela, il compilatore esegue una serie di fork, ossia crea nuovi processi o thread, e tutti eseguono quel pezzo di codice sequenziale (tutti fanno la stessa cosa su dati diversi). Es. elaborazione immagine,
OpenMP usa il modello fork-join per la parallelizzazione:
• Tutti i programmi OpenMP iniziano come un singolo processo: il master thread.
• Il master thread esegue sequenzialmente fino a che viene incontrata il primo costrutto parallel region.
• FORK: il master thread crea un team di parallel threads
• Le istruzioni racchiuse nel costrutto parallel region sono eseguite in parallelo dal team di thread
• JOIN: quando il team threads completa le istruzioni della parallel region, si sincronizzano e terminano lasciando attivo solo il master thread.
Alla fine devo ottenere lo stesso risultato che otterei in sequenziale, ovviamente cosi miglioro le prestazioni.

Vediamo in C:

Quella riga
#pragma omp parallel private(var1,var2) shared(var3)
se la keyword dopo pragma omp, il codice che viene dopo nelle {}, sarà eseguito in parallelo da un certo numero di thread generati dal compilatore.
prima di ogni regione parallela posso anche specificare il num di thread con una funzione.
nella direttiva dobbiamo specificare quali variabili sono condivise o private. Condivise vuol dire che possono essere accedute da tutti i thread.

Ovviamente per la vairabili shared in regione parallela fa nascere problemi di mutua esclusione ecc.
Le variabili private invece, vuol dire che il compilatore quando crea i thread, esso dedica uno spazio di memoria per ogni thread, e ognuno avrà le proprie var private, per esempio var1 e var2 nella foto.
La variabile privata consente ai thread di lavorarci senza problemi di mutua esclusione.
il numero di thread sono fissati dinamicamente o tramite una variabile di ambiente.

i thread li riusciamo a identificare, sono numerati da 0 (master thread) fino a N-1 (sostanzialmente è un tid).

Costrutti di work-sharing.
• Un costrutto work-sharing divide l’esecuzione tra i
diversi thread della regione che segue.
• Un costrutto work-sharing non crea nuovi thread
• All’ingresso non ci sono sincronizzazioni
implicite, mentre ce ne sono alla fine.
Dopo che ho distribuito le iterazioni ai thread, alla fine del join c'è una attesa in cui tutti i thread aspettano che ha finito, oppure potrei decidere che un thread qunado finsice và avanti senza aspettare gli altri.

direttiva single faccio eseguire un pezzo di codice a un solo thread.
do/for è data parallelism proprio, i thread si smezzano le iterazioni.

Direttiva do/for all'interno di una direttiva parallel, quindi dentro una regione parallela, fà si che le iterazioni di un ciclo vengono distribuite a ciascun thread.
Come tutte le direttive ha delle opzioni.
#### Scheduling
descrive come le iterazioni di un loop sono divise tra i thread.
Statica, è una sorta di round robin. C'è un parametro chunksize che indica quante istruzioni ogni thread si prende.
opzione dinamica: ho sempre chunksize, in cui non le distribusce tutte ma solo alcune, per esempio se ho 100 iterazioni, gli dico comincia a distribuirne 50.
no wait è l'opzione della direttiva do/for che prevede una sincroniccazione o no, di default c'è sincronizzazione che è più sicura, altrimenti no.
esempio do for, ci sono due vettori che vengono sommati.

### Lezione 9 - 9/10/2024
Esercitazione javarmi
### Lezione 10 - 15/10/2024
esempio javarmi con un interfaccia job boh. voleva dimostrare che possiamo passare anche codice se passiamo un oggetto (i metodi capo).
recap enorme openmp
sections ha detto pop due parole
direttiva critical che, in una regione parallela, identifica una regione critica, che deve essere eseguita da un thread alla volta perchè si accede alla memoria condivisa.
direttiva barrier.
librerie runtime.
come si usa tutto cio col compilatore??
si usa l'opzione -fopenmp, usiamo gcc.
le gpu hanno memoria condivisa. Ci sono i thread processor.
Cuda fa si che ogni iterazione di un ciclo venga distribuita sulle migliaia di thread processor, a differenza di openmp che distribuisce blocchi di iterazioni.
arronzato cuda e gpu.
### Lezione 11 - 16/10/2024
esercitazione openmp.
### Lezione 12 - 16/10/2024
passare parametri per riferimento con javarmi.
se voglio passare oggetti per riferimento, il parametro che passo deve essere a sua volta non definito come un oggetto normale, ma dobbiamo definire quell'oggetto come interfaccia remota, che sarà implementata lato client. Ovviamente anche quest altra interfaccia remota avrà dei metodi. Una dovrà essere implementata lato client e una lato server. Il client non si registra, ma quando viene attivata una funziona che usa gli oggetti dell'interfaccia remota, si trasferisce non solo l'oggetto ma anche il riferimento al servizio remoto sul lato cli
### Paradigma message passing
non c'è memoria condivisa, bisogna trasferire informazioni tramite scambio di messaggi.
E' il più generale possibile e più complicato, e richiede al programmatore che per avere miglioramenti prestazionali, richiede un ripensamento della risoluzione al problema.
Il middleware più usato per questo paradigma è MPI, probabilmente l'unico. MPI infatti è una standardizzazione di primitive di scambi di messaggi e sincronizzazione.
MPI è solo una definizione delle interfacce, poi ci sono varie implementazioni.

bisogna creare e inviare dei messaggi.
Abbiamo tanti codici diversi che fanno cose diverse ognuno sui propri dati. modello mimd. La nostra app diventa un insieme di processi ognuno con la propria memoria e dati.
Dobbiamo inserire dentro dei codici sequenziali delle chiamate a delle primitive di comunicazione e sincronizzazione.
La comunicazione è:
- persistente: quando c'è scambio di messaggi tra processi, non è necessario che il ricevemente processo sia in esecuzione. In mezzo c'è qualcosa che mantiene i messaggi, e quando il ricevemente è disponibile se lo va a recuperare, esempio i server di posta.
- transiente
Asincrona: chi invia non aspetta un messaggio di risposta ma può continuare a fare altro.
Sincrona: chi invia il messaggio può riprendere la propria elaborazione se il messaggio è almeno ricevuto dalla macchina destinatario. Il lato client sà cosa sta succedendo al destinatario.
il mittente si sblocca quando il messaggio viene consegnato al processo (secondo livello di sincronizzazione).
Mescolando tutto abbiamo 4 tipi:
- asincrona persistente
- sincrona persisten
- asincrona transiente
- sicnrona transiente

Un middleware per scambi di messassi persistente deve implementare una corrispondenza tra le code e le loro posizioni in rete.
### Lezione 13 - 22/10/2024
fine slide messagge passing.