Sviluppo di un ambiente per la scrittura e la simulazione ... · ho individuato i due principali...
Transcript of Sviluppo di un ambiente per la scrittura e la simulazione ... · ho individuato i due principali...
Sviluppo di un ambiente per la scrittura e lasimulazione di programmi per il processore PD32
Matteo Leonetti
Introduzione
Il PD32 è un processore didattico studiato nei corsi di Calcolatori Elettronici in diverse
università italiane. La sua struttura e quella dell'assemblatore originale sono descritte in [1]. Il
simulatore a corredo di tale libro è stato realizzato nel 1994, e gli undici anni che ha alle spalle
iniziano a rendere evidente la necessità di un nuovo ambiente di sviluppo con le caratteristiche di un
software moderno.
Questo progetto ha l'obiettivo di creare un'alternativa portabile, che costituisca le
fondamenta di una suite di applicazioni per la didattica relative al processore ed agli insegnamenti di
Calcolatori Elettronici. Si prefigge, inoltre, di migliorare la gestione delle periferiche permettendo la
simulazione di periferiche complesse.
1. Architettura
DISsimulator1 è scritto in Java ed è basato sulla versione 1.4 del JSDK. Questo ne
garantisce la portabilità su tutti i sistemi per cui sia disponibile una Java Virtual Machine.
Per permettere l'estensione del software con moduli esterni, è necessario utilizzare un framework che
1 Il nome è ispirato ad una frase del “ritratto di Catilina” di Sallustio: “Omnium rerum simulator ac dissimulator”.Inizialmente questo progetto mirava a realizzare una macchina universale di cui il PD32 fosse una specializzazione.Dunque un “simulatore di ogni cosa”. Oltre a ciò, la parola dissimulator inizia con l'acronimo del Dipartimentod'Informatica e Sistemistica e sembrava dunque particolarmente adatta.
metta a disposizione una serie di servizi.
Tenendo sempre presente l'aforisma di Eric Raymond:
“Good programmers know what to write. Great ones know what to rewrite (and reuse)” [2]
ho individuato i due principali framework basati su Java: NetBeans di Sun Microsystems® ed Eclipse
di IBM®. Entrambi sono software completi e molto complessi. Ho optato per il primo per la possibilità
di implementare l'interfaccia grafica con la libreria Swing piuttosto che con la SWT di IBM®, che non
fa parte delle librerie standard di Java. Sebbene possa essere utilizzata per creare applicazioni di
qualunque genere, la NetBeans Platform è la base del noto NetBeans IDE ed è quindi particolarmente
adatta per la realizzazione di ambienti di sviluppo. La piattaforma di NetBeans mette a disposizione
dei moduli la gestione delle impostazioni, delle finestre, delle eccezioni, e un editor con ampie
possibilità di estensione. Un modulo per NetBeans è un file jar, il cui manifest contiene alcune
informazione lette dalla piattaforma ed utilizzate per il caricamento delle classi. I moduli possono
dichiarare delle dipendenze da altri che vengono aggiunti al loro classpath.
DISsimulator è costituito da due moduli: il core e nbplugin. Il primo realizza tutte le
funzionalità relative al PD32 e non dipende in nessun modo da NetBeans, mentre il secondo
interfaccia il core con il resto della piattaforma: in questo modo è possibile utilizzare il core in
un'applicazione autonoma o in qualche altra piattaforma. Analizzerò ora il core nel dettaglio, trattando
più avanti il modo in cui DISsimulator dipende da NetBeans.
I due
blocchi funzionali
del core sono l'as-
semblatore ed il si-
mulatore. Schematicamente, il core appare come in Figura 1.
L'assemblatore riceve dall'utente un programma e costruisce una struttura di dati che lo rappresenti. Il
1
Figura 1 Uno primo sguardo ai blocchi funzionali
ArchitetturaArchitettura
simulatore quindi riceve la struttura di dati e procede con la simulazione.
Tutti i package sono sottopackage di it.softeaware.dissimulator, quindi nell'indicarne il
nome sottintenderò questa parte comune.
Ad esempio it.softeaware.dissimulator.assemblatore sarà indicato più
semplicemente con assemblatore.
2. L'assemblatore
L'assemblatore è costituito da due parti: l'analizzatore sintattico (o parser) e l'analizzatore
semantico. Analizzando la sintassi il parser costruisce una rappresentazione del programma in
memoria, tramite un'opportuna struttura di dati detta albero sintattico astratto (Abstract parse tree)
[3]. Sarebbe anche possibile effettuare l'assemblaggio direttamente nelle azioni semantiche del parser,
come avveniva alcuni anni fa, ma ciò renderebbe il codice dell'analizzatore sintattico difficile da
leggere e da modificare. I calcolatori odierni non hanno problemi di memoria, almeno per quanto
riguarda la compilazione o il solo assemblaggio dei programmi di dimensioni ridotte (come in questo
caso). Il parser, quindi, verifica che il programma rispetti la grammatica (Appendice A) generando
delle ParseException nel caso in cui riscontri degli errori. Se il programma risulta
sintatticamente corretto il parser completa la costruzione dell'albero, rendendolo disponibile per la
successiva fase di analisi semantica. L'analizzatore semantico termina i controlli sul programma ed
assegna a variabili ed istruzioni l'indirizzo di memoria a cui dovranno essere caricate; l'albero astratto
viene quindi elaborato e la sua versione definitiva trasferita al simulatore.
Nel caso in cui qualche verifica fallisca l'analizzatore semantico genererebbe delle
eccezioni di tipo AssemblaggioException (di cui ParseException è una sottoclasse
privata). Le eccezioni prodotte dall'analizzatore sintattico e da quello semantico vengono raccolte e
3
L'assemblatoreL'assemblatore
mostrate all'utente. L'interfaccia del package verso il resto dell'applicazione consiste nella classe
Assemblatore (ParserPD32 e AnalizzatoreSemantico sono private) che fa da
wrapper per i due componenti e gestisce lo scambio dei dati, restituendo al chiamante l'albero astratto
al termine dell'elaborazione, o una lista di messaggi d'errore. Sia nell'assemblatore sia nel simulatore,
è molto importante che la generazione e lo scambio di dati siano ben individuati per evitare memory
leak1.
Una rappresentazione del flusso di dati all'interno dell'assemblatore è mostrata in Figura 2
mentre la Figura 3 illustra la sequenza delle operazioni.
L'albero astratto
è realizzato con una gerar-
chia di classi, contenute nel
package istruzioni.
Questo package è di grande
importanza per tutta l'appli-
cazione perché è ad esso
che vengono delegate gran
parte delle operazioni. Il pd32 possiede circa cinquanta istruzioni, ed otto modi di indirizzamento. Per
realizzare le funzioni specifiche di ogni istruzione senza rendere il codice un unico blocco
difficilmente gestibile è necessario un approccio fortemente orientato agli oggetti.
1 Gruppi di oggetti non più necessari ma raggiungibili dallo stack (anche indirettamente) che per questo non possonoessere deallocati dal garbage collector.
4
Figura 2 Il flusso di dati all'interno dell'assemblatore
Le istruzioniLe istruzioni
2.1 Le istruzioni
Come accennato, il package delle istruzioni è ortogonale sia all'assemblatore sia al
simulatore, costituendo una libreria di notevole importanza per tutta l'applicazione.
La sua struttura è rappresentata nel diagramma UML della Figura 4.
Un programma è composto da un Eseguibile principale, da una lista di Driver ed
una di Dichiarazione eventualmente vuote. Ogni Eseguibile contiene, a sua volta, una lista
di Istruzione. Ognuna di queste liste è riempita dal parser durante il riconoscimento delle relative
sezioni del programma. Le istruzioni dell'assembly PD32 sono rappresentate da una gerarchia di classi
la cui radice implementa l'interfaccia Istruzione. Su questa struttura si basa gran parte
dell'applicazione, che operando tramite un'interfaccia delega (mediante il polimorfismo) la funzione
da svolgere alla classe concreta più appropriata. Questo schema comporta alcuni vantaggi: il parser è
indipendente dal set di istruzioni; il codice riguardante ogni istruzione è molto breve e, di
conseguenza, facilmente manutenibile (meno di 100 LOC per classe); i metodi si possono condividere
5
Figura 3 Sequence diagram dell'assemblatore
Le istruzioniLe istruzioni
o nascondere seguendo le regole di visibilità e dell'ereditarietà; l'interfaccia dell'intera struttura verso
il resto dell'applicazione è estremamente semplificata, essendo costituita da due sole interfacce Java.
Le istruzioni svolgono due diversi ruoli all'interno dell'applicazione: nell'assemblaggio per
il controllo dei parametri e la creazione del codice binario; nell'esecuzione per lo svolgimento delle
6
Figura 4 Diagramma del package istruzioni
Le istruzioniLe istruzioni
operazioni caratteristiche dell'istruzione. Questi due ruoli sono realizzati dalle interfacce Java
istruzioni.Istruzione ed esecuzione.Esecutore. Ogni classe concreta che
implementa Istruzione ha una classe annidata statica che implementa Esecutore. Essendo
statica, non c'è alcuna dipendenza tra questa classe e quella che la contiene, ma è stata implementata
annidata per includere in una sola classe tutto quello che riguarda un'istruzione. Così, ad esempio, è
possibile aggiungere un'istruzione modificando un solo file, o localizzare eventuali errori molto
facilmente.
Poiché tutte le classi che implementano Istruzione ed Esecutore sono private, il
resto del programma necessita, di un intermediario all'interno del package per accedervi. La classe
IstruzioneFactory reperisce la sottoclasse concreta, che rappresenta l'istruzione desiderata, e
la restituisce come riferimento di tipo Istruzione (o Esecutore). Quando avviene la prima
richiesta IstruzioneFactory crea una tabella di hash che contiene le istruzioni ed una matrice
con gli esecutori. La tabella mappa l'insieme dei nomi delle istruzioni su quello delle sottoclassi che le
rappresentano, e viene riempita esaminando l'albero delle classi in profondità. L'analizzatore sintattico
al riconoscimento di un'istruzione richiede ad IstruzioneFactory di creare un oggetto
Istruzione appropriato. IstruzioneFactory, quindi, ottiene (in tempo O(1)) un riferimento
all'oggetto di classe Class relativo alla particolare istruzione, e tramite la reflection1 lo istanzia e lo
restituisce. Per gli esecutori la procedura è ancora più semplice e non richiede l'introspezione. Ogni
sottoclasse di Esecutore è un Singleton, ed ogni oggetto non conserva alcuna informazione sullo
stato dell'esecuzione. Questo rende lecito l'utilizzo degli stessi oggetti in più di una computazione
parallela. Un esecutore viene richiesto al momento del fetch dell'istruzione, di cui si può ricavare
(tramite opportune maschere binarie) la classe ed il tipo. Queste due informazioni indicizzano la
matrice dalla quale si può immediatamente ottenere un riferimento all'esecutore richiesto.
1 La reflection o “introspezione” è la possibilità, offerta dalla JVM, di ottenere riferimenti ai campi, metodi o costruttoridi una data classe. In IstruzioneFactory la reflection è utilizzata per reperire un riferimento di tipojava.lang.reflect.Constructor al costruttore della classe concreta che implementa Istruzione, edistanziare un oggetto.
7
La simulazioneLa simulazione
3. La simulazione
La simulazione consiste nel prelevare la struttura generata dall'assemblatore, creare gli
oggetti necessari all'esecuzione del codice, caricare il codice binario in memoria ed eseguire le
istruzioni mostrando lo stato della computazione all'utente.
Sono state definite delle interfacce Java per i tipi PD32 e Memoria e le loro
implementazioni predefinite esecuzione.DefaultPD32 e gui.MemoriaTableModel. Per
8
Figura 5 Diagramma delle classi del package esecuzione
La simulazioneLa simulazione
ottenere il duplice effetto di realizzare la simulazione indipendentemente dall'interfaccia grafica e
tenere la GUI costantemente aggiornata, ho integrato il package della simulazione nel modello MVC1
della libreria Swing. La memoria di lavoro è implementata dal modello di una tabella (JTable) e le
operazioni effettuate su di essa si riflettono immediatamente sull'interfaccia grafica. Il processore e le
periferiche non sono direttamente modello di qualche componente grafico ma possono registrare dei
listener a cui notificare i cambiamenti. Il pannello che visualizza lo stato del processore si registra
come listener dell'oggetto PD32 che esegue la computazione. Alla ricezione di un evento questo viene
memorizzato e ogni decimo di secondo un javax.swing.Timer aggiorna l'interfaccia.
L'aggiornamento non avviene immediatamente nel codice che notifica l'evento perché questo è
eseguito dal thread del processore mentre solo il gestore degli eventi2 (thread AWT nella macchina
virtuale) può modificare l'interfaccia grafica. Sarebbe, inoltre, molto rischioso che il thread del
processore tentasse di modificare anche i componenti sincronizzati perché potrebbe richiedere un lock
su un oggetto Swing , pur essendo stato interrotto, terminando conseguentemente con un'eccezione
(cfr. Capitolo 4). Questo, per altro, permette a due eventi riguardanti lo stesso registro e giunti
nell'intervallo tra un aggiornamento ed il successivo di sovrascriversi evitando di aggiornare due volte
(inutilmente) l'interfaccia. Il diagramma delle classi del package esecuzione è mostrato in Figura
5 .
3.1 Avvio dell'esecuzione
La costruzione di tutti gli oggetti necessari alla simulazione non è un operazione semplice
ed è stata implementata in BuilderEsecuzione. Avviare per la prima volta l'esecuzione richiede:
1 Swing ha un'architettura basata su una variante del pattern MVC (Model-Control-View) nota come “separable modelarchitecture”. View e Control di MVC sono fusi in un unico componente responsabile della visualizzazione e delcomportamento nell'interfaccia grafica mentre il Model contiene i dati e notifica gli oggetti associati dei cambiamenti.
2 In Swing un solo thread può modificare l'interfaccia ed è il “gestore degli eventi” o thread di AWT (Abstract WindowToolkit). Alcuni componenti sono sincronizzati e possono essere modificati da altri thread ma l'accesso al loro statorichiede l'acquisizione di un lock.
9
Avvio dell'esecuzioneAvvio dell'esecuzione
1. La creazione della memoria, cioé di un oggetto la cui classe implementa esecuzione.Memo-
ria (gui.MemoriaTableModel).
2. La creazione di una DaisyChain inizialmente vuota.
3. La creazione del processore, cioè di un oggetto la cui classe implementa esecuzione.Pro-
cessore (DefaultPD32).
4. La scansione del file con le impostazioni delle periferiche, la loro creazione ed inserimento nella
daisychain.
5. La creazione dell'interfaccia grafica e la sua registrazione come listener del processore e della
memoria.
Il processore, all'atto della sua istanziazione, crea un oggetto StatoEsecuzione che
racchiude tutto il necessario alla simulazione. Tramite questo è possibile ottenere dei riferimenti alla
memoria di lavoro, alla daisychain ed al processore stesso. Lo StatoEsecuzione contiene anche
delle variabili usate dagli esecutori (cfr. Capitolo 2.1) per effettuare la computazione.
Quando tutti gli oggetti sono stati creati il programma assemblato deve essere caricato in
memoria dalla classe Loader ed il registro PC del processore inizializzato con l'indirizzo della prima
istruzione.
3.2 Le periferiche
La simulazione delle periferiche è l'innovazione più importante tra quelle introdotte da
DISsimulator. Nel precedente simulatore l'utente era completamente responsabile del comportamento
delle periferiche dovendo personalmente modificarne lo stato. In DISsimulator le periferiche sono
autonome ed eseguite in un proprio thread in concorrenza con quello del processore.
Una periferica è costituita da una classe che implementa eseuzione.periferi-
che.Periferica e da almeno una classe che implementa esecuzione.periferiche.In-
10
Le perifericheLe periferiche
terfacciaIO. Prima di procedere con la descrizione è indispensabile una precisazione riguardo al
termine “interfaccia”. Con “interfaccia Java” si indica un tipo del linguaggio di programmazione
definito in [4]; con “interfaccia hardware” si indica il circuito della periferica deputato allo scambio di
dati con il processore. La periferica e le interfacce sono simulate ognuna da una classe che deve
implementare le interfacce Java menzionate. Per semplificare la scrittura delle periferiche, renderle
meno soggette ad errori, e standardizzare le operazioni comuni sono disponibili delle implementazioni
di default che possono essere ereditate ridefinendo pochi metodi.
Alla creazione degli oggetti responsabili della simulazione viene analizzato il file xml che
contiene le impostazioni delle periferiche installate. Per ognuna viene memorizzato il nome della
classe che implementa Periferica e per ogni interfaccia hardware l'IVN e l'indirizzo. Tutte le
interfacce hardware sono inserite in una lista (daisychain) da cui dipende la priorità della periferica.
Nel PD32 ogni interfaccia hardware deve specificare un IVN ed un indirizzo. Un array di
256 elementi da quattro byte memorizza gli indirizzi dei driver di ogni interfaccia. L'Interrupt Vector
Number è l'indice dell'interfaccia hardware in questo array. L'indirizzo è il numero che identifica
l'interfaccia hardware. Quando il processore riceve un' interruzione manda un segnale di ACK alla
prima interfaccia hardware installata. Nel caso in cui quella fosse l'interfaccia che ha richiesto
l'interruzione, il segnale di ACK sarebbe mascherato e la periferica avvierebbe il protocollo di
comunicazione con il processore. Altrimenti il segnale di ACK verrebbe propagato all'interfaccia
successiva. Da questa descrizione si evince che l'ordine in cui le interfacce hardware sono installate
nel sistema (costituendo la daisychain) ne determina la priorità. Per molte periferiche con più di una
interfaccia hardware è fondamentale che l'ordine tra queste sia rispettato.
Come precedentemente accennato, il nome delle classi delle periferiche installate viene
letto dal file xml e la periferica istanziata. Successivamente l'inserimento delle interfacce nella
DaisyChain viene delegato alla periferica poiché questa conosce l'ordine in cui devono essere
installate. Ogni interfaccia hardware ha un riferimento ad un oggetto di tipo
11
Le perifericheLe periferiche
RicevitoreDiSegnali che rappresenta una vista del processore mostrando solo il metodo
sendInterrupt() ed impedendo la modifica diretta del suo stato. In DefaultPD32 una variabile intera
memorizza il numero di interruzioni pendenti e viene incrementata ad ogni invocazione di
sendInterrupt(true) da parte delle interfacce hardware. Se il numero di interruzioni ricevute è
maggiore di zero, al termine dell'istruzione corrente, il processore ottiene dalla DaisyChain l'ivn
della prima (per posizione) interfaccia hardware che ha richiesto l'interruzione, ed imposta il registro
PC all'indirizzo contenuto nella relativa cella dell'Interrupt Vector. In questo modo l'istruzione
successiva sarà quella del driver della periferica. Il driver deve modificare il flag status (con le
istruzioni start o reset) dell'interfaccia per rimuovere l'interruzione ed evitare di essere eseguito
all'infinito. L'invocazione del metodo sendInterrupt(false) decrementa il numero di interruzioni
pendenti nel processore. In questo modo eventuali interruzioni giunte durante l'esecuzione della stessa
istruzione non si sovrascrivono e gli eventi che vengono simulati sono esattamente quelli previsti nella
progettazione del PD32.
3.2.1 Scrivere nuove periferiche
La principale conseguenza delle innovazioni apportate alle periferiche è la necessità di
realizzarne di nuove. E' certamente possibile scrivere delle periferiche molto semplici che si possano
usare esattamente come nel vecchio simulatore ma limitarsi a questo renderebbe vano il nuovo
sistema.
Per poter essere installata dall'utente una periferica deve apparire nella lista di quelle di-
sponibili che corrisponde al contenuto del file esecuzione/periferiche/periferiche.-
list. Quest'ultimo è un file di testo le cui righe riportano il nome della classe della periferica. Ad
ogni periferica è associato un file “periferica.properties” in cui vengono specificati il nome ed una
descrizione da mostrare all'utente. Nel pannello “Gestione periferiche” sono visualizzate a sinistra le
12
Scrivere nuove perifericheScrivere nuove periferiche
periferiche disponibili ed a destra quelle attualmente installate. Inserendo un periferica a destra questa
sarà installata nella stessa posizione della daisychain in cui figura nella lista. Subito dopo la sua
creazione, all'oggetto Periferica viene assegnato un riferimento ad un RicevitoreDiSe-
gnali necessario per l'azione successiva. Alla creazione della periferica segue l'installazione delle
interfacce con i metodi Periferca.installaIn() e Periferica.installaOut().
Come indicato precedentemente, ogni periferica è eseguita in un proprio thread. Nel
nuovo thread viene invocato il metodo Periferica.esegui() tramite il quale il controllo passa
alla classe della periferica. Con lo scopo di agevolare la realizzazione di nuove periferiche sono state
create le classi PerifericaAstratta, InputAstratta e OutputAstratta che
ridefiniscono gran parte dei metodi richiesti. Esattamente come le interfacce hardware sono standard
per tutte le periferiche del PD32, InputAstratta e OutputAstratta implementano tutti i
metodi e bisogna aggiungere solo quelli per la comunicazione con la propria periferica che, dovendo
generalmente essere package private, non possono comparire nella definizione dell'interfaccia. La
classe che estende periferica astratta, invece, oltre ai metodi per l'installazione delle interfacce
hardware deve definire dei tamplate method ([5]):
• eseguiInizializza(): invocato prima di qualsiasi stard(). La periferica può creare
un'interfaccia grafica ed effettuare qualunque operazione di inizializzazione.
• eseguiStard(): esegue l'operazione caratteristica della periferica
• eseguiTermina(): chiamato alla fine della simulazione, quando l'utente clicca sul tasto
“Stop”. Esegue tutte le operazioni per una corretta terminazione, ad esempio nasconde la GUI.
Quando tutti i metodi sono stati realizzati è sufficiente aggiungere il nome completo (cioè
includendo il nome del package) della classe che estende PerifericaAstratta in periferiche.li-
st.
13
Il ciclo istruzioneIl ciclo istruzione
3.3 Il ciclo istruzione
E' possibile eseguire la simulazione in tre modalità:
• Intero programma
• Un'istruzione
• Una fase
Dopo che tutti gli oggetti sono stati creati seguendo la procedura descritta nel capitolo 3.1
il GestoreSimulazione diventa il responsabile della creazione e terminazione dei thread. A
questo oggetto è associato il seguente diagramma di stato:
Nello stato iniziale il GestoreSimulazione è in attesa di un oggetto
StatoEsecuzione da cui ottenere riferimenti a tutti gli oggetti creati per la simulazione. Ricevuto
uno StatoEsecuzione il GestoreSimulazione può avviare la simulazione. A questo scopo
crea ed avvia un thread per il processore ed uno per ogni periferica. In questa fase la simulazione è in
corso ed il thread del processore è fermo in attesa di un comando. I comandi vengono inseriti in una
lista dall'esterno e estratti dal thread del processore secondo il classico modello del produttore-
consumatore. I comandi sono oggetti di classi che implementano un'opportuna interfaccia Java con un
14
Figura 6 Diagramma di stato di un GestoreSimulazione
Il ciclo istruzioneIl ciclo istruzione
solo metodo: esegui(); Queste classi sono l'equivalente orientato agli oggetti dei puntatori a
funzione e sono descritti come pattern Command in [5]. Esistono tre comandi, corrispondenti alle
modalità d'esecuzione. Si può eseguire un'intera istruzione o una fase e ad ognuno corrisponde un
comando. Quello dell'intero programma, invece, mette in coda il comando per una istruzione e poi se
stesso realizzando una sorta di ricorsione utilizzando la coda.
L'inserimento di un comando attiva il thread del processore iniziando il ciclo
dell'istruzione. La prima operazione è l'individuazione dell'Esecutore corretto. L'istruzione puntata
dal registro PC viene letta dalla memoria per estrarne la classe ed il tipo che indicizzano la matrice
degli esecutori (cfr. Capitolo 2.1). L'esecuzione viene quindi delegata all'oggetto individuato che
esegue fetch e completamento (separatamente, se richiesto) agendo come un Visitor (uno dei pattern
descritti in [5]) dell'oggetto StatoEsecuzione del processore. Dopo ogni istruzione viene
verificato lo stato delle interruzioni ed eventualmente si esegue il salto alla prima istruzione del driver
dell'interfaccia che ha mandato il segnale d'interruzione.
Quando l'utente clicca sul tasto “Stop” il GestoreSimulazione transita verso lo
stato “Thread in esecuzione” con la simulazione non più in corso. In questa fase ai thread è inviato il
comando di terminazione, alla ricezione del quale le periferiche possono rimuovere la GUI e
completare le proprie operazioni correttamente. Quando tutti i thred hanno concluso il loro ciclo vitale
la simulazione può essere riavviata.
Al termine della simulazione lo stato del processore e delle periferiche non viene
reimpostato se non esplicitamente richiesto dall'utente. Dunque l'esecuzione può essere ripresa dallo
stato in cui era stata sospesa in qualsiasi momento.
In Figura 7 è mostrato il DFD del simulatore.
15
Il ciclo istruzioneIl ciclo istruzione
4. L'interfaccia Grafica
Gran parte dell'interfaccia grafica dipende dalla piattaforma (in questo caso NetBeans) e
quindi non è integrata nel core. Alcune parti però possono essere realizzate in modo piuttosto generico
ed in comune tra tutte le piattaforme, in particolare il pannello per comandare l'esecuzione e quello
per le interfacce hardware. In questi casi ho deciso di realizzare un'implementazione generica che
potesse essere utilizzata dai client del core ed è il motivo principale per cui ho scelto Swing piuttosto
che SWT e conseguentemente NetBeans piuttosto che Eclipse.
Nell'analisi dello svolgimento della simulazione è stato illustrato come questa non dipenda
dall'interfaccia verso l'utente e possa procedere autonomamente come un'applicazione batch. Lo stato
della simulazione è costituito dal processore, dalla memoria e dalle periferiche, e tutto è raccolto
16
Figura 7 DFD del simulatore
L'interfaccia GraficaL'interfaccia Grafica
nell'oggetto StatoEsecuzione che contiene anche le variabili utilizzate dagli esecutori. Il
meccanismo con cui avvengono gli aggiornamenti dell'interfaccia si basa sulla notifica degli eventi e
su un Timer di Swing.
Nel package esecuzione.eventi sono definite le classi ModificaRegistroE-
vent, StatoInterfacciaEvent e StatoSimulazioneEvent con i relativi Listener.
Questi eventi vengono generati rispettivamente dal processore, dalle interfacce hardware e dal
GestoreSimulazione. Il pannello dell'esecuzione si registra come listener di eventi del
processore e del gestore della simulazione, mentre il pannello delle interfacce hardware con
l'interfaccia a cui è associato. Una cosa da tenere in considerazione, nella scrittura del codice dei
metodi che ricevono la notifica di un evento, è che ad eseguire il codice del metodo è il thread che
genera l'evento cioé, in particolare, il thread del processore. D'altra parte in Swing un solo thread può
modificare l'interfaccia ed è il gestore degli eventi o thread AWT. Solo alcuni componenti in Swing
sono sincronizzati. Aggiornare l'interfaccia grafica nei metodi di notifica comporterebbe due
svantaggi: la simulazione dovrebbe accollarsi la responsabilità dell'aggiornamento, risultando
fortemente rallentata, e nel modificare dei componenti sincronizzati il thread del processore dovrebbe
richiedere dei lock1. Il thread del processore, quando non ha dei comandi da attuare, sospende la
propria esecuzione fino all'inserimento di un nuovo comando nella coda e alla conseguente
invocazione del metodo interrupt() sul thread stesso. Ciò significa che il flag interrupted associato al
thread viene costantemente settato e resettato. I componenti di Swing con metodi sincronizzati, come
ad esemempio JTextField.setText() implementano un meccanismo di locking ad un livello
superiore dei semplici blocchi sincronizzati. Infatti, permettono a più thread di leggere il contenuto
dell'oggetto contemporaneamente (nel caso di JTextField il testo memorizzato nel modello,
generalmente una sottoclasse di AbstractDocument) ma ad uno solo di modificarlo. Tale
sincronizzazione ha però una limitazione non documentata: il thread che vuole modificare il
1 In Java ogni oggetto ha associato un lock, cioé una chiave che permette al solo thread che è riuscito ad ottenerla dieseguire i metodi sincronizzati sull'oggetto.
17
L'interfaccia GraficaL'interfaccia Grafica
componente non può essere interrotto durante il tentativo di acquisizione del lock. Nel caso in cui un
thread fosse interrotto sarebbe generato un Error che quindi porterebbe all'immediata terminazione
del thread senza possibilità di gestire l'eccezione. Un thread continuamente interrotto come quello del
processore certamente non riuscirebbe ad evitare che questo accada.
Il pannello dell'esecuzione possiede un array di eventi indicizzato dal codice associato a
ciascun registro. Quando il thread del processore notifica un evento imposta la cella dell'array relativa
al registro coinvolto nel cambiamento di stato. Ogni decimo di secondo nel thread AWT viene
eseguito il metodo di aggiornamento dell'interfaccia che per ogni evento ricevuto apporta le dovute
modifiche. Se più di un evento dovesse avvenire tra due aggiornamenti dell'interfaccia, solo l'ultimo
comporterebbe una effettiva modifica. Poiché l'utente non è in grado di cogliere due cambiamenti in
meno di un decimo di secondo questo limita il numero di modifiche da apportare alla GUI e riduce i
tempi d'esecuzione.
Le modifiche alla memoria di lavoro si riflettono immediatamente sulla tabella che ne
visualizza il contenuto poiché la memoria stessa è anche il modello della JTable che la rappresenta.
La RAM a disposizione del PD32 ha una dimensione di 10KB e non sarebbe stato efficiente
duplicarla nel simulatore e nell'interfaccia grafica dovendo, in tal caso, risolvere i conseguenti
problemi di sincronizzazione. Anche il pannello dell'esecuzione riceve la notifica dei cambiamenti
nelle memoria e la salva in una variabile per selezionare, al successivo aggiornamento dell'interfaccia,
la riga con l'ultima cella modificata.
Il pannello delle interfacce hardware è a disposizione delle periferiche. Può essergli
assssegnata un'interfaccia hardware e viene automaticamente aggiornato alla notifica dei
StatoInterfacciaEvent.
18
NetBeans platformNetBeans platform
5. NetBeans platform
Il modulo nbplugin integra il core con la piattaforma di NetBeans. NetBeans è costituito a
sua volta da un core non accessibile dall'esterno, da un'api (openide) e dai moduli. Tutte le
funzionalità non relative alla piattaforma sono realizzate dai moduli.
Un modulo è un file jar contenente, oltre alle classi che utilizza, dei particolari attributi nel
manifest il cui valore specifica: nome, descrizione, versione ed eventuali dipendenze da altri moduli,
da una versione della JVM o della piattaforma stessa. L'installazione di menù, actions, toolbar ed altri
componenti grafici o relativi al comportamento dell'interfaccia avviane mediante dei file xml.
La aspetto della piattaforma può essere modificato tramite il branding (personalizzazione)
con un meccanismo analogo alla localizzazione (tramite file properties).
5.1 Il caricamento delle classi
Una piattaforma basata su dei file jar riconosciuti durante l'esecuzione pone particolari
problemi relativi al caricamento delle classi dato che il classpath non è noto a priori. Ad ogni modulo
viene assegnato un ClassLoader che gli permette di accedere all'api e alle classi nel proprio file
jar ma non a quelle di altri moduli. Per poter richiedere il caricamento di classi in moduli esterni è
necessario specificare la dipendenza nel file manifest. In tal caso il ClassLoader relativo al
modulo avrà nel path anche tutti i jar dei moduli da cui questo dipende.
Inizialmente il progetto di DISsimulator prevedeva che le periferiche non fossero
contenute nel core ma ognuna in un proprio file potendo essere facilmente scritta ed aggiunta da
qualsiasi utente. Una tale realizzazione richiedeva la creazione di un URLClassLoader con il
percorso del file jar della periferica per accedere alle classi. Il modulo doveva provvedere al
19
Il caricamento delle classiIl caricamento delle classi
caricamento delle classi della periferica creando il classloader necessario come figlio1 di quello
associato al modulo in modo che la periferica potesse avere acesso anche all'api di NetBeans.
Purtroppo questa soluzione si è rivelata inattuabile per via di un meccanismo di serializzazione interno
alla piattaforma.
NetBeans, all'avvio, tenta di ripristinare la precedente sessione deserializzando i
componenti attivi al momento della chiusura. Se una periferica crea un pannello all'interno della
finestra principale questo viene registrato e serializzato. Quando la piattaforma tenta di ripristinare il
pannello non è più in grado di localizzarne la classe, essendo questa stata caricata dal modulo e non
dal classloader di NetBeans. Questa evenienza genera numerosi messaggi d'errore nel file di log e,
sebbene non sembri avere altre conseguenze, ho preferito unire le periferiche al core dove possono
essere regolarmente caricate. Non è possibile nemmeno l'inserimento delle periferiche in un modulo
esterno, diverso dal core perché i due moduli (core e periferiche) dovrebbero dichiarare delle
dipendenze reciproche e l'attivazione dei moduli è un processo strettamente sequenziale. In questo
caso la piattaforma non sarebbe in grado di attivare uno dei due
Conclusioni
Questo progetto è nato con l'intento di modernizzare un software datato avvicinandolo ai
suoi utenti e facendo tesoro dell'esperienza accumulata nei dieci anni in cui è stato utilizzato. L'intero
design dell'applicazione è stato concepito per permetterne facilmente la modifica e l'estensione.
Sono già in fase di realizzazione dei programmi a scopo didattico che faranno presto parte
di DISsimulator, con la speranza che il progetto possa diventare una suite completa e matura.
Il software è opensource ed è rilasciato con licenza Gnu GPL (General Public License).
1 I classloader sono organizzati in maniera gerarchica ad albero. Ognuno di essi nel tentativo di localizzare una risorsadeve prima fare riferimento al padre (il primo classloader verso l'alto nella gerarchia) e solo se questi non riescenell'operazione cercare tra le proprie risorse.
20
Appendice A: La grammatica dell'assembly PD32
parse := ( <FINE_LINEA> )*
programma
<EOF>
programma := "ORG" ( numero )? <FINE_LINEA>
( <FINE_LINEA> )*
( dichiarazione <FINE_LINEA> ( <FINE_LINEA> )* )*
"CODE" <FINE_LINEA>
( <FINE_LINEA> )*
( istruzione <FINE_LINEA> ( <FINE_LINEA> )* )*
( driver )*
"END"
( <FINE_LINEA> )*
driver:= "DRIVER" numero ( "," numero )? <FINE_LINEA>
( <FINE_LINEA> )*
( istruzione <FINE_LINEA> ( <FINE_LINEA> )* )*
numero:= ( ( "+" | "-" ) )? restoNumero
restoNumero := ( <IDENTIFICATORE> | <INTERO> )
dichiarazione := <IDENTIFICATORE> <DIRETTIVA_DI_DEFINIZIONE> numero
( "," numero )*
istruzione := <IDENTIFICATORE> ( ( ":" ( <FINE_LINEA> )*
<IDENTIFICATORE> restoIstruzione ) |
( restoIstruzione )
)
restoIstruzione := ( parametro ( "," parametro )? )?
parametro := ( conRegistro |
immediato |
assolutoOSpiazzamento |
"-" ( assolutoOSpiazzamento | predecremento ) |
indirettoOPostincremento )
21
conRegistro := <REGISTRO>
immediato := "#" numero ( ( "+" | "-" ) <INTERO> )?
assolutoOSpiazzamento := restoNumero ( ( "+" | "-" ) <INTERO> )?
( "(" ( <REGISTRO> | "PC" ) ")" )?
indirettoOPostincremento := "(" <REGISTRO> ")" ( "+" )?
predecremento := "(" <REGISTRO> ")"
22
Bibliografia
1: Cioffi G., Jorno A., Villani T., Il processore PD32, 1994
2: Eric Steven Raymond, The Cathedral and the Bazaar, 2000
3: Andrew W. Appel, Modern Compiler Implementation in Java, 1998
4: Gosling J., Joy B., Steele G., Bracha G., The Java Language Specification, 2000,
http://java.sun.com/docs/books/jls/second_edition/html/j.title.doc.html
5: Gamma E., Helm R., Johnson R., Vlissides J., Design Patterns, 1995
Indice generale
Introduzione.............................................................................................................................................2
1. Architettura..........................................................................................................................................2
2. L'assemblatore.....................................................................................................................................4
2.1 Le istruzioni..................................................................................................................................6
3. La simulazione....................................................................................................................................9
3.1 Avvio dell'esecuzione.................................................................................................................10
3.2 Le periferiche..............................................................................................................................11
3.2.1 Scrivere nuove periferiche..................................................................................................13
3.3 Il ciclo istruzione........................................................................................................................15
4. L'interfaccia Grafica..........................................................................................................................17
5. NetBeans platform.............................................................................................................................20
5.1 Il caricamento delle classi...........................................................................................................20
Conclusioni............................................................................................................................................21
Appendice A..........................................................................................................................................22