Articoli |
Packet Language for Active Network |
29.10.1999 |
per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto! |
di Alessandro Luparello
Il PLAN è un semplice linguaggio di programmazione funzionale, usato per scrivere il codice delle capsule che viaggiano all'interno di reti attive. Si provvederà ad introdurre (brevemente) il concetto di rete attiva, a dare una visione d'insieme del linguaggio PLAN (con degli esempi) e ad affrontare alcuni dei vari problemi che è possibile incontrare ne lla programmazione pratica.
E' bene sottolineare che le reti attive sono ancora in piena fase di ricerca e sviluppo, poco può trovarsi di definitivo e statico.
Perchè sono nate
Si è cominciato con l'osservare che, mentre è comunque
possibile sviluppare nuove routines di servizio in sistemi
terminali, la loro implementazione in nodi interni della rete
spesso riusciva ad offrire migliori funzionalità e consentiva
sensibili vantaggi nelle prestazioni. Sfortunatamente l'attuale
processo di modifica dei protocolli di gestione di rete è lento
e particolarmente difficoltoso.
Le reti attive puntano a risolvere il problema dell'evoluzione
dei servizi di rete mediante l'introduzione di un certo grado di
programmabilità all'interno delle stesse infrastrutture di rete,
permettendo quindi un rapido e semplice inserimento di nuovi
servizi.
Le capsule
Tradizionalmente le reti effettuano un passivo smistamento di
pacchetti tra nodi terminali (end-to-end): i dati utente sono
trasferiti in modo "nascosto", la rete è "insensibile"
ai bits che la attraversano e si limita ad instradarli senza
effettuare alcuna modifica.
Le reti attive abbandonano questa filosofia, permettendo
al coimplesso dei nodi di eseguire elaborazioni "personalizzate"
sui dati.
I pacchetti "passivi" dell'architettura attuale,
lasciano quindi il posto a capsule "attive", contenenti
piccoli programmi eseguibili nei routers attraversati.
Tali capsule, che contengono incapsulati i dati utente, possono invocare metodi predefiniti (routines di servizio sui nodi) o impiantarne di nuovi all'interno della rete (permettendo quell'evoluzione rapida e "personalizzata" cui si è fatto riferimento). L'architettura di un nodo può infatti essere semplicemente schematizzata come segue:
Una capsula PLANet ha un formato a dimensione non fissa (dipende dalla dimensione del codice inseritovi) ed è costituita dai seguenti campi:
Capsule nelle reti
Le capsule, iniettate all'interno della rete,
possono essere valutate in nodi remoti, instradate, modificate,...
e possono sud dividersi in altre capsule (alle quali fornire,
interamente o in parte, il proprio codice). Intuitivamente questo
comporta pericoli per la "sicurezza" nella rete stessa
(si immagini quello che una crescita esponenziale incontrollata
delle capsule potrebb e causare, se non altro in termini di
congestione). Altri pericoli sono invece implicitamente legati al
concetto stesso di esecuzione di codice (violazione di spazi
riservati, esecuzione di programmi "dannosi",...).
Le strategie implementate per consentire il mantenimento
della sicurezza all'interno della rete attiva, possono
suddividersi in:
Limitazioni al linguaggio di programmazione del codice delle capsule. (1)
Limitazione alle risorse assegnabili ad un pacchetto. (2)
Per quanto riguarda le limit azioni
imposte al linguaggio di programmazione, queste si
realizzano evitando che possano eseguirsi operazioni
potenzialmente dannose per il sistema.
Il linguaggio non consentirà, ad esempio, l'effettuazione
di cicli condizionati di alcun genere, le potenzialità
saranno ridotte al minimo indispensabile e l'intera
gestione di dati residenti sui nodi (dati che è
possibile scrivere/leggere dai nodi) risulterà
fortemente limitata (e lo sarà ancor di più in futuro).
La filosofia che guida le scelte dei ricercatori e
dei progettisti è essenzialmente quella di vedere i
programmi PLAN, che viaggiano sulle capsule attraverso la
rete, semplicemente come "collante" tra i vari
servizi di rete (ossia come una breve comunicazione tra
nodi): il PLAN deve essere flessibile a sufficienza
per scrivere utili programmi, ma limitato abbastanza da
non permettere che i suoi programmi pongano rischi di
sicurezza.
E' quindi l'aspetto più astratto (si pensi, in un
progetto informativo, all'algoritmo principale di
risoluzione) ad essere legato al codice delle capsule, e
quindi a viaggiare nella rete. Tutto il lavoro "pratico"
(si pensi adesso all'implementazione delle singole
funzionalità dell'algoritmo precedente) dovrebbe invece
essere svolto dalle routines di servizio (residenti sui
nodi).
Dovrebbe essere, quindi, cura dei progettisti,
scrivere nuove routines ad hoc per l'applicazione da
eseguire, e caricarle dinamicamente sui vari nodi della
rete (ovviamente solo determinati enti, è questa la
strada che si delinea, potranno effettuare questa
operazione di loading; ad essi dovranno sottoporsi le
routines, che dovranno essere vagliate, accettate ed,
infine, caricate sui routers).
Per evitare che una capsula possa
rimanere indefinitamente all'interno della rete e per
evitare che possa generare un numero incontrollato di
capsule "figlie", si è provveduto a porre un
limite al numero di nodi visitabili dalla singola capsula.
Quando si inietta una capsula, le si associa (proprio
all'atto della iniezione), un valore intero di resource
bound (RB, visto nel formato del pacchetto)
Il valore delle risorse viene decrementato di una
unità ogni volta che la capsula effettua un salto verso
un nodo adiacente e, inoltre, se una capsula genera nuove
capsule figlie, può distribuire a queste parte delle sue
risorse (non può assegnare più risorse di quanto
essa stessa non ne possegga).
Quando in un nodo giunge una determinata capsula con RB=0, questo provvede ad eliminarla.
"Vita" di una capsula
Per comprendere come viene valutato un programma PLAN, è assolutamente necessario conoscere dove viene valutato (ossia conoscere il suo ambiente di valutazione: evaluation environment: la rete PLAN (PLANet), descritta -grosso modo- nella seguente schematizzazione:
Una rete PLAN consiste in hosts, che
rappresentano i nodi terminali dell'ambiente, e routers,
che formano la struttura stessa della rete (active cloud:
letteralmente "nuvola attiva"). Ci si riferisce a
routers ed hosts, insieme, come ai nodi della rete.
Tutti i nodi devono essere in grado di valutare programmi
PLAN (inseriti nelle capsule), gli hosts si distinguono dai
routers anche per il fatto che in essi è possibile eseguire
applicazioni host, magari come parte di applicazioni PLAN.
Una capsula PLAN inizia la sua "vita"
in un host: un'applicazione host, infatti,
"costruisce" il pacchetto PLAN e lo inietta
nella rete attiva "consegnandolo" all'interprete
PLAN residente sull'host stesso. Ciò avviene mediante
una porta PLAN (che funge da interfaccia) tra l'applicazione
e l'interprete. Tale porta, che prende il nome di porta
di iniezione (injection port)
o porta di riferimento (implicit port), permette
all'interprete di mandare l'output verso l'applicazione
ed all'applicazione di sottoporre nuove capsule PLAN.
L'interprete locale procede allora con l'instradare
il pacchetto verso la sua destinazione (evalDest), mediante l'utilizzo della
funzione di routing (routFun),
definita nel pacchetto stesso, che fornisce il nodo,
adiacente a quello in esame, verso il quale la capsula
dovrà essere indirizzata (next-hop).
Ogni nodo intermedio (senza effettuare la
valutazione del codice) valuta solamente la specifica routFun che, a sua volta, lancia una service-call
a servizi del nodo stesso.
Lungo la strada verso il nodo di destinazione, ad
ogni hop (passaggio da un nodo all'adiacente),
le risorse del pacchetto (RB, resurce bound) vengono
decrementate (come detto) di una unità. Qualora si
raggiungesse il limite di risorse nulle, il pacchetto
verrebbe terminato.
Quando un pacchetto raggiunge la sua
destinazione, il suo programma viene analizzato:
1. Il codice è analizzato (parsed)
secondo uno schema top-down per registrare tutti
i collegamenti dell'implementazione di variabili e
funzioni (top-level bindings).
2. Si procede con la valutazione vera e propria,
mediante la chiamata a funzione (function call: execFn) f(a1,...,an)
definita nell'invocazione.
3. Tutte le eccezioni che non si è provveduto a
catturare (e gestire) nel programma stesso, saranno
gestite dall'handler designato nelle specifiche
del pacchetto
Introduzione
La versione 3.1 del PLAN (quella che stiamo
introducendo) è implementata in OCaml, versione 1.07. Le
motivazioni di questa scelta sono essenzialmente relative alla
completa indipendenza dalla macchina che l'implementazione in
questo linguaggio consente (e la conseguente semplicità di
caricamento/scaricamento di codice, qualità indispensabile per l'active
loading delle estensioni attive dei nodi).
PLAN è un semplice linguaggio di programmazione
funzionale, con l'aggiunta di primitive per la valutazione remota.
I programmi PLAN possono invocare alcuni servizi, che sono
elencati in librerie di funzioni residenti sul nodo attivo.
Programma PLAN
Un programma PLAN può pensarsi suddiviso in due parti:
Codice: Consiste in una serie di definizioni che legano nomi ed astrazioni; queste astrazioni hanno la forma di funzioni, valori ed eccezioni.
Invocazione: Consiste in chiamate
a funzioni (function calls) e relativi argomenti
(bindings). Queste saranno valutate (potenzialmente
anche in nodi diversi da quello della chiamata, qualora
se ne richiedesse una valutazione remota) solamente dopo
che gli argomenti siano stati tutti a loro volta valutati
(passaggio che avviene all'atto della chiamata
della funzione e non alla sua valutazione).
Il PLAN adotta infatti una semantica che permette
esclusivamente passaggi per valore dei
parametri (call-by-value semantics).
Per rendere più chiaro il concetto espresso ci
si avvale subito della definizione di chunk, concetto
essenziale nella logica della programmazione PLAN:
chunk
I chunks sono costrutti di programmazione PLAN comprendenti un segmento di codice (code segment) ed una chiamata a funzione (suspended function call). Provvedono a supportare l'incapsulazione ed altre tecniche di programmazione di pacchetti...
I chunks hanno tre componenti:
codice effettivo (in linguaggio PLAN).
funzione di "ingresso" (entry-point function) al codice.
collegamenti (bindings) con i parametri di tale funzione.
Un chunk può essere visto -per
semplicità- come una tradizionale chiamata a funzione, nella
quale però gli argomenti sono stati assemblati ed il codice è
parte integrante della chiamata stessa. Quando un pacchetto PLAN
giunge al suo nodo di destinazione, si accede al codice mediante
la sua funzione di ingresso (entry-point function),
chiamata con i parametri forniti. L'aspetto che riveste di
maggiore potenzialità i chunks, è che essi sono facilmente
gestibili nell'ambito della programmazione stessa: infatti la
sintassi
| f | (expr-1,..., expr-n)
è un'espressione di tipo chunk che crea un nuovo
chunk (si perdoni il gioco di parole). Intuitivamente,
la barretta verticale ( |, pipe) individua la parte dell'espressione
la cui valutazione deve essere posticipata (la chiamata a
funzione stessa, ovviamente). Specificatamente, il chunk
creato consiste nello stesso codice attualmente in esecuzione,
nella funzione di accesso f e nei collegamenti ai valori (value)
ottenuti a seguito della valutazione delle espressioni poste come
argomenti (expr-1,... ,expr-n). Il chunk
può essere adesso manipolato, copiato, passato come argomento
oppure -da notare con particolare attenzione- può apparire come bindings
in altri chunks (in realtà tale incapsulazione
di un chunk in altri chunks
rappresenta un meccanismo chiave in tutta la programmazione PLAN).
Oltre ad essere trattati come dati, il principale aspetto di un chunk
è ovviamente quello dell'esecuzione: il servizio eval effettua tale valutazione mediante il
caricamento (loading) del segmento di codice e l'invocazione
della funzione d'accesso (entry-point function) con i
suoi argomenti.
Solamente a titolo di completezza accenniamo al fatto che
è possibile "tradurre" il chunk in una
rappresentazione concreta (del tipo stringa di bit) mediante file
di tipo blob.
Un esempio dell'incapsulamento dei chunks
potrebbe essere il seguente (per quanto stupido possa sembrare).
Si considerino le due funzioni così definite:
fun viewIsZero (numero :int,
thisIsZero:bool) :unit= ( print ("il numero "^toString(numero)); if thisIsZero then print ("è zero") else print ("non è zero") ) |
fun isZero (numero:int):bool= if numero=0 then (true) else (false) |
Supponiamo adesso la chiamata:
viewIsZero (3,eval (|isZero|(3)) )
ed osserviamo lo schema seguente:
Eccezioni
Il
PLAN consente due costrutti sintattici relativi al trattamento
delle eccezioni:
(per ognuno dei costrutti presentati, viene
considerata una porzione di codice a titolo esemplificativo...)
try etry
handle E=> ehandle
Esegue l'espressione etry.
a). E indica un'eccezione (è un'exception
literal).
Se tale eccezione (E) viene riscontrata nell'esecuzione
di etry, allora si procede con la
valutazione dell'espressione ehandle.
b). E non indica un'eccezione, viene vista come il
nome di una variabile alla quale, ogni eccezione
sollevata durante l'esecuzione di etry,
viene collegata per la seguente valutazione di ehandle. In tal modo (evidentemente) ogni
eccezione viene catturata.
es:
fun divisione (dividendo:int,
divisore:int): int=
try ( dividendo/divisore
) handle DivByZero => (...)
raise E
Solleva l'eccezione E.
Qualora tale espressione sia contenuta all'interno
di un blocco try...handle (specifico
per l'eccezione E evocata, caso a precedente, o
generico per ogni eccezione, caso b, allora
implicherà la valutazione dell'espressione ehandle.
E' opportuno evidenziare la grande importanza che
questo costrutto può avere, all'interno di una
programmazione che vuole essere semplice e lineare, per
via della possibilità data al programmatore di
definire esso stesso alcune eccezioni (tramite
la parola chiave exception,
usata in un contesto di definizione (let-in-end)).
es:
Gestione degli errori (error handling)
Ogni
eccezione "alzata" da un programma PLAN può essere
catturata da un costrutto try...handle
di livello superiore. Tuttavia, nel caso che l'eccezione si
presenti al di fuori dell'esecuzione del programma (come accade,
ad es., se il pacchetto eccede le risorse a disposizione prima di
poter essere valutato), oppure nel caso che non venga catturata,
l'handler (esplicitato come campo del pacchetto) viene
utilizzato per la gestione. Questo è -in realtà- il nome di una
funzione che viene invocata per essere eseguita nel nodo sorgente
(del pacchetto).
Il linguaggio fornisce un handler di default: defHandler . Inoltre esiste un servizio PLAN,
abort, che produce gli stessi effetti
di un handler invocato.
Costrutti e primitive del linguaggio
Passiamo
adesso ad analizzare e descrivere le primitive ed i costrutti
forniti dal linguaggio PLAN. E' questa la parte più importante (insieme
a quella che segue successivamente, relativa ai servizi forniti)
per coloro che intendano comprendere il linguaggio, non solo come
impostazione generica ed astratta, ma dettagliatamente nei suoi
aspetti concreti e realizzativi.
Dalla grammatica del PLAN è possibile estrarre
informazioni sui costrutti presenti, sui tipi di valore possibili
e sulle operazioni. Analizziamo adesso i costrutti:
Costrutti
del linguaggio
(Per ognuno dei costrutti presentati, viene
considerata una parte di codice a titolo esemplificativo. Non
vengono presi ulteriormente in considerazione i costrutti
relativi al trattamento delle eccezioni, già analizzati in
precedenza)
let <defs-list>
in <expr> end
Utilizzato per la definizione di variabili o
funzioni locali (valide esclusivamente all'interno del
blocco in...end).
if
<expr> then <expr>
else <expr>
E' il tipico costrutto if-then-else, non
è prevista la possibilità di omettere l'else.
Primitive del linguaggio
Le primitive fornite dal linguaggio PLAN sono molto poche e suddivisibili in due categorie, che andiamo adesso ad esaminare:
primitive di rete:
Le primitive di rete (network primitives)
rappresentano forse l'aspetto di maggior interesse del
PLAN, permettendo una computazione "mobile"
mediante creazione e trasmissione di nuovi pacchetti
attivi.
a. OnRemote ()
La primitiva base OnRemote è pressochè generale.
La sua sintassi è la seguente:
Il suo significato è, banalmente: valuta E in
H. Viene utilizzato il servizio Routing per
determinare il persorso necessario al raggiungimento
della destinazione H. Infine, il pacchetto in oggetto
("padre", quello che chiama la OnRemote), dona Rb delle sue risorse
al pacchetto "figlio".
In caso di successo, la chiamata di OnRemote
implica la creazione di un pacchetto PLAN che è
trasmesso ad un host adiacente (nella direzione della
destinazione H, come da risultato della routine di
instradamento (Routing) ), decrementando la
riserva di risorse del pacchetto "padre" in
esame della quantità destinata al "figlio" (Rb).
In caso di insuccesso, verrà evocata un'eccezione.
b. RetransOnRemote ()
Questa seconda primitiva, RetransOnRemote, è
molto simile alla prima; eccetto che per il fatto che tenta
di assicurare una consegna affidabile dei
pacchetti PLAN. La sintassi è:
Tutti i chunks elencati nella lista Cs, data come parametro, saranno
trasmessi verso la destinazione H mediante la routine di
instradamento specificata e con un ammontare di risorse
pari ad Rb, per ogni
chunk trasmesso. Il pacchetto viene ritrasmesso
ogni secondo fino a che si riceva un acknoledgment
di ricezione, per un massimo di n
volte.
c. OnNeighbors ()
Simile alla OnRemote, con la restrizione che il pacchetto
"figlio" generato debba essere eseguito in un
nodo adiacente a quello attuale. La sintassi è:
Iteratori di lista
Gli iteratori di liste (che ne permettono, in pratica, la
scansione sequenziale) sono basati sul costrutto fold, con possibilità di
associazione a destra o a sinistra. PLAN, per adesso
almeno, non supporta polimorfismi parametrici a livello
di linguaggio o funzioni di alto livello, tali iteratori
di lista sono forniti come primitive di linguaggio
piuttosto che come specifiche funzioni.
Servizi
Una parte importante delle funzionalità del linguaggio PLAN proviene dalle routines di servizio. Mentre la prima release del PLAN offriva solamente pochissimi servizi, quella attuale fornisce una ricca libreria nella quale scegliere. I servizi possono essere suddivisi in due categorie:
Servizi essenziali (core services), presenti su tutti i routers della rete PLAN;
Servizi aggiuntivi (package services). Ogni package consiste in una o più routine di servizio a disposizione dei programmi PLAN.
Come tutte le altre funzioni chiamate in PLAN, le funzioni di servizio possono ricevere più argomenti (si tratta davvero di diversi argomenti; non di un unico argomento, eventualmente una tupla nella quale si fanno convogliare più dati, come accade per l'ML) e ritornano sempre un valore (un servizio, così come una qualsiasi altra funzione PLAN, ritornerà una unità (unit) nel caso in cui l'uscita -comunque presente- non abbia significato per il programma).
Servizi essenziali
Non vuole essere un gioco di parole ma tali servizi sono
essenziali per davvero. Non verrà fatto qui un
sistematico elenco di tutte le funzioni fornite ma si
provvederà comunque ad analizzare quelle che sono
sembrate particolarmente interessanti ed utili.
a. getRB: () = int
Non necessita di argomenti e ritorna un intero (come si
evince dalla sintassi). Fornisce l'ammontare delle
risorse attualmente a disposizione della capsula. E' un
servizio utilizzato moltissimo nei progetti (specie
quelli di esplorazione, per i quali importantissimo è il
controllo delle risorse).
b. getSource: () = host
Ritorna il nome (come host) del nodo specificato nel
campo source del pacchetto. Rappresenta il nodo
che ha dato origine al più "anziano" pacchetto
antenato dell'attuale (potrebbe essere il pacchetto
stesso, se non discendente da altri pacchetti).
c. getScrDev: () = dev
Ritorna l'interfaccia dalla quale la capsula è arrivata
sul nodo in esame. Viene utilizzata, ad. esempio, per
individuare il nodo dal quale la capsula (giunta su un
router qualsiasi) proviene.
d. getNeighbors: () = (host x dev)
list
Utilizzando la tabella di routing di default, questa
funzione determina i nodi adiacenti (neighbors)
di quello analizzato.
e. abort: chunk = unit
Funzione molto particolare: comporta l'interruzione
immediata dell'esecuzione del programma di una capsula e
la trasmissione di una routine (per trattare tale
situazione) al nodo sorgente (definito nel campo source del pacchetto stesso). Ad una
chiamata di abort -dunque- l'esecuzione
del pacchetto viene terminata, ed uno speciale pacchetto
viene creato e trasmesso al sorgente. Quando vi arriva,
setta a zero il suo resource bound e
valuta il dato chunk (passato come parametro).
f. eval: chunk = a
Sottrae una unità dall'ammontare attuale delle risorse
del pacchetto, e valuta il dato chunk. L'ambiente
viene prima riinizializzato allo stato base (di default)
del nodo stesso; il risultato della valutazione del chunk
è ritornato dalla eval. E'
importante sottolineare come tale funzione potrebbe
essere utilizzata per simulare la ricorsione.
Servizi aggiuntivi
Vi sono dieci package di servizio messi a
disposione con l'attuale versione del PLAN. Sono
estremamente diversi tra loro e forniscono un vasto
ventaglio di tipologie di funzionalità, di queste
verranno citate e commentate solo quelle ritenute
maggiormente significative (almeno per un primo approccio).
I dodici package sono i seguenti:
install | Permette l'installazione di
nuove routines di servizio (eventualmente scritte
dall'utente stesso, in OCaml) mediante
caricamento dinamico. Ricordiamo, infatti, che è data possibilità al programmatore di scrivere alcune proprie routines di servizio (in OCaml, ribadiamo) che potranno successivamente essere "caricate" sul singolo router (uno per volta) proprio mediante l'unica funzione del package install: la installServices. Questo, come sembra evidente, rende estremamente interessante la prospettiva di costruzione di programmi, siano pure complessi, in PLAN; cosa niente affatto scontata al solo esame del linguaggio. |
port | Questo package
provvede a rendere disponibili i mezzi perchè un
programma PLAN possa interagire con delle
applicazioni host. |
resident | Dati residenti sono quelli che
permangono sul router anche dopo che il programma
PLAN che li ha creati è terminato (oppure ha
abbandonato il nodo per proseguire la
propagazione all'interno della rete). Questa
caratteristica è stata aggiunta al PLAN per
permettere ai programmi una agevole strategia di
attraversamento ed esplorazione della rete. I dati residenti sono memorizzati in una tabella sul router, indicizzata mediante un nome identificativo del dato ed una chiave di sessione. La chiave serve essenzialmente a differenziare dati memorizzati da differenti istanze della medesima applicazione PLAN. Un potenziale pericolo, insito nel fatto stesso di permettere la memorizzazione di dati residenti, è quello che programmi PLAN potrebbero (anche se solo inavvertitamente) lasciare grandi quantità di dati su un router, continuando così a consumare memoria anche dopo la loro stessa terminazione. Per ovviare a questo problema, ogni singolo dato residente ha associato un tempo di permanenza sul nodo (timeout). Questo tempo è settato dall'utente all'atto stesso della memorizzazione del dato, ma non può comunque superare i 300 secondi (5 minuti). Sembra opportuno almeno citare qualche funzione di questo package a titolo di esempio: a. get: (string, key) = A Riporta il valore memorizzato sul nodo che può essere individuato mediante mediante la stringa (identifier) e la chiave della sessione. Qualora non esistesse un valore in corrispondenza di questa indicizzazione, verrebbe presentata un'eccezione di NotFound; b. put: (string, kay, A, int) = unit Inserisce una nuova riga nella tabella dei dati residenti sul nodo in esame (del tipo di quella schematizzata prima), associando al dato A (terzo argomento) la coppia per la identificazione (string e key). Il quarto argomento specifica il numero di secondi che al dato è permesso di "vivere" (come residente sul nodo, allo scadere di questo timeout, il dato verrà automaticamente cancellato. |
RIP | Implementa un protocollo di routing distribuito, basato sul protocollo standard RIP. Mette a disposizione del programmatore funzioni per l'interazione diretta con la tabelle RIP stesse. |
arp | Questo package fornisce servizi necessari alla implementazione di strategie di risoluzione di indirizzi (address resolution). E' basato sullo standard Address Resolution Protocol (ARP). |
flow | In qualche applicazione,
potrebbe essere utile (o intelligente)
instradare i pacchetti secondo una strategia
particolare, comunque diversa da quella di
default, basata sul numero minimo di nodi da
attraversare. La metrica basata sul numero degli
hop tende ad approssimare la latenza dei
pacchetti nel loro percorso; applicazioni che
richiedono un'ampia larghezza di banda dovrebbero
però essere instradate secondo una metrica che
tenga in considerazione anche questo concetto (ben
venga una strada "più lunga" che
consenta però una larghezza di banda superiore).
Questo package fornisce routines di servizio atte alla "creazione" di nuove metriche e nuove strategie di instradamento. |
dns | Questo package contiene una sola funzione (getHostByName), che implementa la risoluzione dei nomi (nome del nodo - nome "fisico"). Il mapping viene effettuato su una tabella statica, generata a partire da un file specificato al momento dello startup del nodo. |
frag | Fornisce un insieme di servizi
usati per frammentare programmi PLAN in piccoli
pezzi (di dimensione scelta) per poi
riassemblarli alla destinazione remota della
trasmissione. L'utilizzo di questo servizio è
reso necessario dal fatto che la rete presenta un
limite massimo nella dimensione dei pacchetti
trasmettibili (si tratta del Maximum Transferable
Unit, MTU) |
reliable | Questo package fornisce al
programmatore una serie di funzioni utilizzabili
congiuntamente alla primitiva RetransOnRemote
(già analizzata in precedenza), per assicurare
una trasmissione quanto più "sicura"
ed attendibile dei dati/programmi PLAN. Tra le
funzioni presenti, analizziamo quella che sembra
più utile: sendAck (chunk,key, int) = chunk Costruisce un pacchetto incapsulandovi il chunk e la chiave ricevuti come parametri. Il chunk viene "racchiuso" in un altro chunk che consente la trasmissione, al nodo di provenienza (sent back), di un segnale di ack (= trasmissione effettuata con successo, pacchetto ricevuto). Tale segnale di ack è quello necessario all\rquote interruzione della sequenza di spedizione dei pacchetti (della RetransOnRemote, se ne è già fatto cenno). |
csum | Riguarda la strategia di incapsulazione |
authproto | Riguarda i sistemi di sicurezza (security) |
security | Anche questo package contiene routines inerenti i meccanismi di salvaguardia della sicurezza. |
Costruzione di una rete PLANet
E' necessario pianificare la rete da costruire. Indicare, in uno schema abbozzato, il "nome" di tutti i nodi, tutti i loro collegamenti (specificandone le interfacce),...Verrà data una visione generale ai comandi ed ai files necessari e, successivamente, sarà portato avanti un esempio completo (molto semplice).
Costruzione
del nodo
(ossia lancio del demone pland)
Comando
UNIX:
(opzionali i parametri tra parentesi quadre)
Opzioni:
-router: specifica che il nodo
funziona in modalità router: non può essere un nodo terminale (non
esiste una implicit port
-ip (incoming port) indica il
numero della porta di riferimento (default: 3324).
-l (log): specifica il nome
del file dovce scrivere il file di logging
-rf (routfile): specifica che
deve essere usata una tecnica di instradamento statica, la cui
tabella è espressa proprio nel rout-table-file
-hf (hostfile): specifica
il file da utilizzare per risolvere i nomi degli host; (default: EXP_IP_ADDR,
nella directory di lavoro, dove è pland).
-keypub (pubfile): specifica
il file per la codifica della chiave pubblica di un nodo (default:
public.key.portnum, portnum
è quello specificato nell'opzione -ip).
-keypriv (privfile): specifica
il file per la codifica della chiave privata di un nodo (default:
private.key.portnum).
-policy (policyfile):
specifica il file contenente la descrizione della politica di
mantenimento della "sicurezza" nel nodo (node's
security policy)
ifc-spec-file è il file che descrive l'interfacciamento
del nodo.
Come si inietta una capsula
Comando
UNIX:
(opzionali i termini tra parentesi quadre)
Opzioni:
-v: attivazione del verbose mode
-p (portnum): identifica la porta di iniezione nella
rete (default: 3324)
-ed (evalDest): specifica il nodo dove il programma sarà
valutato (default: nodo di iniezione)
-rf (routFun): specifica la funzione di routing
utilizzata (default: defaultRoute)
-hf (hostfile): specifica il file di risoluzione dei
nomi (default: EXP_IP_ADDR)
-filename: identifica il nome del file (scritto in PLAN)
rappresentante il codice della capsula da iniettare. Attenzione a
specificare l'intero path
-RB Resource Bound: Quantità iniziale di risorse
associate alla capsula che si inietta nella rete.
Come sono i files necessari
Host file:
Elenco di identificatori per la risoluzione dei nomi
degli host:
es:
ifc_spec_file (file di
interfaccia):
Elenco dei nodi raggiungibili (con un solo hop)
da quello in esame.
es:
Osservazioni sulla rete PLANet
impossibilità di
scambiare risorse tra due capsule
E' quasi certamente il più grosso inconveniente che mi
è possibile attribuire alla politica di gestione della
rete. E' fin troppo ovvia la forzata impossibilità di
aggiungere, in modo del tutto arbitrario, risorse ad una
capsula già iniettata all'interno della rete (si
violerebbe uno dei pilastri principali che, come
specificatamente spiegato, vuole limitare la
proliferazione incontrollata, ed incontrollabile, delle
capsule); di contro non si riescono però ad immaginare
motivazioni tali da impedire lo spostamento di risorse da
una capsula ad un'altra.
Ciò non risulterebbe neanche eccessivamente distante
dall'attuale gestione delle risorse: si ricorda, infatti,
come sia tuttora possibile che una capsula, durante la
sua esecuzione, crei altre capsule ("figlie"),
e trasmetta ad esse parte delle proprie risorse.
poco flessibile
gestione della trasmissione "sicura" del
pacchetto
(e come superare l'errore di retransmit
table is full)
Come già descritto precedentemente (nella
sezione dedicata al linguaggio di programmazione: PLAN),
esiste una funzione di trasmissione remota, la RetransOnRemote, che permette l'invio
di un pacchetto con eventuale ritrasmissione fino alla
ricezione di un ack che ne confermi la ricezione.
Ebbene, la gestione di questo meccanismo funziona
mediante l'utilizzo di una tabella di ritrasmissione (retransmit
table): Ogni volta che la RetransOnRemote
viene chiamata con una lista di chunks, viene
aggiunta una nuova riga (entry) nella tabella di
ritrasmissione, indicizzata tramite la chiave della
sessione (utilizzata come sequenza numerica). Solo quando
viene ricevuto l'ack, questa riga viene cancellata.
La tabella è inizializzata per permettere la
memorizzazione di 10 linee, è quindi una tabella statica,
a dimensione fissa. Sarebbe stato certamente molto più
flessibile, adattivo ed efficiente considerare una
tabella che possa crescere dinamicamente (growable),
come ammesso dagli stessi progettisti. La limitazione
della dimensione della tabella può creare alcune
difficoltà al corretto funzionamento dei programmi (messaggio
d'errore Retransmit table is full).
Si può rimediare a questo problema (se non si ha la
buona volontà di riscrivere il codice (in ML) della
gestione della ritrasmissione, in un'ottica maggiormente
dinamica) solamente modificando il codice del file
reliable/reliable.ml,
nel punto dove viene stabilita la dimensione massima
della tabella:
let retransmit_store_size=10
Ad esempio si può pensare di portarlo da 10 a 100.
Osservazioni sul linguaggio di programmazione PLAN
Del linguaggio PLAN si è già abbondantemente parlato nella sezione dedicata (PLAN), viene quindi presentata una sorta di schematizzazione (con un tentativo di analisi) dei vari punti "dubbi" incontrati e di possibili soluzioni (si consiglia vivamente, qualora si stesse lavorando ad un progetto di ampio respiro, di svolgere le funzionalità complesse attraverso la costruzione di apposite routines di servizio: il PLAN non è stato progettato per fornire determinate funzionalità).
dati residenti
Il sistema di gestione dei dati residenti (timeout<300
millisecondi) non può pensarsi definitivo (sicuramente
appare grossolano ed inelegante) e difatti gli stessi
autori auspicano implementazioni più efficienti (e
friendly) per il mantenimento della "pulizia"
dei nodi (magari mediante una sorta di garbage
collection, dei dati inutilizzati).
Un altro punto non propriamente "inattaccabile"
nella gestione dei dati residenti è quello che riguarda
la, forse eccessiva, libertà consentita nell'accesso
alle risorse sui nodi. Anche qui sembrano allo studio
eventuali procedure di autentificazione che possano
abilitare o meno la scrittura sul nodo.
dichiarazione di tipi
complessi
Non è affatto possibile, in PLAN, effettuare
dichiarazioni di tipi di dati. E' però consentito, come
unica (comunque non poco importante) forma di "personalizzazione",
costruire tuple (date dal prodotto cartesiano di
uno o più tipi base).
funzioni ricorsive
Non sono stati implementati, in PLAN, metodi per
realizzare la ricorsione; questa è comunque facilmente
simulabile, in modo indiretto, utilizzando il servizio eval. Si faccia attenzione al fatto
che l'utilizzo pratico di funzioni ricorsive può
generare problemi (non rari già se l'utilizzo è appena
non banale) di saturazione dello stack (StackOverflowError).
costrutti di loop
Punto estremamente dolente nella mia esperienza di
programmazione PLAN: il linguaggio non consente di
effettuare ciclii di alcun genere, fatta eccezione per
quelli di scorrimento (incondizionato) di una lista; e -in
questo caso- neppure le routines di servizio possono
venire in soccorso.
Proviamo ad analizzare le tecniche adoperabili per
ovviare tale problema:
a. ciclo di scorrimento condizionato
di una lista
Si rende necessario nella ricerca di un dato elemento all'interno
della lista. Serve, quindi, una scansione della stessa
lista fino all'individuazione del simbolo cercato.
soluzione implementabile:
Si può utilizzare la fornita funzione di scansione (incondizionata)
della lista stessa (foldl), con
l'aggiunta di una memorizzazione temporanea (come dato
residente) delle informazioni ricercate (ad esempio la
posizione di un determinato elemento), quando ottenute.
Alla fine del ciclo (terminata la scansione dell'intera
lista) si deve provvedere a riportare il dato memorizzato
e quindi a cancellarlo.
b. ciclo d'attesa di un evento
primo caso:
Si immagini un contesto di mutua esclusione: quando una
capsula tenta l'accesso ad un nodo e trova il semaforo
acceso (alt), dovrebbe attenderne lo spegnimento (effettuato,
da buona norma, dalla stessa capsula interna al blocco).
soluzione implementabile:
Si può scegliere di rimandare indietro (ad un nodo
adiacente) la capsula che ha fallito il tentativo d'accesso,
per farla poi subito rispedire nuovamente verso la
destinazione (nuovo tentativo).
secondo caso:
Ciclo di "coordinamento" o di supervisione (si
pensi, ad esempio, a cicli tipo quello di Polling)
soluzione implementabile:
Si può pensare di servirsi della ricorsione. Al termine
del test sul verificarsi di un evento (test di uscita) si
procede col richiamare la stessa funzione, ripetendo
quindi nuovamente le stesse istruzioni e lo stesso test...
spreco di risorse
Si è fatto riferimento al problema della gestione, non
proprio efficientissima, delle risorse. In realtà si
tratta di una vera e propria enorme voragine che implica
uno spreco spesso inaccettabile di resource bound.
Riassumiamo (in una sintetica schematizzazione) le
motivazioni di questa dispersione:
a. La necessità di garantire una
trasmissione affidabile delle capsule ha spinto all'utilizzo
della RetransOnRemote che,
ripetiamo, suddivide le risorse equamente
a tutte le possibili ritrasmissioni, anche quando queste
non risultino necessarie:
nella semplice trasmissione da un nodo al
suo adiacente, considerando un limite massimo di tre
ritrasmissioni, si ha una perdita di 667 (su 1000) unità
di risorsa (contro l'unica (1) necessaria)
b. L'impossibilità
di trasferire risorse da una capsula all'altra è
ulteriore vincolo ad una politica di recupero delle
risorse stesse.
(a): fase di esplorazione: divisione equa
delle risorse tra tutte le capsule "figlie"
(b): fase di rientro, nell'ipotesi che l'ultima capsula a
rientrare sia quella con RB=3
osserviamo (in verde) come ben più efficiente sia la
gestione delle risorse qualora fosse possibile realizzare
un "riutilizzo" di quelle rimanenti.
per Active Networks
per PLAN, versione 3.1
un esempio
Questa pagina è curata da: Alessandro Luparello |