Modulo Neighborhood - Analisi Funzionale

Ruolo del modulo

Il ruolo fondamentale del modulo Neighborhood è il rilevamento dei collegamenti (detti archi) che si possono realizzare con altri nodi diretti vicini, la loro realizzazione e la misurazione del costo associato a tali archi.

Per l'esattezza, il nodo diretto vicino che viene rilevato è il processo ntkd che è in esecuzione su tale nodo. Infatti tale processo all'avvio si crea un identificativo univoco, che chiamiamo NeigborhoodNodeID e lo trasmette periodicamente sui domini broadcast a cui il nodo è collegato con le sue interfacce di rete. Ogni nodo crea un solo NeigborhoodNodeID e lo mantiene invariato per tutta la sua vita.

Per distinguere un vicino da un altro, il modulo guarda il NeigborhoodNodeID. Inoltre, il modulo distingue anche le singole interfacce di rete che riesce a rilevare di ogni suo vicino. Cioè guarda alla coppia NeigborhoodNodeID + MAC address.

Come diretta conseguenza del ruolo di realizzazione e mantenimento degli archi, al modulo Neighborhood è demandato anche il compito di permettere agli altri moduli la comunicazione tra nodi:

Identità multiple in un nodo

Introduciamo il concetto di identità. In un singolo nodo possono in dati momenti sussistere diverse identità. La ragione di essere di queste identità sarà discussa in dettaglio nella trattazione del modulo QSPN, quando si parla di nodi virtuali.

Ogni identità di un nodo ha un suo identificativo. Questo identificativo è distinto dal NeigborhoodNodeID, il quale è un concetto interno al modulo Neighborhood. L'identificativo assegnato ad una identità di un nodo lo chiamiamo semplicemente NodeID.

Il NodeID assegnato a ogni identità è essenzialmente un intero di 32 bit scelto a caso, assumendo che sia univoco a livello dei domini di collisione in cui il nodo partecipa con le sue interfacce di rete. Questo dettaglio implementativo non è di pertinenza del modulo Neighborhood. Il modulo sa solo che il NodeID può essere usato come identificativo univoco che si riferisce ad una precisa identità all'interno di un preciso nodo.

Il modulo Neighborhood non ha conoscenza diretta di quali siano le identità che vivono nel suo nodo in un dato momento. Sappiamo solo che ogni nodo in ogni momento ha una e una sola identità principale. Parleremo in seguito delle caratteristiche peculiari di questa particolare identità, ma non sono informazioni di pertinenza del modulo Neighborhood.

Abbiamo visto che la presenza di identità multiple in un nodo è supportata dal framework ZCD. Con questo intendiamo evidenziare che è possibile realizzare uno stub che possa essere usato da una precisa identità del nodo a, indichiamola con a0, per chiamare un metodo remoto su una precisa identità del nodo b, indichiamola con b0.

Abbiamo detto inoltre che fra i ruoli del modulo Neighborhood c'è quello di permettere agli altri moduli dell'applicazione la comunicazione con altri nodi. Ora aggiungiamo che un particolare modulo può essere interessato a questo tipo di identificazione precisa della identità all'interno di un nodo. Un altro modulo, invece, potrebbe essere agnostico rispetto a queste identità, e voler semplicemente chiamare un metodo remoto su un particolare nodo. Il modulo Neighborhood, lato client, deve saper produrre uno stub per ogni esigenza. Inoltre, lato server, deve saper individuare un elenco (con zero, uno o più elementi) di root-dispatcher a partire da un messaggio ricevuto (cioè da un UnicastID o un BroadcastID).

Nei casi in cui l'identità interna al singolo nodo è rilevante, il NodeID che identifica una identità di un nodo è una parte essenziale delle classi che si usano come SourceID, UnicastID e BroadcastID nella produzione di stub per chiamare metodi remoti.

