Appendice 4 - Siti personali
Transcript
Appendice 4 - Siti personali
Ap p u n t i d i Ca l co l a t o ri El et t ro n i ci Appendice 4 – La memoria L A MEMORIA CACHE ........................................................................................................... 1 Introduzione .................................................................................................................. 1 Il principio di localizzazione ........................................................................................... 2 Organizzazioni delle memorie cache ................................................................................ 3 Gestione delle scritture in una cache ............................................................................... 8 L A MEMORIA VIRTUALE ...................................................................................................... 9 Introduzione .................................................................................................................. 9 La paginazione............................................................................................................... 9 Meccanismi di paginazione ........................................................................................... 12 La paginazione su richiesta........................................................................................... 16 Politiche per la sostituzione delle pagine ....................................................................... 17 La dimensione delle pagine e la frammentazione ............................................................ 18 L hee orriiaa ccaacch mo meem Laa m Introduzione Storicamente, le CPU sono state sempre più veloci delle memorie: quando le memorie sono migliorate, lo hanno fatto anche le CPU, mantenendo la distanza. Ciò comporta, in concreto, una conseguenza importante: dopo che la CPU ha emesso una richiesta di memoria, deve rimanere inattiva per un tempo rilevante, aspettando la risposta dalla memoria. Tipicamente, la richiesta della CPU avviene in un ciclo di clock e poi devono passare due o tre cicli successivi prima di ottenere i dati. Questo rallenta troppo l’esecuzione complessiva, per cui è il caso di porvi rimedio. E’ importante capire che il “problema” è di natura economica e non tecnologia: infatti, gli ingegneri sanno bene come costruire memorie veloci quanto le CPU, ma sanno anche che esse sarebbero molto costose, tanto che dotare un calcolatore di anche un solo megabyte di tali memorie sarebbe improponibile. Per questo motivo, si è scelto di avere solo una piccola quantità di memoria veloce (eventualmente espandibile a cura dell’utente) ed una grande quantità di memoria lenta. Esistono varie tecniche per combinare una piccola quantità di memoria veloce con una grande quantità di memoria lenta, in modo da ottenere, allo stesso tempo e con un costo moderato, (quasi) la velocità di quella veloce e la capacità di quella grande. La memoria piccola e veloce è detta cache (che deriva dal francese “cacher”, ossia “nascondere”) ed è sotto il controllo del microprogramma. Vogliamo allora descrivere come si usano e come funzionano le memorie cache. Appunti di “Calcolatori Elettronici” – Appendice 4 Il principio di localizzazione Ormai da diverso tempo è stato accertato che i programmi non accedono al loro codice in modo completamente casuale: se capita un riferimento di memoria ad un dato indirizzo A, è molto probabile che il riferimento successivo sia nelle vicinanze di A. Ne è una prova lo stesso programma: • ad eccezione dei salti e delle chiamate di procedura, le istruzioni sono prelevate da locazioni consecutive in memoria; • inoltre, la maggior parte del tempo di esecuzione di un programma viene usata per i cicli, in cui quindi vengono eseguite un numero limitato di istruzioni in maniera ripetitiva. Questa osservazione per cui i riferimenti alla memoria, fatti in un intervallo breve, tendono ad usare una piccola frazione della memoria totale è noto come principio di localizzazione ed è alla base di tutti i sistemi di cache. L’idea generale è la seguente: quando viene fatto un riferimento ad una parola, questa viene trasferita dalla memoria grande e lenta nella cache, in modo da essere accessibile velocemente per le eventuali e probabili successive utilizzazioni. Una architettura molto usata della CPU, della cache e della memoria centrale è illustrata nella figura seguente: Piastra della CPU Piastra di Memoria CPU Memoria piccola, veloce e costosa Memoria centrale Memoria grande, lenta ed economica Memoria Cache BUS Architettura tipica di un calcolatore con memoria cache: quest’ultima è generalmente localizzata sulla piastra della CPU Per renderci conto dell’opportunità di usare una cache, possiamo fare il seguente semplicissimo discorso: supponiamo che una parola venga letta o scritta k volte in un breve intervallo, durante l’esecuzione di un dato programma; se tale parola si trova inizialmente nella memoria centrale e, alla prima richiesta, viene portata nella cache, il calcolatore avrà bisogno di un solo riferimento per la memoria lenta (il primo) e k-1 riferimenti per la memoria veloce. Quanto maggiore è k (ossia, sostanzialmente, quanto più risulta verificato il principio di localizzazione), tanto migliori saranno le prestazioni globali, proprio perché tanto maggiore è il numero di volte in cui viene coinvolta la memoria veloce al posto della memoria lenta. Possiamo formalizzare ulteriormente questo calcolo, introducendo due tempi: tempo di accesso alla cache (che indichiamo con C) e tempo di accesso alla Autore: Sandro Petrizzelli 2 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale memoria centrale (che indichiamo con M). Sulla base di questi tempi, possiamo calcolare il cosiddetto rapporto di successo (simbolo H), ossia la frazione di tutti i riferimenti che si possono soddisfare con la cache: nell’esempio che abbiamo fatto poco fa, risulta evidentemente K −1 K H= Il duale di questa quantità è il cosiddetto rapporto di fallimento, pari evidentemente a 1− H = 1− K −1 1 = K K Possiamo inoltre calcolare il tempo medio di accesso alla generica parola, nel modo seguente: tempo medio di accesso = C + (1 − H) × M = C + M K Questa formula esprime chiaramente il fatto che il tempo medio di accesso è la somma del tempo di accesso alla cache e del tempo di accesso alla memoria, pesato però quest’ultimo per (1-H) o, ciò che è lo stesso, per 1/K. Di conseguenza, quanto più H tende ad 1, tanto più i riferimenti a memoria possono essere soddisfatti dalla cache e quindi il tempo totale di accesso si avvicina a quello della cache. Al contrario, se H→0, è quasi sempre necessario un riferimento alla memoria centrale, per cui il tempo di accesso si avvicina adesso a C+M: il tempo iniziale C serve a controllare che la parola richiesta sia in cache e, una volta verificato che non è così , serve un tempo M per accedere alla memoria centrale. In alcuni sistemi, per velocizzare il funzionamento, si possono cominciare in parallelo l’accesso alla memoria e la ricerca nella cache: in tal modo, se la ricerca nella cache è infruttuosa, il ciclo di memoria è stato già attivato. Tuttavia, una strategia di questo tipo richiede che si possa fermare la ricerca in memoria quando si ha successo (in inglese hit) nella cache, il che rende l’hardware più complicato. In ogni caso, l’algoritmo di base per cercare nella cache e cominciare (o fermare) l’accesso alla memoria centrale, a seconda del risultato della ricerca nella cache, è gestito dal microprogramma. Organizzazioni delle memorie cache Si usano due diverse organizzazioni fondamentali della cache, oltre ad una terza che è una forma di “ibrido” delle prime due. Supponiamo, per tutti i tre tipi, che la memoria principale sia di 2 m byte (dove m è quindi la lunghezza degli indirizzi di memoria) e che essa sia divisa (solo concettualmente) in blocchi consecutivi, ciascuno di b byte, per un totale chiaramente di 2 m /b blocchi. Così facendo, ogni blocco ha un indirizzo che è un multiplo di b (dove b è generalmente una potenza di 2). Il primo tipo di cache è quella cosiddetta completamente associativa, composta da un certo numero di slot (detti anche linee), ciascuno dei quali contiene tre elementi: un blocco, il numero identificativo di tale blocco ed un bit (chiamato Valid) che indica se lo slot è attualmente in uso oppure no. La figura seguente illustra un esempio di questa organizzazione: aggiornamento: 6 luglio 2001 3 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 Memoria principale Numero di blocco Indirizzo 0 137 0 52 1 1410 2 635 3 4 8 12 Memoria cache Valid Numero di blocco 1 0 137 1 600 2131 1 2 1410 0 1 16 ... ... Valore 160248 90380 .... ..... 22 bit 32 bit 1024 slot 0 .. ... 224 224 1 bit Esempio di schema di cache completamente associativa con 1024 slot (affiancata ad una memoria centrale con blocchi da 4 byte) A sinistra è riportato un “pezzo” di memoria principale, suddivisa in blocchi da 4 byte ciascuno; a destra è invece riportato un “pezzo” di una memoria cache completamente associativa, in cui ogni slot ha la struttura descritta prima. Si considera, in particolare, una cache con 1024 slot ed una memoria di 2 24 byte divisi in 2 22 blocchi da 4 byte ciascuno. Nella cache completamente associativa, l’ordine degli elementi è casuale. Quando il calcolatore viene inizializzato, tutti i bit Valid sono posti a 0, in modo da indicare che, al momento, non ci sono elementi della cache “validi”, cioè utilizzati. Supponiamo adesso che la prima istruzione del programma in esecuzione si riferisca alla parola di 32 bit situata all’indirizzo 0: • il microprogramma controlla tutti gli elementi della cache, cercandone uno valido contenente il blocco numero 0; • non trovando il suddetto blocco (la cache è vuota), emette una richiesta sul bus, per prelevare la parola 0 dalla memoria; • sistema quindi tale parola nel blocco 0 e rende valido quest’ultimo. A questo punto, se la parola dovesse risultare nuovamente necessaria, potrà essere prelevata direttamente dalla cache, eliminando la necessità di una operazione sul bus. Chiaramente, con il procedere dell’esecuzione, un numero sempre crescente di elementi della cache risulteranno contrassegnati come validi. Nel caso in cui il programma usi meno di 1024 parole per il programma e per i dati, si verifica la situazione ideale, dato che tutto il programma ed i suoi dati appariranno alla fine nella cache, per cui esso sarà eseguito ad alta velocità, senza bisogno di accessi alla memoria centrale attraverso il bus. E’ chiaro, però, che questa è una situazione poco realistica; è più probabile, invece, che il programma ed i suoi dati richiedano più di 1024 parole: in questo caso, si arriverà ad un certo punto in cui tutta la cache risulterà piena e bisognerà eliminare vecchi elementi per far spazio a quelli nuovi. La decisione di quale elemento eliminare è di importanza critica e deve tra l’altro essere presa molto velocemente (pochi nanosecondi): alcune macchine Autore: Sandro Petrizzelli 4 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale prendono uno slot a caso, mentre invece generalmente sono usati algoritmi più sofisticati. La caratteristica importante della cache completamente associativa è che ogni slot contiene il numero identificativo del blocco contenuto oltre al blocco stesso. Quando si presenta un indirizzo di memoria, il microprogramma deve compiere quindi due operazioni: • calcolare il numero del blocco corrispondente all’indirizzo specificato; • controllare l’elemento della cache che ha quel numero di blocco, se presente. Mentre la prima operazione è molto semplice, la seconda non lo è affatto. Per evitare una ricerca sequenziale sulla cache, quest’ultima possiede un hardware speciale, che confronta ogni elemento simultaneamente con il numero di blocco calcolato. Questo evita di usare un ciclo del microprogramma, ma rende anche più costosa la cache. Per ridurre tale costo, fu allora inventato un diverso tipo di cache, detta a mappa diretta (o anche ad indirizzamento diretto): in questo caso, si evita la fase di ricerca del blocco, mettendo ogni blocco in uno slot il cui numero può essere facilmente calcolato dal numero del blocco. In altre parole, dato un blocco, esiste un unico slot della cache in cui può essere sistemato e quindi il controllo sulla presenza o meno del blocco in cache andrà effettuato su quell’unico slot. Il numero di tale slot può essere ad esempio calcolato nel modo seguente: si fa la divisione tra il numero del blocco ed il numero degli slot e si considera il resto di tale divisione. Brevemente, si scrive (numero del blocco) modulo (numero di slot) Ad esempio, con blocchi di 4 byte (cioè una parola) e con 1024 slot nella memoria, il numero dello slot per la parola che si trova all’indirizzo A è A modulo 1024 4 dove ovviamente A/4 è il numero del blocco in cui si trova la parola il cui indirizzo è A. Nel nostro esempio, lo slot 0 della cache è destinato alle parole che hanno indirizzo 0, 4096, 8192, 12288 e così via; lo slot 1 è invece destinato alle parole che hanno indirizzo 4, 4100, 8196, 12292 e così via. La cache a mappa diretta elimina dunque il problema della ricerca di un blocco, ma crea un nuovo problema: bisogna infatti capire come indicare quale, delle diverse parole che corrispondono ad un dato slot, lo stia effettivamente occupando. Ad esempio, poco fa abbiamo detto che lo slot 0 può accogliere le parole ad indirizzo 0, 4096, 8192, 12288 e così via. Il modo per indicare quale di queste sia effettivamente presente nello slot è di porre una parte dell’indirizzo di tali parole nella cache, introducendo un apposito campo Tag (indicatore): tale campo viene così a contenere quella parte di indirizzo che non può essere calcolata partendo dal numero dello slot. Facciamo un esempio: • consideriamo una istruzione all’indirizzo 8192 e supponiamo che essa sposti una parola dall’indirizzo 4100 all’indirizzo 12296; aggiornamento: 6 luglio 2001 5 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 • il numero del blocco corrispondente a 8192 si ottiene dividendo per 4 (ossia per la dimensione dei blocchi nel nostro esempio): si ottiene 2048; • dal numero del blocco possiamo poi risalire al numero dello slot che lo può accogliere: facendo 2048 modulo 1024, si ottiene 0. Lo stesso risultato si sarebbe ottenuto usando i 10 bit meno significativi del numero 2048, ossia del numero del blocco. Essendo tale numero di 22 bit, i restanti 12 bit (che in questo caso contengono il valore 2) andranno a costituire il tag. Nella figura seguente è riportata la cache dopo che tutti e tre gli indirizzi sono stati elaborati nel modo appena descritto: Valid Indicatore Valore 0 1 2 12130 1 1 1 170 2 1 3 2142 3 0 4 0 5 0 .. .. .... ..... 1023 1 Calcolo dello slot e dell'indicatore partendo da un indirizzo di 24 bit Indicatore Slot 00 Esempio di cache a mappa diretta, con 1024 slot di 4 byte ciascuno. Nella parte inferiore è mostrato il procedimento di calcolo dello slot e dell’indicatore partendo da un indirizzo di 24 bit Questa figura mostra anche (nella parte inferiore) come si divide l’indirizzo di memoria (da 24 bit, avendo supposto una memoria da 2 24 byte): • i due bit meno significativi sono sempre a 0, dato che la cache lavora sempre con blocchi interi e questi sono multipli della dimensione del blocco, che nel nostro esempio è di 4 byte; • segue il numero di slot da 10 bit; • infine, il numero dell’indicatore (12 bit) preso direttamente dalla cache. Una soluzione di questo tipo è anche facilmente implementabile, dato che si può agevolmente costruire un hardware che estrae direttamente il numero dello slot e dell’indicatore da qualunque indirizzo di memoria. Il fatto che diversi blocchi di memoria possono far riferimento allo stesso slot della cache può comunque provocare dei problemi. Ad esempio, supponiamo che una istruzione di spostamento sposti una parola dall’indirizzo 4100 all’indirizzo 12292 (al posto di 12296 come supposto prima); entrambi questi indirizzi si riferiscono allo slot 1: infatti, 12292/4 fornisce il numero di blocco 3073 e questo numero, diviso per 1024 slot, fornisce un resto pari a 1. In questa situazione, a seconda della microprogrammazione, l’ultimo calcolato finirà nella cache, mentre l’altro verrà eliminato; non si tratta ovviamente di un evento disastroso, ma esso Autore: Sandro Petrizzelli 6 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale contribuisce comunque a rallentare le prestazioni, tanto più quante più sono le parole in uso che si riferiscono allo stesso slot. Per ovviare a questo problema, si può adottare una estensione della cache a mappa diretta, tesa a poter memorizzare più blocchi in ciascuno slot. Si ottiene così la cache associativa ad insiemi, detta anche cache set-associativa ad N vie, dove N sono i blocchi memorizzabili in ciascuno slot: Slot Valid Indicatore 0 1 2 3 4 5 6 7 8 ... Valore Valid Indicatore Valore Valid Indicatore Valore Esempio di cache set associativa a 3 vie (3 blocchi per ogni slot) In effetti, sia la cache completamente associativa sia quella ad indirizzamento diretto sono casi particolari della cache set-associativa: • se usiamo un unico slot, tutti gli elementi della cache si trovano nello stesso slot e dobbiamo perciò distinguerli con indicatori, dato che fanno riferimento allo stesso indirizzo. Otteniamo perciò nuovamente una cache completamente associativa; • se invece prendiamo N=1 (1 blocco per ogni slot), allora abbiamo nuovamente la cache ad indirizzamento diretto. Dal punto di vista di vantaggi e vantaggi dei vari tipi di cache, possiamo citare i seguenti: • le cache ad indirizzamento diretto sono semplici ed economiche da costruire e presentano inoltre un tempo di accesso minore, dato che lo slot corrispondente si può trovare semplicemente indicizzando la cache (usando una porzione dell’indirizzo come indice); • al contrario, la cache associativa ha un numero maggiore di successi per un dato numero di slot, in quanto non si verificano mai conflitti: non può infatti succedere che k parole importanti non si possano mettere nella cache contemporaneamente perché hanno la sfortuna di far riferimento allo stesso slot. Il progettista deve decidere non solo il numero degli slot, ma anche la dimensione dei blocchi. Nei nostri esempi, abbiamo sempre usato blocchi da 4 byte, ma sono usate anche altre dimensioni. Il vantaggio di usare una grande dimensione per i blocchi è che risulta meno gravoso prelevare un blocco di 8 parole rispetto ad 8 blocchi ciascuno di 1 parola, specialmente se il bus permette il trasferimento di blocchi. C’è però anche lo svantaggio che non tutte le parole di un dato blocco possono essere necessarie, nel qual caso parte del prelevamento è sprecato. aggiornamento: 6 luglio 2001 7 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 Gestione delle scritture in una cache Un altro importante problema, nella progettazione di una cache, è quello della gestione delle scritture. Si usano solitamente due strategie: • nella prima, detta scrittura in avanti, quando si scrive una parola nella cache, la si registra immediatamente anche nella memoria. In tal modo, si assicura che gli elementi della cache siano sempre uguali ai corrispondenti elementi della memoria; • nella seconda, detta invece copia all’indietro, la memoria non viene aggiornata tutte le volte che la cache viene alterata, ma solo quando bisogna eliminare un elemento dalla cache per far posto ad un altro. In questo caso, è chiaramente necessario un bit per ogni elemento della cache, che indichi se l’elemento è stato modificato oppure no durante la sua permanenza in cache: solo in caso affermativo, prima di eliminarlo esso andrà registrato in memoria. Ci sono una serie di conseguenze legate ad una ed all’altra scelta: • la scrittura in avanti provoca evidentemente un maggiore traffico nel bus rispetto alla copia all’indietro; • viceversa, se una CPU comincia un trasferimento di I/O dalla memoria al disco e la memoria non è “aggiornata”, allora dati non corretti saranno scritti sul disco. E’ ovvio che una simile evenienza viene comunque sempre evitata, al prezzo però di una maggiore complessità del sistema; • se il rapporto tra letture e scritture risulta molto alto, può essere più conveniente l’uso della scrittura in avanti, tollerando il traffico sul bus, che comunque subirebbe dei picchi saltuari (in occasione appunto delle scritture). Se invece ci sono molte scritture, allora il traffico potrebbe essere eccessivo e sarebbe perciò preferibile la scrittura all’indietro; non solo, ma sarebbe anche opportuno che il microprogramma “pulisca” l’intera cache (ossia la copi interamente nella memoria) prima di una eventuale operazione di I/O. Autore: Sandro Petrizzelli 8 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale L Laa m meem mo orriiaa vviirrttu uaallee Introduzione All’inizio dell’era dei calcolatori, le memorie erano piccole e care; di conseguenza, i programmatori passavano buona parte del loro tempo cercando di comprimere i propri programmi in memorie piccolissime; spesso, era perfino necessario usare un algoritmo che lavorava molto più lentamente di un altro, semplicemente perché l’altro era troppo grande per essere conservato nella memoria del calcolatore. La soluzione tradizionale a questo problema fu, inizialmente, quella di servirsi, oltre che della memoria principale, anche di una memoria secondaria, ad esempio un disco: • il programmatore divideva il programma in diversi “pezzi”, chiamati overlay, ognuno dei quali poteva stare in memoria; • per eseguire il programma, si inseriva il primo overlay e lo si eseguiva; quando era finito, si caricava l’overlay successivo e lo si eseguiva e così via. Il problema era che, comunque, era sempre il programmatore a dover dividere il programma in overlay e a decidere dove dovesse essere tenuto ogni overlay nella memoria secondaria; il programmatore doveva anche curare gli spostamenti degli overlay tra memoria secondaria e memoria principale, senza alcun aiuto da parte del calcolatore. E’ ovvio come questa tecnica richiedesse un lavoro eccessivo e noioso da parte del programmatore, per cui, verso il 1960, fu ideato un metodo per l’esecuzione automatica della gestione degli overlay, senza che il programmatore sapesse neppure cosa stesse succedendo. Questo metodo, che ora prende il nome di memoria virtuale, libera dunque il programmatore da un grosso lavoro di tipo “amministrativo”. Al giorno d’oggi, praticamente tutti i microprocessori presentano sistemi più o meno sofisticati di memoria virtuale. La paginazione L’idea alla base del metodo della memoria virtuale è quella di separare due concetti: lo spazio di indirizzamento e le locazioni di memoria. Per spiegarci meglio, utilizziamo un esempio concreto. Consideriamo un calcolatore nelle cui istruzioni il campo di indirizzamento sia di 16 bit: questo significa che un programma eseguito su questo calcolatore può indirizzare 2 16 =65536 parole di memoria (che costituiscono il cosiddetto spazio di indirizzamento) e questo a prescindere dalla quantità di memoria a disposizione, dato che il numero di parole indirizzabili dipende appunto dal numero di bit utilizzabili per gli indirizzi e non dal numero di parole effettivamente disponibili. Supponiamo, ad esempio, che il calcolatore disponga di una memoria di appena 4096 parole. Prima dell’invenzione della memoria virtuale, sarebbe stata necessaria una distinzione, all’interno dello spazio di indirizzamento, tra gli indirizzi al di sotto di 4096 (da 0 a 4095) e quelli uguali a superiori a 4096: i primi costituiscono lo spazio di indirizzamento utile, dato che corrispondono a locazioni di memoria aggiornamento: 6 luglio 2001 9 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 effettivamente disponibili, mentre gli altri costituiscono uno spazio indirizzamento inutile, visto che non corrispondono a locazioni disponibili. di Spazio di indirizzamento da 64 K Indirizzi 0 Spazio di indirizzamento utile 4095 Memoria centrale da 4 K Spazio di indirizzamento inutile 65535 Suddivisione dello spazio di indirizzamento totale in parte “utile” e parte “inutile” L’idea di separare lo spazio di indirizzamento e gli indirizzi di memoria è la seguente: • avendo a disposizione 4096 parole di memoria effettiva, siamo certi che, in qualunque istante, possiamo accedere a 4096 parole di memoria; • tuttavia, non necessariamente queste parole effettive devono corrispondente agli indirizzi compresi tra 0 e 4095: ad esempio, potremmo “imporre” al calcolatore che, da un certo momento in poi, tutte le volte che si usa l’indirizzo 4096, deve essere usata la parola 0 della memoria; quando si usa l’indirizzo 4097 dovrà essere usata la parola 1 della memoria e così via, fino all’indirizzo 8191, al quale dovrà corrispondere la parola 4095 della memoria. Si realizza, perciò, una corrispondenza uno-ad-uno del tipo schematizzato nella figura seguente: Spazio di indirizzamento da 64 K Indirizzi 4096 Memoria centrale da 4 K 8191 65535 Corrispondenza tra indirizzi dello spazio di indirizzamento e indirizzi della memoria centrale Autore: Sandro Petrizzelli 10 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale In termini molto semplici, possiamo affermare che è stata definita una funzione dallo spazio di indirizzamento sugli indirizzi di memoria effettivi: ogni indirizzo nello spazio di indirizzamento viene “tradotto” in un corrispondente indirizzo fisico, ossia indirizzo della memoria centrale effettivamente disponibile. Supponendo dunque fissata questa particolare “funzione” (o “corrispondenza” o “associazione”) tra indirizzi nello spazio di indirizzamento e indirizzi effettivi nella memoria centrale, supponiamo che un dato programma “salti” ad un indirizzo (nello spazio di indirizzamento) compreso tra 8192 e 12287. Che succede? Se la macchina non è dotata di memoria virtuale, il programma provoca un errore e viene stampato un messaggio del tipo “Riferimento a memoria inesistente”, dopodiché il programma termina. Il motivo è evidente: il calcolatore non ha alcuna corrispondenza tra gli indirizzi compresi tra 8192 e 12287 e gli indirizzi effettivi di memoria, per cui non sa dove trovare i dati o le istruzioni richieste dal programma e quindi non può che arrestarsi. Introducendo, invece, la memoria virtuale, si avvia il seguente processo: • il contenuto della memoria centrale viene salvato in una memoria secondaria (ad esempio un disco); • in questa stessa memoria secondaria vengono individuate le parole da 8192 a 12287; • le suddette parole vengono caricate nella memoria centrale; • la mappa degli indirizzi (ossia la predetta funzione che associa lo spazio di indirizzamento agli indirizzi effettivi) viene modificata, al fine di far corrispondente gli indirizzi da 8192 a 12287 alle locazioni della memoria centrale da 0 a 4095; • infine, l’esecuzione continua come se non fosse successo niente di insolito. In pratica, si è eseguito un qualcosa di simile ad uno “spostamento automatico di overlay”. Questa tecnica prende il nome di paginazione ed i pezzi di programma trasferiti dalla memoria secondaria in quella principale sono detti pagine. In effetti, quella appena descritta è una versione particolarmente semplificata della paginazione; è invece possibile un modo più sofisticato per far corrispondere lo spazio di indirizzamento con gli indirizzi effettivi di memoria, così come vedremo più avanti. Chiariamoci dunque sulla terminologia da adottare: • gli indirizzi a cui il programma può fare riferimento costituiscono il cosiddetto spazio di indirizzamento virtuale (di 64K nel caso di indirizzi a 16 bit); • gli indirizzi di memoria effettivamente cablati costituiscono invece lo spazio di indirizzamento fisico (di 4K nel nostro esempio); • la mappa di memoria è infine quella funzione che lega gli indirizzi virtuali con quelli fisici. Nei nostri discorsi, diamo ovviamente per scontato che la memoria secondaria abbia abbastanza spazio per memorizzare l’intero programma ed i suoi dati. Ogni programma viene scritto come se ci fosse abbastanza memoria centrale per l’intero spazio di indirizzamento virtuale, anche se non è così : i programmi possono quindi caricare o memorizzare una qualsiasi parola nello spazio di indirizzamento virtuale, così come possono saltare ad una qualsiasi istruzione localizzata in un aggiornamento: 6 luglio 2001 11 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 posto qualsiasi dello spazio di indirizzamento virtuale, senza preoccuparsi che, in realtà, non c’è abbastanza memoria fisica. In poche parole, il programmatore può scrivere programmi senza sapere se la memoria virtuale esiste, dando comunque per scontato che il calcolatore possieda memoria sufficiente ( 1). La paginazione dà al programmatore l’illusione di una grande memoria centrale, della stessa dimensione dello spazio di indirizzamento, anche se in pratica la memoria centrale disponibile può essere più piccola (o anche più grande) dello spazio di indirizzamento. La “simulazione” di questa grande memoria centrale, garantita dalla paginazione, è trasparente al programma (ed al programmatore), il quale cioè non si accorge della sua presenza (a meno di non eseguire test particolari): tutte le volte che si fa riferimento ad un indirizzo, la corrispondente parola sembra comunque sempre presente. Meccanismi di paginazione Un requisito essenziale, per una memoria virtuale, è che il programma completo da eseguire sia interamente contenuto nella memoria secondaria. Anche se la realtà è quella per cui i “pezzi” di programma originale si trovano nella memoria principale, mentre le “copie” si trovano nella memoria secondaria, risulta comunque più semplice pensare che avvenga il viceversa, ossia che il programma nella memoria secondaria sia l’originale, mentre i pezzi portati nella memoria centrale di tanto in tanto siano delle copie. L’importante è tener presente quello che avviene nella realtà e, soprattutto, mantenere sempre aggiornato l’originale (in base all’esecuzione che se ne sta facendo): quando si operano dei cambiamenti alla copia nella memoria centrale, questi devono essere riprodotti anche nell’originale ( 2). Dato lo spazio di indirizzamento virtuale, lo si divide in un certo numero di pagine, tutte di uguale dimensione (che deve essere una potenza di 2). Ad esempio, fino a poco tempo fa erano comuni pagine da 4096 indirizzi ciascuna. Nella prossima figura, ad esempio, è riportata la suddivisione di uno spazio di indirizzamento virtuale di 64K in 16 pagine (da 0 a 15) da 4K ciascuna e con 4096 indirizzi ciascuna. Evidentemente, anche lo spazio di indirizzamento fisico (la memoria centrale) viene diviso in modo simile, con ogni pezzo (detto page frame) che ha la stessa dimensione di una pagina, in modo tale che ogni pezzo di memoria centrale sia capace di contenere esattamente una pagina. In questo esempio, si considera in particolare una memoria principale da 32K, divisa perciò in 8 page frame, numerati da 0 a 7 ( 3). Nei calcolatori reali, il numero di page frame nella memoria centrale va da poche decine fino ad alcune migliaia nelle macchine più grosse. 1 Quest’ultimo concetto è fondamentale, in quanto contrapposto a quello che si usa nella cosiddetta segmentazione, in cui invece il programmatore deve essere a conoscenza dell’esistenza dei segmenti. 2 Non è detto, comunque, che l’aggiornamento nella memoria secondaria debba essere effettuato subito. 3 Nel precedente esempio, invece, la memoria centrale era da 4 K, per cui conteneva un unico page frame. Autore: Sandro Petrizzelli 12 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale Spazio di indirizzamento da 64 K Memoria centrale da 32 K 0 4096 8192 12288 16384 20480 24576 28672 32768 36864 40960 45056 49152 53248 57344 61440 0 Pagina 0 4096 Pagina 1 8192 Pagina 2 12288 Pagina 3 16384 Pagina 4 20480 Pagina 5 24576 Pagina 6 28672 Pagina 7 Page Frame 0 Page Frame 1 Page Frame 2 Page Frame 3 Page Frame 4 Page Frame 5 Page Frame 6 Page Frame 7 Pagina 8 Pagina 9 Pagina 10 Pagina 11 Pagina 12 Pagina 13 Pagina 14 Pagina 15 La memoria virtuale del nostro esempio sarebbe implementata, al livello 2 (livello della macchina standard), per mezzo di una tabella delle pagine, contenente 16 elementi (tanti quante sono le pagine). Il meccanismo della paginazione funziona allora nel modo seguente: 4 • quando il programma tenta di accedere alla memoria per prelevare dati o memorizzarli oppure per prelevare istruzioni o saltare, genera dapprima un indirizzo a 16 bit corrispondente ad un indirizzo virtuale compreso tra 0 e 65535 ( 4); • tale indirizzo virtuale a 16 bit può essere interpretato in vari modi; in questo nostro esempio, supponiamo che esso venga visto come diviso in due parti: i primi 4 bit corrispondono al numero di pagine virtuale (da 0 a 15), mentre i restanti 12 bit individuano una locazione all’interno della pagina selezionata. Ad esempio, nella figura seguente si considera un indirizzo di valore 12310: separando i primi 4 bit dai restanti 12, si individua la pagina 3 e, in essa, l’indirizzo 22: Per generare questo indirizzi si possono usare gli indici, l’indirizzamento indiretto e tutte le altre tecniche comuni. aggiornamento: 6 luglio 2001 13 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 0 Pagina 0 0-4095 Pagina 1 4096-8191 Pagina 2 8192-12287 Indirizzo virtuale a 16 bit Pagina 3 12288-16383 4 bit (numero di pagina virtuale) 12 bit (indirizzo entro la pagina virtuale) Pagina 4 16384-20479 Pagina 5 ... Pagina 6 ... Pagina 7 ... Pagina 8 ... Pagina 9 ... Pagina 10 ... Pagina 11 ... Pagina 12 ... Pagina 13 53248-57343 Pagina 14 57344-61439 Pagina 15 61440-65535 0 1 1 0 0 0 0 0 0 0 1 0 1 1 0 Numero di pagina: 3 Indirizzo entro la pagina virtuale: 22 Possibile struttura di un indirizzo virtuale a 16 bit Da notare che l’indirizzo virtuale 22, all’interno della pagina 3, corrisponde all’indirizzo fisico 12310 (=12288+22); 5 • quando il sistema operativo si accorge che il programma richiede la pagina virtuale 3, deve scoprire dove essa è collocata. Ci sono allora, nel nostro esempio, 9 distinte possibilità: le prime 8 sono relative alla posizione della pagina virtuale richiesta in memoria centrale (dato che quest’ultima è costituita da 8 page frame), mentre l’ultima possibilità è che la pagina virtuale non si trovi nella memoria principale, bensì nella memoria secondaria (dato che non necessariamente tutte le pagine virtuali possono stare contemporaneamente nella memoria centrale). Per capire quale possibilità risulta verificata, il sistema operativo legge il contenuto della tabella delle pagine: ogni elemento di tale tabella corrisponde ad una precisa pagina virtuale e ne indica l’attuale posizione; • una possibile struttura della tabella delle pagine, sempre con riferimento al nostro esempio, è riportata nella prossima figura. Si suppone che ogni elemento della tabella abbia 3 campi: il primo campo è composto da un unico bit, il quale indica se la pagina virtuale si trova in memoria principale (valore 1) oppure no (valore 0); il secondo campo (da 12 bit) fornisce l’indirizzo di memoria secondaria in cui si trova la pagina virtuale ( 5), nel caso ovviamente in cui essa non si trovi nella memoria centrale; l’ultimo campo (4 bit), invece, indica il page frame della memoria centrale in cui si trova la pagina, qualora ovviamente essa non si trovi nella memoria secondaria. E’ ovvio che l’uso del secondo campo esclude quello del terzo campo e viceversa, a seconda del valore del primo campo. In particolare, vengono indicati la pista ed il settore del disco. Autore: Sandro Petrizzelli 14 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale Pagina 0 Pagina 1 Pagina 2 Pagina 3 Pagina 4 Generico elemento della tabella delle pagine Pagina 5 x 0 0 1 1 0 1 1 0 1 1 0 1 1 1 Pagina 6 0 Page frame Indirizzo nella memoria secondaria Pagina 7 Pagina 8 1 se la pagina è nella memoria centrale 0 se la pagina è nella memoria secondaria Pagina 9 Pagina 10 Pagina 11 Pagina 12 Pagina 13 Pagina 14 Pagina 15 La tabella delle pagine contiene tanti elementi quante sono le pagine virtuali. Ogni elemento è costituito, in questo esempio, da 3 campi, rispettivamente da 1 bit (flag), 12 bit (indirizzo di memoria secondaria) e 4 bit (pagine frame nella memoria principale) Supponiamo adesso che la pagina virtuale richiesta (la numero 3) si trovi nella memoria principale, per cui il primo bit dell’elemento corrispondente, nella tabella delle pagine, è posto ad 1. Gli ultimi 3 bit di questo stesso elemento indicano il page frame in cui è contenuta la pagina (si tratta del page frame 6 nell’ultima figura): tali 3 bit vengono allora posti nei 3 bit più a sinistra del MAR (Memory Address Register), mentre l’indirizzo all’interno della pagina virtuale, ossia i 12 bit più a destra dell’indirizzo virtuale originale, vengono posti nei 12 bit più a destra del MAR. In questo modo, viene formato un indirizzo di memoria centrale a 15 bit (adeguato perciò per una memoria fisica da 32K come quella presa in considerazione in questo esempio), così come mostrato nella figura seguente: Indirizzo virtuale a 16 bit 0 0 1 1 0 0 0 0 0 0 0 1 0 1 1 0 42444444 3 14243 144444 Tabella delle pagine 0 1 2 3 1 0 0 1 1 0 1 1 0 1 1 0 1 1 1 0 4 5 6 7 8 9 10 11 12 13 14 15 64748 1 1 644444 47444444 8 0 0 0 0 0 0 0 0 1 0 1 1 0 MAR Formazione di un indirizzo di memoria partendo da un indirizzo virtuale aggiornamento: 6 luglio 2001 15 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 A questo punto, l’hardware può usare l’indirizzo contenuto nel MAR per prelevare la parola desiderata e porla nel registro MBR (Memory Buffer Register) oppure, viceversa, può prelevare il contenuto del registro MBR e memorizzarlo all’indirizzo contenuto nel MAR. Possiamo ora osservare una cosa: se il sistema operativo dovesse convertire ogni indirizzo virtuale delle istruzioni di livello 3 in un indirizzo effettivo (secondo il meccanismo descritto), una macchina di livello 3 con memoria virtuale sarebbe molto più lenta rispetto a quella senza memoria virtuale, il che implicherebbe l’abbandono del concetto di memoria virtuale, proprio perché non foriero di benefici. Al contrario, per velocizzare il processo di “traduzione” degli indirizzi virtuali in indirizzi fisici, generalmente la tabella delle pagine viene memorizzata in registri speciali dell’hardware e la stessa “traduzione” viene effettuata direttamente nell’hardware. Una soluzione alternativa potrebbe essere invece quella di mantenere la tabella delle pagine nei registri veloci e di far fare la traduzione al microprogramma, per mezzo quindi di una programmazione esplicita: la maggiore o minore velocità di questa soluzione rispetto alla precedente dipende dall’architettura del livello della microprogrammazione, ma comunque essa ha il vantaggio di non richiedere circuiti speciali e modifiche dell’hardware. La paginazione su richiesta Nell’esempio considerato nel paragrafo precedente, abbiamo supposto che la pagina virtuale ricercata dal programma si trovasse nella memoria centrale; è chiaro che non necessariamente questo si verifica, in quanto alcune pagine virtuali potrebbero essere contenute nella memoria secondaria, a causa ad esempio dei limiti di capienza della memoria centrale. Quando la pagina virtuale richiesta non si trova nella memoria centrale, si dice che si è verificato un page fault (mancanza di pagina); in questa situazione, il sistema operativo deve compiere le seguenti operazioni: • leggere la pagina richiesta nella memoria secondaria; • trasferirla nella memoria principale; • aggiornare la tabella delle pagine con la nuova posizione della pagina richiesta; • ripetere l’esecuzione dell’istruzione che ha causato il page fault. Avendo una macchina dotata di memoria virtuale, è possibile iniziare un programma anche se nessuna parte del programma stesso si trova nella memoria centrale: basterà infatti inizializzare la tabella delle pagine per indicare che tutte le pagine virtuali si trovano nella memoria secondaria (primo bit a 0). Quando la CPU prova a prelevare la prima istruzione del programma, ottiene subito un page fault, il che implica che la pagina contenente tale istruzione venga caricata in memoria principale e ovviamente che il corrispondente elemento della tabella delle pagine venga aggiornato. A questo punto, l’esecuzione dell’istruzione può cominciare. Se, ad esempio, essa richiede due indirizzi in pagine diverse tra loro e diverse dall’unica pagina caricata in memoria principale, si hanno altri due page fault e quindi altri due trasferimenti dalla memoria secondaria a quella principale (con relativi aggiornamenti della tabella delle pagine). L’istruzione successiva potrà eventualmente causare uno o più altri page fault e così via. Autore: Sandro Petrizzelli 16 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale Questo modo di operare su una memoria virtuale prende il nome di paginazione su richiesta: le pagine vengono infatti caricate nella memoria principale solo se ne viene fatta esplicita richiesta. Chiaramente, dopo aver eseguito un certo numero di istruzioni del programma, ci saranno molte pagine virtuali caricati nella memoria principale e quindi i page fault saranno molto meno frequenti, tanto meno quanto più capiente è la memoria principale. Politiche per la sostituzione delle pagine Fino ad ora, abbiamo sempre supposto implicitamente che, dovendo caricare una pagina virtuale dalla memoria secondaria a quella centrale, ci sia un page frame libero disposto ad accoglierla. Generalmente, questo non succede (tutti i frame page sono occupati) ed è quindi necessario liberarne uno (cioè spostare il suo contenuto nella memoria secondaria) per ospitare la pagine virtuale richiesta dalla CPU. Bisogna allora implementare un algoritmo per la scelta del page frame da liberare, ossia quindi della pagina virtuale da “sostituire”. Il metodo più semplice è quello della scelta casuale, ma non si tratta decisamente del metodo migliore: infatti, se capita che venga scelta la pagina contenente la prossima istruzione da eseguire, si verificherà un altro page fault non appena la CPU cercherà di acquisire quella istruzione, con conseguente rallentamento dell’esecuzione. Di conseguenza, molti sistemi operativi tentano di individuare la pagina virtuale meno utile tra quelle presenti, ossia quella la cui mancanza avrebbe il minore effetto negativo sul programma in corso. Un modo abbastanza intuitivo di far questo consiste nell’individuare la pagina che con meno probabilità sarà usata a breve. Dal punto di vista pratico, questo lo si può ottenere individuando la pagina usata meno di recente, dato che la probabilità a priori che non debba essere usata è alta. Si parla allora di metodo Least Recently Used (LRB). Generalmente, questo metodo funziona bene, ma ci sono comunque situazioni “patologiche” in cui invece fallisce miseramente. Un altro algoritmo è quello cosiddetto First In First Out (FIFO): esso toglie la pagina caricata meno di recente, a prescindere da quando sia stata referenziata. Si procede allora nel modo seguente: • ad ogni page frame della memoria centrale viene associato un contatore, memorizzato nella tabella delle pagine; • inizialmente, tutti i contatori sono posti a 0; • dopo ogni operazione di caricamento per page fault, il contatore di tutte le pagine già presenti in memoria viene incrementato di uno, mentre invece quello della pagina appena introdotta è posto a 0; • in questo modo, quando è necessario scegliere la pagina da togliere, si prende quella con il contatore più alto, visto che è stata presente per il maggior numero di pagine e quindi è stata caricata prima di qualsiasi altra pagina nella memoria, per cui si spera abbia la maggiore probabilità a priori di non essere più necessaria. Facciamo osservare che, se la pagina che deve essere “sfrattata” dalla memoria principale non è stata modificata da quando è stata letta (cosa molto probabile se contiene parte del programma invece di dati), non è necessario riscriverla nella memoria secondaria, poiché ne esiste già una copia fedele e quindi è inutile aggiornamento: 6 luglio 2001 17 Autore: Sandro Petrizzelli Appunti di “Calcolatori Elettronici” – Appendice 4 “perdere tempo”. Se invece essa è stata modificata, allora la copia nella memoria secondaria non è fedele e quindi bisogna necessariamente riscriverla. Il modo più semplice per indicare se una pagina è stata modificata o meno nel tempo in cui è rimasta nella memoria principale, è quello di associarle un bit nella tabella delle pagine: il bit viene inizializzato a 0 quando la pagina viene caricata e viene posto ad 1 in corrispondenza della prima modifica. Esaminando questo bit, il sistema operativo può dunque capire se ci sono state modifiche (si parla di pagina sporca) o meno (si parla di pagina pulita) ed agire di conseguenza. E’ chiaramente auspicabile mantenere un alto rapporto tra pagine pulite e pagine sporche, in modo da minimizzare il numero di riscritture nella memoria secondaria. Quasi tutti i calcolatori sono comunque in grado di copiare le pagine dalla memoria centrale alla secondaria mentre la CPU sta lavorando, usando ad esempio il DMA (Accesso Diretto alla Memoria) o le apposite unità di canale. Addirittura, alcuni sistemi operativi approfittano di questo parallelismo tutte le volte che il disco è libero: viene individuata una pagina sporca, preferibilmente con alta probabilità di essere scritta a breve (perché vecchia), e ne viene imposta la copia sul disco, nonostante non ci sia stata ancora la necessità di togliere la suddetta pagina dalla memoria centrale. Naturalmente, può capitare che quella stessa pagina venga “risporcata” immediatamente dopo il procedimento di copiatura o addirittura durante: in questa situazione, se è vero che la “copia anticipata” è stata inutile, è anche vero che il costo di tale operazione è stato piccolo, dato che il disco era comunque inutilizzato e la CPU era comunque in grado di lavorare dopo aver imposto la copia. Le scritture su disco fatte con l’intenzione di “pulire” le pagine sporche sono chiamate scritture false. La dimensione delle pagine e la frammentazione Quando il programma utente ed i suoi dati riempiono totalmente un certo numero di pagine, non c’è spazio sprecato quando essi si trovano nella memoria centrale. Viceversa, se essi non riempiono esattamente tutte le pagine, c’è senz’altro dello spazio inutilizzato nell’ultima pagina. Questo “problema” va sotto il nome di frammentazione. Supponiamo che la dimensione di pagina sia di N parole: si può subito dire che la quantità media di spazio sprecato nell’ultima pagina è di N/2 parole; per ridurre tale quantità, bisogna evidentemente ridurre N, ossia fare le pagine più piccole. Ma fare pagine piccole significa fare più pagine e quindi anche ingrandire la tabella delle pagine: se tale tabella è gestita dall’hardware, il suo ingrandimento comporta un numero maggiore di registri per la sua memorizzazione e dei relativi circuiti, ossia quindi un costo maggiore del calcolatore. Non solo, ma sarà anche necessario più tempo per caricare o salvare tali registri tutte le volte che il programma viene fermato e poi fatto ripartire. Ancora, l’uso di pagine piccole rende inefficiente l’uso di memorie secondarie con lunghi tempi di accesso, come tipicamente i dischi: infatti, dato che bisogna aspettare 10 ms o più per accedere alla prima parola da trasferire e iniziare il trasferimento, è preferibile poter trasferire grossi blocchi di informazioni, dato che il tempo di trasferimento risulta generalmente minore della somma del tempo di ricerca delle parole e del tempo di rotazione del disco. Al contrario, se la memoria secondaria non presenta ritardo rotazionale (ad esempio una memoria a nuclei), il tempo di trasferimento è proporzionale solo alla dimensione del blocco e quindi sarebbe opportuno avere pagine piccole. Autore: Sandro Petrizzelli 18 aggiornamento: 6 luglio 2001 Compendio sulla memoria: memoria cache e memoria virtuale Tutte queste considerazioni mostrano che è sempre necessario trovare un compromesso, sulla base delle proprie esigenze e dell’hardware a disposizione, per scegliere una dimensione delle pagine ottimale. Una nota legge dell’informazione (legge di Amdahl) suggerisce che è sempre opportuno ottimizzare il caso più frequente: la scelta delle dimensioni delle pagine andrà perciò fatta in modo da ottimizzare le situazioni che si presentano con maggior frequenza, anche eventualmente a scapito di quelle meno frequenti. Autore: Sandro Petrizzelli e-mail: [email protected] sito personale: http://users.iol.it/sandry aggiornamento: 6 luglio 2001 19 Autore: Sandro Petrizzelli