




















Studia grazie alle numerose risorse presenti su Docsity
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Prepara i tuoi esami
Studia grazie alle numerose risorse presenti su Docsity
Prepara i tuoi esami con i documenti condivisi da studenti come te su Docsity
Trova i documenti specifici per gli esami della tua università
Preparati con lezioni e prove svolte basate sui programmi universitari!
Rispondi a reali domande d’esame e scopri la tua preparazione
Riassumi i tuoi documenti, fagli domande, convertili in quiz e mappe concettuali
Studia con prove svolte, tesine e consigli utili
Togliti ogni dubbio leggendo le risposte alle domande fatte da altri studenti come te
Esplora i documenti più scaricati per gli argomenti di studio più popolari
Ottieni i punti per scaricare
Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium
Libro di Michael Dahlin, Thomas Anderson tradotto in italiano (13 capitoli).
Tipologia: Appunti
1 / 28
Questa pagina non è visibile nell’anteprima
Non perderti parti importanti!





















Capitolo 8 (Professore)
Quando scriviamo un programma, non scriviamo indirizzi fisici, ma soltanto indirizzi simbolici. Dal punto di vista del programmatore la traduzione degli indirizzi è un qualcosa di magico.
Per semplicità supponiamo che tutti gli indirizzi non fisici di un programma partono dalla locazione 0.
Def traduzione degli indirizzi: I nostri programmi virtualmente partono dalla locazione 0, ma quando vanno messi in esecuzione, il codice dal disco viene messo in posizioni diverse della memoria in base all’occupazione che vi è disponibile.
Gli indirizzi che mette il compilatore nel nostro programma vengono chiamati indirizzi virtuali , nel momento in cui viene messo in memoria gli indirizzi vengono definiti indirizzi fisici che sono relativi ad un dato processo.
Pensando all’esecuzione di un’istruzione macchina, dovrei fare tante traduzioni degli indirizzi per la fetch, operandi, e rimettere in memoria il risultato. Quindi abbiamo bisogno di un meccanismo per essere efficienti.
Vedremo:
In generale il processore ha nelle istruzioni degli indirizzi virtuali e questi devono essere tradotti in indirizzi fisici. Vediamo per ora come se fosse una scatola nera che mi permette di fare tale lavoro, con annessi controlli.
La traduzione degli indirizzi ha degli obiettivi principali:
Per poter fare velocemente la traduzione degli indirizzi, mi ci vuole un hardware di supporto che mi faciliti la traduzione degli indirizzi. Non solo ma anche il compilatore deve fare le cose in modo tale da facilitare la traduzione degli indirizzi, poiché possono essere ridotte le prestazione del sistema.
A cosa serve la traduzione degli indirizzi? Isolare i processi/thread che possono toccare zone di memoria che non gli appartengono. Per implementare zone di memoria condivise da più thread/processi. Il codice può essere condiviso tra più processi. Eseguire un programma prima che esso sia effettivamente caricato totalmente in memoria. Allocazione dinamica della memoria.
Come si compila una memoria virtuale?
Il compilatore vede un processo in questo modo:
Viene preparato il programma per l’esecuzione:
Mi occupa un blocco contiguo di memoria.
+ overhead.
Bound: Mi serve a vedere se sto toccando una zona di memoria che non è mia. Quindi contiene la dimensione della memoria riservata per un dato programma. Quindi quando controlleremo l’indirizzo virtuale, basta che questi sia <= bound. Altrimenti sollevo un’eccezione.
Indirizzo fisico: Indirizzo virtuale + base.
Questo tipo di hardware lo si può trovare nella MMU.
Vantaggi: È molto semplice. 2 registri un confrontatore e un addizionatore. Posso cambiare il registro base per far puntare tutti gli indirizzi virtuali a diverse zone della memoria.
Svantaggi: È possibile che il programma accidentalmente fa overwriting del proprio codice. Non è possibile condividere codice/dati con altri processi. Non è possibile far crescere lo stack e lo heap.
Fin ora abbiamo fatto l’ipotesi di un programma visto come unico blocco, anche se in realtà non è così (dati, codice, heap, stack).
SEGMENTAZIONE
Un processo quindi è fatto di più segmenti. Come faccio a dividere un programma in tanti pezzi?
Segmento: È una oggetto che possiede funzionalità particolari. Questo oggetto viene allocato in una regione contigua della memoria. Se spezzassi il blocco unico in più segmenti, lo potrei posizionare in punti diversi della memoria; per sapere dove sono memorizzati tutti questi segmenti vi è una tabella dei segmenti contenuta nel descrittore del processo (contiene il base and bound di ciascun segmento e i suoi relativi diritti di accesso (read-only, read-write) avendo così un controllo molto fine. Quindi con la segmentazione posso gestire la cooperazione, mutua esclusione ecc..
Il fatto di usare la segmentazione porta a degli aggiornamenti per la FORK di Unix. Quando faccio la fork, faccio una copia completa del processo, ereditando anche i segmenti (ovvero la segment table). Per far si che questi segmenti possano essere modificati, mette il diritto di accesso del figlio e del padre in read-only e può essere eseguito il figlio. Se il padre o il figlio ha voglia di eseguire una write, questo causa una trap (poiché tutti i diritti di accesso sono solo in read-only).
Zero-on-reference: Di alcuni oggetti io so la lunghezza a priori (oggetti statici: dati, codice, ecc..), di altri no (heap o stack). Inizialmente gli do 0 byte, quando un programma in generale vuol scrivere nello heap e quest’ultimo è pieno => provo a scrivere fuori dall’heap causando un’eccezione chiamata Segmentation Fault ; l’OS si occuperà di gestire tale interruzione, se fosse relativo al codice e dati viene terminato il programma. Altrimenti, se sono segmenti che possono crescere dinamicamente posso dargli più memoria; quindi viene azzerata la memoria, viene spostato in una nuova porzione di memoria che possa contenere la nuova dimensione e riavvio il processo.
Vantaggi: Posso condividere codice e dati tra processi. Proteggere il codice da overwriting. Far crescere in modo trasparente l’heap e lo stack. Posso scegliere se fare la copy-on-write.
Svantaggi: Frammentazione esterna (spazio sprecato tra i vari pezzi); risolvo tale problema, con pezzi tutti della stessa dimensione. Questa si chiama:
Paging (Lezione 24/04/18)
All’interno di ogni singola pagina, abbiamo più segmenti; tutte le pagine hanno tutte la stessa dimensione.
Un frame fisico (una pagina fisica), è rappresentata da ogni singolo bit. Esiste una tabella simile alla tabella dei segmenti, quindi ad ogni processo viene assegnata una tabella delle pagine (che mi serve per tradurre gli indirizzi).
Multi level Paging
Riepilogo (26/04/18)
Considerando il problema visto precedentemente, abbiamo una memoria divisa in pagine riservate ad un particolare processo.
Avendo 32 bit di indirizzo, 10 per la tabella di primo livello, 10 per la tabella di secondo livello e 12 per l’offset. Quindi mi conviene mettere una tabella in un'unica pagina. Le pagine sono 4kb dove ciascuna entry è 4Byte: 3byte offset + 1 byte accesso.
Faccio in modo che carico soltanto i livelli di pagine che mi servono.
Nell’hardware x86 supporta la Paged Segmentation a più livelli.
La segment table viene chiamata Global Descriptor Table , ciascun puntatore punta ad una tabella della pagine, la quale è realizzata multi-livello.
Utilizzo solo quello che mi servono per aumentare la velocità di calcolo e un maggior parallelismo.
L’allocazione della memoria è semplice attraverso la bitmap. La condivisione può essere fatta a livello di pagina o di segmento.
Ovviamente a livello virtuale tutti i processori hanno le stesse opportunità. Se un processo è stato scritto per un indirizzo a 32 bit, e gira su un architettura a 64, potremmo utilizzare 32bit più significativi dei 64 disponibili, realizzando una maggiore portabilità.
I moderni sistemi operativi utilizzano le tabelle hash al posto degli alberi, le tabelle si chiameranno inverted page table. L’elemento puntato dall’hashing di un particolare indirizzo punta alla pagina fisica in memoria.
Translation lookaside buffer (tabella delle pagine con cache)
Per poter eseguire una traduzione degli indirizzi molto efficiente si usano le memorie associative (estremamente costose se molto grandi). Più in generale quindi utilizzo una memoria più veloce “ cache” ma molto più piccola rispetto al resto della memoria. All’interno di essa vi sono le pagine che accediamo più frequentemente. Se vi è un cache hit => traduzione diretta, altrimenti uso la tabella delle pagine normali.
Costo traduzione : Costo TLB lookup + probabilità (TLB miss) * costo della lookup nella page table.
Il problema qui è la dimensione delle pagine. Possiamo risolverlo o con i segmenti (ma ho frammentazione), o avendo un insieme di pagine contigue le quali vengono viste come una sola super-pagina.
Nell’x86 quindi posso avere o una pagina o una super-pagina (con relativa dimensione) per ogni entry.
Cosa faccio nel caso di un cambio di contesto?
Butto fuori dalla cache tutto ciò che non è condiviso con gli altri thread, quindi scartando ciò che non mi serve per poter andar avanti.
Il traduttore prende ogni istruzione e riferimenti alla memoria dati generati dai processi, controlla se l’indirizzo è legale, e converte quest’ultimo in un indirizzo fisico che può essere utilizzato per la fase di fetch o memorizzare istruzioni o dati. Il dato in sé – qualsiasi cosa è memorizzata nella memoria – viene restituito così come; in ogni caso non è modificato. La traduzione di solito è implementata ad hardware, e il kernel del sistema operativo configura tale hardware per realizzare i suoi scopi.
Il compito principale in questo capitolo è capire in dettaglio come la scatola nera descritta in precedenza funzioni. Se avessi pensato ad un array, un albero, o una tabella hash, non sarebbe sbagliato – tutti questi approcci sono stati utilizzati dai sistemi reali.
Dato che il numero di differenti implementazioni possibili, come possiamo valutare le differenti alternative? Ci sono alcuni obiettivi che vorremmo per la nostra scatola di traduzione; la costruzione finale dipenderà da come vorremmo bilanciare tali obiettivi.
Finiremo con un meccanismo abbastanza complesso per la traduzione degli indirizzi, quindi adesso inizieremo con il più semplice ed aggiungeremo funzionalità solo quando ne avremo di bisogno. Sarebbe di aiuto che durante la spiegazione si abbia in mente 2 viste della memoria: il processo visto come la sua memoria, che usa i suoi indirizzi. Chiameremo questi ultimi indirizzi virtuali , perché non è necessario che quest’ultimi corrispondo agli indirizzi fisici reali. Al contrario, per la memoria del sistema, ci sono soltanto indirizzi fisici – locazioni reali in memoria. Dal punto di vista della memoria, sono dati indirizzi fisici, si fa una lookup e si memorizzano i valori. Il meccanismo di traduzione converte tra due viste: da indirizzi virtuali a indirizzi fisici della memoria.
8.2 Verso una flessibile traduzione degli indirizzi
La nostra spiegazione della traduzione degli indirizzi è divisa in due step. Nel primo, metteremo il problema di realizzare una lookup efficiente da parte, e ci concentreremo su quale sia il miglior modo per raggiungere gli obiettivi che ci siamo posti: assegnamento flessibile della memoria, efficienza di spazio, protezione e condivisione, etc. Una volta che abbiamo definito tutto vedremo come realizzare una lookup efficiente.
Figura 8.
Nel capitolo 2, abbiamo illustrato le nozioni di protezione dell’hardware della memoria usando il più semplice hardware immaginabile : base and bound. La box che si occupa della traduzione consiste di due registri extra per processo. La base specifica la partenza della regione fisica della memoria del processo. Il bound specifica l’estensione di tale regione. Se il registro base è aggiunto ad ogni indirizzo generato dal programma, allora non avremmo più bisogno di un relocation loader – gli indirizzi virtuali del programma iniziano da 0 e finiscono in bound, e quelli fisici iniziano da base e finiscono in base + bound. La figura 8. mostra un esempio di traduzione degli indirizzi con la tecnica base and bound. Dato che la memoria fisica può contenere molti processi, il kernel resetta il contenuto dei registri base e bound ogni volta che vi è un cambio di contesto con i valori appropriarti per il processo nuovo.
La traduzione base e bound è semplice e veloce, ma mancano molti aspetti di cui abbiamo bisogno per supportare i programmi moderni. Tale traduzione supporta solo una protezione a grandi linee nel livello di un intero processo; non è possibile prevenire un programma dall’overwriting del proprio codice, per esempio. E’ anche molto difficile condividere regioni di memoria tra due processi. Poiché la memoria per un processo ha bisogno di continuità, supportare regioni di memorie dinamiche, come heap, stack dei thread, o memory mapped per i file, diventa impossibile.
8.2.1 Memoria Segmentata (Segmented Memory)
Molte di queste limitazioni di base e bound possono essere risolte con un piccolo cambiamento: invece di mantenere soltanto una coppia di registri base e bound per processo, l’hardware può memorizzare un array di coppie per ogni processo. Questo è chiamato segmentazione. Ogni entrata dell’array controlla una porzione, o segmento , di uno spazio virtuale. La memoria fisica per ogni segmento è memorizzata in modo contiguo, ma segmenti differenti possono essere memorizzati in locazioni differenti.
Allo stesso modo, le librerie, come quella grafica, possono essere memorizzate in un segmento condiviso tra processi. Come prima, la libreria dei dati dovrebbe essere in un segmento separato e non condiviso. Questo è frequentemente fatto nei sistemi operativi moderni con librerie dinamiche collegate. Un problema pratico è che differenti processi possono caricare differenti numeri o librerie, e quindi potrebbero essere assegnate le stesse librerie a differenti segmenti. Tutto ciò dipende dall’architettura del processore, la condivisone potrebbe ancora lavorare, se il codice delle librerie usa indirizzi di segmenti-locali , quest’ultimi sono relativi al corrente segmento.
Possiamo utilizzare i segmenti anche per le comunicazioni interprocess, se i processi hanno i permessi di lettura e scrittura per lo stesso segmento. In Multics (OS 1960), un segmento è allocato per ogni struttura dati, permettendo una protezione accurata e la condivisione tra processi. Certamente, questo comporta ad una tabella certamente più grande. I moderni sistemi operativi usano i segmenti solo per regioni grossolane della memoria, come il codice e dati per un’intera libreria condivisa, piuttosto che per ogni struttura dati della libreria.
Come esempio finale per la potenza dei segmenti, vedremo la gestione efficiente dell’allocazione dinamica della memoria. Quando un sistema operativo riusa la memoria o lo spazio del disco che è stato precedentemente usato, deve prima azzerare il contenuto della memoria o del disco. Altrimenti, i dati privati di un’applicazione potrebbero inavvertitamente perdersi in un’altra applicazione potenzialmente malevola. Per esempio, potresti inserire la password in un sito, per esempio una banca, e dopo esci dal browser. Comunque, se la memoria fisica sottostante utilizzata dal browser è riassegnata ad un nuovo processo, la password potrebbe essere persa in un sito malevolo.
Certamente, vorremmo soltanto pagare l’overhead dell’azzeramento della memoria se sarà usato. Questo è un problema particolare per l’allocazione dinamica della memoria da parte dello heap/stack. Non è chiaro quando un programma inizia quanta memoria userà; l’heap potrebbe passare da pochi kilobyte a molti giga, dipendendo dal programma. Il sistema operativo può indirizzare usando zero-on-reference_._ Con quest’ultima tecnica, il sistema operativo alla regioni di memoria per l’heap, ma azzera soltanto pochi kilobyte. Invece, setta il registro bound nella tabella segmenti al limite del programma solo alla parte azzerata della memoria. Se il programma espande il suo heap, genererà un eccezione, e il sistema operativo può azzerare il contenuto addizionale della memoria prima di riprendere la sua esecuzione.
Fornendo tutti questi vantaggi, come è possibile stopparlo? Il principio sottostante alla segmentazione è l’overhead di gestione di un grandezza variabile e dinamicamente crescente di segmenti di memoria. Col tempo, come i processi sono creati e finiti, la memoria può essere divisa in regione che sono usate e altre no utilizzabili da nuovi processi. Queste porzioni di memoria possono avere grandezze differenti. Quando creiamo un nuovo segmento, avremo bisogno di spazio per esso. Sceglieremo la regione più piccola che lo contiene, o la regione più grande disponibile?
Comunque, poiché scegliamo di posizionare un nuovo segmento, poiché molta più memoria viene allocata, il sistema operativo ricerca un punto dove c’è abbastanza spazio per tale segmento, ma quest’ultimo non è contiguo. Questo problema è chiamato frammentazione esterna. Il sistema operativo è libero di compattare memoria per creare porzioni contigue senza avere impatto sulle applicazioni, perché gli indirizzi virtuali non cambiano quando viene rilocato un segmento nella memoria fisica. Però, compattare potrebbe costare in termini di overhead da parte del processore: una configurazione tipica del server dovrebbe prendere approssimativamente circa 1 secondo per la sua memoria.
Tutto questo diventa sempre più complesso quando i segmenti della memoria crescono. Quanta memoria dovremmo settare a parte per l’heap dei programmi? Se mettessimo il segmento per l’heap in una parte della memoria fisica perderemmo tante porzioni di segmenti, quindi perderemmo memoria per un programma che avrebbe bisogno di un heap piccolo. Se facessimo l’opposto, avremmo bisogno di copiare qualsiasi cosa se l’heap dovesse cambiare di dimensione.
8.2.2 Memoria Paginata (Paged Memory)
Un alternativa alla memoria segmentata è la memoria paginata. Con il “paging” la memoria è allocata in pezzi di dimensione fissata chiamati frames page (frame di pagina). La traduzione degli indirizzi è simile
alla segmentazione. Invece di una tabella dei segmenti la quale contiene voci le cui posizioni puntano a segmenti di dimensione variabile, esiste una tabella di pagine per ogni processo le cui posizioni puntano ai frame di pagina. Poiché le pagine hanno dimensione fissa a potenze di due, le voci della tabella delle pagine hanno bisogno di fornire la parte più significativa dei bit come l’indirizzo del frame di pagina (Page #), essendo così più compatto. Non abbiamo bisogno del bound nell’offset; l’intera pagina è allocata nella memoria fisica come un unità. La figura sottostante (Figura 8.6) mostra tale traduzione.
Quello che sembrerò strano, e probabilmente cool, è che nel frattempo che il programma pensa che la sua memoria sia lineare, in effetti la memoria potrebbe esserlo, e di solito lo è, in realtà è una sorta di mosaico astratto. Il processore eseguire una istruzione dopo un’altra usando gli indirizzi virtuali; tali indirizzi sono ancora lineari. Tuttavia, le istruzioni memorizzate alla fine della pagina saranno memorizzate in una regione di memoria fisica completamente differente dalla prossima istruzione all’inizio della prossima pagina. Le strutture dati appariranno contigue utilizzando gli indirizzi virtuali, ma una matrice enorme potrebbe essere sparpaglia attraverso molte frame di pagine fisiche.
Il paging risolve il problema principale della segmentazione: lo spazio libero allocato è molto schietto. Il sistema operativo può rappresentare la memoria fisica come una bit map, in modo tale che ogni bit rappresentare il frame di pagina fisica che è libero o in uso. Trovare un frame libero è un matching semplice tra bit.
La condivisione della memoria tra processo è anche conveniente: abbiamo bisogno di settare le posizioni della tabella di pagine per ogni processo che condivide una pagina in modo che punti allo stesso frame di pagina fisica. Per una grande regione condivisa che si estende su frame di pagina multipli, come librerie condivise, questo potrebbe richiede di settare un gran numero di posizioni nei frame di pagina. Dato che abbiamo bisogno di sapere quando la memoria viene rilasciata da parte di un processo che ha finito, la memoria condivisa richiede un po’ più di informazioni per mantenere traccia delle pagine condivise ancora in uso. Tale struttura dati è chiamata core map ; memorizza le informazioni di ogni frame di pagina fisica tipo quali sono le voci di una tabella delle pagine.
Molte delle ottimizzazione viste sotto la segmentazione possono essere fatte anche qui. Per la copy-on-write, abbiamo bisogno di copiare le voci della tabella e settarla in solo lettura; una store per una di queste pagine, possiamo fare una copia reale della pagina sottostante prima di rieseguire il processo. Allo stesso modo, per la zero-on-reference, possiamo settare le voci della tabella di una pagina all’inizio dello stack per essere invalidate, causando una trap nel kernel. Questo ci permette di estendere lo stack solo se ne avessimo di bisogno.
Le tabelle di pagina permettono di aggiungere altre funzionalità. Per esempio, possiamo iniziare un programma eseguendo prima che tutto il suo codice e dati sono memorizzati nella memoria. Inizialmente, il sistema operativo marca tutte le posizioni della tabella come invalide; quando le pagine sono portate dal disco, queste vengono marcate come in sola lettura (per le pagine del codice) o lettura-scrittura (per le pagine
Paged segmentation
Iniziamo da un sistema che ha solo 2 livelli nell’albero. Con la page segmentation, la memoria è segmentata, ma invece di avere ogni puntatore all’entrata della tabella di segmento collegato direttamente ad una contigua regione della memoria fisica, ogni entra è collegata ad una tabella delle pagine, la quale punta alla memoria che punta a quel segmento. L’entrata della tabella del segmento è il “bound” e descrive la lunghezza della pagina di tabella, che è, la lunghezza del segmento in pagine. Poiché il paging è usato al più basso livello, tutte le lunghezze dei segmenti sono multipli della dimensione delle pagine. La figura sottostante mostra l’implementazione della paged segmentation
Sebbene le tabelle di segmento sono talvolta memorizzate in registri hardware speciali, le tabelle di pagina per ogni segmento sono un po’ più grandi in aggregazione, quindi sono normalmente memorizzate nella memoria fisica. Per mantenere l’allocazione semplice, la dimensione massima del
segmento è scelta per permettere alla tabella delle pagine per ogni segmento di essere un multiplo piccolo della dimensione di pagina.
Multi-level Paging
Un approccio molto simile alla paged segmentation è usare livello multipli di tabelle di pagina. Come mostrato nella figura sottostante
la pagina di tabella al top level contiene le posizioni le quali puntano al secondo livello della tabella delle pagine, le cui posizioni sono puntatori alle tabelle di pagine. In SPARC (Sun Microsystem – processore), come nella maggior parte dei sistemi che usano livelli multipli di tabelle di pagina, ogni livello della tabella delle pagine è costruito per adattarsi ai frame di pagina fisica. Solo al top level la tabella delle pagine deve essere riempita; i livello più bassi dell’albero sono allocati solo se queste porzioni di spazi di indirizzi virtuali sono utilizzati in un particolare processo. I permessi di accesso possono essere specificati ad ogni livello, e quindi la condivisione tra processi può avvenire ad ogni livello.
Multi-level paged segmentation
Possiamo mischiare questi due approcci usando la memoria segmentata dove ogni segmento è gestito da tabella delle pagine a multi-livello. Questo approccio è usato da x86, sia per 32 che 64bit di indirizzamento.
Descriveremo il caso a 32 bit inizialmente. La terminologia x86 differisce leggermente da come l’abbiamo usata sino ad ora. L’x86 ha una Descrittore Globale della Tabella (GDT = Global Descriptor Page), equivalente alla tabella di segmento. Il GDT è memorizzato in memoria; ogni entrata (descriptor)
Tra le più interessanti di queste sono le strutture dati usate per tradurre da virtuale a fisico. Per il software della tabella delle pagine, abbiamo tutte le stesse opzioni che avevamo prima. Il software della tabella delle pagine ha bisogno di non utilizzare le stesse strutture della tabella delle pagine dell’hardware sottostante; Facendo ciò, il sistema operativo è facilmente portabile, permettendo alle strutture dati di essere un po’ diverse tra di loro.
Le strutture dati per la traduzione degli indirizzi all’interno del modello del sistema operativo Linux utilizza sia segmenti che tabelle di pagina multi-livello.
Un approccio differente, preso piede in una ricerca nel sistema operativo chiamato Mach e dopo in Apple OS X, era quello di usare le tabelle hash piuttosto che alberi, per il software di traduzione dati. Per ragioni storiche, l’uso della tabella hash per la traduzione degli indirizzi di pagina è chiamata inverted page table. In particolare, per muoverci dai livelli più profondi della tabella delle pagine multi-livello, sia una hash table per velocizzare tale traduzione.
Con un inverted page table, il numero di pagine virtuale è “funzione hash()” nella tabella di dimensione proporzionale al numero di frame di pagina fisica. Ogni entrata nella tabella hash contiene tuple della forma:
inverted page table entry = {
process or memory object ID,
virtual page number,
physical page frame number,
access permissions
}
Come mostrato nella figura 8.9, se ci fosse un match tra il numero di pagina e il processo ID, la traduzione sarebbe valida. Alcuni sistemi adottano 2 fasi di lookup: la prima mappa gli indirizzi virtuali all’id dell’oggetto in memoria. Se la memoria è maggiormente condivisa, questo può salvare spazio nella hash table evitando l’eccessivo rallentamento dovuto alla traduzione.
Una inverted page table ha bisogno di gestire le collisioni hash, ovvero quando due indirizzi mappano nella stessa entrata della tabella hash. Per risolvere questo problema possiamo adottare delle tecniche standard.
Un particolare conseguenza di avere il livello di portabilità per la gestione della memoria è che il contesto della traduzione hardware multi livello può essere trattato come un hint (traccia). Un hint è il risultato di molte computazioni i quali risultati potrebbero non essere più validi, ma utilizzare hint non validi porteranno alla gestione di tale eccezione.
Con un livello portabile, il software della tabella delle pagine è la verità di fondo, mentre l’hardware tabella delle pagine sia un hint. Tale hardware può essere usato in modo sicuro, fornendo traduzioni e permessi che sono un sottoinsieme delle traduzioni nel software della tabella delle pagine.
8.3 Verso l’efficiente traduzione degli indirizzi
La maggior parte dei meccanismi hardware che abbiamo descritto coinvolgono almeno due possibili e forse anche quattro riferimenti extra a memoria, in ogni istruzione, prima di ricercare la locazione fisica di memoria. Dovrebbe sembrare completamente impraticabile per un processore fare molte lookup in memoria per ogni fetch di istruzione, e ancora di più per istruzioni di tipo load o store.
In questa sezione, discuteremo di come migliore la performance della traduzione degli indirizzi senza cambiare il comportamento logico. In altre parole, ogni indirizzo virtuale è tradotto esattamente alla stessa locazione fisica di memoria, e ogni eccezione sui permessi causa una trap, esattamente come sarebbe successo senza l’ottimazione sulla performance.
Per questo motivo useremo delle cache, una copia di qualche dato che può essere acceduto molto più velocemente dell’originale. In questa sezione ci contreremo su come utilizzare le cache per migliorare la performance di traduzione. Le cache sono largamente usate nei pc, sistemi operativi, sistemi distribuiti, e molti altri sistemi; nel prossimo capitolo, discuteremo più genericamente quando le cache lavorano e quando no. Per adesso, tuttavia, ci focalizzeremo sulle cache per ridurre l’overhead per la traduzione degli indirizzi. C’è una ragione per questo: Le prime cache hardware sono usate per migliorare la performance di traduzione
8.3.1 Translation Lookaside Buffer
Se stai pensando su come il processore esegue le istruzioni con la traduzione degli indirizzi, ci sono molte vie per migliorare tale performance. Dopo tutto, il processore normalmente esegue le istruzioni in sequenza:
…..
add r1,r2,r
mul r1,r2,r
….
L’hardware per prima tradurrà il program counter per l’istruzione di add, camminando nella tabella di traduzione multi-livello per trovare la memoria fisica dove l’istruzione di add è memorizzata. Quando il PC è incrementato, il processore deve camminare tra i livelli multipli di nuovo per trovare la memoria fisica dove l’istruzione di mul è memorizzata. Se due istruzioni sono nella stesso spazio di indirizzamento di una pagina, allora dovrebbero essere nella stessa pagina in memoria fisica. Il processore ripeterà questo lavoro per tutte le istruzioni
Una translation lookaside buffer (TLB) è un tabella hardware piccola che contiene i risultati delle traduzioni degli indirizzi recenti. Ogni entrata nella TLB mappa una pagina virtuale ad una pagina fisica come nel codice sottostante:
TLB entry = {
virtual page number,
physical page frame number,