Facciamo un esempio di un modulo (ad esempio QSPN) in cui una precisa identità del nodo a, indichiamola con a0, vuole chiamare un metodo remoto su una precisa identità del nodo diretto vicino b, indichiamola con b0. Il nodo a chiama un metodo di Neighborhood per produrre uno stub di tipo identity_aware_unicast. In questo metodo il nodo passa il NodeID di a0 e questo sarà usato dal modulo Neighborhood per produrre un IdentityAwareSourceID. Inoltre in questo metodo il nodo passa il NodeID di b0 e questo sarà usato dal modulo Neighborhood per produrre un IdentityAwareUnicastID. Per il lato server vedremo sotto come il nodo b aveva istruito il suo modulo Neighborhood perché potesse gestire questo tipo di UnicastID.

Facciamo un altro esempio di un modulo in cui una precisa identità del nodo a, indichiamola con a0, vuole chiamare un metodo remoto su un set di precise identità dei nodi diretti vicini, diciamo b0, b1, c0 e d0. Il nodo a chiama un metodo di Neighborhood per produrre uno stub di tipo identity_aware_broadcast. In questo metodo il nodo passa il NodeID di a0 e questo sarà usato dal modulo Neighborhood per produrre un IdentityAwareSourceID. Inoltre in questo metodo il nodo passa i vari NodeID di b0, b1, c0 e d0 e questi saranno usati dal modulo Neighborhood per produrre un IdentityAwareBroadcastID. Per il lato server vedremo sotto come i nodi, ad esempio b, c, d ed anche e, avevano istruito ognuno il suo modulo Neighborhood perché potesse gestire questo tipo di BroadcastID.

In altri casi l'identità interna al singolo nodo non è rilevante. In questi casi è il NeigborhoodNodeID che costituisce la parte essenziale delle classi che si usano come SourceID, UnicastID e BroadcastID nella produzione di stub per chiamare metodi remoti.

Facciamo un esempio di un modulo in cui un nodo a vuole chiamare un metodo remoto su un nodo diretto vicino b. Il nodo a chiama un metodo di Neighborhood per produrre uno stub di tipo whole_node_unicast. Il modulo Neighborhood usa il suo NeigborhoodNodeID per produrre un WholeNodeSourceID. Inoltre in questo metodo il nodo passa il INeighborhoodArc di b (vedremo sotto come questo oggetto sia fornito dal modulo Neighborhood al suo esterno, mentre il NeigborhoodNodeID è interno al modulo); il relativo NeighborhoodNodeID sarà usato dal modulo Neighborhood per produrre un WholeNodeUnicastID. Per il lato server vedremo sotto come il nodo b aveva istruito il suo modulo Neighborhood perché potesse gestire questo tipo di UnicastID. Anche il WholeNodeSourceID ricevuto dal nodo b sarà sufficiente perché il modulo Neighborhood sappia associarlo (anche su richiesta di un altro modulo) ad un particolare INeighborhoodArc.

Facciamo un altro esempio di un modulo in cui un nodo a vuole chiamare un metodo remoto su un set di nodi diretti vicini, diciamo b, c e d. Il nodo a chiama un metodo di Neighborhood per produrre uno stub di tipo whole_node_broadcast. Il modulo Neighborhood usa il suo NeigborhoodNodeID per produrre un WholeNodeSourceID. Inoltre in questo metodo il nodo passa i vari INeighborhoodArc di b, c e d; i relativi NeighborhoodNodeID saranno usati dal modulo Neighborhood per produrre un WholeNodeBroadcastID. Per il lato server vedremo sotto come i nodi, ad esempio b, c, d ed anche e, avevano istruito ognuno il suo modulo Neighborhood perché potesse gestire questo tipo di BroadcastID.

Ovviamente anche lo stesso modulo Neighborhood, il quale nelle sue comunicazioni con i diretti vicini non è interessato a distinguere le identità, può usare in completa autonomia le modalità sopra esposte per effettuare chiamate Unicast e Broadcast.

All'inizio dovrò dire al Neighborhood:

Operazioni del modulo

Il modulo fa uso delle tasklet, un sistema di multithreading cooperativo. Attraverso di esso esegue il monitoraggio delle schede di rete lasciando libero il chiamante di svolgere altri task.

Il modulo fa uso del framework ZCD, precisamente appoggiandosi ad una libreria intermedia prodotta con questo framework per formalizzare i metodi remoti usati nel demone ntkd.

Quando si inizializza, il modulo produce l'identificativo NeigborhoodNodeID del proprio nodo.

Il modulo viene subito inizializzato con le callback da chiamare per gestire le chiamate IdentityAware e lo skeleton da usare per le chiamate WholeNode. Di modo che è pronto a gestire le chiamate che il nodo potrà ricevere.

Il modulo riceve subito l'elenco delle interfacce di rete che deve gestire. Ad ognuna associa un indirizzo locale detto indirizzo di scheda.

Rileva i nodi vicini raggiungibili attraverso una (o più) interfacce di rete e ne reperisce l'identificativo NeigborhoodNodeID + MAC. Si veda sotto la trattazione dell'argomento degli archi multipli con un unico nodo vicino: essi sono ammessi solo se diversi MAC del vicino sono rilevati da diverse interfacce di rete del nodo corrente. Per verificare questo vincolo è necessario identificare un nodo vicino come singola entità (con il NeigborhoodNodeID) e non basarsi soltanto sui distinti MAC address.

Con ogni vicino, poi, si accorda per la creazione (o meno) di un arco. L'accordo potrebbe non essere raggiunto perché uno dei due vertici ha già un numero di archi elevato. Se l'accordo viene raggiunto entrambi i nodi vengono anche a conoscenza dell' indirizzo di scheda del vicino.

Sia k un arco che il modulo nel nodo corrente a ha creato con un vicino b. La struttura dati che il modulo mantiene per l'arco k contiene:

Quando il modulo crea un nuovo arco, esso imposta le rotte nelle tabelle del kernel per rendere possibile la comunicazione via TCP con il vicino attraverso gli indirizzi di scheda.

Nel tempo, il modulo gestisce la costituzione di nuovi archi, la rimozione di archi, i cambiamenti del costo degli archi.

Creazione di una nuova identità

Abbiamo detto che la creazione di una nuova identità di un nodo non è una scelta del modulo, ma del suo utilizzatore. Vediamo come questo avviene.

Esaminiamo il caso in cui, a fronte di una migrazione, il nodo corrente a crea una nuova identità a1, cioè un nuovo NodeID, basata su una sua precedente identità a0.

Tutte queste operazioni non coinvolgono direttamente il modulo Neighborhood. Esso resta comunque in grado, ricevendo dall'utilizzatore i NodeID aggiornati, di produrre uno stub per comunicare dalla sua nuova identità ad uno o più diretti vicini. Resta anche in grado, dato un messaggio ricevuto, di identificare, per mezzo delle callback ricevute all'inizio, se è per la sua nuova identità e da parte di chi.

Esaminiamo il caso in cui un nodo vicino b crea una nuova identità b1 basata su una sua precedente identità b0. La precedente identità b0 era collegata attraverso un arco (o più di uno) alla identità del nodo corrente ak, la quale non è cambiata.

TODO

Tutte queste operazioni non coinvolgono direttamente il modulo Neighborhood. Esso resta comunque in grado, ricevendo dall'utilizzatore i NodeID aggiornati, di produrre uno stub per comunicare dalla sua vecchia identità alla nuova identità del vicino. Resta anche in grado, dato un messaggio ricevuto, di identificare, per mezzo delle callback ricevute all'inizio, se è per una sua identità da parte della nuova identità del vicino.

Rimozione di un arco-identità

Deve essere possibile richiedere al modulo Neighborhood la rimozione di un certo arco-identità dal set mantenuto in un certo arco.

Di norma questo avviene quando l'utilizzatore del modulo decide che una certa identità del nodo corrente non deve avere più archi verso una certa identità di un suo vicino; quindi l'operazione va fatta su tutti gli archi che collegano i due nodi. Quindi diciamo anche che l'utilizzatore del modulo deve poter chiedere al modulo l'elenco di tutti i suoi archi e per ogni arco l'elenco degli archi-identità associati. Così che possa controllarli e poi sapere di quali richiedere la rimozione.

Quando il modulo Neighborhood rimuove un arco-identità (ricordiamo che questo avviene solo su richiesta diretta del suo utilizzatore) si occupa anche di rimuovere la rotta che collegava i relativi indirizzi di scheda. Quindi in precedenza l'utilizzatore del modulo si era dovuto occupare di rimuovere (o cambiare) tutte le rotte che usavano quell'arco come gateway.

Caratteristiche degli archi

Quando si crea un arco esso esiste per entrambi i nodi.


Tra due nodi vicini ci possono essere più collegamenti. Ad esempio i nodi A e B possono avere entrambi una interfaccia di rete wireless e una ethernet ed essere collegati sia con un cavo (direttamente o per il tramite di un hub) sia con le interfacce wireless (in modalità ad-hoc o per il tramite di un access point). Oppure ancora, un nodo C con due interfacce di rete ethernet può essere collegato tramite due cavi ad un unico switch al quale è collegato anche il nodo D.

Ci chiediamo: in quali casi può essere utile che i due nodi tengano in considerazione più di un arco? Se i collegamenti sono su distinti domini di collisione, è utile considerarli in modo distinto, poiché le trasmissioni fatte su un collegamento non influenzano la capacità dell'altro collegamento. Ad esempio i nodi A e B possono sfruttare in parallelo i due collegamenti (uno via cavo e l'altro via etere). Consideriamo il caso del nodo C che ha le due interfacce ethernet C0 e C1 collegate tramite due cavi ad uno switch1. Supponiamo che il nodo D sia collegato allo stesso switch con un solo cavo collegato alla sua interfaccia D0. Supponiamo che sia in corso una trasmissione di dati da D0 a C0. Allora questa trasmissione influenza la capacità del collegamento tra D0 e C1; quindi non potremmo usare questi due collegamenti in parallelo in modo efficiente.

Supponiamo, invece, che il nodo D sia collegato allo stesso switch con due cavi collegati alle sue interfacce D0 e D1. Supponiamo che sia in corso una trasmissione di dati da D0 a C0. Allora questa trasmissione, per le caratteristiche di uno switch, non influenza la capacità del collegamento tra D1 e C1; quindi potremmo usare questi due collegamenti in parallelo in modo efficiente.

Si consideri inoltre che uno switch, pur realizzando distinti domini di collisione, forma un unico dominio broadcast; questo significa che il nodo C tramite la sua interfaccia C0 con un messaggio in broadcast rileva la presenza del nodo D anche attraverso la sua interfaccia D1. E la stessa cosa vale per la coppia D0 e C1. Però non ha senso considerare tutti questi 4 possibili archi: lo sfruttamento ottimale si ha con due archi, ad esempio D0-C0 e D1-C1, che possono essere usati per due trasmissioni in parallelo che non si disturbano a vicenda.

Ovviamente un nodo non è in grado di sapere se il segmento di rete a cui è collegato tramite una sua interfaccia è supportato da uno switch o da un semplice hub. Per avvicinarci allo sfruttamento ottimale descritto sopra, implementiamo questa regola: il modulo consente la costituzione di un secondo arco tra i nodi A e B solo se le interfacce di rete di entrambi i nodi sono diverse da quelle su cui è realizzato il primo arco.


Il costo di un arco in una direzione (da A verso B​) può essere diverso dal costo nella direzione inversa. Cioè ogni vertice effettua una misurazione del costo dell'arco indipendente da quella fatta dall'altro vertice. Questo non è utile quando il costo rappresenta il round-trip time, ma può esserlo se rappresenta la larghezza di banda in uscita.


Quando un nodo rimuove un arco tenta di comunicarlo al vertice collegato perché faccia altrettanto.

Requisiti

Deliverables

Classi e interfacce

L'implementazione del sistema di tasklet è passata al modulo dal suo utilizzatore. Si tratta di una istanza dell'interfaccia INtkdTasklet che è descritta nel relativo documento.


Una interfaccia di rete passata al modulo è un oggetto istanza di una classe di cui il modulo conosce l'interfaccia INeighborhoodNetworkInterface. Tramite questa interfaccia il modulo può:

L'utilizzatore del modulo Neighborhood può fornire il meccanismo di misurazione della latenza (o in generale del costo associato ad un arco) secondo due modalità. Come primo tentativo il modulo chiama il metodo 'measure_rtt' passando alcune informazioni tra cui l'indirizzo di scheda (del vicino) associato all'arco. Se l'oggetto fornito dall'utilizzatore è in grado di misurare la latenza con accuratezza (ad esempio eseguendo un comando "ping") questo primo tentativo va a buon fine. Altrimenti il modulo procederà con il secondo meccanismo.

Il meccanismo alternativo prevede la collaborazione del modulo nel nodo vicino. In questo caso il compito dell'oggetto fornito dall'utilizzatore, sia nel nodo che fa la misurazione sia nel nodo vicino, sarà quello di inviare e ascoltare un messaggio concordato su una porta UDP qualsiasi nella interfaccia di rete interessata. Con questa modalità la misurazione risulterà certamente meno accurata; questo è dovuto al fatto che il modulo utilizza dei thread cooperativi che non offrono tempi di risposta certi.


La stub factory è un oggetto di cui il modulo conosce l'interfaccia INeighborhoodStubFactory. Tramite essa il modulo può:


Il manager di rotte e indirizzi è un oggetto di cui il modulo conosce l'interfaccia INeighborhoodIPRouteManager. Tramite essa il modulo può:

Il modulo lo usa per rendere possibile la comunicazione via TCP coi vicini tramite un indirizzo fisso. Il modulo associa ad ogni interfaccia di rete che gestisce un indirizzo detto indirizzo di scheda. Per ogni arco che realizza, il modulo aggiunge la rotta con scope link verso l'indirizzo di scheda dell'interfaccia del vicino collegata all'arco. Quando rimuove l'arco rimuove anche la rotta. Quando il modulo cessa di gestire un'interfaccia rimuove il relativo indirizzo.

Tramite questo meccanismo il modulo gestisce solo gli indirizzi di scheda della identità principale del nodo corrente, nel network namespace default. Allo stesso modo, esso imposta le rotte verso gli indirizzi di scheda della identità principale di ogni nodo vicino, sempre nel network namespace default. Per la gestione delle altre identità, sia come indirizzi propri sia come rotte verso gli indirizzi dei vicini, il nodo le gestisce in autonomia, senza l'intervento del modulo Neighborhood.


L'identificativo del proprio nodo, come anche l'identificativo di ogni vertice rilevato, è un oggetto il cui contenuto non è noto al modulo neighborhood. L'interfaccia di questo oggetto nota al modulo (INeighborhoodNodeID) gli consente di:


Un arco è un oggetto (NeighborhoodRealArc) noto al modulo. Grazie alle informazioni memorizzate in esso (my_nic, mac) il modulo è in grado di evitare la creazione di ulteriori archi verso lo stesso vicino se non usano interfacce di rete distinte da ambo i lati, ed anche di segnalare che il vicino ad esso collegato non è fra i destinatari di un messaggio inviato in broadcast. Sempre con le informazioni memorizzate in questo oggetto (nic_addr) il modulo è in grado di produrre lo stub che realizza la chiamata di un metodo remoto in TCP (reliable).

L'interfaccia dell'oggetto arco nota all'esterno del modulo, INeighborhoodArc, permette solo un sottoinsieme di operazioni:


Quando si chiama il metodo che produce uno stub per l'invio di messaggi in broadcast, può essere passato un oggetto che contiene il codice e i dati necessari a gestire l'evento di 'mancata ricezione di un ACK da un arco entro il timeout'. Tale oggetto implementa l'interfaccia INeighborhoodMissingArcHandler. L'interfaccia permette di:


Il costo di un arco può essere espresso con diverse metriche (latenza, larghezza di banda, ...). Attualmente l'implementazione del modulo misura la latenza e la esprime con un intero in microsecondi.

La latenza è il tempo che impiega un messaggio da noi a raggiungere il vertice collegato. In realtà quello che si può misurare, quindi quello che il modulo memorizza come costo, è il round-trip time (RTT).