Vai al contenuto

Utente:Miguel1983

Wikibooks, manuali e libri di testo liberi.
Interno di un hard disk

Due organizzazioni diverse rispetto al CD-ROM che è a spirale e assomiglia a disco in vinile. Qui tutte corone circolari tra loro distinte. Disco che ruota con testina che si muove lungo il raggio. Elemento ottico per dire questo è settore 0, corone circolare divise in record, che sono normalmente composte da 512 o 1024 byte con codice a ridondanza ciclico contro errori. C'è preambolo perché bisogna sincronizzare lettura.

Un file si compone di moltissimi record, che non sono tutti consecutivi su stessa corona o corone vicine, perché mano a mano che ci sono file di dimensioni diverse e c'è una lista (tabella) con tutti i record che lo compongono: famosa traccia 0 del disco. Corrispondenza tra file e posizione record (esempio. traccia 22, quindicesimo record), somma di tutti questi messi in ordine dà disco. Se vogliamo aumentare efficienza dobbiamo fare deframmentazione, in modo che tutti record di un file siano vicino. Così non dobbiamo aspettare tempo di accesso al settore. Capacità non formattata ci danno di solito, ma quando lo formattiamo lo suddividiamo in record, e in ogni record c'è pista di accelerazione e CRC. Formattazione costruisce la struttura dei vari record all'interno.

Ci sono diverse formattazioni (esempio LBA). Larghezza di corona è di circa un paio di micron (10-6). Dischi organizzati in cilindri perché testine si muovono in parallelo su più piatti. Quindi è come se avessimo un cilindro in cui si va a leggere a diverse altezze. Quindi se leggiamo tutti i piatti contemporaneamente ci permette di leggere un byte in parallelo, quindi tempo di trasferimento molto più basso. Una volta che ci siamo posizionati su una traccia, abbiamo il tempo di trasferimento che dipende da tempo rotazione del disco. Interessante un elemento importante: quanti megabyte vengono trasferiti al secondo? Diciamo tra i 20 e gli 80. Se io trasferisco 80 MByte al secondo, trasferisco un MByte ogni 12 nanosecondi. Un processore quando va molto forte va a 4 Ghz, vuol dire che c'è un clock ogni 250 picosecondi, ma a quella velocità non riesce a leggersi un byte alla volta, non gli sta dietro. Quindi dobbiamo avere strumento ad hoc che riesca a star dietro a queste frequenze. Processore non è proprio in grado di svolgerlo. Problema ovvio è che se numero di settori su disco è costante, avremo densità di registrazione al centro molto alta e bassa all'esterno.

Oggi ogni traccia ha numero diverso di settori, in maniera che velocità trasferimento e densità sia costante.

Sistema RAID-1. A1, A2, etc. sono replicati tra i due dischi, aumentando l'affidabilita e la velocità in lettura

Dischi che si usano per aumentare affidabilità o velocità e ci sono sei diverse tipologie di RAID (0, 1, 2 .. 5). Abbiamo descrizione dei vari RAID. Quello che si fa è suddividere i byte in più dischi e poi si raddoppiano dischi o se ne mettono uno in più che contiene parità o rilevazione di errore, in modo da individuare quale disco si è rotto e cercare in qualche maniera di ricostruire i byte. Oggi la tecnologia Winchester si chiama (prima casa Seagate).

Oggi praticamente tutti i dischi che ci sono in giro hanno questa tecnologia. Ma indipendentemente dalla tecnologia l'organizzazione dei dischi è rimasta sempre la stessa.

Possono esistere sul bus del processore più agenti. Ce ne sono quelli totalmente intelligenti in modo tale che possiamo avere architetture a multi-processore, oppure agenti a doppia faccia: talvolta slave, talvolta master. Per esempio un controllore intelligente di disco è un agente: io gli dico leggi il tal file, poi il processore si disinteressa e si aspetta che gli venga posizionato in un certo posto il file. Altro esempio sono i controllori video: per esempio per un quadrato il processore dice fai un quadrato di un certo spessore, in un certo centro, ecc .. e ci pensa il controllore a disegnarlo. Tutto questo per scaricare processore da compiti “ingrati”. 8259 è controllore interruzione, che sappiamo essere una chiamata a subroutine via hardware che ha come scopo quello di evitare che il processore rimanga in attesa del completamento di un evento: è invece l'evento stesso a scatenare “interesse” nel processore. Quindi dispositivo in maniera autonoma esegue compito assegnatogli da processore a volte ripetitivo o che a volte il processore non riesce proprio ad eseguire (come lettura disco) e poi segnala a processore esaurimento di questo compito.

Questo è slave in fase di programmazione e master quando deve trasferire dati perché deve accedere a memoria e deve impadronirsi del bus in certe fasi, e quindi mette fuori gioco il processore dal bus. Nel caso del disco il trasferimento è sincrono: se io scrivo dato su una stampante scrivo un dato alla volta e se per caso tra due dati aspetto un secondo, la stampante aspetterà ma non perdo nulla; ma nel caso del disco il disco continua a ruotare e i dati devono arrivare alla velocità a cui vuole il disco e non a quella che imponiamo noi.

Si chiama trasferimento sincrono quello che ha vincoli stringenti. Il fatto che sia un agente ad impossessarsi del bus diciamo che abbiamo architettura MULTIMASTER, cioè vuol dire generare indirizzi e segnali di controllo lettura/scrittura e generare e controllare tutti gli altri segnali che sono utili al trasferimento. Qui ci rifacciamo ad esempio dell'8088: in minimum mode c'è un ingresso HOLD e un'uscita HOLDA, questi tipici segnali a cui processore si rapporta con DMA controller. Sistema si chiama anche cycle-stealing perché processore durante quei clock è ibernato (in pratica non si accorge che tempo sta passando fino a quando segnale HOLD non si abbassa).

A differenza di interruzioni il protocollo HOLD, HOLDA non è condizionabile (non esiste eneable), processore termina e poi tira su le mani dal bus, nel momento in cui tolgo le mani dal bus genero HOLDA. Segnale HOLD non può essere inibito. Mettere processore in TRISTATE (“taglia” fili), quindi quei piedini verso il bus è come se fossero alla molla: segnali privi di potenziale.

Questi dispositivi sono dispositivi ancora oggi presenti nei PC, solo che anziché vederli come dispositivi a sé stanti noi li vediamo annegati nei chipset (in generale case produttrici processori non producono chipset). Parte di sinistra è la parte di interfacciamento verso processore e parte di destra è quella che colloquia con mondo esterno: esempio scanner, disco, stampante. Interfaccia verso processore è interfaccia classica:

  • CS*(chip select) che è l'individuazione da parte degli indirizzi che esattamente in quel momento si vuol utilizzare dispositivo
  • IORD* e IOWR*: freccia non solo entrante ma anche uscente perché dispositivo quando è programmato è slave (e quindi entra), ma quando master è lui a generare e pilotare
  • MEMRD* e MEMWR* solo in uscita perché hanno significato solo in master

essendo master problema di READY che deve colloquiare con dispositivi di velocità diversi indirizzi bidirezionali perché quando slave indirizzi entrano, quando master indirizzi vengono emessi. Indirizzi devono anche essere campionati all'esterno, quindi segnale equivalente al segnale ALE del'8088.

Bus multiplexato quando è master, cioè nella prima fase di indirizzamento emette sui piedini D0-D7 (la parte alta dell'indirizzo) lui è in grado di generare indirizzi a 16 bit, quindi abbiamo spazio di indirizzamento da 64 KByte. Nella prima fase genera su D0-D7 genera 8 bit più significativi dell'indirizzo e parte bassa emessi A0-A7. Dispositivo sincrono quindi segnale CLK, non lo stesso del processore. Ad esempio clock diviso per due del 8284 segnale di RESET segnali HOLD (verso il processore) e HOLDA (dal processore). Nel momento in cui riceve HOLDA sa che è padrone del bus.

Questo sa gestire 4 canali (4 dispositivi esterni) (direct memory access channel). Quando un dispositivo esterno ha bisogno di trasferire dati o perché è pronta a riceverli o pronto a scrivere o leggere. Quando è pronto attiva DREQ (0, 1, 2, 3). Processore risponde con DACK (0, 1, 2, 3). Possiamo considerarlo come gestore interruzioni via hardware che si sostituisce al processore. Ce ne sono 4 con priorità diverse, quindi se più richieste arrivano contemporaneamente la 0 + prioritaria di 1, che e + prioritaria di 2 ... Questi dispositivi possono essere messi in cascata. EOP* (end of process)

Diagramma temporale qualitativo: AEN lo lasciamo perdere .. Dopo HOLDA il DMA comincia ad emettere indirizzo di memoria coinvolto in quel particolare trasferimento (trasferire dati da memoria a I/O o viceversa o memoria/memoria). Emette segnale ADSTB (address strobe) e genera parte alta indirizzi sul suo bus dati, quindi progettista deve predisporre latch per campionare questa parte, mentre parte basse permanentemente emessa su A0-A7 (quindi non c è bisogno di campionamento). Viene generato DACK e poi dispositivo generare contemporaneamente il segnale IORD* e MEMWR* oppure l'altro. Quindi quello che il processore avrebbe fatto in due passi qui avviene insieme. Problema di emissione indirizzi in memoria e in I/O. Soluzione con segnale DACK, ricordando che indirizzamento I/O sempre + semplice che memoria. Indirizzo a 16 bit del processore sempre e comunque indirizzo di memoria coinvolto.

Notiamo che questo meccanismo accelera di molto i trasferimenti e da qui da il nome di canale ad accesso diretto perché fa transitare dati da I/O a memoria che in inglese si chiama fly-by (vola e gira intorno ai dispositivi, non passa attraverso nessun dispositivo di controllo). Nel momento in cui DMA controller emette indirizzi memoria devono essere attivati anche dispositivi di decodifica. Qui esempio trasferimento tra 8255 e memoria. Vediamo come si trasforma CS memoria quando abbiamo DMA. Quando è master processore e quando è master DMA, quindi c'è l'OR. Questa decodifica va bene quando non siamo in DMA, quindi quando HOLDA è a zero, se no decodifica non sarebbe valida. Ora notiamo che qui bisogna mettere decodifica completa indirizzo quando c'è DMA, ma nel caso specifico abbiamo supposto di avere unico dispositivo di memoria e unico canale, quindi è evidente che decodifica si riduce a DACK0. CS8255 è decodifica del 78H moltiplicato per HOLDA! perché è valida quando NON siamo in DMA. Quando siamo in DMA basta questo segnale per individuare che 8255 è selezionato. Utilizzando DACK0 abbiamo selezionato insieme memoria e 8255. 8255 non ha solo CS ma anche due segnali che selezionano la porta (00 porta A, 01 porta B, 10 porta C, 11 controllo). Quando siamo in DMA vogliamo leggere memoria e scrivere su 8255 dobbiamo definire porta. Porta B ha A0=1 e A1=0. Quindi quando siamo in DMA dobbiamo anche garantire che la cella di memoria sia generata, ma anche la porta che ci interessa. Quindi dobbiamo condizionare A0 e A1 che vanno nel 8255. Ogni canale è collegato ad una particolare coppia di dispositivi in I/O (e quindi in progettazione si individuano anche decodifiche); se ci mettessi MUX potrei andare ad avere diverse configurazioni.

CS_PERIFERICA: quando non siamo in DMA ci troviamo in una situazione in cui dipende dall'indirizzo emesso. Nel secondo termine HOLDA è inutile perché se c'è DACKi implica che HOLDA sia vero.

FLY-BY: dati passano direttamente da memoria a dispositivo senza essere depositati senza nessun registro intermedio; FLOW-THROUGH: deposito temporaneo, leggono dato in registro temporaneo e poi lo sparano fuori. Questo semplifica circuiteria esterna, ma rispetto a modalità FLY-BY è + lento, perché accesso al bus è doppio (in lettura e scrittura). Esiste particolare trasferimento anche da memoria a memoria. Questo era molto importante per evitare fenomeno frammentazione (ritagli di blocchi liberi inutilizzati perché non continui). Ogni trasferimento due cicli di bus (lettura + scrittura). Questo problema non esiste più oggi in nessun processore moderno perché abbiamo l'impaginazione, che è proprio un metodo che mi consente di poter inserire file anche in pezzi di memoria frammentati. Quando facciamo deframmentazione facciamo in modo che i dati di uno stesso file siano vicini i record.

Ricordiamo che il ciclo di DMA inizia con un indirizzamento, però se il DMA va di seguito (non fa un solo trasferimento, ma fa diversi trasferimenti, esempio caso del disco). I trasferimenti avvengono per indirizzi di memoria consecutivi. Quindi DMA lavora indirizzi contigui che siano in crescere o in diminuire ed è quindi chiaro che la parte alta non cambia ogni volta, ma ogni 256 trasferimenti. Il nostro dispositivo è furbo, poiché la parte di indirizzamento costa dei clock: parte alta degli indirizzi che è estratta D0-D7 non avviene ad ogni indirizzamento ma solo quando quel dato deve essere incrementato (ogni 256 trasferimenti). Innanzitutto va a depositare la parte alta ogni volta che inizia un trasferimento perché ogni canale ha una sua zona dove andrà a lavorare che può essere programmata e che non è la stessa zona di lavoro rispetto alla precedente. Naturalmente in generale lo fa ogni 256 trasferimenti ma se il primo trasferimento che faccio è il penultimo dopo devo re-istanziare la parte alta degli indirizzi. Esiste segnale bidirezionale EOP* (end of process) che pulsa durante l'ultimo trasferimento di un programma di canale. Questo serve a dire al processore che il trasferimento a quel canale l'ho terminato. Naturalmente quel segnale deve essere collegato a un piedino di interruzione perché dal momento in cui viene fatto partire al momento in cui termina, il processore non è consapevole (viene ibernato), diventa consapevole quando il canale ha terminato. EOP* è unico per tutti e 4 i canali, quindi è evidente che il programma deve interrogare il DMA per sapere quale/i canali hanno fatto scatenare EOP*. Può essere che più canali siano completati se il processore aveva le interruzioni disabilitate. 4 modalità di funzionamento programmabili: single transfer mode: dopo ogni trasferimento rilascia bus, questo per dischi lenti demand mode: fa sì che processore venga fermato (DMA controllo del BUS) fintanto che DREQ è attivo block mode: trasferimento che una volta fatto partire deve essere portato a termine il programma di canale qualsiasi sia il canale DREQ cascade mode: posso utilizzare HOLD e HOLDA di un dispositivo a valle, quindi posso avere serie di DMA in cascata

Ogni dispositivo DMA controller deve essere programmato in maniera individuale. Hanno significato globale soltanto durante fase di DMA, così come 8259. Come nel caso di 8259 un canale può avere un altro dispositivo connesso a valle, oppure essere direttamente connesso a dispositivo esterno. Qui possiamo avere n livelli, con limitazione logica perché quando i canali sono infiniti il processore non lavora più. Quando abbiamo 5 o 6 canali DMA sono già esagerati. perché oltre a trasferire dati il microprocessore deve anche leggerli i dati. Questa possibilità di cascata è solo teorica: quando ci sono due livelli di DMA già situazione di saturazione.

Registri interni: permettono di individuare il funzionamento generali Command Register

  • bit 7: se DACK è attivo (vero) alto o basso
  • bit 6: se DREQ è attivo (vero) alto o basso
  • bit 4: se priorità fissa o rotante, nella stessa condizione di 8259: se fissa canale 0 + prioritario di 1 + prioritario di 2 ... Nel caso in cascata ciascuno configurabile. Ruotante priorità su circonferenza
  • bit 2: controller enable, possiamo avere bisogno che il nostro DMA non ci disturbi (molto rischioso), se posto a 0 il nostro dispositivo non attiva segnale HOLD (quindi è un po' come se ci sia HOLD enable, non c'è in processore, ma c'è qui). Logico che se disabilito master disabilito anche tutti slave
  • Bit 1: channel 0, hold address caso in cui mantengano sempre stesso valore indirizzo memoria, questo per riempire con stesso valore
  • bit 0: se attivato implica che i canali 0 e 1 sono riservati ad un trasferimento memory to memory. Indirizzi del canale 0 sono sorgente e canale 1 sono destinazione, quindi canali sono utilizzati solo per effettuare dei memory to memory
  • Bit 3 e 5: più facilmente comprensibile se si va a vedere diagrammi temporali. Noi sappiamo che esiste segnali di ready, noi possiamo anche lavorare con dispositivi molto veloci e quindi possiamo comprimere trasferimenti

LATE/EXTEND write permette di ritardare o anticipare segnale di write, perché alcuni dispositivi vogliono i segnali stabili prima del write (late se sono in ritardo, ext se non lo siamo) Mode Register: Questo registro deve essere programmato per ogni canale

  • bit 7 e 6 funzionamento già visto
  • bit 5 auto incrementato e auto decremento, dice se susseguirsi indirizzi è in incremento o decremento (occhio che non c'è riporto, quindi dopo che arriva a 0 torna a 65535).
  • 2 3 ci dicono se sono in scrittura o lettura in memoria, quindi automaticamente settati (R) IOREAD-MEMWRITE, (W) IOWRITE-MEMREAD. Quando siamo in cascade riporta semplicemente HOLD e HOLDA

Request Register: un trasferimento può essere fatto partire anche da processore, ad esempio il memory to memory parte solo su esplicita richiesta, quindi scrivendo set request al canale 1 faccio partire il memory to memory. Naturalmente può partire non solo se il processore richiede, ma anche se l'output lo richiede (esempio stampante). Il nostro DMA esegue subroutine hardware molto simili a subroutine hardware in interrupt.

Come nel 8259 è possibile mascherare dei canali, è possibile tacitare uno o più canali interni. Sempre in paragone aveva maschera delle interruzioni: esisteva l'interrupt eneable che possiamo far corrispondere al command eneable, poi il mask register, poi quello interno. Interruzione verso processore equivalente richiesta di canale verso DMA: DMA controller è entità attivo e sostituisce processore in alcuni casi quindi similare. Due registri mascheramento: uno selettivo per canale o uno complessivo. Nel mask register 1 individuiamo il canale e il bit di mascheramento (0 se attivo 1 se tacitato). Nel mask 2 in un colpo solo possiamo individuare mascheramento canali. Se li abbiamo in cascata, per ciascun sotto sistema devono essere tutte programmate individualmente. Status word: per sapere cosa sta succedendo. Per esempio possiamo andare a sapere se c'è richiesta pendente su un canale anche se è disabilitato, poi altri 4 bit ci indicano quale/quali canali hanno raggiunto il terminal count, che si riallaccia sull'end of process. C'è un unico piedino che ci dice se uno o più canali hanno terminato.

EOP* segnale bidirezionale: open drain, cioè transistor in cui drain è lasciato volante. E' indispensabile predisporre resistenza, se dispositivo non c'è quando arriviamo a terminal count internamente generato stop e quindi si ferma canale. Se ce n'è uno anche da fuori possiamo abortire programma di canale. Tipicamente succede quando c'è trasferimento da disco lungo e il primo record è stato letto male, quindi non ha senso proseguire. Se in block mode il processore non può abortire, se in demand e in single ci sono tratti in cui microprocessore è attivo e può andare a mettere ' sui bit che mancano da leggere.

Indirizzi ai quali troviamo registri di controllo. Quelli che hanno A3 = 1 permettono accesso registri di controllo: posso leggere e scrivere tutti gli status register. C'è clear che li cancella tutti, poi c'è Clear byte flip flop: quando noi andiamo a programmare il nostro dispositivo noi sappiamo essere in grado di accedere alle memorie con 16 bit di indirizzamento e fare trasferimenti fino a 64KByte di indirizzo, quindi byte a 16 bit, mentre interfaccia è da 8, quindi quello che non si fa in parallelo si fa in serie. Dobbiamo quindi fare due accessi: uno per parte alta e uno per parte bassa indirizzi. Quel byte flip flop ci fa puntare a byte basso o alto: al primo accesso parte bassa di indirizzo o dato e secondo accesso alla parte alta. Quello fa da commutatore interno. Clear quel flip flop vuol dire garantirsi che di avere parte giusta sotto. esempio Mi può capitare interruzione in mezzo, in cui la subroutine mi viene a rompere. Con quel comando sono sicuro di quello che ho sotto.

Poi indirizzi base/current word address. Per ogni canale abbiamo due registri: tutti con A3 = 0, uno che indica indirizzo iniziale del nostro trasferimento per ogni canale e uno che indica quante parole dobbiamo trasferire in incremento o decremento in base a come abbiamo programmato. Qui anche scritto base/current: in realtà due indirizzo, uno quello iniziale, l'altro che mano a mano viene decrementato e incrementato. Il processore quando è master può andare a leggersi questi registri che si va a leggere valore corrente indirizzo. Nel bit 4 del mode register valido per ogni canale (AUTOINIT), quando un programma di canale cioè il word count è arrivato a zero e allora viene generato l'EOP e quel canale si ferma. esempio banale: nei televisori il pennello elettronico ripercorre sempre il dato che deve essere presente. Se noi utilizzassimo DMA saremmo sempre dietro a riprogrammare il nostro DMA, si può quindi evitare questo mettendo a 1 questo bit e quindi quando il canale finisce si va a riprendere indirizzo iniziale e va a ripercorrere tutto quanto e fino a quando il processore non va a cambiare l'auto init. Logico che se metto auto-init e poi metto il processore il block mode non avrò più controllo.

Estensione dell'indirizzo: Il problema è che il nostro sistema è in grado di indirizzare 64 KByte che è quantità ridicola al giorno d'oggi: oggi equivalente non è più a 16 bit di indirizzamento ma a 32, che non cambia nulla (interfaccia a 16 anziché a 8). Si può però ovviare al problema anche in questo caso: posso artificialmente aggiungere i 4 bit più significativi usando 4 373 se per ciascuno vado ad impostare i bit più significativi usati dal canale. Questo lo posso ottenere utilizzando questi 373 come dispositivi di input/output. Il limite di questa operazione è che un canale lavora solo in uno dei 16 possibili diversi slot da 64K. Se invece volessi evitarlo dovrei con un circuito esterno individuare le transizioni e in questo caso al posto di un 373 usare un contatore e incrementare il contatore o decrementarlo ogni qual volta c'è questo passaggio tra il primo indirizzo e l'ultimo, è quindi possibile farlo ma ci vuole elettronica esterna. Cosa più semplice individuare per ogni canale slot su cui lavorare. Numero massimo è 64K perché è valore registro interno, il problema è se posso trasferire bit che sorpassano questo valore. In generale no, sì solo nel caso in cui ci sia elettronica esterna con contatore.

C'è un piedino particolare MIN/MAX* che avevamo messo a massa. Questi piedini in realtà hanno un altro nome, quando piedino messo a massa. Vuol dire che internamente a 8088 vengono generati più segnali e con quel segnale min/max posso decidere quali andare a leggere sui piedini 24-31 sia in minimum mode che maximum mode. Meccanismo che si ritrova nei processori successivi. Non sono stati portati fuori tutti insieme perché avevamo problemi di impaccamento dei circuiti elettronici, problemi meccanici.

Oggi abbiamo processori con 4-500 piedini. Quando andiamo in max mode questi piedini portano informazioni simili ma in modo codificato e in maniera più sofisticata. Nei processori moderni non c'è memory read write, ma abbiamo tipologia di trasferimento e questi 3 segnali stato rimangono in tutta catena intel fino ad oggi. Ci dice quale ciclo di bus sta facendo. (dall'halt si esce solo attraverso interruzione) oppure con trasferimento in DMA, ma processore rimane costantemente in HALT fino a quando interruzione non lo fa uscire da questo stato. Poi info maggiori sulle memorie: perché abbiamo solo memory read attivo e non sappiamo cosa sta facendo. Poi segnale idle che processore ci dice che non sta facendo cicli di bus. Tutti i cicli di bus (anche quello di interruzione) individuati univocamente da segnale ALE che non è generato da processore. Se ciclo idle è tutti 1, non appena processore mette a 0 uno di questi vuol dire che vuol iniziare ciclo di bus e circuito esterno può generare segnale ALE. Per fare questa operazione siccome andando in maximum mode che si utilizzava allora non per uno sfizio ma perché era l'unica maniera per potere utilizzare il processore matematico che era circuito integrato distinto: fino a 386 il processore matematico era dispositivo esterno, perché tecnologia non era in grado di integrare tutti transistor. Oggi integriamo mezzo miliardo di transistor.

Esistono circuiti integrati ad hoc, questo 8288 che vedremo nella scheda: nel caso specifico riceve segnale S2 S1 e S0 e genera tutti segnali che ci servono, quindi tutti i segnali che in minimum mode erano generati da 8088. AEN è importante perché quando andiamo in DMA in minimum mode processore mette segnali di controllo in tri-state. Questo segnale sarà quindi destinato a mettere in tri state questi segnali. E' quindi un processore suddiviso in due circuiti integrati.


Processore che di fatto conosciamo già, perché è 8088 versione 16 bit. L'unica differenza sostanziale dal punto di vista esterno è che noi abbiamo multiplexati non solo 8 bit ma 16 bit. Tra l'altro DLX era macchina a 32 bit. Quello che non abbiamo fatto con il DLX è stato guardare da vicino il meccanismo della decodifica con processori a parallelismo maggiore di 8. Per il resto l'unica vera differenza è che sul pin 34 che nel caso del 8088 era lasciato volante c'è BHE, che ha a che fare con trasferimento a 16 bit. Gli altri segnali identici. Quindi rimangono segnali sia in minimum mode, sia in maximum mode. Unica differenza interna è che internamente coda di prefetch è da 6 byte e non da 4. Qui per recuperare sei byte ci vogliono sei cicli di clock, nel 8088 per 4 byte, 4 cicli di clock. Tutto assolutamente identico .. Bisogna ricordare che la stessa istruzione assembler esplicitata come elemento di programma dà luogo ad istruzioni totalmente diverse a livello macchina perché lettura a 16 bit è diverso da lettura a 8 bit. Leggere byte ad indirizzo dispari diverso da leggere word ad indirizzo pari. Noi per comodità stessa dicitura, ma compilatore ce le compila in maniera diversa. Memoria nell'8086: Parallelismo a 16 bit: quando abbiamo processore a 16 bit, abbiamo 2 bus da 8 bit perché elemento minimo trasferito è byte e tutti i processori a 16 bit hanno questa caratteristica: indirizzi consecutivi sono su bus diversi. Byte di indirizzo 0 sta su bus basso, byte 1 sta su bus alto. Sta vuol dire che processore lo vuole leggere su quel bus, quindi indirizzi pari (sia MEM che IO) devono essere erogati da dispositivi collegati a bus basso, viceversa per i dispari. Quando processore accede ad indirizzo pari si aspetta dato su bus basso, quando scrive/legge a byte dispari si aspetta su bus alto. E' fatto così perché quando vogliamo leggere word di una memoria leggiamo due byte consecutivi e li mettiamo su due bus consecutivi per leggerli insieme. Quando parallelismo è 32 indirizzi consecutivi stanno su bus diversi: bus 1, bus 2, bus 3 .. Sempre per leggere contemporaneamente più dati. Qui abbiamo ipotizzato di avere una memoria dell'8086, quindi le memorie vanno sempre in coppia (uno su bus basso e una su bus alto). Due per le memorie, per l'IO invece possiamo non avere questo. Se c'è una sul bus alto ce n'è una anche su bus basso. Sulla sinistra memoria logica vista da programmatore: la vede come sequenza di byte ad indirizzi consecutivi. A livello delle EPROM o delle RAM succede che in realtà le nostre due EPROM (una su bus alto e una su bus basso), hanno ognuna un loro indirizzo zero, ma come corrispondono ad indirizzo generato da processore?!? Il byte A lo va a prendere all'indirizzo 0 della EPROM che sta su bus basso e byte B su indirizzo 0 dell'EPROM su bus alto. Ad indirizzo zero EPROM ci sta A e B .. Una per bus basso e una per bus alto. A questo punto dove si trovano i vari byte? .. Una word si trova come indirizzo interno a valore pari ad indirizzo emesso da processore diviso 2 e a seconda che resto sia 0 o 1 si trova su bus basso o alto. Word ad indirizzo 23: 23/2 fa 11 con resto 1. Un byte ad indirizzo 11 e un byte ad indirizzo 12. Questa word è word ad indirizzo dispari, vuol dire che parte bassa sta su bus alto mentre word parte alta della word sta su bus basso. Word ad indirizzo dispari dà sempre luogo a DUE cicli di bus. Se uso EPROM da 64K, poiché vado sempre in coppia è chiaro che vuol dire che io comunque realizzo banco da 128K. Dimensione memoria realizzata è pari a parallelismo di bus (come numero di byte) moltiplicato per dispositivi che ho. Tutti i processori a parallelismo superiore a 8 hanno dei segnali che dicono quale bus o quali bus sono coinvolti da trasferimento: o solo bus basso o solo alto o entrambi. Per esempio se faccio trasferimento di word ad indirizzo dispari il processore mi attiva prima bus alto, poi bus basso. Se faccio trasferimento a 16 bit di word ad indirizzo 32 bit lui mi fa unico ciclo bus attivando entrambi bus. Segnali attivazione bus si chiamano BE0 e BE1, in altri processori si chiamano BLE o BHE. In 8086 abbiamo BHE e il BLE coincide con A0, che lo possiamo considerare BLE. Quando sono entrambi a zero abbiamo trasferimento a 16 di word ad indirizzo pari composto da indirizzo indicato più quello dopo. Se abbiamo zero su bus alto stiamo trasferendo byte ad indirizzo dispari. Quando entrambi a 1 nessuno dei due bus abilitati. Questi li genera il processore in funzione di operazione che sta eseguendo. Notiamo che se leggo byte dispari e lo voglio scrivere su parte bassa di registro: per esempio MOV Al, 59 vuol dire che io prendo byte indirizzo 59 sommato a base, che abilito bus alto ed internamente a processore è diretto a parte bassa registro. Internamente ci sono MUX, ma all'esterno indirizzi dispari sempre su bus alto, pari bus basso. Relazione tra indirizzo emesso da processore e indirizzo interno: indirizzo interno è indirizzo esterno diviso per 2. Un numero binario per dividerlo per 2 si trasla a destra di una posizione. Sul piedino 0 dell'EPROM è l'A1 del processore, sull'1 EPROM ci va A2 del processore. A0 è BLE che ci serve per discriminare tra bus alto e bus basso. Indirizzo collegati ai nostri dispositivi sono indirizzi processore traslati di uno. Adesso dobbiamo generare CS. Le memorie vanno in coppia quindi è evidente che se io ho memoria di taglio 64K, vuol dire che realizzo banchi da 128K. Supponiamo di avere quelle 2 memorie e di voler realizzare il banco di memoria da 128K che va da 40000 a 5ffff. Quanti banchi da 128K ci sono nell'8086? Ce ne sono 8, quindi i CS saranno dai 3 bit più significativi. Quindi i primi 3 individua il banco da 128K. Questo non basta, perché quel banco è fatto di due dispositivi e quindi ci vuole anche il bus eneable. Quindi in realtà ne abbiamo 16 di dispositivi da 64K. E' chiaro che una cosa è il banco, una cosa sono i dispositivi fisici. Indirizzo memoria logica centrale fa riferimento a memorie fisiche diverse. Caso dell'8086 ogni dispositivo di memoria ha suo CS* che è l'OR del CS dell'intero banco più l'altro segnale A0 e BHE che indica quale dei due bus è coinvolto: selezione banco e selezione bus. D0-D7 (byte indirizzo pari) vanno su bus basso, D8-D15 (byte indirizzo dispari), word ad indirizzi pari e word ad indirizzi dispari. Nei caso dei processori nuovi esistono comandi che si richiede al compilatore di farli iniziare ad indirizzi pari. Memorie basse e memorie alte, decodificatore dovrà decodificare sia indirizzi che CS. Dispositivi di I/O nell'8086: Ora parliamo di dispositivi di IO, che a differenza di memorie non necessariamente devono andare in coppia. In questo caso posso avere dissimmetria. In caso di IO posso decidere a quali indirizzi accedere. Qui abbiamo fatto esempio di avere due 8255: uno ad indirizzo 78 (tipico indirizzo porte parallele) e l'altro ad indirizzo 81. E' chiaro che 8255-1 vedrà i suoi 4 indirizzi all'indirizzo 78, 7A, .. (i suoi indirizzi andranno di 2 in 2). Sul A0 del dispositivo porterò A1 del processore, sul A1 dispositivo porterò A2 .. Discorso analogo che si trova sul bus alto e avrò solo indirizzi dispari. Qui non abbiamo problemi di tipo word, quindi possiamo noi imporre uso di certi indirizzi. Notiamo due cose: che lo spazio di indirizzamento occupato da dispositivo non è più di 4, ma di 8 perché andiamo a perdere locazioni ad indirizzi dispari. Questo ci spiega anche perché nella scheda non erano connessi ad A1 e ad A2 perché quello stesso programma scritto per 8088 si voleva usare anche per 8086 e in questo caso dovevamo usare o solo indirizzi pari o solo indirizzi dispari. D'altronde data l'abbondanza di indirizzi di IO non abbiamo nessun problema a sprecare qualche indirizzo in più. Si possono anche forzare: qui preso 8255 che non abbiamo connesso né a bus alto né bus basso, ma ad entrambi attraverso 245, questo ci permette di usare indirizzi consecutivi perché quando usiamo indirizzo basso abilitiamo 245-2 e se no 245-1, questo vuol dire che possiamo colloquiare con 8255 usando dispositivi 245. Questo ridicolo. Interruzioni: uguali all'8088. Il bus su cui il processore si aspetta Interrupt type (quel byte che viene letto ..) è il bus basso. Quindi è ragionevole collegare 8259 direttamente su bus basso. Se è su bus alto deve essere previsto bypass.

8237 con 8086

[modifica | modifica sorgente]

La cosa è più complessa. perché 8237 è dispositivo che mettiamo o su bus basso o su bus alto, quindi vuol dire che noi colleghiamo ad A0 dell'8237 l'A1 del processore. E sul A1 colleghiamo l'A2 e questo va bene 8237 quando è slave, ma quando diventa master su A0 indirizzo che lui emette dobbiamo far andare A0 dell'intero sistema. Quando slave c'è un tipo di collegamento, quando è master ce ne è un altro. 8237 è a 8 bit quindi al massimo fa trasferimenti su un bus. Quindi è chiaro che segnale A0 emesso da 8237 è di per sé o bus alto o bus basso. Mai in contemporanea perché vede trasferimenti solo da 8 bit. Come colleghiamo gli indirizzi? Qui abbiamo due 244: uno in ingresso e uno in uscita. Quando il segnale HOLDA è basso quindi non siamo in DMA è abilitato il 244 superiore e quindi su A0 dell'8237 ci va BADR1; su A1 di 8237 ci va BADR2 quindi quel traslamento indirizzo già previsto. Quando siamo in DMA è aperto 244 di sotto e in questo caso diventa BADR0 perché lui è master e sono indirizzi che vanno a sostituirsi a quelli emessi da processore. Qui esempio da 8255 a memoria. Tipica situazione: quando 8086 è master problemi non ce ne sono, 245 è disabilitato. Quando diventa master 8237 allora 8255 deve trasferire ad indirizzi consecutivi. Indirizzi consecutivi di memoria stanno su bus diversi: uno su bus basso, uno su bus alto .. E allora abbiamo questi 245: quando da 8255 dobbiamo scrivere su indirizzo pari dovremo aprire 245 e abilitare memoria bassa, quando dobbiamo scrivere ad indirizzo successivo 245 dovrà essere chiuso. Direzione 245 è da bus alto a bus basso: in realtà in questo caso in DMA non dovremmo neanche disabilitare 245 perché va a scrivere su tutte e due le memorie. Discorso diverso se noi dovessimo scrivere sulla stampante perché non potremmo abilitare entrambe le memorie. Caso successivo che viene gratis: memorie a parallelismo 32. Qui abbiamo disegnato i 4 dispositivi e memoria logica a fianco: che rapporto c'è tra indirizzo fisico e indirizzo logico? Ora divido per 4. Naturalmente che in un caso come questo collegherò ad A0 dei dispositivi non più A1, ma A2 perché devo shiftare di due posizioni. In questo caso avrò 4 bus eneable. In questo caso potrò trasferire dati a 32 bit purché non siano più allocati ad indirizzi pari ma a multipli di 4. Quindi dati ALLINEATI: in questo caso il trasferimento avviene in un unico ciclo di bus. Occhio al DLX perché lì la word è considerata a 32 bit, mentre qui la word la consideriamo a 16 bit. 1 byte sempre allineato. Nei RISC il posizionamento di parole a indirizzi non multipli della loro posizione (word non allineate) dà origine ad exception, in tipologia di Intel invece non c'è perché le istruzioni sono di lunghezza variabile (da 1 byte a 15 byte). Dati viaggiano sempre sui bus di riferimento. Ora riprendiamo discorso fatto per 8086. Banco di memoria da 2MB visto da punto di vista di indirizzi logici in blu: lo realizzo con 4 dispositivi da 512 KByte. Quindi devo prima di tutto individuare banco (parte riquadrata) che individua 2 MB che mi interessano. Comune per tutti i 4 dispositivi, distinti da BE0, BE1 .. Se avessi un banco da 1MB dovrei avere 4 dispositivi da 256K, ma non esistono e quindi dovrò usarne 8 da 128K. Lucido 36: Riprendiamo il discorso dall'ultimo lucido dell'altra volta: abbiamo parlato di parallelismo a 32 bit e abbiamo visto che per realizzare una zona di memoria logica nel caso del parallelismo a 32 bit dobbiamo utilizzare 4 dispositivi perchè memorie vanno di taglio uguale su tutti i bus perchè esiste la possibilità che il processore voglia leggere double-word (o word) e indirizzi successivi se li aspetta su bus diversi (consecutivi). Qui esempio che realizza zona da 2 MB con 4 dispositivi da 512 Kbyte e abbiamo visto che il CS (a parte le possibili semplificazioni) è dato da individuazione di zona di memoria moltiplicata per bus enable corrispondente. E' chiaro che per quanto riguarda l'I/O nel caso di parallelismo a 32 bit ci troviamo nella stessa condizione dell'8086. I dispositivi di I/O non hanno bisogno di essere replicati su tutti i bus. Mentre nel caso dell'8086 questo implicava che lo spazio di indirizzamento fosse doppio rispetto alle necessità dei suoi registri; è chiaro che nel caso di 32 bit diventa uno spazio da 16, perchè i singoli registri vengono indirizzati ogni 4 indirizzi perchè dobbiamo tornare su bus di partenza (generalizzazione dell'8086). Nel caso di parallelismo ancora maggiore il tutto è analogo, si amplia soltanto la distanza. Non c'è nessun vincolo di partizionamento su bus diversi, ma carichiamo dal punto di vista elettrico meno ogni singolo bus, quindi abbiamo meno probabilità di dover usare buffer per indirizzare quel bus. Supponiamo ad esempio di avere due 8255 nel caso dell'8086. Uno su un bus e un altro su un altro bus e potrei caricarli contemporaneamente con un'operazione di 16 bit. Scrivo una word che scrive sia su bus basso che su bus alto; dal punto di vista di efficienza l'operazione di I/O è talmente rara che è più semplice fare più cicli di bus su due bus diversi. Lucido 37: Poi c'è lucido identico a prima nel quale abbiamo riportato bus, struttura generale. Qui abbiamo ancora più esplicitato il rapporto che vi è tra il contenuto delle EPROM e delle RAM e la posizione dei byte all'interno della memoria logica. La prima memoria (EPROM 0) al suo indirizzo 0 contiene il primo dato e così via gli indirizzi logici consecutivi si trovano a posizioni identiche dei vari dispositivi fino a quando non passiamo multiplo di 4. La posizione di memoria logica alfa, beta, ... si trova nel dispositivo che si ottiene dividendo il valore dell'indirizzo logico per 4 e si trova nel dispositivo che dà il resto di questa divisione. Lucido 38: nel lucido successivo i CS sono della RAM e non della EPROM. Nel caso specifico noi cerchiamo di realizzare una RAM da 256K posta all'indirizzo 84000000. Il discorso fatto per la decodifica si pone uguale in questo caso perchè è evidente che dobbiamo realizzare due banchi da 128K perchè le RAM da 64K non esistono. Qui troviamo i CS di tutte le RAM da 32K che dobbiamo usare. Qui abbiamo volutamente usato decodifiche non semplificate perchè non è scopo di questo argomento, ma è assolutamente evidente che esiste anche in questo caso di bus multipli esiste decodifica semplificata; questa non è altro che un metodo per ridurre complessità delle funzioni che individuano una particolare zona di memoria. Riduciamo numero dei termini. Quindi stesse tecniche di riduzione della complessità si applicano uguale anche nel caso dei processori a parallelismo superiore. L'unica differenza è che una volta individuata quella zona, quella stessa zona è suddivisa nel numero di bus. Lucido 39: ultimo lucido per esercizio. Noi ricordiamo che esiste possibilità di posizionare delle memorie in posizioni non allineate e dà luogo che in qualche maniera i dispositivi usati devono essere interpretati come due o più dispositivi per ciascuno dei quali bisogna individuare CS. Quel banco di memoria non allineato richiede che ogni singolo dispositivo abbia due CS, uno per ciascuno della metà della dimensione che ogni dispositivo ha. Qui c'è esempio da analizzare. L'esigenza di disallineamento avviene come in questo caso quando l'indirizzo iniziale del byte non è un multiplo intero della dimensione dei nostri dispositivi.

Architetture parallele

[modifica | modifica sorgente]

Problematiche di parallelismo nei processori. Già in parte affrontato quando abbiamo visto che ci sono processori con più bus. Quali problemi abbiamo per parallelizzare quanto più possibile l'esecuzione delle istruzioni. Abbiamo già fatto primo passaggio in materia: da DLX sequenziale in cui esecuzione cominciava, terminava e poi nuova istruzione .. Siamo passati a DLX pipelined in cui più istruzioni in parallelo. Questo tende a portarci vicino ad un CPI (clock per instruction) vicino a 1. Oggi invece vogliamo eseguirne di più. Da un lato vuol dire avere in esecuzione più istruzioni insieme e finirle anche. Quindi abbiamo un CPI < 1. Due concetti diversi: numero istruzioni terminate per clock e poi esiste densità. Questo vuol dire che il throughput è alto ma non che un'istruzione dura un clock. Nelle pipelined due problemi tipici: read after write (leggere da register file dato più recente, soluzioni forwarding, problemi di alee e stalli istruzioni). Qui riprendiamo alcuni discorsi già fatti: cosa sia architettura, abbiamo 3 definizioni: comportamento funzionale di un PC (definita dal suo linguaggio macchina che ci dice anche efficienza, se set ricco programmi più corti, se set lungo programmi più lunghi). Questo non vuol dire che set di istruzioni più semplici sia meno efficiente. Architettura sovrastante implementazione: DLX sequenziale e DLX pipelined, due implementazioni diverse ma stesso linguaggio macchina. Programmatore non so se sotto c'è una o l'altra: forse lo intuisce dall'efficienza. E poi esiste realizzazione: esistono diverse combinazioni di pipeline (n di registri, ecc ..). Questo non fa altro che confermare che ogni funzione logica ha infinite realizzazioni. Mentre quello che sotto sta a linguaggio macchina cambia molto rapidamente (oggi sul micron), sappiamo che il linguaggio di macchina tende a non cambiare, tende ad espandersi aggiungendo istruzioni ma mai rimuovendo o cambiando istruzioni, questo per investimenti in software. Nei pochi casi in cui si è tentato di farlo recentemente ha dato flop giganteschi (caso di Itanium Intel), mentre processori prodotti da grande casa competitiva (caso Opteron che hanno portato a parallelismi superiori ma aveva compatibilità e quindi molto più successo, rincorsa quindi di Intel). Dynamic Static Interface: ha concetto molto semplice. Dove finisce influenza software e comincia hardware? Più si va verso RISC l'hardware diminuisce di complessità, quindi affida al software compito di scrivere programmi più efficienti. Questo vuol dire motore molto forte di ottimizzare programmi (compilatore). Dall'altra parte invece potremmo immaginare macchina che interpreta linguaggio di più alto livello. Ma se l'interprete java lo realizzassimo in hardware avremmo una macchina che comprende linguaggio evoluto. In mezzo ci sono CISC e per i quali esistono hardware capaci di analizzare linguaggio macchina e di stabilire all'interno di un paniere individuare insieme di istruzioni che potenzialmente potrebbe essere eseguiti anche prima dell'istruzione corrente, salvo poi riportare risultati nell'ordine richiesto. Altra possibilità è quella di Very Long Instruction Word, in cui sono contenute più istruzioni. Compito lasciato a compilatore. Mentre nell'altro caso compito lasciato ad hardware. Questo per dare panoramica dei nomi che possiamo trovare in letteratura. Qualche nome che possiamo trovare: sequenziale: una alla volta pipeline: molte insieme super pipelined: quelli per cui (come P4), un singolo stadio pipeline (che ha suo ritardo) lo divide in tanti stadi più veloci, questo vuol dire che il numero di istruzioni con il guaio che quando sbaglio un jump o un branch devo svuotare pipeline molto più lunga diversamente dal caso in cui devo svuotare pipeline più corta. Alcune volte è scoraggiato questo processori scalari:una sola pipeline superscalari: più pipeline (tipicamente 3, con conseguenti 3 istruzioni eseguite insieme, ma con problema di alee) VLIW: più pipelines, quindi istruzione composta da più istruzioni (che possono essere CISC o RISC). Scelta fatta a tempo di compilazione. Multithread: tipicamente nel P4 o Opteron. É un processore con unica pipeline, ma delle batterie di registri (di solito 2 separate), per cui è possibile fare partire sulla stessa pipeline due diverse istruzioni, questi lavorano in alternativa ma quello che vediamo da fuori sembra di avere due pipeline che lavorano in parallelo. Se uno dei due processi in cui ha bisogno di dato mentre uno aspetta l'altro può andare. Oggi siamo già in presenza di multicore: sullo stesso chip due processori indipendenti con cache in comune con all'interno multithread, a breve 4-core .. Si può verificare che aumento di efficienza con l'aumentare dei processori arriva fino ad un certo punto (4 massimo 8), dopo non dà più benefici. Per esempio P4 è super scalare multithread. Memory level: memorie che possono rispondere a due richieste. E' vero soprattutto per le cache. DLX rivisitato: DLX rivisitato per FP: processori importanti per alcune cose perché sono RISC e quindi tralasciano complessità dei CISC. Ci siamo occupati fino ad oggi di pipeline intere: il floating point lo lasciamo fuori. Peccato che non sia elemento trascurabile, anzi fa parte integrante del linguaggio macchina che usiamo regolarmente, quindi dobbiamo analizzare da vicino effetti del floating point perché ha caratteristica drammatica: non dura un singolo clock, anzi normalmente lo stadio di esecuzione dipende dalla tipologia dell'istruzione e può variare molto. Di solito cercheremo di ridurre a casi più semplici, ma se pensiamo alle istruzioni disponibili abbiamo logaritmo, esponenziazione, seno, coseno, che hanno tempo di esecuzione di migliaia di clock. Qui preso piccolo esempio semplice in cui stadio di esecuzione è suddiviso in 4 tipologie: una intera, una legata a moltiplicazione, una che è la somma o sottrazione floating point (3 4 cicli), poi abbiamo preso divisione. Questo problema c'era anche in quello intero, che però avevamo tralasciato. Qui abbiamo supposto che moltiplicazione e somma in floating point permettano di avere in esecuzione più istruzioni. E' solo un esempio. Qui abbiamo un esempio: quelle indicate con F davanti ne avevamo parlato nel secondo anno. Oltre a registri interi esistevano anche registri floating point, ma ora tornano. Qui ne abbiamo prese alcune: ipotizzato di fare quadrato ... La FMUL passa sia per IF che ID, ma poi passa per 7 stadi moltiplicazione, FADD passa due stadi iniziali poi 4 stadi per eseguirla poi per MEM e WB, la LOAD dura canonici 5 clock. E anche STORE. Istruzioni successive possono terminare prima delle precedenti, situazione caotica, possiamo avere un WRITE after READ (istruzione che vuole leggere F5 e una dopo che finisce prima che mi va ad alterare F5, quindi quando se la va a prendere può prendere valore alterato). Oppure WRITE after WRITE (può essere che istruzione che segue finisce prima di istruzione che precede, quindi risultato nel registro è quello prodotto dalla prima). In questo caso supponiamo che la FADD sia FADD F6. Cosa avremmo in F6? Quello della FADD o della FLD?!?!? .. Tutte le alee precedenti più nuove alee che oggi si presentano. Noi possiamo avere una istruzione in ogni ramo dell'EX e quindi risultato prodotto può finire in tempi diversi e dare problemi. Se non abbiamo il forwarding che in questo tipo non si usa, ma si usano tecniche più sofisticate. Abbiamo situazioni anche un po' ridicole: qui la FMUL termina dopo 7 cicli e dopo il mem ed è lo stesso momento in cui termine la FADD e la FLD. Qui abbiamo 2 tipi di problemi: 3 istruzioni cercano di scrivere nel register file e per come lo abbiamo visto è capace di scrivere un risultato alla volta. Ci troviamo anche in WRITE after WRITE. Se noi vogliamo usare questa struttura e così com'è è particolarmente inefficiente. Del tutto evidente che nello stadio ID conosciamo tutto di un'istruzione (quali registri usare e registro in cui andare a porre risultato) e in quel caso potremmo inserire logica che se abbiamo caso di Write after Write possiamo decidere di stallare di un clock la FLD e farla finire un clock dopo. Questo confronto di istruzioni a livello di decodifica, si possono inserire molti stalli, ma usando tecnica di questo genere si finisce che ne abbiamo così tante che diventa lenta. Questo problema già pensato negli anni 60: pur non esistendo RISC, esisteva 6600 che si era posto problema di gestire istruzioni in cui dovevano finire in tempi diversi: vie diverse di esecuzione per avere tempi di fine diversi rispetto alla regolare susseguirsi era già pensato. Quando si deve stallare istruzione si può stallare in molti modi: si può stallare anche alla fine di stadio di EX. Molto spesso stallo individuato nello stadio di EX, purché naturalmente non vada ad alterare logica di programma. Che cosa possiamo fare per risolvere problemi? Molte tecniche: noi analizziamo le 3 principali, che corrispondono anche a ciò che è successo nel tempo. Primo tentativo fu adottato da calcolatori di CBC. Una tecnica trovata si chiama di Tomasulo. Reorder buffer o esecuzione fuori ordine: tecniche moderne. Abbiamo detto che adesso analizziamo tecnica di score board (tabella del punteggio, delle attività). Prendiamo sequenza semplice, notiamo che Write after Read si chiama anche anti-dipendenza (Write after Write è dipendenza). Nel caso del DLX in cui quell'unità che fa somma fa anche differenza problema non si pone perché FSUB finisce dopo FADD. Nel caso invece che ci siano più somme sottrazioni per la floating point se la FADD fosse più lenta avremmo WAW e alla fine errore.

Score-boarding

[modifica | modifica sorgente]

Questo schema generale Score Boarding: lo score boarding è anche un po' equivalente di quello stadio ID di pipeline tradizionale perché individua se e quando operandi sono disponibili, quindi avviarla verso unità giusta. Immaginiamo che ci sia o la memoria o la coda in cui ci sono istruzioni estratte in sequenza ed inviate a relative unità funzionali. Scoreboard è una rete combinatoria che tiene conto di tutte le info. Non solo tiene in conto di istruzioni in attesa, ma anche delle variazioni che avvengono all'interno di RF perché se istruzione non può essere messa in esecuzione perché in attesa di valore nel registro nel momento in cui questo valore scritto istruzione può essere avviata se la sua unità funzionale è libera. Ci sono 4 fasi: emissione: prendere istruzione da coda, avviarla verso unità esecuzione corrispondente solo se è libera, la fase successiva è lettura operandi, se operando è disponibile bene, se no nostra istruzione rimane unità esecuzione e rimane in stallo e aspetta lì dentro che operandi siano disponibili nel RF. Se operando non disponibile nostra istruzione rimane in stallo aspettando che operandi siano disponibili nel RF. Esecuzione: in questo clock o in questi clock Scrittura: se c'è WAR e intera situazione del nostro sistema ce lo dice perché sappiamo sempre cosa c'è in esecuzione, quindi dipendenza di anti-dipendenza è confronto da destinazioni e sorgenti. Non abbiamo neanche problema del forwarding perché aspettiamo e non appena è prodotto subito scritto dentro RF. E' come se avessimo eliminato stadio di MEM, che per quelle in cui si scriveva in RF era stadio morto. Esempio: fra la LD F6 e LD F2 problema perché valore del floating point parte bassa può essere usata come indirizzamento. Poi FMUL deve esser in grado di leggere LD F2 ... Poi RAW anche tra FSUB e FADD in modo che legga valore prodotto ... Ipotizziamo quei cicli di clock. Non è quindi problema di architettura ma di implementazione: linguaggio macchina è sempre lo stesso, l'implementazione è diversa. Stato della istruzione: manca stadio di MEM, ma non vuol dire che non ci siano operazioni. LD e STORE assimilabili a operazioni di tipo intero quindi stadio integer. Abbiamo certo numero di info usate poi da scoreboard sulla base della quale clock per clock emette comandi verso unità funzionali e verso tutti gli altri dispositivi. Avevamo sorta di scoreboard anche nel DLX: EPROM di 40 uscite, dimensione molto elevata. Il controllo della pipeline era legato stadio per stadio a reti combinatorie presenti in ogni stadio che individuavano sulla base dell'istruzione qual era operazione che clock per clock quello stadio della pipeline doveva dare. Questi stadi dovevano interagire solo al problema del forward. Esempio: Qui disegnato tabellone: busy se unità occupata o no, Op tipo di operazione per quella unità funzionale in esecuzione, Fi registro di destinazione, Fuj e Fuk ci dicono da dove arrivano operandi (nel nostro caso solo dai registri, ma potrebbe essere che questi registri non siano ancora aggiornati, ma tramite questi quali sono le unità da cui arrivano tramite Qj e Qk), Rj e Rk flag se indicano se dati ci sono e non ancora letti. Quando unità funzionale deve produrre dato per cui è in attesa un'altra unità, questo ce lo segnalano i due flag. ESEMPIO: quella che qui chiamata LD è FLD ... Nella tabella centrale ci sono stadi in cui si trova ogni singola istruzione e clock in cui questo stadio viene raggiunto. Sotto tabella che tiene conto di tutte unità funzionali. Sono stato delle unità funzionali. Sotto registri di register file in cui scriviamo valori o sorgenti da cui valori sono attesi. Clock numero 1: supponiamo di prendere macchina che parte da zero. Poniamo che sequenza sia in alto a sin (34(R2) vuol dire 34H sommato R2). Load si trova in emissione, Integer diventa occupata (busy), l'operazione è LOAD, ha bisogno di R2 e scrive in F6, il registro di cui hai bisogno è già disponibile (flag Rk) e sotto diciamo che F6 è in attesa di un risultato che viene da Integer. Clock: abbiamo anche time (il numero di clock che devono ancora passare perché unità funzionale produca risultato). Nostra load sta eseguendo e notiamo che non posso far avanzare altre istruzione perché istruzione che segue è ancora load e quindi deve usare stessa unità funzionale. E siccome le istruzioni emesse insieme, prima devo riuscire ad emettere precedenti. Clock dopo: dato pronto per esser scritto nel nostro registro (nella prima tabella scritto al clock nel quale si trova disponibile) Ora scriviamo in questo clock il registro di destinazione, il che vuol dire che andiamo a liberare unità integer, risultato va a finire dentro registro destinazione. Clock dopo: quella prima ha completato e quindi ora posso eseguire nuova load (ora supposta durare 4 clock, dipende da implementazione delle nostre unità funzionali). Adesso abbiamo vantaggio perché multiply che segue può andare ad unità funzionale diversa, quindi in questo caso l'unità multiply era libera. Avviamo la prima e di unità ne abbiamo addirittura 2, ma ha bisogno di F2 che è prodotta da integer (c'è valore dentro ma non ancora giusta). F2 è in attesa di valore che viene da integer (Fuj: dato in produzione dentro ad integer), quindi il nostro flag ci dice che non è ancora pronta, infatti LD F2 .. non ha ancora finito, solo allora posso andare avanti. Altro clock: nei registri abbiamo dove quel valore è in produzione (F0 mult, F2 Integer). Questa info dice allo scoreboard che i valori non sono ancora utilizzabili, sono ancora valori vecchi. Clock dopo possiamo far andare sub, visto che unità add non ha ancora nessuno che la sta impegnando, intanto le altre avanzano (LD avanza, MULT aspetta valore prodotto da LD). F2 della SUB non è stata ancora prodotta, quindi anche SUB si ferma. Potremmo anche far andar avanti la DIV ma visto che ha bisogno della F0 prodotta da MUL non facciamo andare neanche lei. F2 è prodotta e al prossimo clock possiamo far andare avanti le istruzioni. Al clock 8 la DIV è stata emessa, va ad occupare l'unità funzionale, avendo bisogno della F0 essa non può però andare ancora avanti. Ora finalmente la MUL e la SUBD possono avanzare in stato operativo (tutti registri disponibili e quindi possiamo proseguire). Non emettiamo ancora ADD perché è occupata da SUBD. Sulla sinistra c'è scritto numero di clock che mancano. I registri F0 F8 F10 sono in attesa di ricevere nuovi valori (ma hanno valori dentro non più validi però). Clock 11: ciclo 10 saltato perché non succede nulla visto che SUBD prende 2 clock. Nella 11 la SUB termina ed è pronta a scrivere risultato nel clock dopo. Non abbiamo possibilità di emettere nessuna nuova istruzione perché stadio ADD è ancora occupata da SUBD. Ciclo 12: finalmente è stata prodotta la F2 e la MULT può usarla mentre la SUBD finisce scrivendo F8, che condiziona ADD .. Clock dopo: la SUBD ha lasciato unità funzionale, quindi ADD può essere immessa, la ADD ha tutti i registri che gli servono già aggiornati quindi ADD procede senza aspettare, finirà comunque molto prima della MULT e della DIVIDE. Rimangono 40 clock fintanto che non sono disponibili tutti i registri. La DIVIDE è sempre ferma perché aspetta MULT. Ciclo 15: ADD procede ci mette due clock, MUL sta andando avanti .. Ciclo 17: ADD non può andare avanti a casa del WAR con DIVD. MUL termina nel ciclo 19: a questo punto DIV parte. Una volta che DIV ha cominciato ha usato F6 e quindi ADD può proseguire. Ecco che nel clock successivo la ADD può andarlo a scrivere. Nel caso di interruzione dipende da quando capita. Problema dell'interrupt preciso: ci sono alcune istruzioni che sono a metà, alcune che hanno terminato .. In generale ci si riporta all'ultima istruzione che è stata eseguita, rischio di dover fare moviola se istruzioni hanno scrittura intermedia. Le eccezioni sono condizioni eccezionali che richiedono se quell'istruzione ha già scritto che torni a riportarsi allo stato iniziale. Anche qui problemi di salti: qui neanche presa in considerazione. Si cerca di presentare maggioranza dei problemi, ma di nessuno diamo completa panoramica. Ci sono alcuni aspetti che limitano questa tecnica: la prima è che i problemi di WAR stallano le operazioni (ADDD che non poteva terminare perché DIVD non aveva ancora usato registro). WAW: viene stallata in fase di emissione una istruzione che va a scrivere in un registro che deve scrivere sullo stesso. Altro problema che istruzione non può essere immessa fintanto che le precedenti non sono immesse. In realtà se le istruzioni sono completamente distanti sarebbe possibile anche questo. Sono richiesti molti bus tra unità funzionali e registri (bus verso tutti i registri). Questo pone dei limiti: i bus non sono semplici fili, ma devono essere realizzati di silicio poli-cristallino. Aumentare bus aumenta complessità. Quindi abbiamo tutti una serie di problemi che dobbiamo migliorare. Specialmente per le dipendenze. Questo si ottiene attraverso rinomina, che verrà usata su struttura che assomiglia a questa, ma che sarà fortemente ampliata nel caso di calcolatori Intel dove oltre al reneame c'è anche il reorder buffer.

Algoritmo di Tomasulo

[modifica | modifica sorgente]

Siamo dunque costretti a stallare applicazione per WAR RAW .. Non possiamo inoltre immettere istruzioni fintanto che non sono stati liberati stadi. Tomasulo risolve problemi di immissione legati a RAW, WAW .. ma non di istruzioni fuori ordine. Lucido 39 abbiamo potenziale sequenza. Problema legato soprattutto al floating point: durata dei periodi di esecuzione è molto variabile, il che comporta produzione risultati intermedi diversa, che origina alee che non si originavano in DLX sequenziale. Questa sequenza ha alcuni problemi: WAR (FSUB non deve scrivere prima che FADD legga F8), WAW (FMUL non termina prima della FADD), RAW (tra FSUB e FMUL). La tecnica che viene utilizzata da Tomasulo si chiama in genera “renaming”: principio che per certi aspetti abbiamo già visto. Siccome i risultati vengono prodotti in certi tempi, andiamoceli a prendere non appena è possibile e nel frattempo le istruzioni in attesa le parcheggiamo in reservation stations, le quali al loro interno quando istruzione non può proseguire perché manca 1 o più operandi, vengono trasformati nel senso che queste in attesa non hanno registro di cui hanno bisogno, ma unità funzionale di cui hanno bisogno. Quindi nel caso i registri sono in produzione andiamo a puntare a unità funzionali che li stanno producendo. Nel caso ci sia valore alterato di RF noi non sbagliamo perché andiamo a puntare a stadi che stanno producendo risultato. Il dato è presente in RF, se dopo avvengono variazioni noi il problema non ce lo poniamo perché istruzione va a prendere operando da unità funzionali che producono risultato. Questo è in qualche modo un renaming. FADD produce F6 e al posto di questo nome diciamo che la produce in un buffer chiamato S. La STORE sa che il valore è prodotto da FADD e lo facciamo puntare scrivendo S. Rispetto a prima rispetto a score boarding in cui dati andiamo comunque a prenderli nel register file. Nel caso di alee noi evitiamo sovrapposizioni le bloccavamo. Ora non ci interessa perché anche se andiamo ad alterare RF noi sappiamo dov'è la sorgente del dato. Se al posto di indicare esattamente il registro di cui ho bisogno vado ad indicare il posto in cui quel valore è prodotto, io sto facendo rinominazione. FADD che produce per F6 teoricamente avrebbe conflitto con FMUL. FSUB e FMUL problema analogo. Noi sappiamo istante per istante qual è il produttore che ci dà info giusta. Se non ci sono modifiche in giro che riguardano registri le andiamo a prendere direttamente nei registri, ma se c'è variazione li andiamo a prendere direttamente dal produttore. Reservation station: è rete logica in cui depositiamo le istruzioni che debbono essere eseguite, a mano a mano che le estraiamo nella coda che devono essere eseguite, le andiamo a depositare nei buffer di ogni unità funzionale e andiamo ad indicare o il registro o il nome del produttore non ancora collocato ma che a noi serve. Per certi aspetti equivale un po' a quello che era il forwarding. Grosso vantaggio: qui il RAW è subito risolto. E non abbiamo bisogno di controllo centralizzato perché nel momento in cui depositiamo istruzione nella reservation station automaticamente indichiamo nome produttore. Rimangono problemi WAR e WAR e vedremo che sono anch'essi automaticamente risolti. Il fatto che reservation station possano catturare risultati nel momento in cui sono prodotte comporta presenza bus (ogni produttore dovrebbe andare a tutti consumatori) e questo pone problemi perché sappiamo che numero di bus vuole limitato. In questo caso supponiamo che non si ponga problema, ma quando numero di bus è ridotto dobbiamo fare arbitraggio (usato da un unico produttore alla volta), nel caso di vie di accessi multipli dovremo avere anche stadi con più porte. Prendiamo un altro esempio che è quello di prima con lo scoreboarding per fare confronto. Abbiamo potenziale WAR tra FLD e FADD anche se dal punto di vista concettuale è molto improbabile, ma se al posto di FLD ci fosse FDIV questa finirebbe molto dopo della FADD e quindi problema si potrebbe porre. Con Tomasulo “quando ci sono più destinazioni identiche nell'ambito delle istruzioni che sono in quel momento analizzate viene fatto scrivere in RF solo quello più recente”. Quelle altre producono risultato ma non va a scriverla sul RF. Il discorso del register renaming: questa F6 noi in qualche maniera gli diciamo che deve andare a scrivere in un registro di nome [T]. Non porta mai a totale compimento la propria operazione. La cosa è proprio reale: quando andiamo a mettere istruzione nella reservation station anziché scrivere F6 andiamo a scrivere stadio dal quale arriva risultato. Istruzioni della reservation station avrà istruzioni proprio diverse da quelle del linguaggio macchina. E' l'hardware che sostituisce il valore del registro indicato. Ripetizione concetto nel lucido dopo. Dopo funzionamento step by step si capirà meglio! Possibile struttura che fa riferimento in parte a quella dello scoreboarding anche se è leggermente più realistica perché assomiglia molto al IBM360 e la cui implementazione tramite algoritmo di Tomasulo e in particolare del renaming è alla base dell'ulteriore passo che è l'esecuzione fuori ordine e cioè le strutture che permettono immissioni istruzioni prima che altre siano state immesse. Supponiamo che unità moltiplicazione faccia anche divisione, supponiamo coda istruzioni (from instruction unit, no problem algoritmo sia per istruzione RISC che per CISC), abbiamo registri floating point (FP registers), ci sono bus alimentazione: alle reservation station che ci sono piccole code in mezzo, queste debbono potere alimentare le unità funzionali perché quando non ci sono problemi di alea noi andiamo a prendere istruzioni direttamente dai register file. Abbiamo supposto di avere 3 reservation station, 5 per load e store. Abbiamo CDB che è quello che raccoglie risultati per mandarli a registri floating point, che va anche ad alimentare reservation station, cioè quando i dati pronti vengono anche catturati da reservation station in attesa. Istruzioni vengono estratte da coda e a seconda dell'operazione avviate a reservation station. Essendoci più reservation station ci possono essere più istruzione dello stesso tipo in coda. Più è complicata architettura in termine di unità funzionali e reservation station, più complicato sarà rete combinatoria che va a gestire aperture e chiusure perché deve tenere in conto di più condizioni. Questa logica di Tomasulo è applicabile a qualunque tipo di architettura. Algoritmo di Tomasulo si BASA sul concetto di renaming. Meglio della scorebarding perché anziché far aspettare qui mettiamo in collegamento produttori e consumatori, perché prima avevamo bisogno di usare RF come punto di condivisione, qui invece istruzioni rinominate possiamo avviare perché sappiamo dove andare a prenderci risultati. Per certi aspetti è generalizzazione di forwarding. Qui modifichiamo proprio nome sorgente. Quando reservation station è piena aspetto. Lì il compilatore avremmo fatto un pessimo lavoro. Se mi trovo in condizione di dover produrre dei dati per le istruzioni successive e non ho possibilità di produrle il sistema si ferma. Ci sono 3 fasi: emissione: ogni volta che immettiamo istruzione dentro reserv station andiamo a prenderci i dati o dal RF o facciamo renaming all'interno di istruzione. L'emissione è in ORDINE, quindi non posso emettere istruzione che ha bisogno di risultato che non è ancora stata immessa. Non posso emettere istruzione che “sorpassa” altra istruzione. Quindi i dati o sono in RF o lo sta producendo uno stadio. esecuzione: quando dobbiamo leggere dato è chiaro che lo stiamo aspettando, quindi è evidente che RAW sia risolto. Ogni registro sa da dove aspettarsi dato. Scrittura: produzione dato che va su CDB e su registro, se non è “condiviso”. Esempio: Tabella: operazione, valore operandi, Qj e Qk che sono il renaming. Se in Qj o Qk c'è valore diverso da blank stiamo aspettando da unità funzionale operando. Busy dice che RS è occupata. RFS: dice quello per tutti i registri del RF qual è l'unità funzionale che dovrà andarli a scrivere. Siamo più o meno in stessa situazione di prima: ci sono istruzioni con j e k (componenti che servono ad istruzione che sono OFFSET o registri). Abbiamo le tre fasi (4 nel caso dello scoreboarding) e dentro a quella tabella indichiamo ciclo in cui avviene. Sotto situazione RS, qui non sono riportate le LD e ST perché tabella diventa troppo lunga. 3 RS per adder (anche per SUB), 2 per MUL (che fa anche DIV). Ultima fila elenco di tutti i registri con le sorgenti che andranno a scrivere registri stessi. Ciclo 1: emettiamo LOAD F6 34+R2, RS1 è occupata. F6 punta a Load1. Dire RS o dire unità funzionale è uguale, in realtà è più corretto di RS perché potrei avere RS che sta eseguendo e l'altra che sta calcolando indirizzo. Ciclo 2: ho emesso una nuova load (ce ne sono 5 di RS). Tempo accesso memoria è 2 cicli. Intanto nella seconda ho immesso istruzione e la seconda non ha vincoli rispetto a prima. E anche per la seconda non abbiamo puntatori a risultato. I registri FP possono essere spezzati in due registri integer e se spezzo F6 in 2 .. ecc ecc .. ma non succede mai. Mondo FP completamente diverso da integer. Qui F2 punta a seconda RS. Quindi nel register result status si sta aspettando dato da Load2. Ciclo 3: adesso la nostra load è nel secondo clock di esecuzione, quindi seconda può avanzare. Siccome RS della MUL è libero possiamo far avanzare MUL. Situazione è che LD1 sta per finire, LD2 nel primo ciclo, MUL in immissione. MUL ha bisogno del risultato di LD2, infatti nel secondo registro della MUL, in Vj non c'è dato che ci serve ma puntatore a Load2. Notiamo che in F0 abbiamo come indicatore il risultato della moltiplicazione, quindi F0 si sta aspettando risultato di Mult1. Questo primo caso in cui una RS non ha operando che serve, ma effettuato un renaming. Ciclo 4: la LD1 finisce, quindi in F6 abbiamo valore prodotto (ad esempio M(A1)), alla fine di questo clock in F6 abbiamo quel valore che abbiamo ottenuto leggendo con LD. LD2 sta leggendo memoria, la MUL è in attesa purtroppo ancora della LD2 quindi è ferma. SUBD può avanzare. La SUBD ha un dato che alla fine di questo clock è nel RF e un altro dato che deve essere prodotto dalla Load2 e stessa cosa per MULT. Clock 5: finalmente LD2 scrive (M(A2)), la nostra SUBD che in questo momento si trova RS1 dell'adder ha entrambi i valori che servono, quindi SUB potremo farla andare avanti dopo questo clock. Anche MULT può andare avanti perché ha entrambi i dati che le servono. Intanto abbiamo fatto anche emettere DIV. Lì dentro abbiamo bisogno di risultato che c'è già in F6, ma è in attesa della MUL. MUL che però non è ancora finita. DIV quindi emessa ma non può andare avanti. Clock 6: prima in F6 c'era risultato della LD1 e ora puntiamo al risultato prodotto che viene emessa in questo momento, quindi se per caso c'era nome di sorgente non ancora finita comunque ora mettiamo questa nuova che abbiamo emesso in questo clock. Risultati scritti nei registri al termine della Write result. Stavolta è avanzato poco: MULT ha tutti i suoi operandi e può agire, abbiamo emesso ADD ma ha bisogno del risultato della prima Add1 che è una SUBD. Intanto stiamo facendo avanzare in fase di esecuzione la SUBD che era ferma perché aveva bisogno di risultato della prima LD. Tutta la sequenza è abbastanza facile .. Ciclo 7: unica cosa successa SUBD e MULT sono avanzate. Ciclo 8: produciamo risultato della SUBD e quindi lo scriviamo in F8, ma la distribuiamo anche a chi stava aspettando, cioè ADDD, quindi ha tutti operandi che servono e la somma fp è pronta per proseguire, al prossimo ciclo parte. Notare che somma fp finisce prima di MUL e DIV, ma questo non ci dà problema perché WAW è risolto da Register result status. SUBD finalmente finisce e va a sbloccare ADDD. RF concedere o bloccherà la scrittura di una richiesta di scrittura in base alle sue informazioni di chi deve andare a scrivere. Dato finisce solo se è risultato che RF si aspetta. Se abbiamo due unità funzionali che vogliono scrivere chi andiamo a prendere: quello che ha iniziato dopo. Supponiamo che una vuole scrivere in F6 e la seconda ancora in F6. Quando finisce prima deve andare a distribuire dato alle RS che servono e la scrittura su RF non è più così importante perché ci penserà l'istruzione che finisce dopo a farlo per quelle istruzioni che non sono ancora state immesse: il risultato finale di questa serie di operazioni è quello dell'istruzione dopo. Emissione avviene in ordine e quindi le eventuali istruzioni in mezzo puntano alla RS giusta. Register result status fondamentale per quando le istruzioni durano di più. Qui reale parallelismo nel tempo, mentre nel software è solo virtuale: perché dipende dal quanto di tempo (molto piccolo) assegnato. Notiamo che nei vari registri c'è sempre o l'indicazione della RS che mi produce risultato o i risultati che nel tempo sono stati prodotti. Maniera per rendere più chiara sequenza. ADD si impossessa del risultato che era in attesa, quindi altri 2 cicli poi termina .. Ciclo 9: SUBD terminata, ADDD è in esecuzione MULT va avanti Ciclo 10: va avanti e nel Ciclo 11 finisce ADDD ... ..... Ciclo 57: tutto è finito. Lucidi vecchi DEEP PIPELINE: nel caso del P4 i singoli stadi sono stati spezzati in più stadi per aumentare frequenza clock. Si chiama DEEP PIPELINE: 5 stadi DLX vengono suddivisi in stadi più piccoli (con ritardo minore) e possibilità di avere clock più veloce. Diverse figure grafiche delle diversi tipi di architettura. Pipeline diversificate assomigliano un po' a quello che è stato visto adesso. Nei discorsi non abbiamo mai preso in considerazione branch: non lo facciamo, è garantito comunque che è possibile attuare algoritmo di Tomasulo anche in caso di branch. Struttura è riconducibile a questa. Quello che andremo a vedere è ulteriore potenziamento di algoritmo di Tomasulo, in particolare renaming e andremo a vedere ulteriore condizioni su cui emissioni delle istruzioni può violare principio di sequenzialità: “esecuzione fuori ordine”.

Gerarchia delle memorie: più memoria grande più è lenta e più è piccola e più veloce. Il processore ha necessità di colloquiare con memorie molto veloci. Uno dei principali colli di bottiglia sono quelli del clock. Oggi frequenze di bus al massimo 1-2 Ghz di clock al contrario del processore che internamente arriva anche a diversi Ghz. Questo per la lunghezza dei fili dei bus e perché le memorie non si sono velocizzate moltissimo (min 50 nanosecondi). C'è un principio fondamentale di località: se un processore sta eseguendo un pezzo di programma (specialmente oggi che i programmi sono organizzati con linguaggi strutturati). Confinano il 90% ad un certo settore di memoria. In generale finisce che questi programmi operano su insiemi di dati omogenei e questi dati sono strutturati da tutti i compilatori in modo che siano continui. Questo è il principio di località. Questo ha un impatto non indifferente: presa l'intera memoria c'è in realtà una porzione piuttosto ridotta con la quale il processore ha necessità di colloquiare. Se noi riusciamo a copiare parte di memoria principale in una molto più vicina al processore nel 90-95% dei casi colloquierà con quella. Questa piccola memoria che è copia di memoria principale si chiama cache e le cache proprio perché siano il più possibili veloci sono (almeno per il I livello) all'interno del processore stesso, in modo da poter colloquiare con velocità uguale a quel del clock interno. Queste memorie vanno caricate e scaricate: in un loop che si occupano di certi dati, poi dobbiamo cambiare memoria con nuove istruzioni. Questo processo di caricamento e scaricamento è piccola frazione percentuale di quello che il processore esegue. Problema di carattere generale: coerenza, dobbiamo essere certi che, se in quella memoria centrale più processori accedono, i dati devono essere coerenti. Problema per il quale studiamo algoritmi specifici. In generale però gerarchia non è fatta solo da cache e memoria centrale. Oggi si tende ad avere vari gradi: una piccola e velocissima a contatto con processore (I livello integrata), poi cache di II livello (un po' più grande e un po' più lenta che può accelerare il passaggio di dati nella cache primaria nel momento in cui devo cambiarli con quelli della memoria). Esistono processori con cache di terzo livello. Si tende ad avere un “continuo” tra processore e memorie RAM: una sorta di frizione che cerca di armonizzare due sistemi che hanno velocità diverse, per rendere sistema compatibile. Memoria centrale contiene quantità molto più elevata di dati. Memoria centrale è di per sé un'altra cache: quello di dimensione maggiore è disco. La cache di I livello dell'ordine di 64-128-256 KByte. II livello 512Kb-1Mb (alcune integrate). III (raramente integrate, esempio XEON che sono dei P4 usati soprattutto per server) fino a 4Mb. Memoria centrale ordine di alcuni GB, nei server fino a 64-128 GB. Poi dischi dell'ordine del TByte. Cache si interpone sul bus, nel caso di II o III livello è un'ulteriore frammentazione. Se dato che stiamo cercando (di cui processore ha bisogno) si trova nella cache abbiamo avuto un “hit”, se non è nella cache abbiamo un “miss”. Normalmente in una cache i dati non sono i singoli byte, ma gruppi di byte tra loro continui. Noi parliamo di linea di cache. Gruppi di byte: 32-64 o 128 contigui allineata. Ci fa comodo avere sulla linea tutta la word. E' bene che i dati siano allineati.

Memorie associative

[modifica | modifica sorgente]

Per ora memorie sempre concepite con un meccanismo in cui noi diamo indirizzo e la nostra memoria al suo interno effettua una sua completa decodifica dei bit meno significativi che non fanno parte dei CS ed individua una cella ben specifica con bit meno significativi. Diverso meccanismo per memoria associativa: noi presentiamo intero indirizzo a memoria e la memoria è costruita con un insieme di slot (celle), ogni slot contiene una linea di dati (32, 64, 128) e tutti i bit di indirizzo più significativi di quella linea. Se abbiamo bus a 32 bit e linea da 32 byte (5 bit interni linea e 27 restanti dove si trova linea). Questo nome della linea si chiama TAG (cartellino): identificativo. Non è organizzato in modo sequenziale per le linee, caricate in questa cache con le logiche che vedremo. Presenteremo indirizzo che stiamo cercando e la nostra memoria fa un confronto in parallelo tra tutti i TAG e quello che abbiamo presentato: perché associa dei dati la parte più significativa del loro indirizzo. L'indirizzo presentato con tutti i TAG della nostra memoria associativa. Se confronto ci dà risultato positivo abbiamo hit, se no miss. Avendo presentato l'intero indirizzo, la parte meno significativa individuerà all'interno della linea il dato che dobbiamo cercare. Se la rete di confronto è sufficientemente rapida ovviamente abbiamo reperimento dato in un tempo rapidissimo. Nelle memorie normali perdiamo molto tempo nella decodifica (lungo catena di decodificatori e il tempo di accesso è il tempo di assestamento di questi decodificatori). Memorie più piccole sono più veloci: RAM piccole hanno rete di decodifica più piccola e quindi rete di accesso minore. Se qui vogliamo essere veloci dobbiamo avere dei comparatori molto veloci. Nel caso specifico di linea da 32 byte e parallelismo da 32 bit: 27 comparatori da due bit le cui uscite devono andare in uno stesso AND. La parte più significativa si chiama TAG, quella meno significativa si chiama OFFSET (spostamento all'interno della linea). Naturalmente andiamo a leggere in base al parallelismo della memoria. C'è un grosso problema: abbiamo detto che il confronto richiede 27 comparatori da due bit, ma questo vale per ogni linea della cache: noi abbiamo n linee; dobbiamo confrontare tutte le linee 27 comparatori + and a 27 ingressi moltiplicato per il numero di linee. Quindi esplode numero dei comparatori, tanto che abbiamo più comparatori che linee di cache. Bisogna venire a patti. Vantaggio è che deve essere velocissima. Contiene serie di dati che mi servono, ma non tutti. Naturalmente ci sono algoritmi per scegliere linee di cache. Tendenzialmente si scarica linea con accesso più vecchio. Linea = 32 64 o 128 dati consecutivi corrispondenti o presenti in uno slot della cache. Per ridurre complessità dei comparatori si ricorre a trucco limitativo.

Cache set-associative

[modifica | modifica sorgente]

Si usano cache set-associative: questi comparatori sono troppi, trovo modo per ridurre numero comparatori o che comparazione avvenga solo con una certa linea di cache. Supponiamo di scomporre TAG in due parti: uno è TAG vero e proprio e l'altro è Index. Index mi va a puntare tramite decodifica ad una particolare linea di cache. E con il TAG di quella linea di cache faccio il confronto. Questo ha senso se quando carico linea di cache la vado a caricare a quello slot per i 3 bit intermedi all'INDEX che sto usando. Questo cache: nella cache non posso avere più linee con stesso INDEX. Quante linee esistono che hanno stessi bit intermedi? Infiniti! Ma solo uno è quello che mi serve. Noi sappiamo che se c'è già è lì dentro. Per ogni INDEX ci può essere una sola linea. Per ogni INDEX c'è uno slot. In questo caso (set-associativa): l'INDEX punta ad uno slot, in quello slot c'è linea il cui INDEX è sicuramente quello che stiamo cercando, non sappiamo se gli altri bit corrispondano ai bit che stiamo cercando o meno. Tag-num bit INDEX: ridotto complessità comparatore. Index non sono i più significativi per non andare a prendere quelli consecutivi: concettualmente posso metter INDEX dove mi fa più comodo. Memoria precedente è full-associativa. TAG linea complessiva è sempre quella: noi li usiamo in maniera diversa. Questa limitazione è piuttosto pesante: devo ridurre flessibilità nostra cache. Per ogni INDEX siamo molto vincolati. Si cerca di allentare questa complessità concedendo più decodificatori (aumentando complessità cache), si usano cache associative a più vie. Per ogni INDEX anziché avere un solo slot ho o 2 o 4 o 8 linee, tutte corrispondenti a stesso INDEX. Quindi confronto sarà fatto con quei 2 4 o 8 bit che rimangono. Onesto compromesso: se abbiamo cache associative a 4 vie. Per ogni INDEX abbiamo 4 vie ognuna caratterizzata da stesso INDEX. Esplicitati i concetti nel lucido 8 Disegno generale: blocco di cache è dato da dato (linea), TAG che lo contraddistingue e bit di stato non ancora citati. Supponiamo di accendere macchina: ci sono dei dati casuali nella cache e in quel momento all'atto del RESET tutte le linee siano considerate non valide. Bit di stato, ma in realtà più di uno. Qui è un disegno riassuntivo, l'INDEX viene usato da decodificatore per selezionare un livello della cache, nel livello della cache ci sono tante linee quanti sono quegli elementi e il confronto del TAG viene fatto in parallelo tra TAG presentato e tutti TAG di quelle linee di quel particolare INDEX. Confronto può dar luogo a hit o miss. Oggi con delle politiche ben fatte delle cache abbiamo probabilità di hit di I livello superiori a 90% e intorno a 97% avendo anche cache di II livello. C'è rimbalzo a secondo livello se non si trova al primo. Se cache è piccola risponde in un ciclo di clock. Oggi più grandi e richiedono diversi cicli di clock. Se c'è hit su cache di secondo livello, ma devo trasportarlo nella cache di prima livello. Cache di secondo livello consegna dato a processore e intanto viene spostato nella cache di primo livello. Processore presenta indirizzo, di fianco c'è sistema delle cache: viene fornito dato quando rispondo o cache I livello, o II livello .. La pipeline non sa da chi viene il dato. Sistema associato a processore che è quello delle cache. Scrittura: cosa succede quando devo scrivere dato? Se dato è nella cache lo scrivo nella cache. O la scrivo nella cache e basta oppure sia sulla cache che in memoria. Le scritture sono più rare rispetto alle letture (scrittura in memoria costa meno): il processore non perde tempo a scrivere ma consegna dato da scrivere (posted). Se abbiamo sistema multiprocessore, se quando processori scrivono anche in memoria abbiamo sempre coerenza, quindi quando un altro agente (processore, DMA .. agente) vuole andare a leggere in memoria sa che dato è recente, se non è così ci vuole sistema che ci dica se dato è recente o meno. Se voglio scrivere dato che non è in cache: write allocate o no-write allocate. La prima è quel sistema che PRIMA del sistema viene letto dalla memoria e portato in cache (anche se rallenta). Altro se dato non è in cache scrivo nella memoria vista da cache di I livello perché questa non sa se a valle c'è memoria o cache di II livello. Pentium non sa chi c'è a valle della pipeline (se cache o memoria). Il vero passaggio di corrente è quella che avviene durante transizioni. Le cache possono essere programmate. Il write back: scrivo e rimbalzo il dato in memoria: questo è indispensabile per politiche di coerenza. Nel caso specifico lo abbiamo reso più “umano” mediante uso di cache, perché il DLX necessitava di natura Harvard: due memorie distinte, una per lettura istruzioni e l'altra per dati memorie se no alea di accesso a memoria. Con cache si risolve: una cache per dati e una per istruzione che contiene entrambi. Cosa che succede anche nei nostri PC: cache differenziati e memorie centrali unificate. Nel DLX reale in realtà i 5 stadi tipici del DLX funzionano in split-cycle (quel sistema che si ha cambiamento di stato nella prima metà del clock e cambiamento di stato nella seconda metà del clock). In realtà sono riportati gli stadi e i due clock con cui funzionano in contro-fase. Nel primo stadio del fetch abbiamo traduzione indirizzo virtuale: la memoria virtuale non l'abbiamo ancora toccata. Indirizzi prodotti da processore necessitano di traduzione. Nella seconda fase del clock con l'indirizzo così realizzato vado a leggere cache di istruzioni, do per scontato di essere fortunato (se no stallo in attesa della memoria), poi nella prima fase del ID andiamo a fare test del TAG per vedere se è quello giusto e controlliamo parità, seconda fase leggere registri e calcolo destinazione. Discorso analogo accesso da parte della memoria. Seconda parte dello stadio EX generato indirizzo, tradotto, portato a cache dei dati e viene letto nella prima parte della memoria. Idea di come il DLX iniziale stiamo costruendo intorno a DLX: pipeline delle istruzioni floating point. Ora mettiamo attorno cache. Alcune delle cose saranno spiegate più avanti.

Efficienza cache

[modifica | modifica sorgente]

Efficienza cache: alcuni elementi caratteristici cache. Se abbiamo un cache hit l'accesso a cache richiede un po' di tempo. Se clock basso ce la si fa, se clock lento abbiamo un minimo di tempo di latenza. In un certo senso c'è sempre un ritardo tra emissione dato e il momento in cui dato lo vado a prendere che è nel semiclock successivo. Nella realtà i tempi di ritardo sono uno o 3 clock. Pipeline sono fatte in modo da sovrapporre altre istruzioni ai tempi di accesso delle cache. Quando non c'è dato tipicamente pipeline si stalla. Ci sono metodi un po' particolari per accesso a cache, noi vedremo che lo stadio successivo che lo stadio successivo all'algoritmo di Tomasulo è stata l'esecuzione totalmente fuori ordine (o speculativa). Anche per le cache si fa una sorta di speculazione: si dà fuori INDEX e TAG e si opera come se il dato fosse quello giusto, perché controllo del TAG è l'operazione più lenta che ci sia, quindi si fa come branch target buffer (strano sistema per fare evitare dove possibile qualunque stallo legato al branch nell'ambito del DLX, nel IF si presenta l'indirizzo dell'istruzione ad una tabella associativa la quale dice se il dato ce l'ha o no all'interno, se c'è un salto se l'ha preso o no). Si va a prendere dato comunque e si procede come se fosse giusto, salvo verificare se era quello giusto o sbagliato. Se sbagliato si butta via dato e si ferma pezzo di pipeline avanzata. Metodo speculativo: tendenzialmente vuol dire ipotizzato, supposto considerato. Nel 95% ci prendiamo, se sbagliamo paghiamo 7 clock. Tempo di accesso: la cache è insieme fisico di dispositivi, c'è tempo di propagazione (segnale deve andare a recuperare dati nella cache, possono essere vicini o più lontani, sul chip c'è spazio che deve essere percorso). Ci sono una serie di informazioni: più la cache è di grandi dimensioni, maggiore è la distanza Manhattan: somma che il segnale elettrico deve percorrere tutta la via degli slot e poi per recuperare dato. Cache più grandi sono un po' più lente a parità di tecnologie. Se io integro cache di I e II livello i tempi di accesso sono diversi perché diverse le dimensioni. Come progettare dimensioni cache: aumento il numero di linee o aumento la dimensione delle linee. Mi conviene linee grosse o meno, ci sono limiti come numero di vie. Impattano statisticamente sull'efficienza. Elemento puramente statistico: programmi che hanno molti loop di lunga dimensione vantaggiosa la cache. Tutta una serie di parametri che influenzano. Qualche diagramma di come si comportano le cache. L'insieme dei dati e delle istruzioni che in un particolare momento un programma sta usando (loop e vettore che sta operando), si chiama anche working set. 3 parametri: “compulsory miss” è un miss obbligatorio perché all'inizio devo riempire cache. Altro parametro è “capacità” della cache che ha a che vedere anche con il fatto se il programma è stato costruito o meno da favorire l'inserimento della cache. “Conflitti” sono problematiche che si pongono quando in cache associativa devo scaricare uno dei set perché lo devo rimpiazzare con un altro. Queste 3 C che influenzano cache. Frequenze di miss: più grande è meglio è. Quanto incide associatività con frequenza. Dobbiamo anche capire come incide dimensione blocchi con la frequenza. Più i blocchi sono grandi meglio sfruttano la loro località spaziale. Blocchi più grandi tendono a favorire località spaziale, ad avere più dati che sono tra loro contigui. Se il programma si muove rapidamente tra blocchi diversi, avendo blocchi molto grossi sono costretto a caricare e scaricare in continuazione. Il valore ottimale dei blocchi è compreso tra 64 e 256, sopra i 512 i numeri di miss tende ad aumentare. Blocchi grandi riducono miss compulsory. Fa vedere come variano i miss dovuti al conflitto in funzione della dimensione e della associatività. I miss compulsory sono praticamente indipendenti dalla dimensione e dall'associatività, mentre i miss dovuti a capacità, diminuiscono all'aumentare del blocco. Al diminuire dell'associatività aumentano i conflitti: nello stesso INDEX anziché esserci un dato ce ne sono più di uno. Altre considerazioni ..

Branch Target Buffer

[modifica | modifica sorgente]

è una memoria associativa, ha il comportamento della cache. Nello stadio IF confrontiamo l'indirizzo del fetch con gli indirizzi caricati dentro a BTB, caricati tutte volte che abbiamo individuato un indirizzo come branch. Se confronto da hit dobbiamo saltare o no: politica che ci dica se è più probabile o meno che il salto venga o meno eseguito. Ricordiamo che qui non abbiamo ancora decodificato istruzione: qui sappiamo che è branch dall'indirizzo che prendiamo dalla memoria, perché nel passato lo abbiamo riscontrato. In questa memoria c'è anche l'indirizzo a cui dobbiamo saltare. Organizzazione può essere set-associativo: INDEX e TAG e andiamo a fare confronto. Problematiche di predizione: un bit solo non basta. Uso un bit: predico sulla base dell'ultima decisione, ovvero se la volta precedente il salto è stato preso predico che anche questa volta sia preso. Supponiamo di avere un solo bit: quando siamo nel loop 2 questo viene predetto come salto, il bit ci dice che dobbiamo predire che si salti. Quando però arriviamo alla fine del loop 2 la predizione è sbagliata perché dobbiamo entrare nel loop 2, stessa cosa a fine del loop 1. Sistema più sofisticato a due bit: perché predizione venga cambiata deve avvenire che per due volte deve o non prenderlo o prenderlo. Come vediamo dalla rete: due stati prima di cambiare, abbiamo reso leggermente un po' più resistente il sistema, in termini elettronici è un filtro passa-basso: piccola variazione si ripercuota su stato sistema. Tipica situazione a due bit: meccanismo che viene utilizzato spesso. Inizialmente sarà vuoto, alla prima occasione: quando nello stadio ID andremo a mettere quell'istruzione in uno slot. Metteremo come predizione il valore che nello stadio di EX andiamo ad individuare e quindi ci portiamo in uno dei due stati stabili. Da quel momento in poi inizia funzionamento: esiste in tutti i sistemi fase di avviamento. Quando noi accendiamo PC abbiamo transitorio di assestamento del sistema, anche piuttosto lento. Algoritmi avanzati per predizione del branch: Vediamo altri algoritmi: il primo è un preditore adattativo a due livelli. Ci sono due registri. Sono sistemi un po' sofisticati, quindi costano in termini di silicio: bilanciamento quanto silicio si dedica ad una cosa o ad un'altra. E' un problema di bilanciamento del sistema. Qui due tabelle: branch history table e pattern history table. Noi abbiamo una storia dei branch: nel passato indipendentemente dalla predizione abbiamo verificato che un branch è stato preso o non preso (verifica fatta durante esecuzione, che può verificare se predizione è giusta o sbagliata, in questo caso svuotare tutta pipeline). La sequenza verde è la sequenza dei branch presi o non presi nel passato. Non facciamo distinzione tra i branch: unica sequenza che ci dice se preso o non preso. Quelli in blu è una tabella che per ogni possibile configurazione del branch history table quali decisioni nel passato. Se per un branch in presenza di quella storia di definirlo come taken o not taken. Sulla base di che cosa? Qui non è indicato l'algoritmo di decisione che può essere il più complicato o semplice. Ogni processore avrà una sua politica di decisione che stabilisce se volta per volta vuole prenderlo o meno. Sulla base di quella decisione si può decidere cosa fare in futuro. Sulla base di queste informazioni decidiamo che cosa andiamo a predire, quindi nuovo valore nel pattern history table, poi eseguirà istruzione e metterà valore nel BHT. Quindi PHT è predizione, BHT quello che è realmente successo. Algoritmo abbastanza semplice. Più lunga è più le informazioni sono sofisticate. Scelta dimensione è uno dei tanti elementi di progetti che ci porta ad una scelta o ad un'altra. Questo sistema mischia completamente tutti i branch, fa sì che un branch influenzi un altro anche se non sono correlati. Quando noi eseguiamo loop possiamo avere n branch dentro, quindi cambia molto rapidamente. Richiede n flip flop per quant'è la BHT, + m (num flip flop per ogni PHT) moltiplicato per 2^n. (n+2^n*m). Situazione più sofisticata: BHT differenziata per ogni branch; quindi storia differenziata per ogni salto. Potrebbe essere associata alla BTB. La tabella è differenziata per indirizzo: ogni branch ha particolare indirizzo del PC, quando raggiunge certo valore prendiamo BHT e mediante il suo contenuto per quel particolare branch puntiamo ad una delle 2^n pattern history table che abbiamo nel passato. In presenza di quella storia di quel particolare branch quale decisione dal passato? Anche qui c'è debolezza: il sistema che stiamo presentando è uguale alla stessa PHT per tutte le differenti BHT. Abbiamo differenziato le BHT, ma abbiamo PHT unica. Ragionevole avere tabelle di decisione diverse per ogni BHT e per ogni branch presente nel sistema. BHT differenziate e PHT unica. Quanti flip flop? n*k+2^n*m. Algoritmo più sofisticato: abbiamo BHT differenziate e tabelle PHT differenziate. Una PHT per ogni branch, il numero qui aumenta di molto. Questa struttura la si incontra abbastanza raramente. In aggiunta a tutti i flip flop per le tabelle ci sono anche tutte la logica per realizzare l'algoritmo. Questa dà concettualmente miglior risultato, ma se andiamo a vedere i benchmark a parità di algoritmo decisionale si scopre che il miglioramento ottenuto con la struttura più sofisticata rispetto a quella più “brutale” della prima struttura. Molto spesso se si implementa un algoritmo di questo genera si implementa nella versione più semplice (la prima). Algoritmi di rimpiazzamento: Noi abbiamo detto che nelle memorie associative o nelle cache che sono lo scopo delle memorie associative nei nostri sistemi. Normalmente ad un certo punto i BTB si riempono e a quel punto c'è il problema che se ne devo immettere un altro ho un nuovo branch. Per cache dati e indirizzi si usano sempre set-associative. Ma ci sono cache TLB di tipo full-associative. Rimane problema che dobbiamo rimpiazzare e trovare politica intelligente (quale linea rimpiazzare): problema costo prestazioni. Possiamo avere sistemi più o meno sofisticati. Prima politica che è quella del tiro dei dati. Abbiamo memoria set-associative e prendo una via a caso. Mediamente ne andrò a scartare una che mi serve mediamente. Il tutto si limita ad un generatore di numeri casuali a due bit: si prendiamo due uscite shift register e le inseriamo in un sommatore modulo 2 e questa uscita la mettiamo all'ingresso dello shift register otteniamo un contatore che se n sono i clock conta per (2^n-1) solo che i numeri che escono hanno distribuzione uniforme. Un altro sistema di decisione che è il massimo della sofisticazione: ci vuole una rete logica per ogni INDEX, questa rimpiazza sempre quella utilizzata meno recentemente. All'inizio abbiamo uno shift register di 4 bit alla volta che sono i registri Na Nb Nc e Nd: 4 register che sono fra loro connessi. Inizialmente shift register è vuoto, a mano a mano che mettiamo dentro un dato in una delle vie mettiamo numero via nel registro corrispondente. A un certo punto nel nostro shift register avremo tutti i numeri delle vie. Concepiamo questo shift register come il numero della via più a destra che è la via meno recentemente utilizzata. Il numero della via più a sinistra è quella più recentemente utilizzata. Hanno un grado di tempo d'accesso variabile e sempre crescente verso destra. Supponiamo di avere hit su quella via deve diventare quella meno candidata ad essere scartata. A questo punto le altre vie dovranno essere modificate come tempo di accesso. E allora noi in pratica il clock che fa traslare il numero delle vie tra i vari registri è il segnale clock e notiamo che c'è un OR esclusivo tra registro e numero via che ha ricevuto hit. Chiaro che se l'or esclusivo non dà risposta 0, cioè vuol dire che contenuto registro è diverso da via che ha ricevuto hit. Quindi clock si propaga anche stato successivo. Quindi fa traslare fino a che non incontra numero via che ha ricevuto hit. Supponiamo di avere: 2 1 0 e 3. E supponiamo di avere hit su via numero 1. 1 deve entrare in Ra perché è il più recente. In Ra c'era 2: 2 XOR 1 = 1, quindi clock si propaga. Quindi Rb campiona vecchio Ra che era 2. Nb: 1 XOR 1 = 0. Non si propaga. Ora abbiamo 1 in Ra e 2 in Rb, le altre. Il più vecchio era 3, poi 0 poi 1 e poi 2 prima dell'hit. Quindi che 3 e 0 erano entrambi più vecchi di 1 e 2. Ora il più recente era il 2, ma con questo hit 1 diventa più recente e 2 un po' di meno. Situazione finale 1 2 0 3. Clock si propaga fino a quando incontra indirizzo dell'hit. E se invalidiamo una linea? Oltre infatti ad essere riempite possono essere invalidate. Se un altro processore scrive byte di quella linea: è evidente che dato nella cache non è più valido. Quella linea va invalidata. Sappiamo che quella linea non è più valida. Supponiamo: 2 1 0 3. Invalidiamo la 0, dovrebbe avvenire che la 0 debba andare al posto della 3 e all'ora succede che ogni volta che c'è un'invalidazione tutte le altre linee traslano di una posizione a sinistra e la linea invalidata deve essere messa nella linea più a destra. Questa seconda parte del circuito non è disegnata ma è assolutamente duale rispetto a questa. Linea entra da destra e deve far traslare tutte le linee fintanto che non si incontra la linea dell'hit. Questo è sistema più sofisticato. Aumenta di molto la complessità. Di questi circuiti ce n'è uno per ogni INDEX perché per ogni linea ha diverso grado di vecchiaia, quindi circuito molto costoso. Questo sistema ha un corrispettivo con implementazione meno pesante. Logica che ci sta dietro: quando abbiamo hit su una via, tutte le linee erano meno vecchie diventano più vecchie della via che ha subito l'hit mantenendo tra loro stesso grado di vecchiaia. Si concretizza che tutte le vie che non hanno avuto l'hit traslano a destra. La nostra hit diventa la più giovane. Caso opposto nel caso di una invalidazione: questa diventa la più vecchia e candidata ad essere rimpiazzata e tutte le vie che erano più vecchie diventano di un poco più vecchie. In un caso faccio traslare vie più giovani per diventare più vecchie, viceversa per l'atra. Lucido 39: Altro sistema - sistema dei contatori: si associa ad ogni via di ogni SET (cache set-associative a più vie, che è il caso più generale), per ogni via di ogni SET noi associamo un contatore che ha modulo fino al numero delle vie che fanno parte di quel set. Qui abbiamo fatto caso di 4 perché abbastanza comune. Cache associative per SET fino ad 8 vie: oltre non otteniamo più vantaggio perché abbiamo moltissime vie che hanno stesso INDEX ma ci troviamo con pochi INDEX. Per ogni via esiste un contatore di modulo numero delle vie. Questo contatore posto a zero ogni volta che su quella via c'è un hit che equivale a porlo nella prima posizione dello shift register di prima. Se ad ogni posizione dello shift register dessimo un valore, la prima posizione avrebbe valore 0. Ogni volta che abbiamo hit su una via incrementiamo di 1 tutte le vie che precedentemente avevano valore più basso. Noi infatti candidiamo al rimpiazzo quella che ha valore massimo di contatore. Quella via è via più vecchia: metodo di contatori di cui qui abbiamo esempio anche se concetto sempre il medesimo. Ogni volta che abbiamo hit su una linea tutte quelle di valore più basso aumentano di uno e quindi sono più candidate al rimpiazzo. E' esattamente stesso meccanismo dello shift register. Stesso concetto per invalidazione: tutte le vie più vecchie diventano un po' più giovani. In questo caso se abbiamo una invalidazione, questa via assume valore massimo e tutte le vie che precedentemente avevano valore superiore vengono decrementate di uno. Ma il decremento equivale a spostamento verso sinistra. Lucido 40: C'è un altro paio di algoritmi: questo è algoritmo pseudo-ottimale che ha grosso vantaggio di essere molto semplice. Pseudo-LRU (least recently used): cerchiamo via che è stata più precedentemente usata. Qua nel caso di 4 vie usiamo 3 bit per tutte le vie (non una per ogni via) di un particolare SET e usiamo questi bit in questa maniera: I0 suddivide le vie in due gruppi e dice se ha avuto un accesso più recente un gruppo o un altro gruppo. Gli altri 2 bit individuano all'interno di questo gruppo quale delle due vie che è stata più recentemente usata. Questo è algoritmo molto semplice. Ha un difetto perché che nello stesso gruppo possono convivere sia la via più recentemente usata sia quella più vecchia di tutte. Se suddividiamo gruppi in base a quella più recentemente usata, ma contiene anche quella più vecchia. Abbiamo problema di qualità. Il problema va affrontato in ottica costo/prestazioni: siamo così sicuri che questo algoritmo non ottimale dia risultati così scadenti? In realtà non è così: spesso questi algoritmi sub-ottimali danno un tale vantaggio in termini di circuito che abbiamo uno svantaggio piccolo in termini di prestazioni. Lucido 41: Ultimo caso: FIFO: c'è un solo contatore che ogni volta che inseriamo via aumenta. Il più vecchio è quella puntata dal valore massimo. Anche questo è sub-ottimale. Difetto che se invalidiamo linea in mezzo al contatore in questo sistema non siamo in grado di sistemare tutto quanto. Problema relativamente modesto perché caso di invalidazione è molto più raro rispetto ad un hit. Effetto molto modesto, sistema molto semplice: un contatore per ogni set a tanti bit quanti sono le linee. La lettura di un dato da parte del processore con conseguente caching è l'ultima operazione della catena: prima accesso al segmento (tramite registri ombra), poi abbiamo la formazione dell'indirizzo lineare o virtuale, questo indirizzo viene dato in pasto al sistema di impaginazione (se attivo) che si avvale del TLB che è una cache che non contiene dati, ma contiene porzioni della tabella delle pagine (porzione del mapping). Questo ci dà luogo ad indirizzo fisico con il quale alla fine andiamo a prenderci il dato in memoria. Quel dato che andiamo a leggere in memoria, viene anch'esso portato in cache come ultima operazione perchè la cache lavora su indirizzi fisici e quindi è l'ultimo anello della catena ed esistono cache che lavorano su indirizzi virtuali (un po' in anticipo rispetto al sistema visto), ma è del tutto evidente che se la cache lavorasse con gli indirizzi virtuali poichè indirizzi virtuali uguali in task diversi danno luogo ad indirizzi fisici diversi avremo un problema di condivisione dei dati all'interno della cache. E' evidente che gli stessi dati fisici dovrebbero avere accessi da indirizzi logici diversi che all'interno della cache punterebbero ad posizioni diverse. Se gli indirizzi logici per lo stesso indirizzo fisico in task diversi sono diversi è chiaro che cache che si basa su indirizzi logici avrebbe difficoltà a scegliere. Ecco motivo per cui tendenzialmente non si usa!

Del DLX non abbiamo ancora visto il BUS esterno. I bus dei sistemi Intel sono molto complicati, quindi non facciamo analisi complessiva. Riprendiamo la storia dei sistemi e ci occupiamo di una delle pietre miliari: Pentium 1. E' stato il primo processore super-scalare. E' l'ultimo processore che non ha esecuzione fuori ordine (è super-scalare, ha FP separata, bus 64 bit ...). Andremo anche ad analizzare il suo bus, perché è ancora gestibile e si avvicina ancora ai bus con 8086 e 8088, poi breve excursus sui bus più moderni. I bus trasportano sostanzialmente solo i dati. Vedremo metodi efficienti per gestire trasferimenti multipli e per abbattere tempi di wait (caso Pentium successivi), nel quale si emettono più richieste senza aspettare che ritorni: quando sorgente che ci deve fornire risultato dice che ce l'ha. Il tempo morto tra quando emettiamo il dato e la sua ricezione può essere usato per immettere altre richieste o altro .. Tende a sfruttare in modo totale la larghezza di banda del bus, questa dipende sia da parellelismo, sia da tecnologia dei dispositivi sia lunghezza fili. Mentre velocità luce 300.000 Km/s; su un bus siamo sui 7-8-9.000 Km/s; questa velocità è nell'ordine di grandezza dei tempi che noi abbiamo sul bus. Questa lunghezza quindi incide significativamente. Noi vogliamo che fronte (positivo/negativo) arrivi dall'altra parte. Questo è indipendentemente da tempo risposta memoria. Noi schematizziamo come un carico capacitivo il comportamento della linea, ma sulla linea ci sono tutti una serie di effetti: effetti capacitivi, induttivi, di accoppiamento .. Tendiamo a modellare con parametri concentrati o distribuiti, ma questi parametri non fanno altro che riassumere tutti gli effetti sulla linea. Si usano leghe di rame perché sono degli ottimi conduttori. Noi siamo abituati a dei costi dell'elettronica assolutamente irrisori (scheda madre 300-400$). Se venissero realizzati con materiali molto costosi, avremmo processo di produzione molto speciale. Entrano in un processo di costruzione abbastanza standard. Architettura: Qui c'è architettura del Pentium 1, in cui ritroviamo argomenti già conosciuti. Prima di tutto bus esterno a 64 bit e questo è rimasto ancora oggi. Oggi abbiamo bus a 64 bit, mentre internamente processore è a 32 bit: questa differenza era quella che c'era anche nell'8088 (16 bit esterno e 8 bit interno). Avere bus a parallelismo superiore tende ad ottimizzare ritardo di accesso al bus stesso: trasferiamo più dati e quindi ci “mettiamo un po' avanti”. Se dobbiamo scrivere dati in memoria usiamo byte, word e double word. Altre operazioni come dati FP a 64 bit (trasformati internamente a 80 bit). Questi dati sono standardizzati. Standard IEEE (Institution of Eletric and Eletronic Engeniering, che collabora con ISO). Unità FP a 80 bit, lavora a stack con stack ad 8 posizioni. Questo è uno di quei in casi in cui si usa bus a 64 bit, altro caso è il problema dell'approvvigionamento istruzioni. Noi abbiamo cache del codice (code cache), che è distinta da data cache e questo dà ragione dell'architettura Harvard: se ci sono cache distinte (data e codice) è logico che possiamo avere unica memoria, perché struttura Harvard è deputata a cache. Notiamo che potremmo trovare certe realizzazioni una unica cache (dati e istruzioni), in questo caso vuol dire che è cache multi-porta in grado di rispondere insieme a due richieste, quindi è vero che è unica ma è in grado di rispondere a due richieste. Noi abbiamo 2 pipeline che si chiamano U e V e che sono pipeline non identiche: una più agile e una che è capace di effettuare operazioni più complesse. In futuro non sarà così, ma qui eravamo negli anni '90 in cui quantità di transistori da implementare era ancora problema molto grosso (oggi minore legata a temperatura). La data-cache quanto meno doppia porta, perché le due pipeline devono leggere/scrivere insieme. In realtà tripla porta per scrivere anche in memoria. Poi abbiamo nostro predittore dei branch, che è elemento fondamentale per efficienza del sistema. Branch predetti nel primo caso (nel momento in cui estraiamo codice) e verificabile nella fase dell'EX. Qui abbiamo due pipeline che vanno in parallelo quindi in caso di predizione sbagliata dobbiamo svuotare entrambe le pipeline. Abbiamo anche problema di RAW sulle due pipeline. In integer abbiamo anche moltiplicazioni e divisioni. Piccolo confronto tra l'ultima pipeline di un processore scalare (486) e un processore superscalare. Rispetto a pipeline di DLX non c'è fase di MEM e ci sono due stadi di DEC. Questo per solito problema di modalità di indirizzamento molto sofistica e quindi dovremo dedicare un intero ciclo di clock. Ancora oggi questo problema: è così problematico che le istruzioni ereditarie che vengono da 8086 che in realtà nei processori moderni c'è stadio che trasforma quelle istruzioni in istruzioni di tipo RISC. Per fare questa operazione si impiega molto silicio (a volte mappaggio 1:1 tra istruzioni CISC e RISC altre volte diverso). Perché non c'è stadio di MEM? Lo saltiamo perché nel momento in cui produciamo risultato nello stadio di EX e scriviamo in memoria, in quello stadio scriviamo in cache. Scrittura da cache a memoria avviene solo quando dobbiamo rimpiazzare dato in memoria. Chiaro che se dato non c'è in cache, non possiamo scriverlo, ma scriviamo direttamente in un registro che darà luogo a meccanismo di scrittura (posted write). Stadi del Pentium: Alcune precisazioni: pipeline U quella più complicata (che esegue anche istruzioni multi-step), mentre la V esegue solo istruzioni semplici. Le istruzioni sulle due pipeline non possono avere alee incrociate: se questo succede una delle due pipeline viene stallata. Qui parallelismo molto legato al fatto di non avere interdipendenza, questo fatto ci riporta molto alla capacità di compilatore generare sequenzi di istruzioni che evitino questi problemi. La maggioranza di istruzioni semplici sono cablate. Cioè non sono istruzioni micro-programmate: sono istruzioni che per essere eseguite richiedono molti step e questi sono cablati in una memoria che passo passo viene scandita per quella particolare operazione e dà comandi alla ALU, per fare le operazioni. Per esempio nella moltiplicazione ci sarà un algoritmo interno, quindi scritto con una sequenza di micro-istruzioni le quali vengono scandite fino alla fine (shift and add). Altre operazioni hanno controllo molto semplice (realizzabili in uno/due clock) che non sono micro-programmate. Vi sono 2 prefetch buffer, cioè l'alimentazione della pipeline è fatta tramite 2 buffer di 32 byte e guarda caso sono la dimensione della linea di cache presente nel Pentium: quindi con un solo accesso prendiamo linea e riempiamo buffer. Mentre uno dei due buffer alimenta le due pipeline, l'altro va a prendersi i 32 byte successivi. Prefetch che sarà giusto o dovrà esser svuotato a seconda che il BTB individui nelle istruzioni che sono in fase di avviamento alla pipeline una istruzione di branch o meno. Questo può avvenire due volte (svuotare prefetch, il BTB si è sbagliato e c'è da tornare a riempire prefetch). Se ci sono alee vengono individuato nello stadio D1 (anche nel DLX alee individuate nello stadio ID), questo si confronta il nome dei registri coinvolti nell'operazione con i registri in produzione all'interno della pipeline (confronto combinatorio). Se match abbiamo un'alea e quindi di un RAW. Qui non c'è problema di WAW perché istruzioni terminano nella sequenza in cui sono immessi. Neanche RAW. WAW e RAW sono problematiche che si realizzano quando c'è sovvertimento di ordine istruzioni. Le due pipeline vanno di pari passo: se una si stalla, si stalla anche l'altra. Questo giustifica WAW e RAW e producono dati nella stessa sequenza previste. Quindi aumento della produttività non completo e garanzia di produzione in ordine dei risultati. Schema generale: maggiormente evidenziate le caratteristiche specifiche. Le due pipeline sono in verticale. Quella di sinistra ha “barrel-shifter” (circuito combinatorio che permette di effettuare qualunque rotazione o shift in un solo colpo di clock, questo corrisponde al fatto che all'ingresso di ogni stato deve essere mandato uscite di ogni altro stato). Numero di mux cresce enormemente e quindi barrel-shifter dal punto di vista realizzativo. 2 cache a 8 KByte set-associative a due vie. Sulla destra unità FP. Due altri elementi che analizziamo in seguito: page unit e due elementi associati alle cache (TLB). Questi servono a virtualizzazione. C'è control ROM è quella che genera per le istruzioni molto complesse la sequenza di istruzioni che devono essere fatte. Sulla sinistra bus unit, visto che processore ha 32 bit di indirizzamento (fino a 4GB di memoria). Bus 64 bit e ovviamente un segnale di controllo. Segnale di controllo del Pentium sono super-set di 8088 e 8086. Segnali di controllo per quelli vecchi sono i segnali di stato: S0 S1 e S2. Ovviamente c'è Integer Register File. BTB già visto. Questo fa vedere semplicemente come le istruzioni avanzano in parallelo con doppia pipeline. Se riusciamo ad accoppiare istruzioni (solo se una semplice/una complessa e che non abbiano alee). Altra visione del nostro Pentium, anche qui non abbiamo particolari necessità di spiegazione: semplicemente raffinamento colorato della struttura precedente. Generalità: prima cosa dice che istruzioni sono inviate al prefetch. La cache istruzione ha una doppia forza: da un lato estrarre istruzioni da mandare a prefetch e dall'altro caricare dalla memoria. Poi ovviamente lo stadio D1 c'è decodifica codici operativi (normale stadio decodifica). Verifica accoppiabilità. Notiamo quanto è importante compilatore: potremmo avere istruzioni FP e anch'esse è ragionevole che siano distribuite all'interno del sistema, per far in modo che unità FP debba essere sempre impegnata, ma non intasata. E' possibile mettere un controllo se un'operazione FP che precede è già terminata o meno. Esiste possibilità di condizionare avanzamento di istruzione integer al fatto che istruzione precedente sia FP. Abbiamo D2, abbiamo EX. La cache dei dati è tripla porta (2 accessi da pipeline e uno da bus), WB. La realizzazione delle istruzioni x86 porta via molto silicio a causa della ROM. Nelle realizzazione come P2 è stato realizzato tramite trasformazioni delle istruzioni x86 in istruzioni tipo RISC. Stadio D1: abbiamo ingrandito alcune delle zone. Dal pre-fetcher (2 byte, 2 buffer a 32 byte che lavorano in alternativa). In 32 byte ci possono stare più istruzioni (lunghezza è variabile). Può succedere che istruzioni non siano allineate a dimensione del buffer stesso: un pezzo in un buffer e un pezzo in un altro. Logica che individua il confine delle istruzioni: pre-decodifica. Nel caso che un'istruzione si estenda attraverso 2 buffer esistono dei registri che permettono di mantenere informazione per ricostruire istruzione. Le istruzioni vengono avviate nelle code (coda A e B, sono due code che contengono ciascuna 16 byte che possono alimentare sia pipeline U che pipeline V). Sostanzialmente in quei due elementi chiamati “pairing” si fa test per capire se siamo in grado di avviare due istruzioni insieme nelle pipeline. Istruzioni oltre ad essere avviate a pipeline di riferimento vengono anche avviate a BTB per poter poi agire sul pre-fetch. BTB: abbiamo caso che BTB abbia individuato salto, questo cambia prefetch ma può anche avere ripercussione su cache del codice, perché se salto avviene su istruzione non presente in cache dobbiamo andare a caricare istruzione. Mentre nel caso che cache dei dati dobbiamo andare a verificare se dato è stato modificato (e scriverlo in memoria). Questo non succede nelle cache delle istruzioni perché basta sovrascrivere se necessario nuova istruzione. Se predizione è sbagliata (nello stadio di EX la predizione di BTB era erronea paghiamo una penalità di 3 clock). Non è penalità drammatica perché abbiamo pochi stadi, se più lunga dovremmo svuotare tutta la pipeline e dovremmo aspettare più clock (problemi per processori super-pipeline). Penalità diversa se errore avviene in pipeline U o V. Istruzioni accoppiabili: primo caso, se abbiamo caso di alea RAW (per eseguire seconda dobbiamo avere scritto la prima, quindi non accoppiabili); secondo caso (che non arriverebbe mai perché casserebbe la prima, visto che dopo va a sovrascrivere); terzo caso, questo può avvenire: due istruzioni consecutive possono scrivere su due parti diverse dello stesso registro, ma anche in questo caso però non riesce a distinguere e le avvia verso stessa pipeline. Ultimo caso quello è assolutamente lecito perché l'aggiornamento di bx avviene al termine di istruzione, mentre lettura di bx avviene all'inizio, quindi nessun problema di WAR. In processori come questo l'hardware non riesce a compensare una esecuzione non ottimizzata del compilatore, mentre nei futuri riesce ad ottimizzare codice. Qui invece esecuzione codice come generata da compilatore. L'importanza del compilatore è tale che se noi prendiamo manuali di questi processori sono molto grossi. La cosa più interessante è che questi libri avevano appendici non forniti ad utilizzatori, erano info consegnate solo a chi scriveva compilatore, perché era costretto a conoscere ulteriori particolarità della macchina per scrivere software efficiente. Questo avveniva e avviene ancora oggi: hardware è in grado di compensare, ma è altrettanto vero che quanto più generazione codice è ottimale meglio è. Quando parliamo di benchmark dei programmi l'ottimizzazione dei compilatori è molto importante. Quindi compilatore è parte integrante efficienza macchina. Stadio D2: ci sono alcuni elementi non ancora analizzati. Tra poco vedremo internamente a questi processori qual è l'hardware integrato insieme al processore. Qui si anticipa elemento: la verifica delle exception (quelle condizioni che impediscono alla sequenza di istruzioni di continuare, per esempio violazione protezione, cioè cerca di scrivere/leggere zona di memoria sulla quale non ha diritti provoca eccezione che viene risolta in quello stadio e dà luogo in una interruzione e il sistema operativo interviene). In questo stadio uso della segmentazione. Analisi stadio esecuzione. Notiamo che è evidente che in questi due stadi in cui si vanno a verificare l'effettiva correttezza di predizione di BTB, nel caso di predizione errata andiamo a re-influire su BTB per poter tener conto di questa decisione errata. Nel caso del Pentium l'informazione relativa al fatto che il branch sia preso o meno è realizzata con algoritmo a 2 bit. Se predizione sbagliata, faremo transizione di uno stato. Ecco che questa predizione influisce sul percorso degli stadi di quella rete. Stadio di WB: si scrive in cache se dato presente in cache, si scrive in “posted write” se non è in cache. Nel P2 e successivi c'è write allocate: quando dato non è in cache prima si va a prendere (individuare su base INDEX quale slot deve essere emesso, individuare via da rimpiazzare, scaricare contenuto linea se aveva almeno un byte alterato e poi caricamento in quella linea del nuovo dato). 32 byte sono 256 bit il che vuol dire che sono necessarie 4 operazioni di bus (perché bus a 64 bit). Quindi trasferimento linea comporta 4 cicli di bus, questo non è poco perché se pensiamo a memorie che hanno t di accesso di 50 nanosecondi, ma c'è fase di indirizzamento e poi di lettura, quindi nel caso del Pentium il caso più favorevole è di 2 clock. Qui non abbiamo multiplexing tra dati e indirizzi. Comunque sia dobbiamo attendere, parte di indirizzamento non è una per ogni dato, ma una per tutta la linea. Bus unit: Bus unit: contiene i bus, i buffer di scrittura (registri in cui andiamo a scrivere in attesa di andare a scrivere in memoria), transceiver, logica per accesso al bus (questi sistemi sono spesso multi-processore e in questo caso più processori contemporaneamente vogliono leggere/scrivere sul bus, quindi ci deve essere logica di arbitraggio); quando noi portiamo dati nella cache dobbiamo sapere in quale stato li portiamo (se siamo in sistema multi-processore possiamo avere che dato sia solo in memoria o sia anche in un'altra cache di un altro processore, caso ben diverso se dato presente solo nella nostra cache o anche in un'altra cache, se però è presente anche in un'altra cache dovremo invalidare quel dato, si dice dato “stale” cioè vecchio). I buffer di scrittura a 64 bit ce ne sono 5. Ci sono altri due buffer di snoop (ficcare il naso), operazione che viene fatta nel caso di controllo che il dato da utilizzare debba essere invalidato. Qui si rispiega branch, comportamento che abbiamo analizzato nel caso del BTB. Notiamo discrepanza di 3-4-5 clock dal fatto che abbiamo avuto diverse versioni: prima versione penalità 3-5, la seconda 3-4. Pentium negli anni hanno anche seguito velocizzazione dei clock: dai 30 MHz iniziali ai 100 MHz dell'ultimo. Nel frattempo sono anche cambiate le alimentazioni (3V, 2,2V e 1,6V). Cache Pentium: Cache: linee di 32 byte, abbiamo 128 slot vuol dire che 12 bit sono sottratti al TAG (7 per slot, 5 per offset all'interno della linea, quindi TAG è di 20 bit). Cache è data da 128x2 (le vie) * 32 byte = cache da 8 KByte. Oggi vanno cache I livello dai 64-128K, di II livello arrivano fino al MByte, III livello di qualche MByte. Algoritmo di rimpiazzamento è semplice: se abbiamo due vie basta un bit per dirci qual è via più vecchia. Le due cache sono leggermente diverse: tutte e due tripla porta (3 accessi contemporanei), anche se in realtà ci sono due pipeline che devono accedere e una per lo snoop. Quando abbiamo meccanismo di coerenza delle cache è chiaro che abbiamo un ulteriore ingresso dal bus per permettere di fare verifica (snoop). Cache disabilitabili: durante lo start-up sono disabilitate. Disabilitazione fa crollare efficienza macchina, ma ci possono essere casi in cui Pentium è drammaticamente sovrabbondante rispetto alle velocità di calcolo. Per es. sistema molto semplice che usa Pentium e in questo caso si cerca di ridurre complessità dei sistemi intorno. Se non vogliamo preoccuparci della coerenza l'unica cosa è disabilitare cache. Qui c'è disegno che riporta struttura cache del Pentium, occhio che ci sono 2 bit di stato per ogni via di ogni set (slot) perché il protocollo di coerenza di queste cache si chiama M.E.S.I. (Modified exclusive shared invalid, acronimo dei 4 stadi in cui si trova linea). Nome di questi stati possono indurre in errore nella loro interpretazione. I due bit ci danno le seguenti info: se linea è valida e se è stata modificata o meno: stadio modified stato che indica che linea ha avuto modificazione rispetto a prima. Gli altri analizzati meglio. Al RESET tutte le vie di tutti i set invalidi per evitare hit in lettura prima di senso. In altri casi come quando si cambia contesto (si cambia programma, processo). Quando processore esegue esiste un'istruzione specifica che invalidi cache. Oppure quando un altro processore vuole alterare nella propria cache un dato che è presente anche nella nostra. Pentium Branch Prediction: Qui abbiamo struttura del BTB, il nostro BTB è costituito da una memoria associativa a 64 set (64 INDEX, ciascuno delle quali a 4 vie, quindi siamo in grado di avere 64x4=256 indirizzi di branch predicibili), quindi set-associativa a 64 slot e ogni slot è a 4 vie. Per ognuna di queste vie c'è indirizzo di destinazione, quindi indirizzo a 32 bit. Qui abbiamo solo INDEX, il TAG è composto da 32-6=26 bit; 32 bit di destinazione e 2 bit per ogni via per indicare se branch deve essere taken o untaken e poi meccanismo di rimpiazzamento. Dobbiamo anche sapere quale andare a scartare. Quindi BTB è una delle strutture più complicate. Nel Pentium l'algoritmo di taken/untaken. Ma in processori più sofisticati abbiamo più vie e abbiamo algoritmi sofisticati. La funzione in base al branch history table e al pattern history table se la predizione è taken o untaken è uno dei segreti industriali più delicati. Individuazione corretta del branch è uno degli elementi che determina plus-valore intrinseco. Politiche Pentium già affrontate: le scritture in memoria previste dal posted write sono effettuate nello stesso identico ordine in cui sono state immesse. Elemento in più: se fra due scritture si inserisce una lettura che ha bisogno di un dato che corrisponde a istruzione precedente, ma cui scrittura posted non è ancora andata in memoria. Se succede che a seguito di una scrittura ancora in attesa nel posted write vi è lettura dato che vuol leggere dato che non è ancora in memoria, il Pentium se ne accorge e legge dato da posted write: fa una sorta di forward e non stalla pipeline. Notiamo che in certi casi nel Pentium come in altri processori, mentre le store sono tutte in sequenza, le load possono essere eseguite in fuori ordine. Naturalmente purché load riguardino destinazioni diverse. Registri interni: Ci sono un sacco di registri interni che programmatore non vede, ci sono istruzioni privilegiate: se noi vogliamo scrivere programma che le usi (a meno che non abbiamo particolari diritti), danno origine ad eccezione. Ci sono molti registri non noti (così come istruzioni non note). Anche registri visibili sono diversi, rispetto a quelli dell'8086. I registri AX BX CX e DX sono dimensione fino a 32 bit e sono indirizzabili o come 32 bit (EAX EBX ... extended), oppure come dati a 16 bit AX BX .. o dati 8 bit AH AL BH BL .. Parte alta registro EAX, EABX .. non è singolarmente indirizzabile. Parte bassa indirizzabile come AX, ma parte alta non indirizzabile singolarmente. Non posso prendere la parte bassa della parte alta di EAX (o prendo tutto o niente). Abbiamo anche variazione importante per i registri di segmento. I registro di segmento sono (anziché 4) 6, perché registri di dato anziché 2 (DS e ES) hanno anche FS e GS. Il che ovviamente compatibilità del software verso l'alto e non verso il basso. Ci sono molti altri registri che noi scopriremo in futuro. Per esempio abbiamo registri “ombra”, che non sono direttamente indirizzabili. Anche registri indice sono stati portati a 32 bit. Il registro dei flag contiene molti più flag di quelli dell'8086 (era 16 bit, qui 32 bit). E' invece rimasta sempre identica in tutte le versioni del Pentium (dall'8088, quando c'era processore matematico esterno) la struttura del floating point per quanto riguarda registri (sono sempre stack da 8 registri da 80 bit). E' cambiato ovviamente pipeline che realizza istruzioni FP, ma i registri a cui fa riferimento sono rimasti sempre organizzati a stack (in realtà operazioni FP sono per default eseguite tra i primi due livelli dello stack, ma in questo stack posso individuare operazioni tra diversi elementi dello stack). Motivo uso dello stack: si depone sullo stack non appena è prodotto, visto che molto probabilmente mi servirà per istruzione successiva, ma cmq indirizzabile anche in modo random. E' comunque quello che avviene anche per stack tradizionale. Zona di memoria può anche essere indirizzata in modo normale usando base pointer accoppiata con stack setting e così tutta zona di memoria che io vedo come stack posso vederla normalmente tramite base pointer.

Segnali del Pentium

[modifica | modifica sorgente]

Parte più elettronica del Pentium 1 iniziando ad analizzare il suo bus. Ci sono molte cose che richiamano concetti precedenti, quando abbiamo visto bus superiori ad 8 bit. Questo parallelismo a 64 bit. Sulla sinistra segnale di clock e bus dei dati (0-63), raggruppato 8 x 8, perché trasferimento minimo è come sempre byte. In alto a destra bus degli indirizzi (A3-A31), A0 A1 e A2 fanno parte del bus enable, emessi solo BE. Sulla sinistra c'è bus ready che è il READY dell'8086. Nostro processore può essere ritardato per aspettare che fornitore segnali sia stato in grado di produrre segnale che dobbiamo leggere. ADS* è address strobe, che è equivalente di ALE dell'8086. Naturalmente qui ci troviamo in una condizione ben diversa perché non è bus multiplexato e quindi dati e indirizzi coesistono contemporaneamente, quindi bus 29 bit + bus enable per indirizzi e bus a 64 bit per i dati. ADS* viene emesso perché: noi abbiamo bisogno di far partire conteggio ritardo (e per sapere quando deve partire, noi lo dobbiamo far partire quando abbiamo nuovo indirizzo); concettualmente non abbiamo bisogno di campionare questo indirizzo, in realtà questo indirizzo deve comunque essere campionato perché bus indirizzi in certe condizioni deve essere disattivato, la cosa si può capire perché nel momento in cui abbiamo coerenza delle cache (altro processore vuol scrivere dato), dobbiamo sapere se quel dato si trova nella nostra cache per poterlo invalidare. Per fare questo bisogna interrogare processore e l'unica soluzione è dargli indirizzo, se risposta positiva deve essere invalidato. Bus degli indirizzi bi-direzionale, normalmente è pilotato da processore, ma se esiste interrogazione gli indirizzi possono essere forniti dall'esterno per dire se quel dato ce l'ha o non ce l'ha. Questa interrogazione in realtà avviene tramite due segnali ben specifici: AHOLD (mette in tri-state buffer indirizzi del nostro processore, address hold e in questo caso bus degli indirizzi è in grado di accettare indirizzi da fuori) e EADS* (il segnale che campiona nel processore gli indirizzi forniti dall'esterno); quindi normalmente emette indirizzi e ADS*, quando abbiamo AHOLD attivato dall'esterno, il bus emesso si mette in tri-state, indirizzi pilotati dall'esterno e sempre dall'esterno deve essere pulsato EADS*. Risposta a questa interrogazione la troviamo nei segnali cache control (HIT* e HITM*), questa è risposta del processore una volta preso EADS*: se c'è dato non modificato HIT*, se ho dato e modificato rispondo con HITM*. Notiamo che a prima vista si potrebbe pensare che se un altro processore vuole scrivere in memoria per quale motivo mi interessa se il dato è stato modificato nella mia cache. E' chiaro che se vado a scrivere in memoria un dato più recente di quello che c'è nella cache, il dato buono è quello della memoria. Il dato lo devo riscrivere lo stesso prima di riscriverlo in memoria perché linea è di 32 byte e noi non diciamo quali byte siano stati modificato. Molto probabile che i byte modificato dall'altro processore non sia uno dei byte che io ho modificato nella cache. Quindi prima scarico linea modificata e poi sovrascrivere linea modificata. Segnale HIT* dice che ho il dato. Molto importante per protocollo M.E.S.I. Naturalmente in un sistema multi-processore potranno esserci n processori che rispondono contemporaneamente HIT*. Ma invece per motivi di coerenza (proprio per come funziona MESI) solo uno può rispondere HITM* perché se avessimo cache diverse con entrambe stesso dato modificato la coerenza non si avrebbe più. Sempre nel caso della cache abbiamo segnale INV: quando pilotiamo EADS* possiamo anche attivare INV che invalida quella linea di cache se presente. Quindi quando fornisco indirizzo e attivo anche INV vuol dire: “se ce l'hai invalida anche”. Poi abbiamo altri segnali: FLUSH (invalida tutta la cache), poi CACHE* e KEN* (uno in uscita e uno in ingresso). Poi abbiamo WB/WT* che è un altro dei segnali che incide sul MESI e ovviamente provoca diverse transizioni del protocollo a seconda del suo valore. Abbiamo segnali che ci sono familiari: INTR (che funziona nel Pentium come nel 8086, alla ricezione di questo segnale genera se interrupt enable è abilitato genera un doppio INTA, qui non lo vediamo perché viene generato tramite segnali di stato). Segnali di stato sulla destra: Mem/IO* Data/Clock* e Write/Read* e LOCK*. Ancora una volta processore si comporta come il vecchio: alla ricezione di INTR rispondono con un doppio INTA. Anche nell'8086/88 in maximum mode il segnale INTA non era esplicitamente generato ma generava combinazione segnali di stato. Al secondo INTA processore si aspetta di ricevere interrupt type (identica a quella già vista, unica differenza è che valore viene moltiplicato per 8 anziché per 4). Questo legato a descrittore di segmento, che è equivalente del puntatore nel caso 8086. Moltiplicavamo per 4 perché andavamo a prendere uno dei 256 puntatori, ognuno dato da 4 byte. Dal 286 a quelli di oggi questo puntatore è sostituito da descrittore di segmento (puntatore con in più altre info), che è lungo 8 byte. Altra differenza è che la tabella dei puntatori alle subroutine di interruzione non deve risiedere nella prima parte della memoria ma può risiedere ovunque. Poi abbiamo PWT e PCD: questi li vedremo più avanti ma diciamo che il nostro sistema può essere impaginato e questi segnali sono segnali che riguardano impaginazione e devono essere portati fuori per eventuali cache di livello superiore. Sugli altri due segnali a sinistra (FERR* e IGNNE*) non ce ne occupiamo. Esiste invece INIT, identico a RESET solo che non cambia stato cache, mentre quando azzeriamo stato delle cache si trovano in una situazione bloccata dalla quale escono solo attraverso tramite istruzione del processore (scritture in registri di controllo che ne determinano funzionamento e che possono essere scritti o letti solo da istruzioni privilegiate, solo quando è sistema operativo). Livello di privilegio più elevato può averlo solo sistema operativo. Se adesso ci portiamo in basso a destra abbiamo parità: sul bus dei dati vi sono 8 segnali di parità, uno per ogni bus, così come esiste un segnale di parità per gli indirizzi AP: tutti questi segnali sono bidirezionali (sicuramente quelli dei dati, fornito da fuori quando leggo, da dentro quando scrivo). Questo vale anche per l'unico bit di parità associato agli indirizzi: talvolta il processore lo riceve e nel momento in cui lo riceve deve riceverlo dall'esterno. Parità serve? Non serve? Parità come tutti i meccanismi di protezione aumenta probabilità di rivelare errore, ma può anche rompersi questo meccanismo. Cosa bisogna fare quando c'è errore di parità? Il discorso è così vero che se noi pensiamo ai nostri moduli di memoria, questi il discorso della parità lo trascurano perché sono a correzione d'errore (hanno circuiteria per correggere errore, perché della parità non me ne faccio nulla). Stesso discorso vale per processori, tant'è vero che nel nostro processore esiste PEN* (parity enable, vogliamo che il nostro processore controlli o no parità?). Questi segnali sono scomparsi nei processori più avanzati proprio perché controllo della qualità dei segnali è demandata a memorie. Stesso discorso per l'IO: nessun sistema è in grado di gestire parità. Poi abbiamo il segnale di HOLD e HOLDA, poi abbiamo il BOFF* e il BREQ. BOFF*: supponiamo che il processore voglia scrivere in memoria, nel caso di sistema multi-processore dobbiamo capire se dato che dobbiamo scrivere si trova da qualche altra parte per sapere se bisogna o no validare altra cache. Supponiamo che voglia scrivere in memoria in una locazione che è presente in una cache di un altro processore in stato modificato (HITM*). Succede che il controllore della memoria ricevendo un HITM*, allora il controllore di memoria deve emettere un BOFF* e cioè gli dice “sparisci”, ovvero il nostro processore che ha iniziato il ciclo deve tirare su le mani da bus, l'altro processore deve riscrivere prima di tutto (impossessarsi del bus per scrivere dato modificato) e poi BOFF* viene rimosso per il nostro processore che può riprendere. Altro problema è che bus è unico e processori sono molti (ma non solo processori, anche altri agenti ..). Allora abbiamo qui altro segnale che è BREQ, quando ci sono più agenti che vogliono accedere al bus, è normale che vogliano accedere contemporaneamente e allora anziché accedere brutalmente il processore genera BREQ richiedendo di poter accedere senza essere disturbato. Questo fa scattare logica di arbitrazione FAIR (giusta, equilibrata) nel controllore di memoria. Processore messo in wait (generato dal controllore di memoria), fino a quando non è il suo turno. Chiaro che se situazione mono-processore il BREQ non viene utilizzato. Abbiamo LOCK* che abbiamo già visto, BUSCHK* (segnale che proviene da controllore della memoria che dice che non riesce a portare a termine ciclo, dà luogo ad una eccezione all'interno del processore; queste eccezioni sono interrupt che avvengono DURANTE ciclo). Es. dato non in memoria, ma devo andarlo a prendere dal disco. Lasciamo stare IV IU e IBT. Segnale importante è NA* (next address), qui c'è nome che porta fuori strada .. Qui esistono indirizzi pipelined, che non hanno niente a che vedere con pipeline interna. Due cose che usano stesso nome per cose completamente diverse. E' chiaro che ogni trasferimento è composto da almeno due fasi: generazione indirizzo e recupero/scrittura dati e sappiamo anche che talvolta operazione di trasferimento può prendere più cicli, che per loro natura coinvolgono più indirizzamenti. E' possibile che il controllore esterno di memoria mentre sta trasferendo i dati del ciclo precedente dica al Pentium: “dammi indirizzo successivo di cui hai bisogno”, sovrapponendo fase di trasferimento con fase di indirizzamento. Sorta di pipeline, viene fatto questo poiché fase di indirizzamento comporta decodifica e quindi tempo, mentre trasferisco mi faccio dare nuovo indirizzo ed inizio decodifica. Questo ha senso nell'ipotesi che i dispositivi coinvolti nel trasferimento corrente e successivo siano diversi. Nell'ipotesi che questo non sia e quindi NA* segnale che viene da fuori chiedendo nuovo indirizzo anche se trasferimento non ancora terminato. Questo si sposa con interleaving. Caches e Lock: Che cosa succede se ho una variabile che viene scritta da processore nel momento in cui viene letta da un altro. In questo senso in generale il sistema non si occupa di questo: chi prima arriva occupa. Se però esistono variabili sensibili (ad es. semaforo), dobbiamo ricordare due cose: che intere porzioni di memoria possono essere rese non-cacheable, cioè accesso a quelle variabili devono avvenire tramite operazione di memoria. Problema di eventuale disallineamento non si pone. Problema della variabile non-cacheable, rende però non cachable intere aree della memoria, non singole variabili. Di solito quindi una pagina: circa 4KByte di memoria. E allora bisogna trovare qualcos'altro: esiste prefisso LOCK* che possiamo anteporre a qualunque istruzione di macchina. Non succede nulla internamente al processore se non l'emissione, questa può essere però raccolta da controllore della memoria, che la può rendere non-cachable. Come questo avvenga lo vediamo tra poco: ogni volta che cerchiamo di portare dato in cache il controllore di memoria ci può dire sì o no, o perché zona è cachable o perché intera zona è cachable oppure perché quella istruzione è stata emessa con LOCK* e quindi quella variabile non può essere letta in cache. Poi naturalmente segnale LOCK* viene anche utilizzato per garantire una sequenza di operazioni non interrompibili. Questo è caso di test-and-set che nel caso di PC si usa con una exchange memory register. Ci sono 2 bit che risiedono in un registro di controllo (sono 4 nel caso del Pentium e sono in numero superiore nei sistemi successivi, compatibilità verso l'alto). Segnali di controllo della Cache: Durante lo start-up il sistema operativo va a testare un registro di sistema ulteriore che gli va a dire su quale processore sta girando, sapendo su cosa può fare o non può fare. Ci sono 2 bit che indicano il funzionamento della cache (INV e NotWritethrough, è un bit che se attivato fa sì che la scrittura sulla cache venga anche attivata nella memoria). Ricordare che la prima scrittura passa sempre. Questi bit sono in parte funzione anche di quel segnale writethrough, writeback del Pentium. Cache a tripla porta per i dati (2 pipeline e una di interrogazione dall'esterno) e a doppia memoria (accesso da memoria e da prefetch). Questa situazione della nostra cache: cache abilitata è l'ultima di queste configurazioni. Se abbiamo hit in lettura il dato si legge dalla cache, ma questo vale per qualunque degli stadi. Se abbiamo MISS in lettura i primi due stadi non provocano un line fill. Lo provoca nell'ultimo stato in fondo. Questo avviene solo se è attivato il segnale cache da parte del processore e ricevuto un segnale KEN dall'esterno. Fa parte dei meccanismi di accesso al bus. Facciamo un line fills se è possibile, mentre nell'altro caso non tenta neanche di fare un line fills, quindi ha cache bloccata, quindi quello che c'è c'è. L'unica cosa che possiamo fare è diminuire contenuto. Averle invalidate non vuol dire poterle rimpiazzare. Riempimento cache avviene solo sulle letture e non sulle scritture perché non abbiamo politica write allocate. Con le cache in questa condizione anche se avessimo write allocate avremmo cache allocata. Notiamo differenza tra primo e secondo stadio: primo cache separata da mondo. Cosa serve tutto questo? Bohhhhhhh!!!!! Questo sembra dovuto al comportamento della cache. L'ultima: quando noi facciamo line fill abbiamo anche un influsso su quello che sono i due bit di stato della linea (ogni linea ha due bit di stato che riguardano protocollo MESI). Il segnale WB/WT* che proviene dall'esterno che deve essere fornito a processore all'atto della lettura fornito dal controllore della memoria sulla base degli HIT* e HITM* ricevuta da altre cache porta quella linea in stato SHARED o EXCLUSIVE a seconda che il segnale fornito da parte del controllore di memoria sia basso o alto. Le hit in scrittura aggiornano cache e se la scrittura avviene a linee shared abbiamo scrittura anche su memoria, rimbalzo sulla memoria e una transazione della linea verso shared. Elenco segnali già visti.

Spazio indirizzamento

[modifica | modifica sorgente]

Spazio indirizzamento: la nostra memoria è organizzata su un bus a 64 bit e quindi gli indirizzi sullo stesso dispositivo si susseguono all'interno del dispositivo a distanza 8. Notiamo che possiamo fare trasferimenti fino a 64 bit in parallelo, quello che nel caso del Pentium si chiama quad-word. (Questa non vale per DLX perché lì word è a 32 bit) Nel caso dell'IO i trasferimenti al massimo possono essere di 32 bit, non possiamo fare trasferimento di 64 bit in IO: dobbiamo farne 2 da 32. Non esiste in-quad word: esiste in in-double word .. Non possiamo avere delle double-word che superino in confine dei 32 byte. Questo meccanismo di IO. BIOS non è fatto da 8 dispositivi (2 o 4): non è proibito interfacciare sistemi di memoria a parallelismo inferiore su un bus a parellelismo superiore. Naturalmente questo richiede un minimo di intervento. Qui vediamo che abbiamo disegnato memoria a 64 bit, 32, 16 e 8. Chiaro che dobbiamo avere logica di trasformazione che a partire dai BE0*-7* generi per ogni tipo di memoria i senali corrispondenti. BE0*-7* è riassunto di tutti quanti A0 A1 A2. Certamente problema di derivare segnali necessari ai vari dispositivi, ma anche problema che il Pentium quando lavora non sa se sta lavorando con un certo tipo di bus; quindi Pentium emette segnali a 64 bit. Se memoria che risponde è minore ci vuole logica che da un lato mette in wait il Pentium perché dati non ci sono tutti e dall'altro deve andare a prendere tutti i dati nelle memorie, mettere questi dati in registro e spararli al Pentium in parallelo. E' un contatore programmabile con indirizzi iniziale presentato da Pentium, incrementa valore e ogni volta va a scrivere in uno degli 8 latch (caso memoria a 8 bit). Questo risponde a domanda sul BIOS: possiamo avere BIOS uguali su processori a parallelismo diverso. Questo di solito non succede perché anche i BIOS hanno una loro vita e naturalmente vengono migliorati e modificati e soprattutto siccome si riferiscono a processori diversi, questi hanno anche funzioni di controllo più sofisticate. Quindi BIOS vogliono utilizzare questi registri di controllo per sfruttare appieno le caratteristiche. Riportato graficamente quello che succede nelle memorie a parallelismo minore (byte swap: logica composta da contatore e una serie di deMUX che avviano dato letti in momenti diversi nei registri di transito che saranno poi letti da processore). Qui conversioni tra bus enable e segnali generati tra le diverse memorie. Cicli di memoria: Cicli di memoria: i cicli di memoria sono come sempre un super-set dei cicli di memoria dei processori precedenti. I nostri cicli di memoria dipendono da 5 variabili. Una di queste: KEN è un segnale proveniente dall'esterno, fornito dal controllore di memoria. Prima quelli più semplici. I primi 5 si disinteressano del valore KEN e i cicli sono generati in base o all'istruzione che viene eseguita o come risposta all'interruzione. Nei 3 bit meno significativi coincidono con S0 S1 e S2. Ciclo speciale lo lasciamo perdere. Letture dell'IO (letture fino a 32 bit) e non sono mai cacheable. IO non cacheable per definizioni. Poi lettura non cacheable del codice: quando io leggo del codice se tutto va bene me lo voglio portare in cache. Solo due casi in cui non va in cache: cache disabilitata. Primo caso: segnale cache, che è segnale che fa il processore al mondo esterno di leggere un dato per portarlo in cache, è 1. Se cache è disabilitata (nei primi due stadi non si poteva fare line fills). Quindi l'unica cosa che avviene è dato portato attraverso processore non passando attraverso cache. Il problema è sapere se ha solo quel dato o anche tutti quegli altri che fanno parte della linea di cache da portarsi all'interno della sua cache. Cache è solo un blocco che si interpone. Processore vuole quel dato, ma come glielo diamo? Solo dato o tutta linea? Se ha cache abilitate chiede tutta linea, se invece non sono abilitate lo segnala dicendo che non vuole tutti dati della linea. Quindi del segnale KEN si disinteressa. Qualunque sia stata la sua richiesta se KEN è a 1 il dato non è cacheable, quindi il dato è fornito solo quello che processore ha chiesto. K di KEN sta per CACHE. Se il segnale KEN viene normalmente fornito da controllore di memoria: tu processore mi hai chiesto dato da mettere in cache. Se io so che quel dato è cacheable ti permetto di fare intero ciclo di aggiornamento cache, se quella zona di memoria è stata programmata come non cacheable oppure mi hai attivato LOCK* e allora ti rispondo KEN=1 dicendoti che quel dato non puoi portarlo in cache. Terminiamo con il primo caso particolare: CACHE=0 e KEN=0, in questo caso abbiamo condizione particolare: il processore ha chiesto un dato e ha chiesto contemporaneamente di potere portarselo in cache (normale condizione quando c'è lettura di dato in memoria con cache abilitate, così volte dopo non devo tornare in memoria). Controllore dice sì va bene, te lo puoi portare in cache. A questo punto scatta qualcosa di diverso: processore emette il segnale ADS di indirizzo e al ciclo successivo (o dopo qualche ciclo se ci sono wait) si aspetta di leggere dato, se però anche KEN attivo senza emettere di nuovo ADS il processore si aspetta di ricevere tutti gli altri byte che fanno parte della linea. Quindi processore sicuramente nel primo ciclo riceve dato che gli serve, ma contemporaneamente anche se lui ha bisogno di un solo byte, la memoria gli tutti gli 8 byte e in sequenza gli dà anche gli altri byte; quindi controllore di memoria deve essere programmato in modo tale da dare in sequenza (4 cicli) senza reindirizzamento gli dà anche gli altri 24 byte per completare linea. Questo ciclo si chiama BURST, che è il normale funzionamento della cache abilitata, quando andiamo a leggere zone cacheable e notiamo che discorso uguale quando abbiamo un memory read perché cambia solo destinazione cache. E quindi anche in caso di lettura di un dato ci fornisce nel primo ciclo un dato e anche gli altri 24 byte (8 alla volta) per completare, il tutto con un unico ciclo di indirizzamento. Se invece il segnale CACHE non è attivo, questo ciclo non va. Notare una cosa che riguarda scrittura: quando noi scriviamo un dato con il segnale CACHE attivo che cosa vuol dire questo? Il processore quando deve scrivere scriverà word, double-word ... e la scrive o nella cache o in memoria, quindi quando mai avviene di voler scrivere una intera linea di cache? Solo quando linea è modificata e vogliamo scaricarla. Nessun altro caso di scrittura linea di cache. Se vogliamo scaricarla non siamo condizionati da KEN. Quindi controllore di memoria quando riceve comando di scrittura con CACHE attivo si predispone ai 4 trasferimenti con un solo ciclo di indirizzamento per scaricare tutti 32 byte della linea. In lettura è condizionabile, ma se è già in cache vuol dire che è cacheable e non devo essere condizionabile da KEN. I cicli di bus sono sempre un super-set dei precedenti processori, ricordando che a parte la modalità del trasferimento, i bus fanno una unica cosa. I processori dal punto di vista esterno non fa altro che leggere e scrivere. Questa è visione riduttiva: molteplicità di modalità con cui colloquia con mondo esterno; ma modalità è sempre quello. Ricordiamo che il segnale CACHE è emesso dal processore, mentre segnale KEN (cache enable) in molti casi viene trascurato, in particolare tutte le volte che processore non emette CACHE (cioè non è interessato ad attivare cache). Questo se stiamo parlando di letture e abbiamo detto che processore non attiva il segnale CACHE in due casi: non attiva quando ciclo interrupt acknowledge perché è ciclo di IO; lettura codice non cached, avviene solo se cache disabilitate, se no per qualsiasi lettura di codice cerca di portarlo in cache. Se vi è un tentativo di portarselo in cache: cioè se il segnale CACHE è emesso da processore e il segnale KEN emesso dal controllore di memoria (doppio 0, che comunque non sono contemporanei, prima richiesta e poi assenso) allora abbiamo BURST: leggiamo 256 bit, cioè 32 byte (visto che parallelismo è da 8 byte, dovremo fare 4 cicli di bus). Caratteristica di questi cicli di bus BURST è che processore non emette 4 volte l'indirizzo dell'ottetto che vuole leggere, ma emette solo un indirizzo e si aspetta di leggere con una determinata sequenza tutti e 32 byte che fanno parte di quella linea. Processore attende 4 cicli di bus senza interporre generazione indirizzo. Discorso vale anche per la lettura di dati (non solo per lettura di codice), logico che se CACHE è abilitata è ragionevole portarsi linea in cache, discorso diverso vale per scrittura: scrittura di IO non verrà mai cached perché era proibito cacheabilità in IO. Per quanto riguarda scrittura memoria abbiamo due casi: scrittura di un dato che non è presente in cache (perché se fosse presente lo scriveremmo in cache, anche se quando scriviamo prima volta un dato in cache lo scriviamo anche in memoria), ma scrittura avviene in modo singolo: quando scriviamo dato in memoria scriviamo quello e solo quello. Quindi sostanzialmente non viene attivato segnale CACHE. CACHE viene attivato ma sistema non si attarda a verificare KEN solo in un caso: quando stiamo riscrivendo linea che dobbiamo rimpiazzare, in questo caso è logico che dobbiamo scrivere 32 byte. Ci disinteressiamo di KEN perché se dato è in cache vuol dire che dato è cacheable. Questo è elenco dei cicli di IO, alcune piccole precisazioni: segnale KEN viene campionato da processore alla prima evenienza di due diverse possibili situazioni: o all'attivazione di BUS-READY, oppure alla prima volta che dall'esterno viene pulsato NEXT-ADDRESS. Il primo dei due che si presenta fa sì che il processore capisca se ne ha bisogno di risposta. Altro caso in cui segnale CACHE non è attivo: è quando a livello di paging la pagina viene definita come non cacheable, quindi ulteriore raffinamento del sistema di caching che permetterebbe ad un'intera zona di memoria di essere cacheable, ma questa grande zona di memoria più piccole (pagine), possono essere definite non-cacheable, quindi controllore di memoria si predispone di fornire solo il dato senza i dati aggiuntivi. Cicli speciali Pentium: Cicli speciali PENTIUM: a seconda di valori presenti su BE indica qual è tipo di ciclo speciale. Per esempio c'è ciclo di HALT (ci permette di sapere se processore si è fermato). Ciclo di FLUSH, se istruzione intera invalidate, noi potremmo voler invalidare anche cache esterna, quindi questo viene segnalato all'esterno e vedendo questo ciclo provvederà ad invalidare tutta cache esterna. Molto spesso cache esterna (L2 o L3) sono realizzate con delle normali RAM ad alta velocità (statiche) le quali sono contornate da rete logica che fa funzionare come se fossero RAM di tipo associativo. Fa sì che presentando un TAG la RAM esterna sia in grado di dire se dato sia presenti in RAM esterna e provvede a far sì che l'intero sistema si presenti al mondo esterno come se fosse cache associativa. Questo come funziona ciclo BURST: supposto caso migliore possibile. Il Pentium esegue proprie transizioni sul fronte positivo (a differenza dell'8088/86 erano scanditi da fronte negativo). Vedremo che temporizzazione è molto semplice perché tutto è riferito a fronti positivi. Dal Pentium in poi la temporizzazione è risultata sempre molto semplice. Nel primo ciclo (tra primo e secondo, a cavallo perché è importante che segnale sia valido durante fronte positivo) il segnale ADS* viene e processore emette l'indirizzo, che permano per tutto ciclo BURST. Normalmente cicli Pentium sono divisi in T1 (indirizzamento) e T2 (trasferimento). Qui dopo T1 abbiamo 4 cicli T2. Indirizzo emesso da Pentium emette indirizzo dato necessario (non quello più basso della linea). E' ragionevole perché se per caso dato non-cacheable la memoria deve dargli solo dato che gli serve. Qui supposto che BRDY* sia sempre attivo (non ci sia ritardo in memorie). Qui nel primo T2 leggiamo dato di cui Pentium ha bisogno, in questa ipotesi supponiamo che insieme al segnale ADS* venga emesso segnale CACHE attivo e che insieme a BRDY* si attivi KEN. Lucido 26: c'è schema reale. Abbiamo emesso ADS* e contemporaneamente CACHE*. In questo caso abbiamo supposto che per ogni trasferimento sia necessario di un periodo di clock di attesa. Infatti BRDY* intervallati da un periodo di clock. Insieme a primo BRDY* segnale KEN attivato, cioè controllore memoria ha dato ok per trasferimento BURST. E poi vediamo che processore non emetto più segnali ADS*, quindi questo vuol dire che il controllore di memoria deve campionarsi indirizzo emesso e poi andarlo ad aumentare per restituire dati, quindi all'interno deve avere contatore programmabile (deve essere inizializzato ad indirizzo emesso e poi incrementato di 8, che equivale aumentare di 1 il terzo bit). Bus del Pentium: non è riportato CACHE e KEN. Quando processore fa richiesta di trasferimento che può appartenere ad uno qualsiasi dei 4 tetti di una linea, ora è chiaro che primo ottetto trasferito è quello che contiene dato di cui abbiamo bisogno perché è ovvio che partiamo con quello se no andiamo a stallare inutilmente la pipeline. Se dato è contenuto nel primo o nel terzo ottetto (ottetti di indirizzi pari). Se siamo nel primo o terzo la sequenza è a crescere, se siamo nel secondo o quarto è a scendere. Quindi evidente che contatore del controllore di memoria è anche un contatore UP/DOWN visto che deve contare in dipendenza di dato richiesto da processore. Questo discorso di avanti/indietro non è vero per quanto riguarda la riscrittura cache: quando andiamo a riscrivere linea di cache. In questo caso sempre in salire (prima, seconda, terza, quarta). Se ho bisogno del 3° byte di un ottetto, è chiaro che il terzo byte è semplicemente individuato dal bus enable: indirizzo che processore emette è quello dell'intero ottetto. Quindi emette tutti gli indirizzi da A0 ad A31. In realtà è ancora più semplice perché nel meccanismo di cache quali sono i bit coinvolti nel conteggio? Solo il bit 3 e 4 perché gli altri rimangono stabili. In realtà controllore di memoria è fatto di un registro che tiene tutti i bit da A5 a A31 e da un contatore programmabile che contiene A3 e A4 che può essere incrementato o decrementato a seconda dell'ottetto. Le linee sono allineate, quindi gli unici bit che devono portare sono A3 e A4. Qui abbiamo preso linea che va da 80H a 9FH. Questi sono ottetti che nel tempo si vanno a verificare. Sequenza di indirizzi nel caso di scrittura. Qualche altra considerazione sulle memorie. Abbiamo qui esempio di un banco da 4MByte in cui abbiamo usato 8 memorie da 512KByte e abbiamo ipotizzato che fossero ad indirizzi bassi. Anche perché abbiamo già detto che mentre nel caso di 8086/88 l'uso della RAM in basso risultava quasi strettamente necessario perché bisogna scrivere dentro puntatori alle subroutine di interruzione. Mentre nel 8086/88 c'era ragionevole motivo perché sappiamo che tabella dei vettori di interruzione può stare ovunque. Abbiamo 8 diversi dispositivi, ciascuno individuato da BE e la funzione di decodifica è quella che determina individuazione di banco da 4MByte e ogni banco ha in ingresso BE di competenza. Vale sempre principio di decodifica semplificata. Quando noi dobbiamo andare ad individuare un banco (che è indipendente da sua realizzazione), quello è bit più significativi che individuano banco. Parte fissa di CS di questa memoria sarebbe assolutamente uguale se utilizzassimo memoria con dispositivo di 4MByte: il suo CS sarebbe parte superiore di questa. Interleaving: Nelle nostre memorie (quelle del lucido precedente) abbiamo messo su A0 BADR3 perché dividiamo per 8, adesso però c'è cosa da ricordare che le memorie sono dinamiche che hanno caratteristica di tempo di accesso e tempo morto (di pre-charged) che incide su efficienza delle memorie. Quindi si usano tecniche di Interleaving, cioè si fa in modo che ottetti successivi appartengano a dispositivi diversi, nel lucido precedente leggiamo 8 byte ad un valore uguale (allo stesso piano) che vuol dire che quando carichiamo indirizzi successivi andiamo ad accedere a dispositivi uguali nell'ambito dei 4MByte. Con Interleaving si fa cosa più sofisticata: anziché usare per un banco 8 dispositivi da 512KByte, usiamone 16 da 256. Facciamo in modo che ottetti successivi appartengano a dispositivi diversi, quindi A0-7, 8-15 in un'altra .. Questo vuol dire che se facciamo accessi in sequenza accediamo a dispositivi diversi e quindi tempo di pre-charged del successivo viene mascherato da tempo accesso precedente. Il segnale che dobbiamo usare è A3, perché A0 A1 A2 non viene emesse perché sostituito da BE. L'indirizzo A3 o BADR3 è quello che discrimina ottetto da quello successivo e quindi è chiaro che basta prendere A3 ed inserirlo nel CS come se fosse bus enable. Così è chiaro che i due sotto-banchi funzionano in alternativa. Quando è pari ne prendiamo uno, quando è dispari prendiamo l'altro. Sul A0 dei singoli dispositivi vado a mettere BADR4 e ci torno ogni 16 sullo stesso dispositivo con valore di memoria interno superiore di 1. Se BADR3 uso per discriminare batterie dei dispositivi è ovvio che su A0 del dispositivo ci vado a mettere BADR4 perché taglia diventa la metà. Quindi numero di locazioni interne del dispositivo è metà. Qui c'è CS. Qui abbiamo CS00, CS01 ... ciascuno di questi corrisponde ai dispositivi che abbiamo (le due metà che abbiamo). In questo modo indirizzi consecutivi appartengono a dispositivi differenti. Potrei anche inventarmi Interleaving a 4 dispositivi anziché da 2. Quindi questo sarebbe individuato da BE e da A3 e A4. E ci sulle memorie ci andrò a mettere BADR5. I/O: anche su questo abbiamo già visto. Abbiamo parlato di dispositivi che debbono essere indirizzabili individualmente e che non richiedono presenza di tutti i bus del processore. Questo perché non sono cacheable e poi perché scriviamo su IO abbastanza “raramente”. Qui richiamato il CS di quei due dispositivi (8255-1 e 8255-2) supponendo che stiano su stesso banco di indirizzi di IO. I singoli dispositivi sono individuati anche da BE. All'interno andiamo a discriminare tramite bit indirizzo superiori (3 e 4). Dal punto di vista di accesso non c'è differenza tra IO e memorie. DMA: non è obbligatorio metterlo sul bus basso, così come non è obbligatorio mettere su bus basso 8259. L'unica cosa che dobbiamo curarci è di avere by-pass tra bus in cui è messo e bus basso attivato durante INTA (perché processore si vuol leggere interrupt type su bus basso). Nel caso di DMA tipicamente abbiamo problema di trasferire dati dalle memorie che si trovano calettate su tutti i bus verso la stessa destinazione che è sempre la porta dell'8255 che vogliamo coinvolgere. Il dispositivo DMA emette come indirizzo quello di memoria (eventualmente aumentato con bit aggiuntivi dove indirizzo non sia sufficiente). L'indirizzo di memoria è emesso da DMA mentre CS8255 sarà funzione dei segnali DACK quando siamo in DMA. Differenza che abbiamo qui è che ogni volta che effettuiamo trasferimento (ricordiamoci che trasferisce 1 byte alla volta), dobbiamo avere 245 che metta in comunicazione bus su cui è 8255 selettivamente con ciascuno degli altri bus su cui si trovano dispositivi di memoria (indirizzi consecutivi su bus diversi), quindi evidente che ad ogni trasferimento dovremo cambiare apertura di 245 corrispondente. Ogni porta di 8255 può esser in in oppure out. Quindi apertura 8255 sarà funzione di DACK, ma anche di indirizzo di memoria coinvolto perché ognuno di essi si apre in funzione di indirizzo di memoria coinvolta. Direzione 245 dipende da MEMWR o IOWR a seconda di dove vogliamo utilizzarlo. Schemi temporali: Ci sono schemi temporali riportati direttamente da manuale: trasferimento in cui in entrambi i casi CACHE è disattivo, quindi processore non vuole fare trasferimenti in lettura di tipo CACHE. Se vi è errore di parità questo viene segnalato due clock dopo il trasferimento. Qui c'è un periodo in cui processore non interroga memoria, tant'è che non emette ADS*. Il primo ciclo era di read, il secondo è di write (W/R* è a 1). Nel caso del secondo trasferimento abbiamo doppio wait (BRDY* si attiva due clock dopo che si è generato indirizzo, poteva attivarsi subito o dopo 10000 cicli). Non c'è su questo nessuna differenza con 8086/88. Lettura burst già vista prima: il segnale KEN* non viene preso in considerazione, cioè quando linea deve essere scaricata. Hold/Holda: segnali vengono messi in tri-state perché c'è altro agente che si occupa dell'operazione. Ciclo di interruzione: fatto da due cicli di bus, in mezzo c'è cesura perché questi due cicli non sono uno attaccato all'altro. Quello che è certo è che tra questi due cicli non fa altro, ma non è detto che il numero di cicli di clock sia tale che siano uno dopo l'altro. Ricordare che in realtà i due cicli sono sì separati da qualche clock, ma di molto poco e se usiamo 8259 lui ha bisogno che i due cicli siano abbastanza distanti, questo vuol dire che se processore li mette troppo vicini bisogna fare in modo di allontanarli. L'unica maniera è agire su BRDY* (ma occhio che BRDY* lo sente solo dopo ADS*). Viene emesso segnale LOCK (bisogna far in modo che logica mondo esterno non vada a disturbare operazione. Next-address: questo massimizza utilizzazione banda passante del bus. Primo è ciclo burst: contemporaneamente al BRDY* viene attivato KEN* e cioè dice “va bene, fai ciclo di burst”. DA esterno generato anche NA* (perché se controllore di memoria deve essere in grado di fare cicli burst deve essere in grado di immagazzinarsi indirizzo, proprio per questa necessità se li deve essere anche campionati e con NA* dice di dargli quello nuovo). A causa di NA* processore prima di terminare primo ciclo BURST emette ADS*, in modo tale che se controllore di memoria lo sa fare subito dopo burst precedente può darmi dato nuovo. Complessità ulteriore perché dobbiamo avere due contatori UP/DOWN (uno quello di prima e uno che deve campionare primo indirizzo di burst successivo). Così facendo bus sta andando alla sua massima velocità perché ad ogni clock fornisce dato. Questo equivalente a prima con periodo di wait. Temporizzazioni del nostro processore (66MHz), qui ci sono caratteristiche del clock e tempi di ritardo tipici. Sono valori molto piccoli e tutti reperiti a fronte positivo del clock. Normale funzionamento è quello che andiamo ad analizzare. Bisogna prima ritornare qual era il funzionamento dell'8086/88. In questo processore avevamo dei registri di segmento che erano a 16 bit obbligando i segmenti ad esser allineati a multipli di 16, i quali virtualmente traslati formavano l'offset calcolato con tutte le componenti possibili immaginabili. Sommato a questo indirizzo di base offset modulo 64 KBye (scartando 17 bit) dava luogo ad indirizzo fisico. Il che faceva sì che una volta calcolato l'indirizzo non vi fosse nessuna restrizione all'accesso in memoria. Noi abbiamo nei sistemi più evoluti certamente una pluralità di processi che virtualmente si sviluppano contemporaneamente (con time-sharing l'impressione è di avere processi insieme che hanno necessità di garantirsi integrità di propri dati, contro eventuali errori di altri processi). Se non vogliamo avere errore che si propaga, dobbiamo avere meccanismi che ogni processo abbia un proprio spazio nei quali solo lui può accedere; questo non impedisce di avere comunicazione, ma confinata alla particolare zona di memoria che serve per scambio; ma un processo non può influire sulle zone dell'altro in lettura o scrittura. Esiste processo per eccellenza che è sistema operativo. Dobbiamo garantire che sistema operativo non venga disturbato, ma sistema operativo è un processo: avrà privilegi, ma da un punto di vista di propria dinamica è un processo come gli altri. Questo avviene tramite controllo traduzione: quando processore genera indirizzo; quello prima di essere accettato come indirizzo viene verificato ad ogni ciclo di bus. Il sistema effettua via hardware un controllo su questo accesso ed effettua trasformazione di questo indirizzo in modo tale che se questo indirizzo non appare lecito sulla base di info di sistema, quell'indirizzo non viene concesso, avendo dei FAULT. Segmentazione: Descrittore segmento: quali sono gli “oggetti”? Gli oggetti che ci sono nel sistema sono segmenti, le unità conosciute dal nostro sistema sono segmenti. Per potere accedere alle info dovevamo caricare segmento, certo che nel caso specifico equivaleva caricare indirizzo iniziale. Oggi sono oggetti veri e proprie, cioè entità informatiche per le quali sono descritte regole. Per ogni segmento deve essere costruito un “descrittore di segmento”, cioè nel momento in cui viene creato segmento, ne viene costruito anche relativo descrittore. Quest'ultimo contiene tutte e sole info di cui abbiamo bisogno. Il descrittore è composto di 8 byte (dimensione del parallelismo del nostro bus). Contiene info fondamentali, alcune delle quali sono super-set di quelle di prima. Contiene indirizzo iniziale: da dove è stato caricato. Equivalente indirizzo iniziale che caricavamo nel registro di segmento quando volevamo accedere ad un segmento nel 8086. Qui info inserita in contesto più ampio. Si dice anche altra cosa: quanto è lungo. Possono avere dimensioni qualsiasi, ma sappiamo per ogni segmento quanto è lungo, quindi se uno cerca di generare programma il cui indirizzo calcolato va fuori da dimensione del segmento l'hardware lo blocca. Quindi c'è protezione che incapsula segmento stesso. Poi ci dice altre cose: come possiamo accedere in lettura scrittura, chi ci può accedere. Ovvero quali diritti deve avere processo per poterlo usare. Se io ho tabella scheduler sarà bene che quello scheduler sia scritto solo da sistema operativo, se no addio congruenza intero sistema. Macchina non conosce sistema operativo, ma processo con un alto livello di privilegio. Ci dice per esempio se si tratta un segmento di codice oppure di un segmento di dati o se è segmento fittizio, cioè segmenti che servono solo per accedere ad altri segmenti (call-gates). Ci dice anche se quel segmento è presente o meno in memoria. Noi possiamo avere dei descrittori di segmenti i quali descrivono segmento, ma segmento non è in memoria; quindi quando cerchiamo di accedere, macchina si inchioda (eccezione), richiama sistema operativo, questo va a vedere qual è eccezione, prende il segmento dal disco per caricarlo su memoria, eventualmente scaricando altri segmenti (dello stesso processo o di altri processi), carica segmento in memoria, scrive nel descrittore per dire dove si trova. Il processo sistema operativo si basa su hardware che gli risolve problemi. Controllo che indirizzo emesso da processo non vada ad eccedere sua lunghezza è effettuato via hardware. Esempio descrittore di segmento: sono sparse queste info. I bit più significativi della base, non sono contigui agli intermedi e ai bit più bassi. Se prendiamo bit bassi e la allineiamo alla parte superiore, vediamo che base 0-15 e 16-23 sono fra loro contigue. Questo non è altro che ampliamento del descrittore di segmento che veniva usato nel 286 (processore da 24 bit di indirizzo). E' ovvio che posizione bit all'interno del descrittore è assolutamente ininfluente, basta che sappia dove si trova. Qui non abbiamo più segmenti che devono essere allineati a multipli di 16. Tutti bit indirizzamento sono presenti. Questo indirizzo è indirizzo del segmento: quando carico segmento in memoria la prima cosa che fa sistema operativo è mettere lì dentro indirizzo. Limite è max offset all'interno del segmento che è concesso. Dimensione limite in tutto è 20 bit, quindi offset massimo è da 1 MByte, in realtà esiste bit G (granularità) che dice se questo segmento deve essere calcolato come multiplo di byte oppure multiplo di 4KByte. Quindi fino a dimensione del MB abbiamo granularità del singolo elemento, quando debordiamo la dimensione è calcolata in multipli di 4KByte. Chiaro che questa granularità ha un effetto: se per caso questo segmento ha 1 MB e 1 byte, andiamo a sprecare un po' di memoria. Chiaro che granularità con 4 KByte abbiamo maggiore problema di frammentazione della memoria. Poi parallelismo del segmento (se operandi sono a 16 o 32 bit), deriva dal fatto che questo descrittore è un descrittore che è fratello maggiore del 286, che però internamente era a 16 bit. Chiaro che programmi che erano stati scritti per 286 hanno un parallelismo a 16. Problema si sta riponendo di nuovo: ora tutti sono a 32, ma nel momento in cui si passa a 64 interi bisognerà passare a nuovi descrittori a 64 bit. Poi abbiamo livello di privilegio (due bit molto importanti per dire chi può fare che cosa). I livelli di privilegio sono 4. Livello di privilegio massimo (segmento che è più restrittivo negli accessi) è quello che ha valore più basso (00). Livello minimo: 11. Poi abbiamo bit P che ci dice se segmento è o meno presente in memoria, un bit S che dice se è un segmento di sistema o di utente e poi abbiamo il Type. Il Type sono 4 bit che ci dice che nel nostro sistema ci sono 16 diversi tipi di segmento e non soltanto ci dice il tipo, ma dà anche il significato ai bit che sono presenti. Poi esiste un campo AV che può essere scritto dal software (per es. scrivo che questo segmento è stato modificato), purché sia a privilegio 0. C'è un elemento che va sottolineato ed è che quando noi abbiamo un segmento presente in memoria e abbiamo riempito la memoria esiste il problema di scaricarlo dalla memoria quando qualche altro deve essere caricato. Qui non c'è nessun meccanismo che definisca vecchiaia. Scaricare vuol dire o sovrascrivere (se non è stato modificato) o scrivere anche su disco la modifica. Questo segmento verrà paginato, cioè diviso in blocchi, quindi non viene scaricato tutto, mentre si scarica solo blocchi e gli altri avranno informazioni sulla vecchiaia. Descrittori: la parte legata alla definizione del segmento. Abbiamo che segmento S ci dice “1: codice o dati”,“0: descrittori di sistema o gates”. Ci sono 16 tipi diversi, ma in realtà 32, perché anche il campo S contribuisce. Descrittori dati: il primo bit se sono codici o dati, poi c'è bit Expand. Lo stack pointer parte dalla cima dello stack e poi scende. Questo bit ci dice se letture consecutive fanno crescere puntatore verso l'altro (scriveremmo valore iniziale crescendo a mano a mano fino a quando non raggiungiamo massima dimensione) o verso basso (in questo caso deve essere scritto valore iniziale, non superiore alla dimensione segmento). Se segmento è scrittura/lettura o solo lettura e poi c'è un bit interessante perché è una piccola info su vecchiaia. Non sappiamo se segmento è stato scritto o meno. Ci viene detta solo una cosa dal sistema: se uno ha fatto almeno un accesso, in lettura o scrittura. Non è particolarmente sofisticata come info. Diciamo che laddove non esistesse l'impaginazione noi potremmo stabilire come politica del sistema operativo di non riscrivere mai sul disco i segmenti che non hanno avuto nemmeno un accesso. E di scrivere invece tutti i segmenti che hanno avuto almeno un accesso (anche se non sappiamo se in lettura o scrittura). Descrittore di codice: bit C è segmento che di fatto assume come privilegio quello del programma che lo sta utilizzando. Supponiamo di avere segmento che fa radice quadrata (parte di segmento matematico), è chiaro che è un pezzo di software che può servire a noi, sistema operativo, ecc .. Non avrebbe senso averne una per ognuno. Quel segmento che fa radice quadrata viene usato da tutti e quindi si conforma a chi ne richiede l'uso. C'è un bit che dice se sola lettura o usato come dato. Qui c'è tutta tabella che indica per tutte possibili configurazioni la tipologia del segmento. (non importa saperla a memoria). No dirty bit: il dirty bit è il bit che ci dice se segmento è stato modificato, purtroppo nel descrittore non c'è questo bit. Abbiamo definito i descrittori, ma dove stanno? Nel nostro sistema esiste una tabella che è costruita da sistema operativo, che si chiama Global Descriptor Table (GDT), la quale racchiude la maggior parte dei descrittori di segmento. Ricordandoci che il numero massimo di descrittori è 8192. I descrittori non sono entità fisse, ma dinamiche: ogni qual volta compiliamo un programma il sistema operativo crea descrittori del nostro programma. Tabella che è variabile e che contiene descrittori sistema operativo. Quello è processo come un altro che per accedere deve usare meccanismi offerti dalla macchina, differenza è che lui li può anche creare e distruggere, mentre l'utente no. Il sistema è un processo come un altro, ma ha diritti di fare cose che altri non hanno diritto di fare. Questa tabella ha una base che ci dice dove sta e limite per dirci quanti ce ne sono dentro. Se noi andiamo ad accedere ad un descrittore oltre il limite, il sistema ci dirà che non possiamo accedere perché non esiste. Non esiste soltanto la GDT, ma anche la LDT: ogni processo ha diritto ad una propria tabella dei segmenti. La GDT tendenzialmente usa quelli di uso generale, ma esistono segmenti creati e distrutti da singoli processi. LDT una per ogni processo. Apparentemente abbiamo un meccanismo molto pesante: cioè se ogni volta che io faccio un accesso in memoria devo andare a prendermi il descrittore di segmento che si trova in memoria, verificare che offset calcolato non vada fuori sono costretto a fare ogni volta due accessi in memoria. In realtà questi descrittori hanno anch'essi meccanismo di caching, portati dentro a registri ombra in modo tale che siano subito disponibili. Qui non è cache dati o codice, ma cache piccolina che contiene solo descrittori. A questo punto dobbiamo chiederci cosa ci sta nei registri di segmento (nel 8086 c'era indirizzo più basso). Nei registri di segmento è contenuto puntatore al descrittore di segmento che vogliamo utilizzare: non abbiamo valore iniziale alla tabella il cui descrittore (descrittore di segmento puntato) contiene a sua volta l'indirizzo iniziale del segmento. Abbiamo quindi traduzione degli indirizzi. Meccanismo che assomiglia a quello dell'8086. Quando saltiamo da un segmento all'altro cambiamo il selettore, quindi cambiamo puntatore nella tabella che va a puntare a segmento diverso. Indirizzamento: come è costruito un indirizzo all'interno del Pentium. Un indirizzo è una quantità a 48 bit composta da un selettore a 16 bit, contenuto nel registro di segmento. Il selettore punta a un descrittore. Descrittore contiene indirizzo fisico base del segmento, che sommato ad offset mi dà indirizzo fisico del dato che sta all'interno del segmento e contemporaneamente viene fatto un controllo sia di non aver sballato come offset perché offset deve essere più piccolo del limite. Ulteriore controllo che livello di privilegio del nostro segmento sia compatibile con privilegio del chiamante. Tu che lavori a livello più basso non puoi acceder a livelli superiori. Questo indirizzo così calcolato: selettore, descrittore, somma con offset è indirizzo fisico della memoria a meno che non sia stato il meccanismo di paging (impaginazione), altrimenti abbiamo ulteriore traduzione. Se c'è meccanismo di impaginazione indirizzo viene ulteriormente trasformato e ogni trasformazione appesantisce di molto velocità del nostro sistema. E' chiaro che ogni traduzione costa. Tutte queste traduzioni, se non ci fossero meccanismi di caching sarebbero tutti accessi ulteriori in memoria. Tabelle dei descrittori: I descrittori stanno in tabelle, questa tabella si chiama GDT, c'è un registro che contiene suo indirizzo iniziale, ovvero GDT register, ci dice dove si trova. Ci si può chiedere come il sistema faccia a partire. Il sistema non nasce protetto. Soluzione è abbastanza semplice: tutti questi sistemi partono come se fossero degli 8086 (a parte essere a 32 bit) funzionano come un 8086, ciò vuol dire che bootstrap vede in modo libero e senza vincoli tutta memoria sistema e quindi può costruire tutte strutture dati che vuole. Queste sono corrispondenti delle strutture dati già viste. E una volta costruite in modo libero, viene settato un bit in uno dei registri di controllo e da quel momento in poi funziona in modo protetto, ma si trova già tabelle costruite. Quindi costruzione libera e interpretazione in un secondo momento (bit di protection enable). Nel momento in cui viene settato tutto sistema diventa protetto. Quando diciamo che funziona come 8086, in realtà ha spazio di indirizzamento maggiore, vede comunque registri a 32 bit, anche se tipologia di istruzioni è la stessa. Quando sistema lavora in modo nativo ha anche a disposizione tutte le istruzioni di tipo privilegiato: totalmente sprotetto. Quando noi accendiamo e carica sistema operativo, in quel momento c'è transazione tra modo nativo e modo protetto. Si può facilmente dimostrare che per costruire sistema nativo semplice il numero di istruzioni è molto ridotto, che nel giro di un migliaio di istruzioni possa poi passare in modo protetto. Meccanismo di indirizzamento sulla base del quale funziona la protezione è legato al concetto di descrittore di segmento, questi si trovano in due tipi di tabelle: una è GDT (tabella descrittori di segmento usati da sistema operativo o librerie). La nostra GDT viene collocata in memoria all'atto della costruzione del sistema, quando lavoriamo in modo non protetto: la mettiamo dove vogliamo e possiamo accedere sempre in modo nativo alla GDTregister. Cioè registro che punta alla base di questa tabella, poi c'è registro limite che ci dice quanti descrittori validi ci sono. Indice con il quale ci muoviamo all'interno della GDT è selettore che troviamo nei registri di segmento. Questi puntano a descrittori di segmento. Questi nella loro versione più semplice corrispondono ai contenuti dei registri di segmento che avevamo in modo nativo. Questi contenevano indirizzo iniziale. Oggi oltre ad indirizzo iniziale sono presenti info aggiuntive che ci permettono di operare per protezione. In realtà i descrittori di segmento che sono in memoria (e che richiederebbero accesso specifico a memoria), viene portata automaticamente in una piccola cache all'interno del processore per accelerare al massimo accesso. Ricordiamoci che qualunque dato all'interno dei nostri sistemi appartiene ad un segmento, questo significa che anche i dati della tabella dei descrittori possono essere raggiunti solo tramite un descrittore interno alla tabella stessa. Anche le info che riguardano alle tabelle dei segmenti devono appartenere a segmento, individuato da descrittore. Costruiamo quindi all'inizio descrittore che ci permette di accedere a tabella stessa. Abbiamo detto che tabella contiene al massimo 8192, ogni descrittore è da 8 byte, il che dice che tabella GDT al massimo 64 KByte. LDT: noi abbiamo LDT che contiene descrittori del processo in esecuzione, ce ne sono tanti quanti sono i processi attivi. LDT è un segmento che contiene dei descrittori che è puntato e descritto da un descrittore presento nella GDT. Abbiamo struttura dati che non può non appartenere ad un segmento, che deve essere individuato da descrittore. GDT è quella a cui fa riferimento sistema operativo, che quindi deve anche poter gestire le LDT dei processi. Amministrazione processi vuol dire creare e distruggere segmenti dei processi stessi. Come facciamo a recuperare LDT del processo in quel momento in esecuzione. Esiste un registro LDT register il quale contiene un selettore che punta nella GDT al descrittore che descrive il segmento appartenente al processo che contiene i descrittori del processo in esecuzione. Quando il nostro processo accede a un descrittore perché ha bisogno dei dati, deve dire se sta accedendo tramite GDT o tramite LDT. In generale in GDT ci saranno quelli di sistema, nella LDT quelli del processo specifico. Ricordiamoci che nel GDT register ci sta un indirizzo lineare o virtuale. Significato sarà chiaro a breve, possiamo comunque dire che se non c'è impaginazione, l'indirizzo lineare o virtuale coincide indirizzo fisico. GDT register (contiene un indirizzo) e LDT register (contiene selettore). Il caricamento e lo scaricamento di queste info è consentito solo ad istruzioni privilegiate e l'equivalente del limite è contenuto nel descrittore, perché non solo contiene puntatore ad indirizzo iniziale, ma contiene anche massimo valore. Del tutto evidente che informazione è inserita all'interno del descrittore. LDT: contiene descrittori di codice, dati e stack del processo in esecuzione; le call-gates (quando un processo deve accedere ad un servizio, ad es. vuole recuperare dati da disco deve usare servizi forniti da sistema operativo, che ha compito di gestione corretta dei conflitti temporali e accesso; per poterci accedere dobbiamo fare operazioni di verifica, questo controllo affidato a call-gates); task-gates (sono come dei grilletti, dei descrittore che permettono ad un task di mettere in esecuzione un altro task). Aliasing: è un meccanismo abbastanza chiaro, supponiamo di avere un processo (che è compilatore) che se non ha errori genera codice, che è un segmento, quindi è un processo che che può richiedere segmento e descrittore in memoria. Questo segmento è scrivibile, ma un segmento di codice, deve essere non scrivibile, allora c'è discrepanza: necessità di scriverlo mentre stiamo compilando e poi non scrivibile. Due meccanismi: o sistema operativo va a cambiare descrittore, oppure fa cosa più semplice: crea due descrittori che puntano a stessa zona di memoria, una come segmento dati e una come segmento dati. Quindi stessa zona di memoria puntata come uscita compilatore (dati), l'altro punta come codice, per eseguirlo. Questo si può fare perché diamo proprietà (personalità) diverse, individuato ciascuno da un descrittore diverso. Questo fatto sia per GDT che per LDT. Per ognuno esiste relativo alias. Nel momento in cui voglia usare LDT come segmento di dati per dobbiamo scrivere o leggere, in pratica abbiamo fatto alias della tabella, vista sotto altra luce. Selettore: è composto di 3 elementi. Un puntatore vero e proprio alla tabella dei segmenti. C'è un indice da 13 bit. Poi esiste bit Table Identifier che dice se quel selettore è relativo a GDT o LDT. Sappiamo se nel nostro registro di segmento sta descrivendo uno in GDT o uno in LDT. Livello di privilegio ha a che vedere con protezione. Esempio: codice che va bene sia 8086, che Pentium in modo nativo. Stesso codice quando ci muoviamo in ambiente protetto. Quando si deve caricare un registro di segmento, in tutti i nostri processori avviene tramite due operazioni. Prima caricato in un registro e solo da lì può transitare in un registro di segmento. Vecchio vincolo. Caricamento di registro segmento è abbastanza raro. Quindi noi carichiamo dato 00D0 (dato a 16 bit) e lo portiamo in ds. Con istruzione successiva, prendiamo il terzo byte del segmento e lo portiamo nella parte bassa del registro dx. ds è data-segment. Mov dl, [3] vuol dire usare come registro di segmento ds. Se non esplicitiamo niente il sistema considera ds. 00D0 sono i 16 bit più significativi dell'indirizzo (i segmenti allineati modulo 16). Sommiamo 3 che è l'offset e abbiamo indirizzo fisico del byte: 00D03h. Nel caso di sistema protetto: caricare 00D0 vuol dire caricare. In ds carico un selettore. Qui che a che fare con indirizzo sono solo i primi 13 bit, che sono puntatore a tabella GDT (perché abbiamo uno 0). Stiamo caricando il 23° descrittore (perché c'è anche valore 0), nella GDT e lo carichiamo con valore 0, cioè con livello di privilegio massimo. Se la base del descrittore è F5D0, in realtà andiamo a caricare dato all'indirizzo lineare F5D3. Quell'indirizzo è un indirizzo lineare o virtuale che coincide con indirizzo fisico solo se non c'è paginazione. Caching dei descrittori: il processo di accesso richiede per ogni accesso il controllo del contenuto del descrittore, per costruire indirizzo e per verificare limite. Il contenuto del selettore di per sé serve solo come puntatore. Succede che nel momento in cui noi andiamo a caricare un nuovo selettore in un registro di segmento viene letto il descrittore e quello viene copiato in una cache da 8 byte, una per ogni registro di segmento in maniera tale che da quel primo accesso in poi io non debba più andare in memoria a recuperarmi il contenuto descrittore. Registro ombra viene modificato solo quando andrò a caricare nel registro di segmento un nuovo selettore. Notare che il caricamento del selettore nel registro di segmento ed il recupero contemporaneo del descrittore corrispondente è il momento in cui viene effettuato il controllo di accesso, cioè verificato diritto di usare quel segmento. Nel momento in cui viene caricato, quel controllo viene eseguito. Dopo non viene più fatto, con le regole di quel segmento. Indirizzamento completo di un dato: qui c'è disegno che sottolinea questo meccanismo, abbiamo i nostri 6 registri di segmento, questi sono i registri ombra che contengono i descrittori e l'indirizzo che viene costruito viene costruito a partire dal offset displacement, base, INDEX e poi c'è ulteriore elemento disponibile nel Pentium con valore di scala, possiamo così portarci ogni 4, ogni 8 ... Questo offset che può avere valore qualsiasi, viene sommato al contenuto del base address. Questo valore viene anche confrontato con limite, valore complessivo non deve superare valore limite. Tutto questo dà luogo ad indirizzo lineare o virtuale. Avere scritto cache non è la cache dei dati o di istruzioni, sono altri registri che sono associati ai registri di segmenti, ma che non hanno niente a che vedere con la vera e propria cache. Costruzione dell'offset. Impaginazione: Paging – memoria virtuale: l'impaginazione è qualche cosa che ci stiamo portando dietro dall'inizio. L'impaginazione permette di fare due cose: conoscendo bene fenomeno della frammentazione (zone di memoria non contigue libere, la cui somma ci permetterebbe di portare segmenti o altre strutture dati, ma che non possiamo fare per l'assenza di contiguità). Si potrebbe fare un'operazione della rilocazione della memoria. I registri di segmento dell'8086 avevano questo vantaggio: individuando indirizzo iniziale e il resto ricavandolo con offset, io potevo spostare i segmenti senza problemi. Operazioni di locazione è molto costosa: per ogni dato due accessi in memoria. Quindi traslare solidamente grandi quantità di memoria è un'operazione molto costosa e quindi non gestibile. L'unica soluzione è adottare politica diversa: suddividiamo memoria fisica in blocchi che chiamiamo pagine e suddividiamo i nostri segmenti in blocchi. Chiaro che posso avere blocchi liberi e blocchi occupati. In blocchi liberi posso inserire blocchi del mio segmento. Saranno tra loro distanti e allora dovrò avere una tabella che farà corrispondere gli indirizzi del segmento con gli indirizzi di dove si trovano i blocchi del segmento: tabella di mapping. Questa figura fa un passo in più: in realtà problema non riguarda singolo segmento, ma possiamo immaginare che ogni processo si illuda di avere a disposizione tutta memoria (memoria virtuale) e penserà di avere propri segmenti all'interno di questa memoria. Ora se questa memoria la suddividiamo in blocchi della stessa dimensione con la quale abbiamo diviso memoria fisica noi possiamo immaginare di avere rapporto diretto tra memoria virtuale e memoria fisica. Per fare questo è facile: per ogni pagina che chiamiamo virtuale ci sia un indirizzo che ci dica in che pagina fisica è stata messa. Oppure possiamo avere info che ci dice che quella pagina non è in memoria, ma su disco. Succederà che quando nostro processo genera indirizzo, quell'indirizzo viene trasformato da parte di questo mapping in un indirizzo fisico che non ha nessuna correlazione con indirizzi virtuali. Questo mapping è chiamato anche tabella delle pagine, cioè ci dice dove si trovano le pagine fisiche che implementano le pagine logiche (o virtuali), questo ha doppio effetto. Da un lato ci garantisce ragionevole occupazione memoria: abbiamo ancora problema di frammentazione. Abbiamo altro aspetto, che ogni processo vede intera memoria come se gli appartenesse. Quando si trova su disco possiamo prevedere meccanismo basato su sistema operativo, che quando cerchiamo di leggere dato di cui una pagina si trova su disco, genera una interruzione che sistema operativo va ad interpretare andandosi a prendere dato dal disco e se la porta in memoria. Se sono tutte pagine occupate e la più vecchia se la porta su disco e la va a sovrascrivere. Impaginazione dà luogo a due vantaggi: resa disponibile ai processi di una memoria concettualmente illimitata, altro è uso più efficiente della memoria. Questo ha costo che ci deve essere tabella di mapping e questa tabella ha dimensione pari a dimensione memoria fisica diviso dimensione delle parti. Per esempio se supponiamo di avere dimensione max 4Gb e pagine da 4Kb, avremo 1M di possibili slot (celle) e questa tabella deve essere sempre disponibile e quindi bisogna averla in memoria, ma in questo caso che ogni volta che dobbiamo fare accesso dobbiamo andare in memoria. Quindi logico che dobbiamo trovare meccanismi che ci consentano di velocizzare questo processo. Paging: indirizzo è interpretato in questo senso. Data certo dimensione di pagina, i bit meno significativi sono l'offset in pagina, i bit più significativi sono quelli che dobbiamo tradurre per individuare numero di pagina. La funzione di mapping si chiama tabella delle pagine e la parte più significativa è di fatto il puntatore della tabella all'interno della quale troviamo l'indirizzo iniziale della nostra pagina. Associandole di fianco l'offset abbiamo indirizzo fisico della memoria. Notare che in Windows esiste un file “pagefile.sys” che è un file di sistema ed è la tabella delle pagine. Può essere di dimensione maggiore o inferiore. Nella tabella troviamo anche informazioni sulla pagina (se stata sovrascritta o meno). La tabella delle pagine ci obbliga doppio accesso: uno per trovare indirizzo di base della pagina stessa e l'altro per recuperare dato. Impaginazione permette un ulteriore raffinamento dei meccanismi di accesso: supponiamo di avere segmento di dati, questo potrebbe essere descritto da descrittore che gli permette sovrascrittura, ma noi vorremmo che una parte di questo non venisse sovrascritto. Allora noi possiamo indicare uno o più pagine di quel segmento non scrivibili. Meccanismo permette raffinamento di controllo degli accessi. Discorso allargato: potremmo avere segmento di dati scrivibile, ma certe pagine scrivibili solo da sistema operativo. Notiamo che quando cito questi casi non è affatto detto che tutti i sistemi operativi sfruttino tutte queste opzioni, molto spesso non sono usate. Per quanto riguarda livelli di privilegio, tutti i sistemi ne usano sempre e solo 2 (max e min). Il dirty bit dice se pagina stata modificata o meno. Stiamo parlando di meccanismi hardware: questi bit sono cambiati da hardware. Vedremo come facciamo a trovarla nel nostro sistema. Questo ci dice se questa pagina ha avuto riferimenti o meno, quindi se è stata utilizzata o meno (questo significata per politica di rimpiazzo, queste meno candidate di essere sovrascritte). Poi abbiamo presente/assente che ci dice se quella porzione di memoria virtuale di quel processo è presente o meno in memoria. Dimensione pagine: come sempre c'è un discorso di bilanciamento. Se pagine molto piccole abbiamo bassa frammentazione (probabilità di avere pagine non completamente riempite); di converso vuol dire che abbiamo tabella delle pagine molto più grande. Ma abbiamo anche maggiore probabilità che il dato che ci serve sia in una pagina che non sia in quel momento in memoria. Questo dà luogo al fenomeno del trashing: è quel fenomeno quando il sistema non fa altro che caricare/scaricare pagine dalla memoria. Con il fatto che sono piccole non fa altro che fare questo. Notiamo che questo fenomeno possiamo in parte vederlo quando dimensione memoria centrale è troppo piccola, molto alto traffico con il disco. D'altro canto pagine di dimensioni elevate hanno effetto opposto: minore probabilità di trashing, ma effetto grave di maggiore frammentazione. Di solito pagine da 4-32 KByte. Nel Pentium strano perché ha 4KByte o 4MByte, in quest'ultimo caso forte frammentazione. TLB: Translation Lookaside Buffer: altro tipo di cache. Cache delle pagine: se non vogliamo in continuazione dover accedere alla memoria per leggere traduzione tra indirizzo logico e fisico, dobbiamo prendere tabella pagina e rendercela disponibile all'interno del processore. Dobbiamo anche riuscire ad avere indirizzamento il più veloce possibile: richiediamo indirizzo logico, lui ci dà indirizzo fisico. (TLB: buffer di traduzione messo di fianco) TLB conterrà solo sottoinsieme pagine più recentemente utilizzate: stesso meccanismo delle cache dei dati e delle istruzioni. In questo caso meccanismo che ci dà traduzione immediata. Se è vero che ogni processo ha la propria memoria virtuale, è anche vero che ogni processo ha le sue pagine. Numeri di pagine virtuali uguali di processi diversi sono indirizzi fisici diversi: la tabella delle pagine dipende dal processo. Quindi ogni task-switch devo cambiare tabella delle pagina. Il problema della sua dimensione quindi diventa molto importante. Il discorso vale anche per il TLB: se contiene una porzione di tabella delle pagine. Nel momento di cambio di contesto devo andare a scaricarlo. In questi TLB abbiamo tutte le tecniche tipiche del caching, per cui possono essere full-associative, set-associative con le stesse identiche modalità già viste. Dovremmo avere n repliche del sistema operativo per ogni processo, questo sarebbe impossibile. I sistemi operativi sono allocati in posizione fissa di memoria virtuale che danno luogo a traduzioni uguali, quindi tutti i processi possono accedere a sistema operativo. Pagine di pertinenza dei singoli processi sono assolutamente diversi per ogni processo. Non solo abbiamo problema di TLB, ma stesso problema sulla memoria centrale: se abbiamo 32 bit di indirizzo, il che vuol dire che sono 4byte quindi tabella di 4MByte. 4GByte di memoria virtuale diviso 4KByte (dimensione pagina) fa 1M di posizione della tabella. Questo per 32 bit, 4 byte, quindi 4MByte. Se 100 processi avremmo 400MByte solo per tabelle. Quindi bisogna trovare meccanismi più semplici. TLB set-associative: la tipologia è identica a quella che abbiamo già visto, a differenza della cache (lì presentiamo indirizzo fisico e andiamo a vedere con meccanismo della set-associative abbiamo linea che corrisponde ad indirizzo fisico), nel TLB presentiamo indirizzo logico, perché è lui a tradurci in indirizzo fisico. Descrittore presente nelle cache associate ad ogni registro ci dà indirizzo iniziale (virtuale), sommato all'offset che è presente nella istruzione sulla base dei componenti indicati nell'istruzione. La somma dà luogo ad indirizzo virtuale, indirizzo virtuale presentato a TLB (sperando che ci sia dentro pagina, se no bisogna andare in memoria e recuperare indirizzo fisico), l'indirizzo fisico sommato all'offset di pagina ci dà indirizzo fisico che presentato a cache corrispondente (dati o codice) e andiamo a vedere se lì dentro c'è dato. Se siamo sfortunati facciamo un sacco di accessi in memoria: dobbiamo andare in memoria a recuperare traduzione in TLB, recuperato l'indirizzo fisico lo presentiamo alla cache. Se non ce l'ha altro accesso in memoria. Quindi come sempre i primi accessi sia come tabella delle pagine, sia come cache sono sempre costosi. In termini statistici una volta fatto questo passaggio si velocizza il tutto. Pentium TLB: ce ne sono due diversi a seconda che si usino pagina da 4KByte o da 4MByte, dimensione pagina è uno dei parametri di configurazione del sistema. Notiamo che seppure la cosa non ha una stretta correlazione diciamo che la 4KByte-4MByte è un po' il contraltare della granularità tra 1byte e KByte. Il rimpiazzamento all'interno del TLB sfrutta il meccanismo del pseudo-LRU, quello che ha 3 bit per ogni elemento di cui un bit dice quale coppia era meno recentemente usata e il secondo bit quale dei due elementi ha avuto un accesso. Qui c'è rappresentazione TLB con pagina a 4KByte: 4 linee, 32 linee. Offset in pagina è rappresentato da 12 bit che sono offset di pagina. Poi abbiamo 5 bit che sono quelli che individuano linee. Poi abbiamo TAG quelli che devono essere confrontati. Organizzazione tabella delle pagine: Organizzazione tabella pagine: questa è tipicamente un esempio di tabella delle pagine. Prima cosa: ci sono tabelle delle pagine diverse per ogni processo. Sistema operativo, tabella pagina-P1 e tabella pagina-P2 (una per ogni processo attivo), qual è il processo che aggiorna tabella pagine? E' il sistema operativo. Tabella delle pagine che appartengono a segmenti sistema operativo. E' blocco di dati all'interno della memoria fisica, non può essere raggiunto altro che con un accesso di tipo sequenziale e quindi ha suo descrittore. Abbiamo esploso la tabella delle pagine del processo 1: la pagina 0 è mappata nella 27a pagina della memoria fisica. La pagina 1 è mappata nella 44a pagina (indirizzo fisico: 44 x dimensione pagina). La pagina numero due è sul disco: molto spesso nella tabella delle pagine di ogni singolo processo è riportata per le pagine che non sono in memoria centrale l'indirizzo nel disco (settore 8714 è il primo settore di una serie di settori che costituiscono pagina). L'associazione tra indirizzo logico e indirizzo fisico è molto chiara: contenuto tabella pagine per le pagine che sono presenti in memoria sono slot per slot indirizzo fisico della sua pagina. Per quanto riguarda informazioni delle pagine che non ci sono: quando pagina non è in memoria provoca una exception del sistema operativo. Questa richiama subroutine del sistema operativo, dopo di che dove si trova info relativa a dove si trova pagina mancante? Dipende da sistema operativo. E' ragionevole che sistema operativo usi quei bit per andare a reperire indirizzo. Altra rappresentazione: page table register (puntatore alla tabella delle pagine che cambia ad ogni cambio di contesto), questo puntatore nell'ambito dei sistemi di tipo Pentium si trova in un registro Control Register Trap, nella CR3 è contenuto l'indirizzo delle pagine associata a processo in esecuzione. Quando task fatto partire nel CR3 viene caricato indirizzo tabella delle pagine. Organizzazione gerarchica: tabella pagine per sua dimensione, poiché moltiplicata per numero di processi attivi è troppo grande. Abbiamo un altro aspetto importante: molte delle pagine che costituiscono memoria virtuale non sono utilizzabili. Il che dice che tenersi tutta tabella pagine vorrebbe dire buttare via un sacco di memoria. E allora si scompone ulteriormente l'indirizzo virtuale in due parti. E si struttura tabella delle pagine in due livelli: si prende parte più significativa e con questa si indirizza tabella pagine di primo livello, questa contiene al suo interno non l'indirizzo fisico del dato che ci serve, ma indirizzo fisico che contiene altra tabella delle pagine che è indirizzata con contenuto del livello 2. Questo è modo per suddividere tabella in tanti blocchi, quanti sono dimensione livello 2. Questo vuol dire che siamo in grado di tenerci della tabella delle pagine solo quei blocchi che ci servono e gli altri averli sul disco. In pratica impaginiamo anche tabella delle pagine. Andremo a recuperarli a mano a mano che sistema tenta di recuperare indirizzo fisico. Qui esempio: il livello 1 punta a tabella di primo livello. Abbiamo fatto finta che sia da 1024 elementi vuol dire che ognuno dei blocchi corrisponde a 4MByte. Con il contenuto della tabella del primo livello vado a recuperare indirizzo iniziale di tabella di secondo livello. Se noi TAG da 20 bit vuol dire che abbiamo pagine da 4KByte, tabella delle pagine sarebbe 1Milione. Noi non la vogliamo tutta in memoria: teniamoci tabella da 1000 elementi che sono raggruppamenti da 1024 ciascuno (10 bit e 10 bit). Con il puntatore presente nella tabella di primo livello vado a puntare in una delle zone e mi dà valore iniziale di dove trovo tabella di secondo livello. Questo disegno esplicita il tutto. Perché abbiamo bisogno di 32 bit se pagine sono allineate? 12 bit meno significativi sono tutti 0. Ma quei 12 bit ci servono per inserirci tutte informazioni di stato che ci consentono di gestire il tutto. E' vero che tabelle di secondo livello possono o meno essere in memoria e se da tabella di primo livello ho richiesta di accesso a tabella di secondo che si trova su disco avrò eccezione che ci consente di scatenare interrupt del processore. Tutte le tabelle di primo livello di tutti i processi attivi devono essere in memoria. Questo ce lo possiamo permettere perché le tabelle di primo livello sono composte da 1024 elementi ciascuno da 4 byte. Quindi sono tabella da 4096 byte. Con 1000 processi abbiamo 4MByte, dimensione accettabile. Volendo questo meccanismo potrebbe essere esteso a tabelle di terzo livello. Come al solito costo/prestazioni. E se pagine da 4MByte, qui tutto in discesa perché vuol dire che tabella delle pagine è da 1024 elementi. Impaginazione è questa tecnica che ci permette di suddividere memoria in blocchi di dimensione costante e di allocare dei sottinsieme del totale dei blocchi ai vari processi. Ciascuno dei quali ha funzione di mapping tra indirizzi virtuali e indirizzi fisici. Ogni processo ha una propria tabella delle pagine, questa tabella è una tabella gestita da sistema operativo. E' lui che conosce tutte pagine di memoria e attribuisce sottoinsieme di questa tabella ai processi. Nel momento in cui il sistema operativo determina le funzioni di trasformazione, le gestisce come se fossero dei segmenti. Nel momento in cui viene messo in funzione processo, utilizza quelle informazioni in memoria predisposte da sistema operativo come tabella delle pagine. Ci si può chiedere come attribuire tabella pagine al primo processo (sistema operativo), ma all'inizio sistema visto come 8086 e il primo processo può andare ad accedere a tutta memoria e quindi anche tabella delle pagine del sistema operativo. Questa è organizzazione gerarchica, perché abbiamo visto che tabella delle pagine per ogni processo può essere abbastanza notevole. Occupazione eccessiva rispetto a dimensione memoria. Bisogna frazionarla, una delle soluzioni è quella di spezzarla e quindi impaginare tabella delle pagine. Questo vuol dire che avremo tabella principale che è quella che corrisponde alla tabella primaria, la quale a sua volta punta ad una serie di tabelle secondarie in modo da costruire funzione di trasformazione. Tabella primaria è puntata da CR3 che è l'unico che contiene indirizzo fisico, perché bisogna sapere dove si trova questa tabella delle pagine. Questa tabella di primo livello è presente sempre in memoria e ciascuno dei suoi ingressi punta ad una tabella di secondo livello. Indirizzo lineare scomposto in 3 elementi. L'ingresso della tabella di primo livello nei sistemi di tipo Intel è individuato dai 10 bit più significativi, che puntano ad uno dei 1024 slot della tabella di primo livello che è sempre presente e che è lunga 4KByte. All'interno di questa tabella ciascun elemento ha un secondo elemento che punta a tabella di secondo livello che viene scandita dagli altri 10 bit. Riusciamo in questo modo a trovare indirizzo fisico iniziale di cui abbiamo bisogno. Poi attraverso offset andiamo a prendere byte, word, double-word .. Se siamo in un sistema a 32 bit, non ci sarebbe posto per inserire informazione di stato. Tabella delle pagine è allineata, quindi all'interno di ogni slot è compreso solo i 20 bit più significativi, perché quelli meno significativi li andremo a sommare all'offset e quindi non ci interessano; lì allora possiamo andare a mettere informazioni di cui abbiamo bisogno. Qui cerchiamo di esplodere tabelle: qui tabella di primo livello, ogni singolo elemento vale una dimensione di 4MB, tant'è vero che se pagine sono da 4MB abbiamo solo tabella di primo livello che contiene indirizzo fisico del dato. Ovviamente nella tabella di secondo livello abbiamo altri 1024 elementi ciascuno di 4KB (ogni slot corrisponde pagina di dimensione 4KB). Notare che tutti gli indirizzi compresi nella tabella delle pagine sono indirizzi fisici. All'interno della singola pagina, andremo a recuperarci dato che ci serve attraverso offset. Per accedere a pagina facciamo accesso a tabella di primo livello, poi a tabella di secondo e poi a memoria per accedere dato. Quindi carichiamo due cache: corrispondenza tra indirizzo virtuale e fisico e poi dato (se è cacheable). TLB poiché contiene funzione di mapping è legato ad ogni processo: quindi completamente invalidato al cambio di contesto. Qui disegno che riassumere in modo complessivo il metodo di indirizzamento. Ogni dato è reperito attraverso un selettore contenuto in un registro di segmento, il quale ci permette di recuperare descrittore che ci dà indirizzo virtuale che sommato ad offset ci dà indirizzo virtuale del dato che cerchiamo (sia esso dato, istruzione o qualsiasi elemento). Questo indirizzo viene presentato al sistema di impaginazione (se è attivo) e partendo dal contenuto CR3 (che contiene indirizzo fisico del primo elemento della tabella di primo livello), siamo in grado di recuperare nostro dato attraverso le varie tabelle. Se non vi fossero cache e TLB questo sistema sarebbe molto pesante. Cache diventa fattore indispensabile per avere una certa efficienza del sistema. Tabella delle pagine (I livello): è chiaro che abbiamo alcuni bit (tabella di secondo livello è puntata dai bit da 12 a 31, e i 12 bit meno significativi sono considerati nulli), abbiamo 2 bit utilizzabili da software (che può scrivere o leggere); naturalmente questi bit possono essere scritti o letti solo da programmi con massimi privilegi. Bit 8 che è a 0. Dimensione della pagina che ci dice se pagina è da 4KB o da 4MB. Poi bit gestito da hardware che ci dice se pagina è stata utilizzata, per capire quali pagine scaricare o caricare. Se la pagina è presente (importante perché è il corrispondente del bit dei descrittori che ci diceva se segmento era presente o meno in memoria). Nel caso di sistemi impaginati si può dire che tutti i segmenti sono in memoria. Perché il compito di andare a caricarlo o scaricarlo è demandato a tabella delle pagine. Quando si parla di accesso, non si parla solo di dati, ma anche di accesso alla tabella. Con bit P presente dichiariamo se tabella di secondo livello presente o meno. Le cui pagine potranno essere presenti in memoria o meno. Non è detto che una pagina apparentemente ad una pagina di seconda livello non presente in memoria sia non presente in memoria. Nel caso delle tabelle di primo livello si punta ad uno di secondo. Se siamo nel caso di pagine di 4KB ci dice solo se la tabella di secondo livello è presente o meno. Gli altri bit riguardano protezione: qualunque elemento nel sistema è sempre scrivibile dal sistema operativo, che è caratterizzato solo da avere il livello di privilegio massimo. PCD e PWT sono ulteriore raffinamento per quanto riguarda il fatto che le pagine possano essere portate in memoria e politica pagine sono WT/WB*. Possiamo avere pagine specifiche per cui è stabilita politica diversa rispetto a quella di default. Se usiamo politiche WT abbiamo meno problemi dal punto di vista della coerenza in sistemi multi-processore. Abbiamo comunque vantaggio di velocità, ma quando scriviamo se siamo in WT scriviamo anche al livello successivo. E' evidente che se siamo in pagine di 4MB l'offset è di 22 bit, quindi è vero che di quei bit dal 31 al 12 ne usiamo meno. Tabelle di secondo livello: page table entry, qui notiamo che abbiamo info più sofisticate. Abbiamo non solo il bit di utilizzazione, ma anche il bit dirty. Quindi se usiamo pagine da 4KB abbiamo maggiore sofisticazione: qui abbiamo anche info se quella pagina è stata alterata o no. In fase di sovrascrittura di pagina ben diversa è politica se solo usata o anche modificata. Quei segnali PCD e PWT vanno anche all'esterno, sia che siamo in tabelle di 4MB sia in tabelle da 4KB. All'esterno vanno questi due bit (nel caso di pagine da 4MB vanno quelli della pagina di primo livello, nel caso di pagine da 4KB vanno questi di secondo livello), che sono usati da controlli di cache di secondo livello per sapere come gestire le pagine. Quando facciamo accesso in memoria, quel dato lo portiamo in cache di primo e secondo livello? Se non è cacheable per primo livello non lo deve essere neanche per il secondo. Uguale per politica WT/WB*. Qui rappresentato sharing. Una libreria potrebbe stare in memoria e usata da più processi: ridicolo che replicassimo in memoria stesse informazioni. Stessa cosa per sistema operativo: è una serie di subroutine che sono presenti in parte in memoria e in parte su disco. Chiamata a sistema operativo è chiamata a pagine di sistema operativo. Quindi quando costruisce task e indirizzi della tabella delle pagine, per gli indirizzi di librerie condivise inserirà gli stessi indirizzi fisici, per non replicare inutilmente il tutto. Page fault I e II livello: succede un po' come quando ci manca segmento. Se non siamo paginati può esserci o meno, se invece lo siamo saranno le pagina ad essere o meno presenti. Indirizzo che ha fallito (per il quale non abbiamo trovato elemento interessante) va in CR2, per sapere da dove dobbiamo ripartire. Eccezione ha implicazioni non da poco, perché viene bloccata una istruzione durante l'istruzione e quindi istruzioni devono essere restartable. Molto importante che istruzioni scrivano al termine della loro vita, perché in questo modo il blocco a metà non provoca problemi. Viene generato interrupt software, la routine legge CR2, cerca pagina libera o libera pagina (operazione fatta da sistema operativo). Si sistemano tabelle e si torna al livello precedente mediante tipica istruzione di tipo ritorno dalle subroutine (IRET). Quando si attiva subroutine di interruzione sistema disabilita interruzione. Tutto avviene in maniera continuativa e protetta. A quel punto possiamo far ripartire perché abbiamo trovato indirizzo che ci serve. Registri di controllo: Arriviamo ai registri di controllo: qui c'è il dato contenuto nel registro di controllo 3. PCD e PWT sono relativi a pagina o tabella pagina a seconda di avere pagine a 4MB o 4KB. Bit riservati che spesso sono usati da sistema nelle versioni successive degli stessi processori. Quando c'è scritto riservato vuol dire che sistema operativo non li può usare. CR3 è uno dei pochissimi elementi che contengono indirizzi fisici (altri elementi sono tabelle delle pagine). CR0: scrivibili e leggibili solo con istruzioni privilegiate, che fanno riferimento a livello di privilegio massimo (che numericamente è quello più piccolo, livello 0). Occhio a PE (protection enable), che è bit che fa scattare sistema da condizione 8086 a versione protetta: avviene che il nostro software agisce liberamente ovunque come se lavorasse un 8086, quando ha predisposto tutto, nel momento in cui abilita PE, tutti i dati costruiti in memoria vengono interpretati dal sistema come quelle strutture già viste. Altro bit è PG (paging enable), meccanismo con il quale abilitiamo meccanismi di impaginazione. Evidente che quando abilitiamo impaginazione dobbiamo aver predisposto inizialmente tabella pagine. Minimo minimo nel momento in cui abilita il PE, se vuol anche lavorare in maniera impaginata deve avere attivo PG: prima si lavora liberamente e poi le strutture create vengono interpretate in altro modo. Poi abbiamo bit di settaggio delle cache: CD e NW. Già visti quando si parlava di cache. Poi abbiamo un altro bit che è WP (impedire al supervisore di scrivere dati utente). Poi abbiamo un bit che ci dice se vogliamo generare interruzione in caso di accesso di dati non allineati (richiedono accessi multipli in memoria, quindi può esser ragionevole spostarli), poi abbiamo NE (se abbiamo errori nella pipeline, come ad esempio divisione per 0); poi TS (se ci sono stati delle variazioni di task, cioè se un task ne ha richiamato un altro e questo ci dice che dobbiamo salvare stack FP e restaurarlo quando cambiamo di nuovo task). CR2: quando c'è eccezione legato a page fault. CR4: dimensione pagine è importante. Elemento che è ancora utilizzato anche se sta diminuendo di peso. Come si fa ad eseguire programma scritto per 8086 nell'ambito di sistema protetto? Questo richiede creazione di task tutto speciale che ricostruisce l'ambiente 8086, ma deve avere capacità di ricevere interruzioni. Quelle interruzioni devono essere poi rimbalzate nel processore protetto. Argomento che sta sempre più perdendo di importanza. Tabella delle pagine invertita: Questo è altro approccio a dimensione tabella delle pagine. Per ogni task ci obbliga ad avere propria tabella delle pagine di dimensione molto consistente. Qui abbiamo situazione diversa: qui costruiamo un elemento della tabella delle pagine per ogni pagina fisica, non per ogni pagina logica. Prima infatti ne costruivamo una che rappresentava memoria virtuale del processo. Qui invece costruiamo tabella delle pagine che ha tanti elementi quanti pagina fisica. Dimensione memoria di pagina fisica è più piccola di quella virtuale. Questo sistema è tanto più efficiente quanto maggiore è divario tra pagine fisiche e logiche. Oggi abbiamo memorie che comodamente raggiungono i 2 GB (confronto ai 4GB indirizzabili). A mano a mano che proseguiamo nel tempo abbiamo indirizzamenti ancora più potenti. Divario tra memoria fisica e memoria virtuale continua a mantenersi aumentando tecnologia. Avere tabella delle pagine che rifletta la dimensione fisica tende a ridurre complessità. Abbiamo tabella delle pagine per ogni pagina fisica. Associazione di indirizzamento tra elemento della tabella delle pagine e pagina fisica è immediato. Supponiamo di avere 1000 pagine abbiamo 1000 slot collegato in maniera biunivoca a tabella delle pagine. Tabella delle pagine contiene dato che è hash della pagina virtuale ad esso corrispondente. Quando c'è numero di pagina logica, l'allocazione della pagina fisica viene fatta facendo hash pagina logica e dentro alla tabella delle pagine c'è scritto quale pagina l'ha generato. Per sapere se pagina fisica contiene dato giusto si va a fare hash della pagina logica che lo punta e lo si va a confrontare con l'hash all'interno della tabella delle pagine. Questo è un metodo molto efficace, molto usato nei PowerPC. C'è solo il problema di avere in memoria due pagine fisiche corrispondenti a pagine logiche che danno origine a pagine fisiche uguali. Tecnica ottimizzata: non è detto che vi siano tante pagine logiche in uso quanti sono le pagine fisiche. Ci possono essere pagine fisiche di pagine logiche non utilizzate. E allora il sistema contiene tabella delle catene: che ci dice quali pagine fisiche reali sono presenti in memoria e laddove ce ne siano alcune libere allora viene costruito link tra pagina logica che ha bisogno della propria pagina fisica e una delle pagine fisiche libere. E' come se avessimo all'interno dello stesso logico link a pagine logiche diverse, quindi ci portano in slot all'interno di pagine fisiche in cui andiamo a mettere il numero di pagina logica che ha dato origine all'altro hash linkato. Questa funzione di link è funzione che costa. Il tutto viene alleviato da uso di TLB, però normalmente c'è problema che se per caso si presenta a pagina logica il cui slot è occupato da un altro “abusivamente”, bisognerà scaricare. Altra maniera è quella di usare logica associativa: per ogni slot non associamo una pagina, ma più pagine. La tabella delle pagine invertita ha molto significato quando pagine logiche è molto superiore a numero pagine fisiche. Di fatto più ci avviciniamo ad uguaglianza minore è efficienza. Protezione: Tutto il meccanismo che abbiamo visto finora nasce dall'esigenza di avere sistemi sempre più sicuri. Vogliamo non avere errori singoli, ma essere anche sicuri che in presenza di questi errori i riflessi sull'intero sistema siano i minori possibili. Oggi sempre meno frequente quel fenomeno che si presentava di blocco del sistema. Tipicamente eccezione di eccezione di pagina perdiamo info sulla prima. Sistema protetto: meccanismi intrinseci che evitano situazioni tragiche. Uno dei primi elementi è quello di non “sballare”: cioè leggere o scrivere dove non si deve. Quando noi definiamo segmento e ne diamo limite, di fatto abbiamo primo fondamento della protezione. Qualunque tentativo di accedere fuori è intercettato da hardware evitando di eseguirlo, in questo modo si evita di debordare e andare a scrivere in altro spazio di memoria. La protezione si basa su descrittori di segmento: riguarda sia protezione statica che dinamica. Questo sistema diventa attivo solo quando attiviamo il PE. Filosofia protezione è semplice: dato un processo o un programma che supponiamo possa avere livelli di privilegio diversi. Si presuppone che più un processo elevato di privilegio maggiore sia la sua qualità. La filosofia è che ogni processo che lavora ad un certo livello di privilegio può scrivere dati di privilegio inferiore o uguale e può soltanto usare software (subroutine, segmenti) che siano di qualità superiore (di privilegio maggiore). Quando sono ad un livello posso affidarmi a software di livello superiore, ma non posso permettermi di scrivere dati a cui non posso avere accesso (cavallo di Troia). Su quello bisogna stabilire regole. Alcuni possono essere usati altri no. Se io chiedo a sistema operativo di cambiare a mio piacimento la tabella delle pagine, non me lo devo poter permettere. La protezione riguarda anche l'esecuzione di particolari istruzioni. Abbiamo anche singole istruzioni oltre a dati. Per esempio istruzione che carica registri particolari non possono essere avviate, perché vanno ad alterare stato complessivo del sistema. Ci sono anche altri controlli: il sistema controlla che in un registro di segmento carichi solo dei selettori che fanno riferimento a descrittori che descrivono segmenti di un certo tipo. Per es. non posso caricare in un extra segment un selettore che punta a segmento di codice: non mi è concesso. Questo è famoso discorso di alias: se ho bisogno di cambiare segmento di codice. Per scrivergli dentro devo usare selettore contenuto in uno dei segmenti di dato che puntano a dei dati, quindi evidente che se voglio fare quell'operazione devo avere alias. Uno me lo fa vedere come segmento di codice (qui posso eseguirlo) e uno come segmento di dato (qui posso modificarlo). Il sistema ci offre gamma di sicurezze hardware che hanno come contraltare l'attivazione di subroutine di risposta le quali prenderanno le opportune decisioni. I livelli di protezione presenti nel sistema sono 4: 0, 1, 2, 3. Il livello massimo è 0, quello minimo è 3. Qui ho ipotizzato appartenenza categoria a programmi. Ma in realtà oggi sono utilizzato solo 2: 0 al sistema operativo, 3 ai programmi utente. La realtà ha dimostrato che degli altri ce n'è poco bisogno. Abbiamo citato principi di carattere generale nei sistemi protetti. E' interessante ricordare che questi concetti di protezione, così come il caching, così come segmentazione protetta, così come impaginazione esistevano già molto tempo addietro. Negli anni '70 esisteva il Multix che aveva stesse caratteristiche funzionali in hardware che noi oggi vediamo nei processori moderni (4 livelli protezione, segmentazione .. ) ed era di dimensioni colossali. I livelli di protezione previsti sono 4. In realtà di fatto sono 2. Diamo definizioni: DPL ovvero il livello di privilegio attribuito ad un segmento, che fa parte del suo descrittore (nei 64 bit ci sono due bit che riguardano questo) – CPL cioè livello di privilegio presente nel selettore di codice, cioè a cui sta operando il programma in esecuzione. Sotto certe regole può cambiare. Poi saltando segmento confortante c'è RPL, cioè il livello di privilegio con cui viene richiesto accesso a segmento. Cosa vuol dire controllare accesso ad un segmento: per cominciare ad accedere ad un segmento, bisogna porre suo selettore nel relativo registro di segmento, ma in questo momento mettiamo anche bit che ci dice se si riferisce a GBT o LDT ma ci mettiamo anche bit che ci informa sui suoi privilegi. Nel momento in cui cerchiamo di caricarlo, quindi individuiamo qual è segmento al quale vogliamo accedere, il nostro sistema verifica che tutto sia a posto. Verifica che tutte regole siano rispettate. Quando viene caricato nel registro ombra vengono effettuati tutti i controlli del caso. Di solito coincide con CPL, ma non è obbligatorio. Poi esiste EPL: è valore numerico più elevato tra CPL e RPL. E' come valore complessivo che corrisponde a valore meno potente (meno privilegiato, valore numerico più elevato). Poi esistono segmenti conformanti: è un segmento che ha un suo livello di protezione inserito nel descrittore, ma che è accessibile da tutti a tutti i livelli. Quindi si conforma a livello di privilegio del chiamante. Per es. se abbiamo libreria che fa radice quadrata, questa può essere acceduta da più livelli. In questo caso quel segmento si chiama conformante. Protezione dei dati: il sistema nel momento in cui vogliamo caricare un nuovo selettore nel registro di segmento controlla contemporaneamente il CPL, il RPL e il PL del segmento. Se il numero più grande tra CPL e RPL e cioè EPL ha un valore <= del livello di privilegio dei dati a cui voglio accedere, allora posso accedere. Questo è esattamente quello che avviene. Potrebbe essere qualunque tipo di accesso, questo controllo non viene fatto a tutti gli accessi ovviamente, viene fatto nel momento in cui andiamo a caricare registro di segmento. C'è però problema: sistema può cambiare di livello. Ma se cambia cambia verso l'alto non verso il basso, quindi se era verificato per livelli precedenti sarà ancora verificato. Esempio: sulla sinistra abbiamo programma, istruzioni assembler. Per caricare dato da registro di segmento si deve passare tramite registro all'interno del processore. Prima istruzione carica selettore (12 bit più significativi che corrispondono al valore vero e proprio nella tabella). Questo requestor è 2 (perché bit meno significativi sono 10). Lo carichiamo in ds e poi cerchiamo di leggere il 257esimo elemento. La seconda operazione è scrivere dato nel 2100esimo locazione. Riportata struttura generale del descrittore e sotto quello che ipotizziamo di avere (in rosso). Se ipotizziamo che il CPL sia 1 è chiaro che EPL tra 1 (codice) e il 2 (requestor) è il 2. Tranquilli di poter accedere. Messe altre indicazione ipotetiche. Altra cosa importante: la base è un indirizzo virtuale, il limiti è indipendente che sia virtuale o meno (è massima dimensione del segmento). Qui messi tutti i controlli che vengono fatti, viene anche controllato che indirizzo non possa debordare da valore massimo, altra cosa che se tentiamo di scrivere è che segmento sia scrivibile. N.B.: qui c'è piccola considerazione, normalmente il sistema lavora con i propri segmenti, quindi il larga misura sulla LDT e difficilmente deve usare dati della GDT. Segmenti di questo tipo sono a privilegio 0, che sono quelli su cui può operarci solo sistema operativo. Esempio: SS (stack segment). Caricato selettore a livello 3 (programma utente). Dobbiamo caricare il top dello stack segment che è bottom dello stack segment. Chiaro che mettiamo valore massimo all'inizio che andrà a decrescere quando facciamo dei push. Possibili configurazioni del descrittore di segmento. Citato in rosso che, fermo restando che lo stack lavora sempre a word, questi possono essere usati anche con programmi che fanno riferimento a 286. Un'ultima cosa riguarda la protezione a livello di pagina, perché fino adesso protezione a livello di segmento. Vediamo che abbiamo due bit: una protezione su scrivibilità. Se segmento definito come scrivibile noi possiamo volere comunque proteggerle dalla scrittura. Supponiamo di avere tabella che l'intestazione non dobbiamo scriverla, ma solo contenuto: questo per dire che all'interno di una stessa struttura dati ci possono essere campi scrivibili e non. Granularità non arriva al byte, ma alla pagina. In un sistema impaginato i segmenti sono sempre presenti (nella memoria virtuale di chi opera). Abbiamo un ulteriore bit che ci dice se user o supervisor. Possiamo avere pagine scrivibili solo da utente supervisor. Utente supervisore è utente che lavora a livello 0. Possiamo avere sia per accesso che per scrittura protezione più raffinate. Pentium 1 – 4 (08) Il meccanismo di protezione dei salti: si può saltare a procedure con livello di protezione maggiore o uguale a quello attuale. Ci sono però due regole diverse: se si salta ad una procedura nell'ambito dello stesso livello di privilegio, cioè se sto eseguendo una procedura a livello 2 e chiamo una procedura il cui descrittore di segmento è caratterizzato da un livello 2 posso saltare direttamente. Esistono CALL far e CALL short, quelle che da un alto richiedono cambiamento registro di segmento e l'altro nell'ambito dello stesso segmento. Qui stiamo facendo CALL far. Per quanto riguarda le chiamate o i salti (stesso tipo di protezione, differenza solo se ci rimaniamo o ci saltiamo). Se segmento è conformante il problema non si pone (possiamo sempre saltare). Se invece vogliamo saltare a un segmento di privilegio superiore, la cosa è lecita ma dobbiamo passare attraverso forca caudina. Se noi saltiamo a segmento di livello superiore, quello può contenere programmi di tipo molto diversi (programmi leciti o illeciti se chiamati da programmi di livello inferiore). Quindi ci possiamo andare solo se dal livello da cui partiamo possiamo andarci. Qualunque tentativo di violarlo dà GENERAL PROTECTION FAULT. Qua dobbiamo verificare che l'effective privilege level (qui è valore numerico più basso tra quello del codice di segmento e del selettore) sia di valore numericamente superiore o uguale al livello di protezione del segmento chiamato: qui facciamo esattamente contrario di quello che facevamo prima. Call gates: Call gates: questo funziona tutto se ci muoviamo all'interno dello stesso livello. La regola di prima è utile solo per salti allo stesso livello. Se invece vogliamo saltare ad una di livello superiore dobbiamo fare CALL GATE (cancello della call). Questo è un particolare descrittore di segmento che non corrisponde ad alcuna struttura dati in memoria. Dobbiamo passare attraverso descrittore il quale contiene informazione relativa a segmento di destinazione e contiene l'entry point che è concesso. Ci dice che possiamo saltare a quel segmento ma possiamo andare solo a quell'indirizzo. Questa sostanzialmente è descrittore di descrittore. Programma non può scegliere quando usa call gate. Questa deve risiedere a un livello di privilegio numericamente superiore all'effective privilege level. Supponiamo n livelli in cui 0 in alto e 3 in basso. Se noi siamo ad un livello la call gate deve stare sotto. Sotto vuol dire o sotto o allo stesso livello (può anche risiedere allo stesso livello). Effective privilege level deve avere valore numerico minore o uguale a quello della call gate. La call gate è anche meccanismo che se si vuole si può usare per saltare allo stesso livello. Nel caso dello stesso livello posso scegliere se farlo attraverso call gate o meno, se invece lo devo fare tra livelli diversi lo devo fare con call gates. Nel momento in cui programma compilato e linkato il sistema operativo si occupa di fare call gate che serve. Cosa contiene call gate: a parte l'informazione sulla protezione, contiene il selettore al segmento che vogliamo usare, poi l'offset (indirizzo interno del segmento) a cui possiamo saltare. Sistema forzato: nel IP oltre ad andare il selettore nel CS, nell'IP ci va esattamente quel numero lì. Poi c'è D-WORD count, che è numero di double word che viene automaticamente trasferito dallo stack del chiamante allo stack del chiamato. Per ogni livello di privilegio associato ad un task esiste uno stack diverso, proprio per controbattere quei problemi di stack overflow di cui abbiamo già parlato. Dire numero di parole con 5 bit vuol dire che noi ne possiamo trasferire soltanto 32 parole, ma è del tutto evidente che possiamo passare anche puntatore a struttura dati. Questo è esattamente struttura della call gate, notiamo che noi la chiamiamo call gate, ma si può chiamare anche jump gate e la possiamo usare per un salto. Se noi saltiamo a livello superiore noi non torniamo mai più perchè non esiste possibilità di tornare a quella inferiore. Allora il problema introduce un altro problema: sono riuscito ad andare ad eseguire istruzione a livello superiore, ma non potrei tornare verso basso. Esistono due istruzioni che permettono di tornare (quindi non salto, ma salto implicito che è ritorno) che sono RET e IRET. Notiamo che il discorso RET e IRET è uno dei tanti casi in cui si annidano hackers, se riesco in qualche modo a cambiare la RET io posso portare il programma dove voglio. Sfruttamento di buco del sistema operativo. Vediamo caso di funzionamento di call gate. Qui riportato i due descrittori: quello della call gate e quello del segmento di destinazione. Accesso call gate avviene come segmento dati: si accede purchè l'effective privilege level sia maggiore uguale al livello di protezione della call gate. Esempio (1): Esempio (1): quando c'è una call o un jump (ma non c'è differenza concettuale), la call se è una call gate (se stiamo saltando ad un segmento dello stesso livello noi avremo call con descrittore di segmento e offset presente nella call stessa, se facciamo call attraverso una call gate l'offset non ha alcun senso perchè contenuto direttamente nella call gate, quindi unica parte importante è quella del selettore). Qui abbiamo fatto conto di chiamare il 63, che è elemento che messo nel data segment ci dice che l'indice della tabella dei segmenti è il 12. Che si tratta della GDT perchè abbiamo messo 0, che pensiamo di passare attraverso call gate di livello 3. Con livello 3 passano tutti, visto che questo livello è accessibile da qualunque EPL. A questo punto, questo è la nostra chiamata ed è ovvio che chiamandola con livello 3, la call gate deve essere 3. Esempio (2): qui ipotizzato che nel nostro descrittore ci sia questa informazione (ipotizzato che ci sia selettore con valore 150, che vuol dire di andare a prendere un descrittore di posizione 2A all'interno della relativa tabella dei segmenti, il numero del segmento della tabella è data dai 13 bit più significativi del selettore). Nelle call gate RPL non è mai usato e l'offset del nostro sistema è all'indirizzo 3400 (offset rispetto ad indirizzo del segmento), nostro sistema pretende anche che vengano passate due parole dallo stack del chiamante a stack chiamato. Esempio (3): questo supponiamo sia segmento di destinazione, su in rosso il template del descrittore, sotto c'è sua espansione, in cui abbiamo messo indirizzo base virtuale del segmento. Tutte caratteristiche di accesso. Con questi dati avendo passato forca caudina del call gate, il nostro sistema è riuscito a portarsi a destinazione. Call gates: qui c'è visione grafica del salto attraverso call gate. Indirizzo messo nell'ambito del selettore di chiamata punta al descrittore call gate che a sua volta punta al descrittore .. Gli accessi sono condizionati dai livelli di privilegio del chiamante e del descrittore call gate. Esmpio trojan horse. Interruzioni: Interruzioni: chiamata particolare. Ovviamente sistema è in grado di gestire interruzioni e le gestisce in maniera molto simile ai precedenti. Cosa fa il processore a fronte di segnale di interruzione. NMI, non dà luogo a cicli di bus esterni. Nel casi di interruzioni normali (sottoposti a InterruptEnable). All'atto del riconoscimento di un'interruzione viene generato un doppio INTA, che è un segnale costruito a partire da segnali di controllo dei due cicli di bus successivi. Durante primo ciclo non succede nulla (congelamento catena priorità interno), secondo ciclo processore vuole leggersi sul bus 0 l'interrupt type. Questo interrupt type viene internamente moltiplicato per 8 (prima per 4), perchè in realtà attraverso questo IT io genero un indirizzo che sommato ad un indirizzo iniziale contenuto in un registro che si chiama InterruptDescriptorTableRegister (un'altra delle strutture che deve essere stata predisposta). Dato viene moltiplicato per 8 perchè sommato ad indirizzo iniziale della tabella mi dà l'indirizzo dove trovo descrittore della procedura che deve essere messa in esecuzione a causa dell'interrupt. E visto che descrittore deve essere di 8 byte, questo deve essere traslato di 3. Situazione più complessa e quindi dobbiamo moltiplicare per 8. Il dato letto da parte del processore è sempre e comunque lo stesso interrupt type (stesso che dovevamo generare nel caso dell'8088). Anche qui massimo 256 interruzioni, unica differenza rispetto a prima è che non siamo più obbligati ad avere tabella nel primo Kbyte, qui tabella può stare ovunque perchè dipende da IDTR che è indirizzo virtuale. Però così facendo lo posso spostare in qualunque posizione. Va da sé che la modifica dell'IDTR viene fatta solo da istruzioni privilegiate, qui continuiamo a parlare di istruzioni privilegiate, che sono consentite solo da programmi che lavorano a livello 0. E' ovvio che anche nel caso dei sistemi di tipo Pentium esistono INT n (istruzione privilegiata), che sono chiamate ad interruzioni le quali sono sottoposte al controllo dell'interrupt type. Qui solita rappresentazione grafica, anche per questa tabella esiste valore limite. Nel caso della GDT abbiamo costruzione e distruzione di segmenti, qui descrittori che puntano ai segmenti di risposta sono gli stessi. Corrispondono agli interrupt prevedibili. Noi potremmo ipotizzare tabella in cui vi sia anche un descrittore di tutti gli interrupt non previsti. Questa potrebbe essere protezione contro interrupt spuri. Gestione “anomalia”. Questa tabella è molto ridotta (2KB=256*8). Qui un'ulteriore descrizione grafica. Il descrittore che viene recuperato attraverso la moltiplicazione per 8 si chiama interrupt gate proprio perchè non fa altro che mandarci a subroutine di destinazione, indicandoci selettore ed entry point all'interno di quel segmento: è call gate, con piccole differenze per quanto riguarda accessi. Meccanismo e contenuto uguale a quello della call gate. Struttura dell'interrupt gate. C'è aspetto importante che è il contenuto della interruzione. Il contenuto dell'interruzione può essere duplice, qui entriamo in un settore a strettissimo contatto con i sistemi operativi. In realtà il selettore presente all'interno dell'interrupt può puntare sia ad un segmento di codice, sia ad un descrittore di task, quindi è possibile che in presenza di un'interruzione ci sia cambio di contesto. Differenza tra chiamata a procedura e l'attivazione di un task è che una procedura si muove nello stesso ambito (stessi segmenti, stessa tabella pagine, stesso stack ..), chiamata ad un task vuol dire fermare task attuale e reimpostare intero environment dell'altro task. Task è un'unità a se stante, mentre procedura può considerarsi dipendente. Interruzione può richiamare task. Task State Segment: Qui c'è definizione di task, che riflette in hardware quello già detto in sistemi operativi. Qui andiamo a vedere cosa ci offre sistema, hardware di supporto per context switch (salvare tutto vettore stato in zona determinata che è TaskStackSegment, che contengono tutte info necessarie per messa in opera). Nel caso del Pentium, il context switching è fatto totalmente in hardware. Avviene a causa del fatto che nel descrittore di segmento, il selettore contenuto nell'interrupt gate sia un selettore che punta ad un TSS, in quel caso quindi c'è cambio di contesto. Cambio di contesto nel Pentium è sempre richiamo. Quando si salta ad un TSS siamo in un cambio di contesto. TSS: leggermente complicato da vedere. Base del segmento è puntata da Task register, poi sono contenuti tutti gli stack segment e tutti i puntatori degli stack segment del task quando lavora al livello superiore. Poi ci sono tutti i registri di normale utilizzazione, poi ci sono i registri di segmento utilizzati dal task all'interno della sua normale esecuzione a livello thread. Poi c'è selettore che punta a LocalDescriptorTable del task. Poi abbiamo dati che possono essere usati dalla struttura del sistema operativo e notiamo che questi dati sono di valore variabile perchè qui dentro c'è un'altra informazione che ci dice dove comincia una tabella I/O permission map. Cambiando questo valore, ed essendo questi Interrupt Redirection Map fissi. Queste strutture dati che dipendono da sistema operativo possono essere classificate. L'interrupt redirection map è una tabella che prende l'interrupt type (meccanismo di interruzione 8086, quando lavoriamo modo 8086) che vengono dall'esterno e li manda all'interrupt descriptor table come da sistema protetto. Perchè quando lavoriamo in modo 8086 le interruzioni ricevute devono essere sottoposte al sistema Pentium. Ultima cosa è I/O Permission Bit Map, anche questa può avere dimensione variabile. Lunghezza di questa mappa dipende da numero impostato come limite del TSS, più elevato è il limite maggiore è il numero di bit che posso avere all'interno di questa mappa. Nel momento in cui task viene creato, viene anche definita questa tabella che impone dimensione massima. C'è sempre il solito problema di come task ha diritto ad usare istruzioni di I/O. Ci sono due categorie di I/O, esistono gli I/O sicuramente usati da più task (es. tastiera o stampante) e allora il controllo di questi è dedicata a sistema operativo. Nel momento in cui vogliamo usare questi task dobbiamo fare chiamata a sistema operativo (che è call che passa attraverso call gate) che ci permette di accedere a subroutine. Ma ci possono essere casi in cui task ha dei dispositivi di I/O suoi propri che non sono noti a nessun altro. In un sistema embedded potremmo avere dispositivi non noti a sistema operativo ma solo a task. In questo caso nessun altro andrà ad interferire. In I/O Permission Bit Map abbiamo un bit per ogni possibile indirizzo di I/O (che sono 65537), in realtà in termini di byte è al massimo di 8KByte. Contiene per ogni indirizzo 1 o 0 a seconda che quell'indirizzo possa essere direttamente usato da nostro task o sia riservato a sistema operativo. Questa mappa viene inserita nei task e viene inserita proprio attraverso definizione dimensione del task, perchè potrebbe essercene neanche uno e allora faremmo terminare il TSS immediatamente dopo l'ultima InterruptRedirectionMap. Se invece ne abbiamo almeno uno, dobbiamo mettere tanti bit fino ad arrivare ad indirizzo massimo di appartenenza di I/O. In generale nei sistemi che usiamo questa mappa è sempre nulla, perchè non è concepito uso di sistemi di I/O direttamente da parte dei task. Il valore di CR3 a differenza di tutti gli altri contenuti dei registri di controllo, il CR3 è diverso per ogni task, perchè ogni task ha diversa tabella delle pagine, quindi CR3 ci fa puntare a tabella delle pagine di primo livello di quel task (che deve essere in memoria). Contenuto di tutti gli altri registri di controllo non riportato perchè è di carattere globale e non specifico. C'è bit importante di link. Un task può mettere in esecuzione un altro task. Ora sia ben chiaro che differenza c'è tra procedura chiamata o task chiamato (in tutti i sistemi che girano sul pentium esiste concetto di time-slice). Esiste link field: quando task richiama task è come se avesse fatto chiamata a procedura (in realtà è task), ora ovviamente quel task non può richiamare il task chiamante. In quel link field c'è puntatore a task chiamante, in caso di chiamata a quello scatta protezione. Ci sono informazioni riguardanti TSS e link field. Protezione I/O: Protezione nel task di I/O: abbiamo parlato della I/O Permission Bit Map, in generale le operazioni di I/O possono essere usate solo se il livello di privilegio del task in esecuzione (sistema operativo è egli stesso task, anzi una serie di task) è numericamente minore o uguale di un bit presente nel registro di flag (ora a 32 bit, non più a 16 bit) e c'è un bit che è I/O privilege level che è bit che indica qual è livello di privilegio al quale deve appartenere task se vuole usare istruzione di I/O. Se I/O privilege level è 2 vuol dire che possono usare istruzioni di I/O solo quelle che lavorano a 0 1 e 2. Il livello di esecuzione (quello contenuto nel code segment, current privilege level). Qui ci sono molte altre informazioni: c'è alignement check per dirci se ultimo accesso era allineato o no. I bit che esistevano anche nell'8086 si ritrovano qui nei primi 16 bit nelle stesse posizioni. Processo di integrazione/aumento. Molti di questi riguardano sempre il meccanismo di esecuzione di programmi 8086 in quel contesto specifico della finestra DOS. Protezione I/O: ricordiamo che nel caso di sistemi Pentium I/O è a 32 bit, mai a 64. Qui c'è anche spiegazione di cos'è BitPermissionMap. Descrittore del nostro task state segment. Nell'ambito del type esiste un bit che dice se è busy, che vuol dire se è in esecuzione o è stato sospeso perchè ha chiamato un altro task (vale quell'informazione di link che punta a descrittore messo in esecuzione da task attuale). Il TSS è una di quelle info molto protette che lavora a livello 0 e contiene info molto simili rispetto a quelle già viste. Task register: questo è puntatore a task state segment. Per attivare un task (operazione che fa sistema operativo quando vuole iniziare), si carica il selettore al task che punta al descrittore del task state segment (che è segmento di dato in memoria che ha suo descrittore contenuto nella GDT). Quando noi parliamo di task register parliamo di registro che contiene selettore che ci fa puntare a descrittore del task, che a sua volta ci fa puntare al task state segment (di lunghezza variabile). Naturalmente è ovvio che quando parliamo di task register automaticamente in ombra c'è anche il corrispondente descrittore caricato in un registro ombra del task register e non soltanto il descrittore, ma anche limite. Quando carichiamo info nel task register carichiamo automaticamente nei registri ombra l'indirizzo iniziale e il limite superiore. Task gate: così come esistono le call gate, così esistono le task gate (task utente che mette in esecuzione un altro task utente). Normalmente il sistema operativo usa task register: modalità equivalenti, ma caricamento di task register richiede istruzioni privilegiate. Per completare c'è descrizione dettagliata del cambio di contesto e di come lo si può ottenere. Insieme di lucidi che spiega come funziona, poi spiegazione grafica del link. Elenco dei registri di sistema (CR1 non utilizzato nel Pentium, ma solo in quelli successivi). La GDTR e la IDTR contengono indirizzi perchè sono strutture a sé stanti, il TR e il LDR contengono dei selettori (perchè fanno riferimento che è GDT). C'è un ultimo settore che non possiamo analizzare in fino, che contiene né più né meno che un programma semplice di inizializzazione di un sistema operativo. Qui c'è breve programma che permette di costruire tutte le chiamate iniziali. Pentium 1 – 5 (09) Coerenza cache: Qui abbiamo rappresentato un sistema generico. E' chiaro che di queste rappresentazioni ne possiamo avere quante vogliamo, se ne vogliamo una più precisa dovremmo indicare anche controllori PCI. Interfaccia tra processore e mondo esterno è un controllore PCI, che genera un bus su cui si calettano i dispositivi esterni. L'unica cosa con cui colloquia direttamente è la memoria, per lo più (in sistemi multiprocessori) è memoria a doppia porta, che il processore vede come locale, ma essendo doppia porta, è anche facente parte della memoria globale, alla quale possono accedere anche altri processori. Ci sono varie politiche per le cache. Alcune già citate. Write-through fa sì che la cache fornisca un vantaggio di velocità e di non occupazione del bus (perchè vero problema è che quando il sistema è multiagente l'occupazione del bus è uno degli elementi di debolezza). Politica write-through è quella che per ogni scrittura sulla cache viene scritto anche in memoria. In termini puramente statistici la lettura è molto più frequente della scrittura. Anche per quanto riguarda dati, forte prevalenza di letture. C'è anche write-through bufferato che cambia solo efficienza processore: in molti sistemi di questo genere (fra questi i sistemi pentium) c'è il posted write. Nel momento in cui c'è politica write-through per accellerare funzionamento processore, possiamo farlo scrivere sulla cache (nell'ipotesi che dato sia in cache). Questo pone vincolo che nessun altro processore deve accedere a dato in memoria fino a che quel dato scritto in cache non è riscritto in memoria. La politica write-back è la politica che riteniamo più efficiente in termini di velocità, ma ci richiede una maggiore complessità per le reti di accesso. Questa è tale per cui non si sia costretti ogni volta che c'è un accesso ad un dato che è presente in una o più cache si è costretti a fare verifica di quel dato se modificato e laddove modificato dobbiamo scriverlo. Ogni volta che un processore ha un miss si scatena meccanismo piuttosto complicato e lungo. In termini di località una volta portato in cache, fintanto che non c'è scrittura che scombussola carte l'efficienza della read è molto buona. Questo concetto di write-back write-through si accoppia con write-allocate. Anche se non cambia più di tanto i meccanismi: egli ha come unica differenza che prima di scrittura c'è una lettura ed è come se in un'unica operazione ce ne sono due. In realtà la write-allocate qualche vantaggio lo dà: se faccio prima lettura, devo fare broadcast di richiesta per sapere se ha dato modificato e quindi forzare una scrittura. Poi se andiamo a scriverlo dobbiamo fare un altro broadcast. Se usiamo politica di write-allocate: se uno di voi ha il dato modificato certamente è presente in una unica cache. Il meccanismo è: invalido subito, perchè sto per scrivere. Dal punto di vista concettuale si può associare a due operazioni successive: lettura e scrittura. Discorso visto da un'altra parte è quando scrive altro master. Se siamo in politica write-through il problema non c'è. L'unica operazione in caso di scrittura è l'invalidazione della cache, perchè tutte cache hanno dati allineati. Se invece siamo in write-back, in caso di scrittura dobbiamo forzare scrittura di processore che ha dato modificato. Dobbiamo farlo perchè potrebbe accadere cosa strana: il processore master vuol scrivere linea di cache, deve forzare scrittura di un altro processore della sua linea. Linea di cache è composta da 32-64, 128 byte .. Quando diciamo che linea è modificata non sappiamo quale byte sia da scrivere e quale altro byte abbia modificato l'altro processore. Potrà sovrascrivere dei dati che erano stati modificati nella cache di partenza, ma può anche andare a scrivere dati non modificati dalla prima linea. In generale noi abbiamo anche cache secondarie, terziarie, ecc .. E in generale avremo dei segnali (qui alcuni) che danno informazioni. Qui abbiamo L1 ed L2 esterna con segnali. Se il Pentium ha L2 interno, questo disegno si applicherebbe ad L2 e L3. Questo tratteggiato ci dice che intero insieme all'interno potrebbe essere realizzato in stesso dispositivo integrato. Nel caso degli XEON (processori server) anche L3 è integrata. In generale abbiamo cache sempre di dimensioni superiori con tempi di accesso maggiore. Bilanciamento tra tutti questi componenti fa sì che riusciamo ad avere ottimo compromesso. Una delle cose importanti: in generale ogni cache di livello superiore contiene un super-set delle informazioni del livello inferiore. L2 è dimensione superiore ad L1, ma se un dato è in L1, allora è anche in L2; mentre non è vero viceversa. L2 è un super-set della L1, L3 super-set di L2 .. ecc .. Questo è presupposto non obbligatorio che noi adottiamo come assodato perchè è ciò che è adottato da larghissima maggioranza di processori. Qui riprendiamo discorso di prima e cerchiamo di capire cosa succede nelle varie politiche in caso di 2 o n cache. Se entrambe (L1 e L2) sono write-through siamo nel caso visto prima con coerenza assoluta tra cache e con memoria. Come al solito abbiamo problema del deferred write. Un processore non saprà mai se a valle della sua cache ci sia un'altra cache o la memoria, per il processore la cosa è ininfluente sapendo che a valle ha dispositivo in grado di memorizzare. Lui quando scrive non si pone il problema di dove (in un generico esterno). Poi possiamo avere L1 write-through e L2 write-back. Processore è convinto di essere coerente, perchè ogni volta che scrive dato all'esterno lui pensa di scrivere in memoria. In realtà scrive su L2, se questa è write-back, laddove ci sia lettura o scrittura da agente esterno, si dovrà fare su investigazione solo su L2 (perchè in L1 ci sono dati coerenti con L2, sempre se ci sono). Meccanismo che si occupa solo di L2. In caso di scrittura da parte di un master esterno, se la L2 aveva un dato modificato rispetto a memoria coerente con L1, deve andarlo a sovrascrivere in memoria e a quel punto invalidare sia L1 che L2. Se poi L1 e L2 sono entrambe write-back il meccanismo esposto per la L2 diventa anche per L1. Una eventuale lettura deve andare a cercare se in una L2 è stato modificato, in quel caso dobbiamo andare a fare test anche su L1. In L1 è sicuramente più attuale (a causa del fatto che siamo in write-back). In questo caso dato preso da L1, sovrascritto in L2, poi in memoria e a questo punto smistato a chi richiesto. Se dato non c'è in L1, allora L2 risponde come nel caso precedente in cui L1 è in write-through. Nel caso di scrittura siamo in un caso di espansione di quello già visto: in L2 se è stata modificata va a vedere se la L1 è anch'essa stata modificata. Se L1 non ha dato, oppure ha dato coerente con L2, allora quest'ultima scrive dato in memoria e poi vengono invalidati. Se il dato è diverso scritto in memoria dato di L1 e poi entrambe invalidate. Non abbiamo detto come queste condizioni si rapportano fra loro. Gli stati della L1 e L2 come sono tra loro legati: abbiamo solo detto che se dato è in Li, allora è in L(i-1); ma non detto rapporti fra stati. M.E.S.I.: Monoprocessore: Abbiamo solo processore (con L1 e L2) e degli altri agenti che non hanno cache. Un agente è di per sé una sorta di processore ridotto, che permette solo di fare operazioni specifiche. Un controllore video, controllore disco è un agente ed hanno tutti loro cache. Li chiamiamo agenti, ma in realtà sono mini-processori, li chiamiamo così perchè la loro funzione primaria è quella. Il confine è molto labile. Nel caso monoprocessore (nel caso di assenza di altre cache), abbiamo quattro stati: M modified: una cache ha linea in stato modified se quella linea è diversa dalla cache o da memoria che sta a valle; vale per L1, L2, ecc .. Il dato è diverso da quello a valle e più recente di quello a valle. I invalid: è evidente. Naturalmente se dato invalid, se abbiamo lettura abbiamo bisogno di line-fill. Se scrittura di dato invalid se siamo in no-write-allocate (sequenza di due operazioni) abbiamo scrittura in memoria senza coinvolgimento della cache E exclusive: dato si trova nella cache ed è coerente con quello che c'è a valle. E' tale per cui una scrittura su una linea di cache exclusive non comporta una scrittura su un dispositivo a valle. Se L2 ha dato exclusive, vuol dire che dato coerente con memoria. Se scrivo su quella linea di L2, non devo rimbalzare dato in memoria. Allora exclusive nel caso dei sistemi monoprocessore lascia perplessi: tutti i dati sono exclusive. Ma significativo nel caso di monoprocessore vuol dire anche che una scrittura su quella linea non richiede scrittura a valle. S shared: vuol dire che dato ce l'ho, è coerente con dato a valle, ma scrittura su quel dato mi comporta scrittura a valle. Supponiamo (caso reale) che sia la stessa linea di cache presente in L1 e L2, in L1 sia exclusive e in supponiamo che per assurdo sia L2 exclusive. Nel momento in cui vado a scrivere in L1, non avrei bisogno di rimbalzare dato, ma quando ho dispositivo che vuol leggere dato, quando arriva ad interrogare L2 dice che è coerente con memoria. In generale per la stessa linea di cache, più ci si addentra vicino al processore, maggiore è il restringimento del campo di azione. Significato di exclusive e shared assume significato maggiore in caso multiprocessore. Possibili stati di una stessa linea: possibile situazione per come è fatto MESI. Se L2 è I, anche L1 è I, perchè L2 è super-set di L1. Se dato è in E in L2, noi possiamo averlo come S in L1 oppure non presente in L1. Se dato è M in L2, allora in L1 può essere in E, o M o non presente. Questo perchè se in L2 è in M, certamente qualunque test fatto da esterno deve essere rimbalzato all'interno. Ora in L1 potrebbe essere in E, perchè coincide con dato. Oppure può essere M. Notiamo che questo caso la L2 non è mai shared (qui siamo in monoprocessore). Quando una linea shared che viene scritta rimbalza scrittura a valle e si porta in stato exclusive. Qui in stato exclusive (dato coerente, ma scrittura) in caso di scrittura dobbiamo scrivere a valle. Ma perchè scrvere due volte: scrivere a valle per portare ad exclusive e poi scrivere per portarla in modified. Basta che la mettiamo in stato exclusive e scriviamo una volta a valle. Line fill: supponiamo che dato non ci sia né in L1 né in L2, gli stati in cui la linea di cache si porta all'atto della lettura dipendono dal segnale WB/WT: questo segnale (da non confondere con politica cache). Ci sono due cose distinte che hanno nomi uguali: una politica cache che si definisce in CR0 (di cui uno dei due si chiama not-writew-through e l'altro enable/disable). Questo definisce comportamento generale della cache, se è definita in write-through quella funziona così. Se quel bit non è attivato, quella cache funziona in write-back e ci dice che (se possibile) una scrittura sulla cache non viene rimbalzata direttamente a valle. Se siamo in politica WB per motivi di coerenza dobbiamo definire ad ogni line fill in quale stato portare linea letta. Se vogliamo che vada in stato shared, il segnale WB/WT* deve essere tenuto basso. Il dato in questo momento lo portiamo in cache in stato shared. Una cache può benissimo in write-back e una linea in stato shared. In quel caso il segnale WB/WT* lo metto basso. Se quel bit lo avessi messo alto, il dato lo avrei letto in exclusive. Segnale serve per definire lo stato in cui leggo linea. Questo vale per L1 (e verrà fornito da L2, uguale per L2 e memoria ..). Quando noi leggiamo un dato, il dato nei sistemi monoprocessore viene portato in exclusive in L2 (WB/WT* alto pilotato da esterno, cioè controllore di memoria deve essere programmabile tale per cui lettura di memoria attiva quel segnale), e controllore cache secondaria che rimbalza stesso segnale verso L1 deve mettere quel segnale basso in modo che in L1 vada a finire shared. L2 in exclusive e in L1 in shared. Noi dobbiamo fare in modo di fare il minor numero possibile di accessi ai livelli successivi di cache (o memoria), ma dobbiamo farli quanto basta in maniera tale da mantenere coerenza. Discorso exclusive, shared e così via ci consente proprio questo. Come vediamo lo stato nella cache primaria è sempre più restrittivo rispetto a quello cache secondaria. Caso lettura: in caso di lettura da parte di dispositivo esterno (DMA controller). DMA chiede dato a memoria, controllore ha due possibilità: o quell'indirizzo fa parte di linea non cacheable e allora dato non si pone e gli passa dato. Se no se quella linea è cacheable devo fare snoop: dimmi tu controllore della cache secondaria del processore, hai per caso quel dato? .. Se no: non c'è neanche in L1. Se sì dipende da che stato: se exclusive vuol dire che è uguale a memoria (quindi posso portarlo dalla memoria). Se dato modified, controllore di cache L2 potrebbe essere modificato anche in L1 e allora snoop in L1. Se in L1 (può anche non esserci) è Exclusive o modified (ma non in shared, perchè quando il dato è stato letto in L1 il controllore gli ha dato WB/WT*). Esempio: una linea non c'è né L1 né L2. Poi lettura: L2 in exclusive e L1 in shared. A questo punto scriviamo linea, se noi la scriviamo in L2 modified e la linea in L1 si porta in exclusive. Adesso in L1 dobbiamo rimpiazzarla. La linea si trova solo in L2 e non in L1. Ora supponiamo di aver bisogno di nuovo di quella linea. Risponde L2 che si trova in modified. La linea potrebbe essere da linea L1 o in shared o in modified a seconda di come L2 dà segnale WB/WT*. In questo caso controllore della L2 darà segnale WB/WT* attivo in modo tale che il dato venga letto in L1 in stato exlcusive. Siccome in L2 è in modified, darglielo in stato shared vorrebbe dire che se per caso la linea in L1 deve essere scritta, prima la scriverei andandola a scrivere anche su L2, portando L1 in stato exlcusive e poi andrei di nuovo a scrivere in L1 la volta successiva. Ma tutto questo non mi serve: se la L2 è in modified, tanto vale che porti L1 in exclusive in modo che la prossima scrittura scriva su L1, ma essendo L2 in modified qualunque test fatto su L2 viene rimbalzato anche su L1. Lettura di un dato se c'è un miss in L1 e in L2, in L2 si porta in exclusive e in L1 in shared, sempre tramite WB/WT*. Se dato in L2, quindi line fill sono in L1 il controllo di L2 dà WB/WT* a seconda che il dato in L2 sia in exclusive (in modo che si porti in shared) o in modified (segnale 1 in modo che si porti in exclusive). Scrittura di master esterno stessa cosa di prima: enquiry. Se L2 in exclusive allora snoop si ferma lì perchè L1 è in stato shared oppure non c'è. Se in L2 è modified allora richiesta rimbalzata in L1 (non ce l'ho, exclusive quindi è uguale al tuo oppure modified ed il mio è più recente). Caso del DMA (che non ha cache). Ripercorriamo discorso fatto (lettura lucido). La scrittura è la medesima appena detto. Esempio: dato L1 e L2 modified. Doppio enquiry. Dato preso da L1, scritto in L2 e scritto in memoria. Quindi coerenza L1 L2 e memoria, che è la stessa coerenza in cui si ritroverebbe con line fill: L1 shared, L2 exlcusive. Tutto questo avviene tramite WB/WT*. Nel caso ci sia anche L3 all'atto di un line fill cosa succederebbe? La L3 la porto in exlcusive, L2 shared e L1 in shared. Ogni livello successivo dà segnale WB a livello precedente. In base a segnale fornito da livello successivo il livello attuale si pone nello stato giusto. Nel caso di monoprocessore, l'ultimo livello in caso di line fill è stato exclusive, mentre tutti altri in stato shared perchè dobbiamo avere certezza che eventuale scrittura viene rimbalzata ad altri livelli. Nel caso di n cache il livello più vicino al processore dovrebbe scrivere ad ogni livello fino a memoria. In realtà viene fatta unica scrittura su livello più vicino e poi viene rimbalzata a tutti gli altri. In caso di dati modificati dobbiamo portarci in una condizione di uguaglianza. Se dato presente solo in L2, dato viene scritto in L2 verso la memoria, dopo non va a finire anche in L1, ma ci troviamo L2 in stato exclusive. In caso di scrittura del DMA controller, meccanismo uguale a quello della lettura, perchè andiamo a cercare dati modificati dentro alle varie cache e andiamo a portarli a memoria. In questo caso al contrario di prima dati vengono invalidati, meccanismo riscrittura è lo stesso. Multiprocessore: Quando effettivamente ci sono più sistemi con più cache, qui naturalmente abbiamo schematizzato il tutto per il caso in cui ci siano solo cache L2, se ci sono L3 discorso si pone uguale. Qui abbiamo una situazione diversa: per quanto riguarda lo stato modified: in questo caso dato presente solo nella cache di un unico processore, negli altri non c'è quel dato (per come è fatto MESI) ed è diverso dal dato in memoria stato invalid stato exclusive: qui è un po' più comprensibile: quel dato è presente in un solo sistema di cache (in un solo processore) e quel dato è uguale al dato che gli sta sotto (sia esso cache o memoria). Qui exclusive si capisce meglio: c'è solo in quel sottosistema. E' uguale al dato che c'è nella memoria a valle (memoria o altro dispositivo) e come nel caso precedente una scrittura non comporta scrittura a valle. Il dato si porta in modified. se stato shared comporta scrittura a valle e dato si porta in exclusive. Qui abbiamo un ultimo caso che è lo stato shared. Qui tutte le cache possono essere in shared, perchè posso essere presenti nella Ln di diversi processori. E' POTENZIALMENTE presente in altri processori perchè dipende da storia. Esempio: processore 1 line fill di una certa linea. L2 si porta in shared ed L1 in shared. Poi un altro processore legge e quindi i due sottosistemi hanno stessa linea, dunque stato shared. Ora potrebbe succedere che in P2 quella linea devo scaricarla per rimpiazzarla. In generale nella storia di questo sistema ci può essere stato un momento in cui dato era in diverse cache. L2 quando facciamo line fill si può portare in due stati diversi: dobbiamo fare enquire. Qui dobbiamo sapere se per caso linea si trova da qualche altra parte, magari modificata, perchè prima sarebbe da riscrivere in memoria e poi solo dopo fare line fill. Se linea si trova in qualche altro processore L2 si porta in shared. Se invece all'atto della lettura si scopre che dato non si trova da nessun altra parte allora in quel caso si porta in exclusive. E' il segnale WB/WT* che fa portare linea in uno dei due casi pilotato da controllore di memoria, quest'ultimo lo decide in base all'enquiry che ha fatto. Qui nuova definizione, che non è altro che la definizio allargata per il singolo processore. Quindi sappiamo che anche la cache di più basso livello può essere in stato shared perchè può essere presente in più processori, ma può essere in S anche se si trova solo lì, nel caso in cui dato presente in due processori e in uno deve essere rimpiazzata. Dopo il rimpiazzamento una rimane in stato S, non crea problemi. L'unica differenza è che abbiamo scrittura in memoria in più (scrittura su quella linea, provoca scrittura a valle e quindi in memoria) che non sarebbe strettamente necessaria se sapessimo che quella linea è solo lì. La scrittura in memoria serve per far partire il broadcast per la scrittura delle linee di altri processori. Se ci fosse anche L3 non cambia di molto la situazione: è sempre più restrittiva mano a mano che ci si avvicina al processore. Non può in L1 essere in stato E con L2 in E perchè una eventuale scrittura su L1, non rimbalzerebbe la scrittura su L2. Notiamo che questo meccanismo è quello che troviamo ora nei nostri processori. Se noi avessimo un diverso meccanismo, in cui per esempio lo snoop verrebbe fatto su tutte le cache comunque, potremmo avere anche diverso meccanismo. Questo è IL protocollo MESI dei processori Intel, ma non è l'unico MESI possibile. Se il dato in L2 è M, in L1 può essere o M o in E (vuol dire che ho scritto solo una volta in L2). Una volta detto tutto questo meccanismo, in realtà queste spiegazioni diventano abbastanza automatiche. Se nostro processore vuol leggere e ce l'ha in L2, lo va a prendere da lì e se è in stato S, L1 lo va a leggere e si porta in S. Se in L2 è M, L1 va in E. Se in L2 E, se lo porta e va in S. Se invece c'è un miss sia in L1 che in L2, dobbiamo interrogare sistema. Usiamo meccanismi come questi o similari. Fa un broadcast e chiede se negli altri processori c'è dato richiesto. Se nessuno ha dato, dato è letto dalla memoria, L2 in E e L1 in S. Quindi abbiamo dato che è solo nel nostro processore. Vale sempre il discorso del pin WB/WT* per far transitare stati. Per L2 il pin sarà in 1 (WB), per L1 sarà 0 (WT) e quindi ci fa transitare in questi stati descritti. Se il dato è presente in più cache certamente più cache rispondono con HIT. Se tutte rispondono con questo segnale nessuno ce l'ha modificato. Nel momento in cui almeno un processore risponde con HIT (e quindi uguale a memoria) è evidente che noi ci leggiamo il dato nel nostro sistema cache, il dato in L2 va in shared perchè è presente anche in un altro processore e anche L1 va in S. Se poi succede che un processore (uno solo può farlo) risponde con HITM vuol dire che il dato è presente in una delle cache del processore modificato. Il dato può essere solo lì, in questo caso a fronte di HITM il controllore di memoria mette in attesa dispositivo e provoca una riscrittura. Quando riscrittura da controllore memoria a processore, questo va a vedere se L1 o L2 ha dato più recente, il dato più recente viene scritto in memoria, entrambe le cache vengono portate in S. Dato consegnato al processore richiesto le cui cache vanno in stato shared. CHIT/CHITM (intesi come combinazione dei vari HIT e HITM) sono open-collector. Quando faccio snoop chiedo se c'è e se c'è gli dico di portarsi in stato shared. Caso del write: se devo scrivere fino a quando L2 è in E o in M, posso permettermi di scrivere localmente. Certamente scriverò su L2 e scriverò anche in L1. Al momento in cui scrivo su L2 ci sono due possibilità: dato in L2 e non in L1, quindi scrivo solo su L2 (perchè non ho write-allocate); oppure posso avere dato in entrambi: S-S, in questo caso scrivo su L2, ma devo fare snoop per sapere se qualcuno ce l'ha questo dato e gli dico di invalidarsi e poi scrivo su L2. Se L2 in E posso scrivere solo su L2. Se dato presente anche in L1: se L2 in E, in L1 non poteva essere che shared, quindi scrivo sia su L1 sia su L2 (L1 E, L2 M). A mano a mano che vado verso il centro, scriviamo più volte. Alla fine, dopo n riscritture (con n livelli di cache) il processore colloquia con la cache più vicina. Diagramma degli stati: il diagramma delle transizioni del MESI non è diverso a seconda di quante cache ci siano. Ogni cache rispetta protocollo MESI. Questa si comporta in base a stato e da segnale WB/WT* che gli viene da sotto. L'aggiunta di cache intermedie non altera comportamento di cache più vicine a processore. Qui abbiamo in modo tabellare la descrizione del comportamento che sono quelle già indicate. I primi 3 riguardano HIT in lettura (questo non cambia mai stato cache). Un miss può dar luogo a più situazioni. “I” vuol dire invalido o che dato non c'è. Quando ricevo da dispositivo a valle un KEN=1 ho cercato di scrivere in cache, ma non mi viene concesso di farlo. Se invece è abilitato attenzione che dobbiamo fare alcuni casi: è chiaro che possiamo transitare da invalid a E o S solo se sono bassi CACHE e KEN. Ovviamente WB/WT* gioca ruolo decisivo per stabilire transizione. Qui gioca anche altri due segnali: Page Cache Disable e il Page Write Through. Per cui a livello della singola pagina, indipendentemente della politica generale possiamo avere politica diversa. E' chiaro che in questo caso abbiamo override: una cache che avrebbe WB attivo e cache abilitato se PWT se per quella pagina è a 1, diventa in stato shared. Qui abbiamo tutti elenchi in cui abbiamo tutti i segnali di cui abbiamo parlato. E' ovvio che un miss su L2 o un miss su Ln c'è uno snoop su tutto il sistema a microprocessore. Se miss si fa su cache di livello intermedio, lo snoop o la richiesta viene fatto a cache di livello inferiore. Snoop broadcast viene fatto solo quando si è al livello minore. Nel caso scrittura simile. Possiamo avere linea in vari stati. Notiamo che il WriteAllocate là dove c'è non è altro che combinazione consecutiva di un read(miss) più una write. Il tipo di transizione dipende da come quella linea è stata letta. Il vantaggio di avere stato E rispetto a M, mi evita riscrittura inutile. Qui c'è snoop. Tra i tanti segnali del Pentium c'è segnale INV: mi può arrivare questo segnale con snoop. Se ce l'ho modificato lo devo anche andare a scrivere in memoria. Qui stiamo facendo summa delle cose già viste. Notiamo che nei prossimi lucidi abbiamo semplicemente i segnali PCD e PWT. Supponendo di avere impaginazione a due livelli il segnale il PCD e PWT per la directory table ce l'abbiamo in CR3. Il contenuto di ogni singolo slot della page directory table ci dice se la tabella di secondo livello è cacheable e ci dice se è in politica write-through. Negli elementi della tabella delle pagina si chiamano PCD e PWT, questi fanno riferimento (se presenti nella tabella di 1o livello) alla tabella di secondo. Quella info va ad agire con la cacheabilità del segmento che contiene tabella delle pagine. E contenuto PCD PWT della tabella di secondo livello riguardano dati. Per la tabella di 1o livello chi ci dice se è cacheable o politica WT sono due bit in CR3. Il PCD e PWT in 1o livello indicano politica di gestione della tabella di 2o livello e il PCD e PWT della tabella di secondo livello mi dà info su dati. PCD/PWT esce anche da Pentium per indicare al cache di secondo livello come deve comportarsi in presenza degli stessi elementi. Se per caso abbiamo pagine da 4MB (solo tabella di primo livello), escono sui piedini contenuto tabella di primo livello. Il PCD e il PWT come vediamo se il paging è abilitato vanno in AND con segnale di abilitazione della cache e sono segnali che escono. PCD è funzione sia della disabilitazione a livello di pagina, sia funzione della disabilitazione cache in generale. Per quanto riguarda line-fill: cioè se posso leggere dato quando ne ho bisogno e tento di portarmela in cache, questo dipende da KEN e dal fatto che la cache non sia inibita e dal fatto che non abbiamo PCD. In questo caso abbiamo possibilità di portarci dato in cache. Segnale CACHE viene attivato o perchè siamo in WB (in questo caso non c'è KEN che tenga perchè quando dobbiamo scrivere vuol dire che ce lo siamo già portati in chache) oppure quando dobbiamo fare letture. Questo è schema logico, che riflette comportamento globale. Se abbiamo pagine da 4MB non sarebbe segnale di page table ma quello della page directory. Qui diagramma temporale di lettura di un read miss con linea clean. Lo stiamo guardando dal punto di vista di cache L1. Qui abbiamo il segnale ADS e attivazione segnale CACHE. Notiamo che KEN è campionato insieme al NextAddress. Se fosse arrivato prima BRDY l'avrei campionato con lui. Questo non fa altro che in sequenza mi dà 4 ready per leggere dato da memoria e portarlo in entrambe le cache (L1 S e L2 E). Il fatto che controllore di memoria dia NA, vuol dire che il nostro Pentium è abilitato ad emettere indirizzo prima di terminare lettura. Mentre noi abbiamo da leggere il dato in L2 notiamo che in L2 molto probabilmente deve portarselo in una linea precedentemente occupata. Ecco che L2 prima di leggersi il dato, invalida equivalente linea nell'ipotesi che in L1 fosse presente. Se per caso tu ce l'hai invalidala anche tu. In questo caso abbiamo ipotizzato che non fosse modificata in nessuna delle due cache. Questa invalidazione, EADS, generazione indirizzo da invalidare e segnale INV vuol dire a L1 che se ha quel dato deve invalidarlo. Se lui va a sovrascrivere L2 quella linea (che non c'è più in L2) non c'è più neanche in L1. Prima di sovrascrivere linea dice di invalidare linea. Non è detto che L1 si porta nella stessa posizione invalidata in L2. Possono avere politiche e dimensioni differenti. A questo punto terminata lettura dato. In questo caso ipotizzato che lettura possa essere fatto senza stati wait. Notiamo che contemporaneamente al segnale KEN (che riguarda linea B) di nuovo il controllore di memoria sta attivando NextAddress. Quindi è indirizzo di linea C. Abbiamo anche in questo caso NA, ma in questo caso WB attivo ad L1 (quindi può portarsi in modified), vuol dire che linea L2 era in stato modified. Questo caso dirty. Qui sono i diagrammi temporali del nostro sistema, in cui vediamo caso di snoop miss: caso dato non presente. Notare che quando fatto snoop viene generato AHOLD (che mette in tri-state le uscite degli indirizzi perchè sono linee che vengono pulsate da esterno per interrogare le cache del processore). Altro diagramma temporale. Se chiediamo invalidazione vuol dire che snoop avviene a causa di scrittura. Quando c'è lettura non c'è mai segnale di invalidazione. In questo caso ci troviamo con invalidazione attiva. Qui HIT è attivo che dice di averlo, ma nessuno ha attivato HITM: questo dato è presente in uno o più processori, ma nessuno l'ha modificato. Quindi dato può essere portato in shared. In realtà visto che c'è INV lo invaliderò. Quello che succede è che i processori che avranno dato dentro invalideranno le linee all'interno. Snoop miss: identico a caso precedente, con INV attivo, ma con HIT/HITM attivi. Uno solo dei processori ha linea modificata. Vediamo che a causa dell'invalidazione abbiamo riscrittura da parte del processore. Se non avessimo avuto invalidazione avremmo comunque avuto scrittura. Altra politica coerenza: Qui altra politica di coerenza. Fino ad adesso l'assunto che abbiamo detto: riassunto necessario alla coerenza è racchiuso nel MESI. Il meccanismo è distribuito: nessun punto in cui abbiamo informazione. Quello che noi possiamo fare è interrogare tutte le cache e cercare di desumere dalla risposta complessiva qual è condizione. Ora invece directory based: c'è un posto ben preciso dove andare a recuperare informazioni. Il direttorio è una piccola tabella fisica associata a memorie. Notare che in realtà questo disegno è quello che succede: le memorie private dei singoli processori sono anche memorie pubbliche. L'unica differenza è che le memorie locali non mi obbligano ad andare sul bus. La memoria complessiva è quindi distribuita. Il direttorio non è altro che tabella associata alla zona di memoria fisica vicino alla quale si trova e che contiene informazioni sulle linee che fanno parte di quella memoria. Le operazioni che portano delle reali transizioni nei sistemi cache sono read miss e scrittura. Le altre operazioni su blocco esclusivo (sulla mia cache) non hanno influenza in generale. Qui abbiamo 3 informazioni importanti che sono omomorfe con il sistema di prima. E' lo stesso mondo realizzato in modo leggermente diverso. Le informazioni che ci servono sono sapere se linea di cache è da qualche parte e se per caso è stata modificata. Se modificata sta in un processore solo. Ogni direttorio per ogni linea di cache (che può essere di diversa grandezza) ci dice tutto lo stato della cache e ci dice anche dov'è dato. Nodo home: processore la cui memoria locale contiene quella particolare linea. Il nodo remoto è un nodo che ha una linea esclusiva o condiviso (due facce dello stesso problema). Prima volta che leggo ce l'ho esclusivo, poi condiviso. E poi c'è concetto nodo locale, che è nodo da cui si origina richiesta. Dato potrebbe essere da nodo locale 2 e potrebbe trovarsi sia nel nodo 3 che 4. Quello che conta è che c'è punto di convergenza nel quale si possono trovare tutte informazioni. Qui tabella con azioni che avvengono. Messaggio (nodo locale quando ha bisogno di un dato spedisce un messaggio al nodo interessato). Meccanismo fatto in modo esplicito. Notiamo che qui i messaggi sono punto a punto e non più broadcast. Sistema è più lento perchè le operazioni sono fatte in sequenza, ma in compenso sfrutto meglio bus. Read miss: processore deve spedire a direttorio una richiesta e l'indirizzo della linea di cui ha bisogno. Vuole diventare shared (uno dei tanti processori che ha quella linea). Possessore invece è quel processore che ha linea in modo esclusivo (coerente o no con memoria). Nel momento in cui un altro nodo fa richiesta il nostro nodo da owner a shared. Caso di write miss. In questo caso spedito messaggio a direttorio in cui si dice qual è il processore che l'ha originato ed indirizzo. Il processore richiede dati a direttorio. Varie possibilità perchè quella linea potrebbe essere posseduta da un altro o meno. L'insieme di tutte queste operazioni è abbastanza semplice: c'è punto centrale per ogni linea di cache. Quindi qui nei lucidi successivi ci sono diagramma delle transizioni a livello di singolo direttorio. Da lucido 46 in poi saltare!

Ora torniamo indietro e ricominciamo ad occuparci del problema di sincronizzazione a livello della pipeline. C'eravamo fermati all'algoritmo di Tomasulo, algoritmo che aveva questo aspetto che poteva dare dei problemi. E' vero che tramite reservation station era possibile mettere in esecuzione istruzioni che facevano riferimento a dati non ancora calcolati. Ma il momento in cui queste istruzioni erano terminate potevano andare a sovvertire ordine originario. Era possibile che un'istruzione fornisse un'istruzione prima di un'altra. Problema che non abbiamo affrontato a fondo: interrupt precisi. Quando arriva un'interruzione noi dobbiamo sapere esattamente quali istruzioni completamente terminate e quali istruzioni devono ancora cominciare. INT riconosciuto al termine di un'istruzione. Se istruzioni sovvertite potremmo andare a trovare interrupt in posto sbagliato (istruzioni che possono anche aver già alterato stato della macchina). Tomasulo ci dà qualche problema. Altri due problemi da anticipare: il limite superiore nella emissione di istruzioni era dato da numero di slot di reservation station. Quando non aveva più slot tutte le istruzioni che seguivano (indipendentemente da quale reservation station faceva riferimento) venivano bloccate. Primo meccanismo che ci dà garanzia che risultati di istruzioni vanno ad aggiornare lo stato della macchina nello stesso ordine delle emissione delle istruzioni. Commitment momento in cui risultato istruzioni viene scritto in memoria o sui registri. Reorder Buffer Primo meccanismo presente in tutti processori moderni è reorder buffer. Il ROB fa questa operazione: cosa succedeva precedentemente nell'algoritmo di Tomasulo? Quando terminava andava a scrivere nei registri. Registri accettavano o meno il risultato a secondo se puntatore puntassero a quell'istruzione o ad un altra (per evitare WAW). Risultato poi distribuito alle reservation station. Differenza è che risultati anziché andare direttamente nei registri di destinazione vanno in un buffer che preserva ordine delle istruzioni. Considerando buffer come coda circolare, a mano a mano che istruzione che sta in cima (più vecchia) ha terminato il suo risultato è scritto. Gli altri stanno nel buffer in attesa che quelli prima siano scritto. Fino a quando istruzione precedente non ha committato risultato le istruzioni successive rimangono nel reorder buffer, in modo da essere diramati i risultati nello stesso ordine in cui erano. Questo elimina molti problemi. Altro vantaggio è quello di permettere esecuzioni speculative. Quand'è che esecuzione ci lascia in dubbio: se c'è branch tra due istruzioni, quella dopo dobbiamo eseguirla o no? Fino a quando non abbiamo calcolato non lo sappiamo e allora anziché andare a mettere i risultati anziché nei registri di destinazione ma nel ROB, se il branch ci fa prendere altra strada basterà cancellare dati dal ROB. Questo quindi ci permette esecuzione speculativa. Eseguiamo istruzioni sperando che quella sia sequenza giusta, ma se non è così siamo in tempo per azzerare risultati buffer. Nella prima ipotesi è che ROB ci serva solo per inserire risultati, poi vediamo che a mano a mano che unità funzionali sono libere vengono prese, poi vedremo che tutte le istruzioni vanno prima lì e poi nelle unità funzionali. Qui istruzione viene emessa quando c'è una reservation station libera (Tomasulo) e quando c'è uno slot libero all'interno del Reorder Buffer. Il Reorder Buffer ci sono risultati non ancora commited. Il ROB è anche la sorgente dei valori dei registri non ancora commited, ma necessari all'esecuzione delle istruzioni che seguono. In Tomasulo andavamo a prendere gli operandi (nei registri se non oggetto di modifica, ma se erano oggetto di modifica li andavamo a prendere direttamente nelle reservation station). Stessa cosa qui, vado a prendere i dati direttamente da ROB se i dati sono oggetti di modifica. Qui vecchio schema con aggiunta di ROB. Tomasulo: emissione (slot libero per risultato e uno per esecuzione), operazioni fatte nella reservation station prevista, se dato è un dato in fase di produzione il dato lo si va a prendere dal ROB. Quel dato può essere sia pronto che non pronto. Se non pronto si mette in attesa. Qui in realtà punta anziché alla reservation station al ROB. Poi abbiamo scrittura dei risultati (nel ROB), poi c'è commitment. Ogni istruzione se è pronta risultato viene portato nel registro. Discorso del commitment in ordine è un MUST di qualsiasi sistema: nessun sistema può permettersi di fare commitmente fuori ordine, se no andremmo a fare qualcos'altro rispetto a quello che ci dice programma. Nel ROB ci possono essere più istruzioni che fanno riferimento a stesso registro di destinazione. Per ogni istruzione dobbiamo sapere in quale slot c'è informazione che ci serve. Esempio: abbiamo questa sequenza di istruzioni con branch. In alto a sinistra coda istruzioni, il ROB, i registri FP e poi le reservation station. Al primo clock la prima istruzione viene immessa nel ROB. Prima operazione esattamente uguale a Tomasulo standard. Clock 2: siccome c'è posto sia nel ROB che nel reservation station, mentre ancora stiamo eseguendo la LOAD avviamo anche la FADD. Clock 3: nel ROB abbiamo immesso FDIV che si serve di F10, prodotta dall'istruzione di prima. Quindi scriviamo dove andiamo a prendere dato che ci serve (quindi ROB2). Prima scrivevamo reservation station, ora scriviamo numero dello slot. Così facendo abbiamo meccanismo che ci garantisce commitment in order. Clock dopo: è chiaro che nei vari ROB siamo andati a mettere le varie istruzioni e ciascuna di essa abbiamo messo puntatore a ROB che ci dà quel risultato. In FADD F4 è quello calcolato dall'istruzione di prima (ROB5). Quelle istruzioni non sono ancora committed. Si vuole sempre dato calcolato nell'ultima operazione precedente. Se avessimo una istruzione che dipende da due sorgenti precedenti, in “Sorgente” ci saranno due indirizzi ROB. Ciclo 6: notiamo che la store può cominciare prima che la LOAD abbia finito. La presenza del ROB fa sì che molte istruzioni siano emesse ed eseguite anche senza aspettare risultati intermedi. Se per dire il branch che abbiamo a metà avesse dato un risultato diverso da quello previsto, quello che ci ha portato ad eseguire la LOAD la FADD e la STORE, noi l'unica cosa che dovevamo fare era cancellare dati da ROB. Nel caso del Tomasulo classico, noi non potevamo eseguire niente prima che non fosse completamente calcolato il branch. Quando ho registro indicato in un'istruzione che sto emettendo e che usa un registro in produzione all'interno del ROB, è chiaro che vado a prendere il valore dell'istruzione più vicina. Ma come faccio a sapere dentro al ROB qual è il valore più recente? Devo avere rete che tenendo conto della cima, mi dica qualcosa che mi dica qual è il più recente. Si usa invece meccanismo diverso: renaming (al posto di certi registri che sono ancora in calcolo, andiamo ad indicare numero di reservation station o ROB). Questa soluzione è quella di associare ad ogni registro della macchina non un registro fisico, ma più registri fisici distinti e ad ogni istante uno solo di questi registri è quello che rappresenta esattamente il registro architetturale. Ad ogni istante esiste un solo registro che rappresenta quello “virtuale”. Esempio: qui abbiamo sequenza di istruzioni che fa riferimento più volte ad R2. Supponiamo che ad R2 possano essere associati n registri fisici. Al posto di R2, le istruzioni al momento in cui vengono emesse, ogni volta che viene citato R2, viene associato ad esso associato un nuovo registro fisico (R2-1, R2-2, ..). Alla prima load il valore di R2 è R2-0, che è quello che rappresenta R2. Ma nel momento in cui facciamo RADD lo facciamo puntare a R2-1. Fino a quando la nostra RADD non ha finito il registro che rappresenta R2 sarà R2-0. Quando finisce diventa R2-1. Ad ogni registro architetturale (conosciuto dalle istruzioni), associati registri fisici diversi a mano a mano che vengono utilizzati. Questo semplifica le cose, perchè per ogni istruzione che fa riferimento a questi registri noi puntiamo a registro fisico che effettivamente contiene risultato, diventa registro architetturale nel momento in cui finisco operazione. In altre parole scompare concetto di registro architetturale, al suo posto n registri fisici. Ad ogni istante uno solo di questi registri fisici rappresenta un registro architetturale. In P2 c'erano 40 registri fisici (al posto di AX, BX, ..), nel P4 ce ne sono 120. Ne abbiamo tanti perchè siamo pieni di unità funzionali e quindi abbiamo molte istruzioni dentro il ROB e quindi molti registri. Ovviamente dobbiamo avere tabella registri liberi e occupati. Un registro libero è un registro che non rappresenta nessun registro architetturale. Quando istruzione finita, il registro fisico diventa libero e può essere usato da un altro. Ora parliamo dell'esecuzione speculativa: è quella per cui noi, sulla base delle assunzioni che facciamo nei caso dei branch (tramite BTB o altri meccanismi), andiamo ad eseguire delle istruzioni e non facciamo commit di queste fino a quando non siamo sicuri che branch abbia preso quella via. Tutti i processori moderni funzionano in modo speculativo (possiamo anche avere subroutine). Esempio: qui c'è sequenza di istruzioni dove siamo pieno di WAR, RAW .. Il ROB toglie QUALUNQUE di queste incertezze: il fatto di avere commitment in ordine e di puntare a registri “temporanei” ce li risolve tutti. Vediamo prima Tomasulo senza ROB. Abbiamo certo numero di istruzioni .. Seconda FMUL stallata perchè solo 2 reservation station per multiply e quindi la terza operazione stallata. Esempio 2: qui messo anche indirizzo istruzioni. Caso di istruzioni RISC. Ora vediamo con ROB e renaming. Supponiamo che ci siano certo numero di registri di renaming F4, F8 e F6 (se ne avessi di più ne metterei di più). Che sono i registri coinvolti nella sequenza. Inizialmente se io interrogassi il sistema su quale sia contenuto di F4, lui mi darebbe P0, per F6 Q0, ecc .. Quindi registri architetturali puntano a primo registro fisico. Al clock zero questa è situazione (senza aver ancora emesso alcuna istruzione). Clock 1: iniziamo dalla prima istruzione che è FLD. Nella destinazione di FLD c'è P1. Quando avrò commitment questo P1 diventerà registro architetturale F4. Clock dopo: emetto FDIV. Rispetto a Tomasulo non ho più registri in basso. Il ROB è quello che fa commitment. Elenco registri con destinazioni e sorgenti sostituito da Register Allocation Table (RAT) e ROB. Con questa operazione stesso discorso di prima: destinazione è Z1. Clock: altra istruzione. Ancora una volta abbiamo F4 in destinazione e quindi usiamo un nuovo registro fisico. Dopo commitment della FLD diventerà questo il registro architetturale F4. .. Poi vediamo cosa che cambia completamente. Questa FMUL in Tomasulo standard non potevamo emetterla. In questo caso la mettiamo nel ROB, non la mettiamo in esecuzione, ma metterla nel ROB vuol dire che l'istruzione seguente posso emetterla ed eseguirla (in questo caso). A questo punto avendo fatto commitment della prima load il registro che mi rappresenta F4 è P1 e P0 è stato liberato. P2 e P3 però sono ancora occupati. Nella realtà c'è un intero pool di registri fisici che volta per volta sono associati a registri architetturali, anziché come in questo caso registri fisici associati per gruppi a registro logico (che potrebbero alcuni non essere utilizzati).

In questo caso stiamo riassumendo molti concetti già visti e questi si ritrovino nei processori di ultima generazione. Nel Pentium 2 abbiamo architettura super-scalare (più pipelines che lavorano in parallelo), abbiamo una cache non bloccante (la cache è fatta in maniera tale che può accettare più richieste contemporanee), esecuzione fuori ordine (una esecuzione speculativa l'abbiamo già vista), poi ridenominazione registri fisici. Una macchina che funziona in questo senso si chiama “Data flow”, cioè che esegue non sulla base della sequenza delle istruzioni, ma sulla base della disponibilità degli operandi. Una volta completata l'esecuzione, il commitment viene fatto nell'ordine delle istruzioni. Ancora concetto di esecuzione fuori ordine (le istruzioni vengono inserite nel ROB, senza esistere più le reservation station in senso stretto). A mano a mano che dal ROB si scopre che le unità funzionali sono disponibili e che gli operandi sono disponibili vengono usate e poi i risultati nel ROB e poi commitment quando le istruzioni precedenti sono finite. Anche i risultati in realtà vengono commitment insieme, ma sempre in ordine. Se riguardano stessa locazione di memoria, vengono commitment in sequenza. Prefetch speculativo: questi concetti tutti già visti in precedenza. Se eseguiamo istruzioni di branch ad una subroutine in modo speculativo, ci vuole anche buffer di ritorno da questi branch che non può essere stack (che è salto e ritorno “ufficiale”), mentre questo è speculativo. Dobbiamo avere altra struttura parallela. Ovvio che queste istruzioni se risultano speculate in maniera scorretta, vengono azzerate tutte info in ROB, ma anche quel Return Stack Buffer (che contiene indirizzi di ritorno). Esecuzione speculativa: non fa altro che dire ripetere le solite cose. Passiamo a P6: struttura del P6 così come presentata dalla Intel, il P6 è stata la prima architettura che ha realizzato tutte queste caratteristiche che abbiamo detto. E poi si è esplicitata in P2, P3, P4, Centrino, Dual-core ma l'organizzazione di carattere generale è rimasta la medesima, con alcune varianti di tipo quantitativo più che qualitativo. Stesse informazioni di prima esplicitate. Questo è il Prefetcher: non fa altro che estrarre da cache di istruzioni una linea di istruzioni, che non sono di lunghezza fissa e quindi c'è tutto un hardware dedicato all'individuazione dei confini delle istruzioni ed ecco il primo pegno che paghiamo ad una organizzazione complicata come quella dei processori del tipo PC (non solo Intel, ma anche AMD). Qui abbiamo indicazioni di dove si fermano istruzioni. Si ferma anche quando predizione del branch è stata errata (ma questo più avanti). Notiamo che per capire quale tragedia sia la lunghezza diversa delle istruzioni, il 40% dei transistori è destinato a risolvere il problema della legacy. In particolare nel caso del P6 (Pentium 2): tutto quello che si mette per questo, si toglie a cache. Legacy impone anche stadi aggiuntivi della pipeline e quindi costo superiore quando dobbiamo svuotare pipeline. Tutto cosa in maniera drammatica. Qui indicazione dell'esecuzione: prefetch in ordine, esecuzione fuori ordine, commitment in ordine. Ulteriore elemento da tener presente: complessità istruzioni 8086 (arricchite di tutte le istruzioni successive, in particolare istruzioni a privilegio 0) hanno fatto sì che poiché erano ingestibili, queste istruzioni vengono trasformate all'interno della macchina in istruzioni RISC. Questo stadio tutt'altro che semplice e con grossi vincoli. Negli stadi di esecuzione abbiamo istruzioni di tipo RISC, esattamente tutte della stessa lunghezza. Pipeline del P6: la prima zona è in ordine (3 stadi di fetch, 2 di decodifica), uno stadio di RegistryAllocationTable (renaming in modo globale, cioè pull di registri fisici e architetturali e pull di registri fisici non è suddiviso, tutti pescano nello stesso pull di registri), poi ci sono 3 stadi (ROB, DISpatcher che fa funzione di reservation station, cioè quando istruzione ha operandi disponibili viene messo in una coda che riguarda unità di esecuzione, quando questa arriva in cima l'istruzione viene eseguita; poi stadio EX esecuzione), poi 2 stadi retiment (che prima abbiamo chiamato commitment). Notiamo che fra questi stadi (specialmente per i processori più moderni che hanno altissima frequenza di clock) abbiamo code di compensazione (perchè stadio può produrre più o meno risultati), serve quindi per compensare velocità diverse. Questo processore è superscalare perchè gestisce più istruzioni per clock. E' super-pipelined perchè ha molti stadi con alta frequenza di clock e superscalare perchè gestisce più istruzioni contemporaneamente. Abbiamo i 3 stadi di instruction fetch: il primo prende linea da cache e lo fornisce a stadio successivo (come avveniva nello stadio di prefetch del P1). Nello stadio 2 si individuano (operazione non facile) i confini delle istruzioni e per individuarli bisogna decodificare codice operativo, perchè individua lunghezza istruzioni. Nel terzo stadio istruzioni ruotate in modo tale da essere allineate con stadio successivo. Il nostro decodificatore (numero 1) è composto di 3 elementi che possiamo indicare come decodificatore complesso, capace di decodificare istruzioni complesse (IA32) e genera fino ad un massimo di 4 istruzioni RISC per clock. Gli altri due sono decodificatori semplici in grado di decodificare solo istruzioni semplici. Mov è semplice, Add complessa che viene tradotta in un numero superiore di tipo RISC e quindi decodificatore complesso genera tutte istruzioni RISC. In generale istruzioni semplici sono quelle che danno luogo ad una sola istruzione RISC, quelle complesse sono quelle che danno luogo a più istruzioni RISC. In generale quello complesso è in grado di generare fino a 4 istruzioni RISC da una complessa. In totale in nostro sistema è in grado di produrre fino a 6 istruzioni RISC. Pipeline è tripla fino a quando arriva a decodificatore, dopodichè può gestirne fino a 6. Quando andiamo verso ROB possiamo introdurne solo 3. Produciamo molte istruzioni RISC e poi non riusciamo ad introdurle nel ROB, ma in realtà la possibilità di produrne così tante è teorica. Perchè anche se il compilatore non riesce a mettere complessa e 2 semplici, ma ne mette due complesse il sistema comunque rimane bilanciato. Questo sistema corrisponde a simulazioni e si va vedere quante istruzioni RISC mediamente vengono prodotto. Con questo tipo di solito sta in equilibrio, ma non sempre perchè fenomeni stocastici e quindi può esserci grossa affluenza nel ROB. Nello stadio di decodifica 2 abbiamo un'altra possibilità di decodifica: ci sono istruzioni particolarmente complesse (che non si riescono a decodificare con rete combinatoria, ma ne richiedono di più). Succede che quelle istruzioni molto complicate non si decodificano in maniera combinatoria ma ci vuole anche ROM che scandisca in sequenza le varie microistruzioni. Notiamo che sono istruzioni molto infrequenti: nel 95% la macchina si trova di non avere bisogno di queste microsequenze. Il ROB già visto, il DISpacth equivale a reservation station di prima. Rispetto a quella di Tomasulo, qui si entra avendo già a disposizione tutti gli operandi. Quelle a valle possono avanzare e quindi problema della reservation station è alleviato. Normalmente le code sono nell'ordine di 3 o 4 elementi. Ci sono processori in cui coda di dispatch è lunga e altre differenziate. EX normalmente, nella maggior parte dei casi esegue in un clock, ma ci possono essere casi in cui c'è bisogno di due clock. Lo stadio di RET1 è uno stadio che precede il commitment: questa istruzione dice se può essere commitment. Quell'altra scrive (che in realtà vuol dire scrivere puntatore al registro di renaming che in quel momento realizza registro architetturale). Se siamo in caso di memoria, questa viene effettivamente scritta. Ulteriore raffinamento degli stadi. Nello stadio di IF2 abbiamo BTB sofisticato e quando noi estraiamo più istruzioni dal prefetcher, estraiamo contemporaneamente più istruzioni e potremmo estrarre più istruzioni di branch e quindi non è più un elemento che può rispondere ad una sola istruzione, ma deve rispondere più istruzioni di branch contemporaneamente (fino a 4) e quindi questo è in grado di rispondere fino a 4 diverse richieste. Attenzione che ci possono essere dei branch che non sono contenuti nel BTB. Più avanti esiste un predittore statico: sulla base di alcuni parametri prende una decisione (sempre uguale per la stessa istruzione di branch). Poi informerà il BTB che a quell'indirizzo c'è istruzione di branch e in caso di errore lo informa. BTB lavora sempre con indirizzi fisici, a valle del sistema di impaginazione. Branch: già detto. Pipeline: stesso lucido che abbiamo esplicitato alcune funzioni dette prima. Dalla decodifica della lunghezza delle istruzioni individuiamo codice operativo, in quello stadio quindi siamo in grado di individuare branch. Indirizzo a cui abbiamo individuato branch viene passato a BTB per l'opportuna gestione. Se saltiamo a livello del BTB non facciamo altro che cambiare il prefetch (istruzioni che non vanno bene), ma l'istruzione va avanti perchè branch deve essere poi verificato nello stadio di esecuzione. Qui abbiamo IFU3: istruzioni semplici e complesse (tra queste ci sono le istruzioni MMX). Questo è esempio di sequenza di istruzioni semplici e complesse: se noi arriviamo in S1 con un branch, i primi 25 li saltiamo pur dovendo caricare tutta linea. S1, S2 e C3 opportunamente ruotate ci permettono di generare le istruzioni complessivamente perchè C3 andrà ad allinearsi con decoder complesso e S1 e S2 con semplice. S1 ed S2 non si trovano allineate con il decoder 0 che è quello complesso. Quindi C3 è allineato. C4 non lo possiamo far passare perchè è anch'esso complicata e andrebbe allo stesso decodificare. Quando mettiamo C4 possiamo mettere insieme anche S5 ed S6. Successivamente S7, S8 e S9 perchè decod complesso è in grado di decodificare anche istruzioni semplici. Qui abbiamo esplicitazione grafica degli stadi di decodifica. Nel caso del Pentium 2 andavamo all'incirca nell'ordine di mezzo GHz. Oggi 4GHz. Questi stadi non possono andare a questa velocità, quindi unica soluzione è spezzarli. Poi stadio di RAT. Stadio DEC1 e DEC2: ripreso quello già detto. Micro-operazioni vuol dire istruzioni RISC. Nello stadio DEC2 viene attivato anche il BTB statico. Ovvero quello che fa previsioni non sulla base della storia precedente, ma su considerazioni statistiche. Questo è logica di BTB statico: si può leggere qual è logica. Se questa logica è giusta o meno lo può dire solo la storia: sono logiche che derivano dall'esecuzione ripetuta di molti benchmark i quali permettono di individuare che in assenza di altre informazioni questa è regola che normalmente dà miglior risultato. Ma sulla sua affidabilità si può discutere. Questo è RegisterRenaming: i registri sono 40 (rispetto agli 8 registri presenti nel nostro sistema). E' del tutto evidente che non esiste concetto di renaming per quando riguarda registri di segmento: sono caricati con il puntatore al GDT o a LDT e non fanno parte delle esecuzioni correnti. Dobbiamo fare renaming coinvolti in registri che c'entrano con esecuzione. Registri di segmento non fanno parte di calcoli: non lo usiamo come elemento da sommare per calcolare offset. Notiamo che maggiore è il numero di unità funzionali, tanto maggiore è dimensione del ROB (quindi più istruzioni) e quindi all'aumentare della dimensione del ROB devono aumentare anche registri di renaming. Nel caso di P4 registri di renaming sono 128: tutti tra loro non distinti, globalmente tra loro equivalenti. Ma questo del tutto evidente che non cambia nulla della logica già vista. Stadio ROB: anche qui c'è poco da dire, perchè sappiamo bene com'è ROB e come funziona. Ci sono reservation station, risultati messi nel ROB. Qui c'è tutto ciò che abbiamo già detto. Stadio di EX del P6. Sostanzialmente abbiamo 5 unità funzionali: STORE, LOAD, uno stadio di calcolo degli indirizzi (uno dei colli di bottiglia). FP, JumpExecution che ci dice se predizione giusta o sbagliata e quindi influisce su cancellazione ROB. Nelle FP unit vengono fatte anche operazioni trascendenti. Esecuzione istruzioni: visione complessiva che comprende anche il ROB. Abbiamo Prefetch, predittore, MIS (micro-instruction ..), coda, stadio di RAT e ROB e immissione di tre istruzioni alla volta. Nel nostro ROB sono contenute 4 informazioni: indirizzo di memoria (corrispondente all'istruzione di alto livello, poiché ogni istruzioni di alto livello può essere convertita in un numero > di 1 di microistruzioni, lì dentro puntatore a prima microistruzione), poi tipo di microoperazione (cosa dobbiamo fare e se è di tipo branch o non branch), poi registri alias (registri di renaming che sono necessari sia per utilizzarli come sorgenti, sia come destinazioni). Lo stato delle istruzioni sono: SD: istruzione messa in coda ma non ancora in cima DP: siamo in cima alla coda (quella che sarà eseguita per prima da unità di esecuzione che si libera) EX: siamo in fase di esecuzione WB: siamo pronti al prossimo clock a scrivere il risultato dentro il ROB RR: istruzione è stata completata (tutto il risultato nel ROB), ma non possiamo ancora ritirarla perchè qualche microistruzione precedente o non è stata ancora ritirata o non è stata completata RT: quando è in cima al ROB e in quel particolare clock viene commitment Questi stadi in cui si trova microistruzione. Notiamo che il ROB permette anche INT preciso: quando si verifica interruzione durante una microistruzione, quella interruzione viene servita nel momento in cui la microistruzione viene retired e quindi corrisponde effettivamente all'istruzione interrotta. Se c'è qualche microistruzione che precedeva e non è ancora stata retired, quell'interruzione non viene servita, viene fatto solo nel momento in cui micoistruzione corrispondente a istruzione di alto livello viene servita. RESET: qui esempio. Preso prima istuzione che viene eseguita da parte del processore. Per cercare di ricordare cosa succede. Il sistema di protezione virtuale non ha nulla a che vedere con meccanismo di esecuzione. Questo è indipendente dal fatto che il sistema stia eseguendo in modo virtuale o no. Perchè questo riguarda flusso istruzioni estratte da memoria. Il flusso esce esattamente nella stessa identica maniera, quindi fin dall'inizio abbiamo una situazione in cui utilizziamo i decodificatori, il RAT, il ROB ... Il ROB, RAT e tutto il resto non hanno nulla a che vedere con il PE in CR0. Qui abbiamo a che fare con meccanismo di esecuzione istruzioni. Qui abbiamo già fatto tutto e siamo a basso a livello. Quindi che siamo in modo nativo o protetto, il meccanismo non cambia. Qui preso caso del jump iniziale (sistema inizialmente parte da 16 locazioni sotto il massimo degli indirizzi possibili) ed essendo jump tutte istruzioni seguenti sono prive di significato. Il JUMP che è FAR dura 8 byte, questo è linea di prefetch. Prima istruzione ha senso, le altre no. Istruzione complessa che finisce nei decodificatori. Questo è branch non condizionale (sempre tradotto come branch nelle microistruzioni, ma interpretato sempre taken). Finisce nella coda delle microistruzioni. Per quanto riguarda RAT e ROB c'entra poco, l'unico registro coinvolto è PC o se preferiamo IC relativo a registro di segmento. Sono due registri che non hanno renaming. Passa stadio indenne e finisce in cima al ROB e a pag.35 vediamo che è prima isrtuzione che esegue. Per quanto riguarda prefetch, intanto la macchina è andata avanti: le ha messe in un decoder, poi RAT e ROB. RESET – esecuzione e ritiro: esempio che prof non ha fatto .. Ha fatto sorta di fotografia di quel che ci potrebbe esser nel ROB e possiamo andare a vedere nei lucidi successivi a partire da questa situazione. Cima del ROB è al 13 slot, si trovano in vari stadi. 13, 14, 15 ritirate. Quelle 3 dopo già pronte, ma non riesco a ritirarne più di 3. Nelle 3 successive ce n'è una già pronta, ma preceduta da una in EX e una in WB. Quindi nel terzo clock istruzione 21 (e successive) bloccata da quelle prima. Per P6 e successivi non abbiamo più cache enable o cache disable generali, ma abbiamo zone di memoria che possono essere settate a vari comportamenti. Quindi abbiamo sistema complessivo di cache enable/disable a livello generale della memoria, più registri che individuano grandi porzioni di memoria con politiche diverse di gestione della cache, più (nell'ambito di questi grandi spazi di memoria) politiche di raffinamento a livello di tabella delle pagine. Notiamo che qui siamo ancora ad indirizzamento a 32 bit, mentre nei processori più avanti indirizzamento diventa a 36 bit. E' ovvio che con 36 bit abbiamo 16 volte maggiore indirizzamento. Due parole sul bus del P6: anche qui ci sono differenze sostanziali. Noi i bus li abbiamo sempre concepiti come domanda e poi risposta. Quando risposta non arriva mettevamo in attesa processore. Questo non è sistema migliore perchè normalemente in un sistema abbiamo a che fare con sistemi che ci rispondono con tempi diversi. Visto che bus è collo di bottiglia, potremmo avere tempi morti sul bus. Noi vogliamo che bus ad ogni clock sia completamente occupato. Questo ci dà più elevata efficienza possibile. Oggi i bus sono quasi tutti a 800MHz, ma macchine con bus a 1,2Ghz. Quando frequenza del bus corrisponde alla lunghezza d'onda ci sono fenomeni terribili di rimbalzo: è come se fossero onde. Abbiamo sostanzialmente la necessità di avere sistemi intelligenti di risposta: abbiamo visto nel corso il caso del burst. Richiede controllore di memoria intelligente, perchè noi fornendogli indirizzo iniziale lui ci chiede gli altri. Noi possiamo avere sente situazione: processore o agente che ha controllo del bus invia una richiesta e non si aspetta necessariamente che la risposta sia immediata. Potrebbe benissimo dire: ok quando tu dispositivo remoto mi rispondi dicendo che sei pronto e mi dici anche a cosa stai rispondendo. Quindi potrei anche accodare serie di richieste sul bus e dal remoto (anche non nella stessa sequenza) mi arrivino risposte. In questo modo efficienza molto maggiore nel caso del bus. Nel caso del P6 possiamo avere fino ad 8 transizioni pendenti e abbiamo 3 possibilità: mandiamo una richiesta, se chi è dall'altra parte è subito pronto risponde, se non è pronto ma lo è in tempo rapido manda retry oppure mi può mandare risposta deferred (perchè per andare a prendere quel dato ci metto un po', esempio è quello del disco). In quest'ultimo caso il dispositivo che ha inviato la richiesta deferred. Sarà lui che si impossessa del bus capace di competere per possesso del bus. Quindi abbiamo controllori di memoria molto più intelligenti di prima: ora può essere in grado di riprendersi controllo del bus ed informando il processore che è pronto a restituirgli quella particolare informazione. Queste richieste possono essere soddisfatte in un ordine diverso dal quale sono stati interrogati. Bus del P6: bus in questo genere si dice “transaction oriented”. Questa è logica più complessa che possiamo immaginare: noi abbiamo diverse domande e risposte differite. In un certo senso abbiamo anche in questo caso fuori-ordine. Noi possiamo avere fino a 4 diverse transazioni pendenti le cui risposte possono arrivare in ordine diverso. Ovviamente regole in tutto questo: alle LOAD si può rispondere in ordine inverso rispetto a quelle immesse. Non è vero per le STORE e se ci sono LOAD e STORE, bisogna anche tener presente che se STORE precede LOAD non possiamo anticipare load. Possiamo in generale rispondere fuori ordine alle LOAD, ma non possiamo rispondere a LOAD con STORE pendenti. In questo caso controllore di memoria deve essere ancor più intelligente. Qui alcuni esempi: protocollo del bus a pacchetti. Una operazione è una insieme di transazioni. Richiesta, risposta di attesa, poi risposta del dato .. Quindi tutta una serie di operazioni che si verificano sul bus. La transazione è né più né meno che l'invio di un pacchetto che contiene informazioni. Es: voglio che tu mi legga 10 byte all'indirizzo 200. Risposta: non ce l'ho aspetta. Una e l'altra sono due transazioni che comprendono una serie di pacchetti e la fase è uno dei componenti della transazione. Avrò un elemento qual è l'indirizzo, quanti dati sto trasferendo, ecc .. Qui qualche esempio banale. Es 48: primo è caso di una transazione rapida, quella dopo è transazione lunga in cui vediamo che mentre ancora richiedo ho anche una risposta. Quindi transazioni multiple.

Pentium IV M Xeon

[modifica | modifica sorgente]

Guardiamo ultimi sviluppi a partire dal Pentium 4. Pentium 4 che è ancora oggi un cavallo di battaglia dei PC si è spostato di più verso le versioni server. Se acquistiamo oggi processore Intel ce ne danno un altro migliore in termini di dissipazione di potenza (premia durata dispositivo). Pentium 4 rappresenta ancora cardine architetturale. Tutti sono figli dell'architettura del P6 con variazioni più o meno lontane, ma aspetti concettuali e architetturali derivano tutte da quello. Prima ci occupiamo del Pentium 4 e poi guardiamo Centrino e poi diamo un'occhiata ad ultimi sviluppi. Sul Pentium 4 vediamo solo quali sono caratteristiche che lo differenziano da P6. Qui abbiamo una maggiore quantità di unità funzionali e questo permette di avere un numero maggiore di istruzioni in fase di esecuzione, abbiamo una cache secondaria inserita nel processore. In versione XEON (per server) è inserita persino una L3 direttamente nel chip del processore. Qui non è indicato ROB perchè è ovvio che in tutta la parte finale esiste ROB (elemento sottostante che non ha bisogno di essere esplicitato da punto di vista grafico). P4 architettura net-burst. Manca cache L1. Esiste un'altra cache che vedremo, che sostituisce cache L1 che si chama trace cache. Questo processore è in grado di lavorare con un FrontSideBus fino a 800Mhz che è standard del front side bus (bus di collegamento fra processore e dispositivi esterni). Oggi abbiamo FSB fino a 1,2Ghz ma non sono di normale acquisto. Notiamo che abbiamo TLB delle istruzioni, cioè qualcosa di strano: non c'è cache istruzioni, però c'è TLB. Questo processore (già detto in precedenza) ha aumentato a dismisura frequenza del clock: suddividere stadi in più stadi ciascuno, per ridurre tempo di latenza. In maniera tale che clock possa aumentare. Ha una cache di secondo livello di 256KB. Notiamo che qui si comincia ad utilizzare un'altra tecnologia a doppio fronte: non solo sul fronte positivo, ma anche su quello negativo. Tendenzialmente in alcuni sottosistemi dà buoni risultati. Sistema lavora fino a 4Ghz e avendo aumentato notevolmente numero di unità funzionali, abbiamo ROB molto maggiore. Sistemi della pipeline sono sempre gli stessi con numero di stadi variabili in vari processori. Trace Cache: Sembrava uovo di Colombo perchè sembrava che problema grosso era quello della trasformazione delle istruzioni di alto livello in microistruzioni di tipo RISC che sono ottenute a partire da quei 3 decodificatori (due semplici e uno complesso). Questi decodificatori prendo silicio e tempo, quindi nel caso di traduzioni siamo costretti ad utilizzare diversi clock. L'idea è stata: ma perchè avere cache delle istruzioni e non delle microistruzioni. Quindi mettiamo una cache a valle del decodificatore. Decodifichiamo solo la prima volta certe istruzioni, poi dopo cache. Cache istruzioni di tipo RISC. Questa operazione è adottato in vari computer e sembrava fosse la panacea universale: che potesse dare grande vantaggio. In realtà negli ultimi processori viene talvolta utilizzata e talvolta no. E' ancora dubbio il vero vantaggio. Spiegazione: è vero che ci possono essere istruzioni che richiedono più di un clock per la loro traduzione, ma qual è frequenza di queste istruzioni? Questa è abbastanza limitato e anche la frequenza di utilizzazione è piuttosto bassa. Trace cache avendo microoperazioni sono da 128 bit l'una, mentre le altre sono dell'ordine dei 4-5byte. Quindi a parità il numero di istruzioni in generale è molto più basso. Questo nel caso di rapporto 1:1, ma in realtà corrispondono più microoperazioni. Oggi molteplicità di soluzioni. La trace cache nel Pentium 4 contiene circa 12 000 microistruzioni che equivale (sommariamente) tra le 12 e 16K. Un'altro vantaggio esplicitato è che il BTB agisce a monte (nel momento in cui andiamo a recupare istruzioni di alto livello). Questo due vantaggi: tendenzialmente nella trace cache abbiamo salti condizionati risolti, ma anche altro vantaggio. Quando individuiamo branch come taken, tutti bit successivi non devono essere utilizzati. Tutti i bit della trace cache quindi possono essere usati per istruzioni. Quindi migliore utilizzazione cache. Detrattori: se noi non abbiamo branch aumtentare il clock aumenta produzione, avremo tempo di latenza superiore (molte istruzioni in volo nella pipeline), ma cadenza con la quale vengono terminate istruzioni aumenta. Ma quando abbiamo sbagliato predizione del branch il numero di istruzioni da scaricare è molto superiore. Alcuni lucidi per far vedere com'è fatto: floor plan. Piano del pavimento, cioè distribuzione di transistori e unità funzionali. Ci sono 423 pin che portano fuori segnali. A 1,4Ghz con alimentazione a 1,7 Volt consumiamo 52 WATT. Ovviamente questo richiede problematiche di raffreddamento drammatico. Qui ci sono dissipatori. In macchine quadri-processore con Xeon a 4GHz abbiamo raffredammento anche a elio liquido. Disegno con i pin. Questo è chipset: ogni processore ha una insieme di dispositivi fratelli con i quali collaborare. Execution core: struttura interna. Abbiamo diversa situazione per quanto riguarda unità funzionali. Abbiamo due unità funzionali aritmetiche, due FP, due per la memoria e una dedicata ad esecuzione istruzioni complicate. A livello anche di microoperazioni. Per esempio moltiplicazione è operazione di tipo RISC, ma questa richiede un algoritmo e quindi più tempo. Una delle cose interessanti è che dal ROB (sia uscita che ingresso, microistruzioni arrivano in ROB, vanno alle unità funzionali e poi rientrano per essere commitment). Quando abbiamo istruzioni che vogliamo avviare, abbiamo problema bus. Il ROB contiene istruzioni da 128 bit più varie informazioni. Una volta che queste queste istruzioni sono prese devono essere avviate alle porte (stessa porta può portare a destinazioni diverse). Porta è bus e bus costa in termini di silicio (sono silicio policristallino). Queste porte sono in numero relativamente ridotto. Questo vuol dire che c'è problema di congestione. Se la sequenza di istruzioni non è particolarmente intelligente, può esserci rallentamento per traffico tra ROB e unità di esecuzione. Nel caso del Pentium 4 ce ne sono 4 di porte, al contrario delle 5 del P6. Execution core: le unità di esecuzione lavorano su entrambi i fronti (positivo e negativo) e quindi abbiamo notevole velocità. Lavorando su entrambi i fronti le unità aritmetiche semplici. Abbiamo possibilità ogni clock di eseguire 5 istruzioni: 2 unità (veloci) che lavorano su due fronti, più una complessa. Quindi ogni clock siamo in grado di produrre 5 microoperazioni. Che saranno poi commitment quando arriveranno in cima al ROB. Dal ROB sono estratte 6 microoperazioni alla volta (lavoriamo su fronte positivo e negativo, 2*2 su quelle semplici più 2 del complesso). Branch Prediction: branch a 4096 slot. Bello potente. Questo è predittore. Quindi ce n'è uno dinamico con moltissimi ingressi. BTB è uno degli elementi fondamentali. Solito predittore statico con regole che valgono per quello che valgono. Pipeline del Pentium 4: molti di questi stadi sono noti. Ce ne sono alcuni strani: drive e altri che conosciamo che però sono spezzati in due. 15 e 16 sono né più che meno il renaming diviso in 2. Gli stati di drive. Quando si esce da uno stadio e bisogna passare ad uno stadio non troppo vicino. Andando a questa velocità bisogna interporrre uno stadio intermedio che non fa nulla ma lo pilota per portarlo a destinazione in tempo utile. A 4GHz 250 picosecondi è un clock. Interessante citare il caso della allocate, moltissimi registri di renaming e notiamo che ne abbiamo 128 per interi, 128 per FP, ma tutto legato al numero di unità funzionali. Abbiamo anche 128 registri per istruzioni vettorizzate. Esistevano nel P6 istruzioni MMX, vettorizzate. Istruzioni che sono state abbandonate per un'altro tipo di istruzioni SSE. MMX andavano ad utilizzare unità FP. SSE ha gestione autonoma e va ad impattare sulla complex instruction unit. Quando parliamo di istruzioni vettorizzate, stessi problemi. Abbiamo dati in produzione ed altri pronti. Quindi meccanismi di renaming. RAT del Pentium 4. Dispatcher: unità che avvia alle unità di esecuzione. Fino a 6 per clock ne possiamo avviare (2*2 perchè due fronti per quelle elementari, più altre 2 che fanno 6). Register file: quando noi dobbiamo eseguire abbiamo i nostri registri di renaming, ma poi il dato deve essere messo nell'unità di esecuzione. Quindi bisogna mettere dati nel registro di esecuzione (Register file sono due periodi di clock per portarlo). Qui abbiamo ripreso unità di esecuzione: per esempio il Pentium 4 a 4GHz non è indicato quanti stadi ha. Quello a 3 GHz ha 31 stadi. Non hanno cambiato nulla a livello di unità funzionali, ma avranno aumentato code, stadi drive, suddivisione stadi .. Sequenza delle istruzioni: esistono anche istruzioni complessissime (quello che danno luogo a molto microoperazioni), anche in questo caso decodifica fatta attraverso ROM: sequencer che scandisce microoperazioni che corrispondono ad una operazione di più alto livello. Nella trace cache non vengono messe istruzioni molto complesse, ma messo puntatore a ROM che contiene la sequenza microoperazione. Questo per non andare ad intasare la cache. ROM la possiamo intendere come estensione di trace cache. Passaggio su tutti gli stadi: interessante che il TLB (sia istruzioni che precede decodifica, perchè dobbiamo reperire istruzioni ad indirizzo fisico, che ci serve ad accedere alle istruzioni di alto livello che è L2) sono full-associative. Sappiamo che se è full ovviamente è molto più flessibile, ma anche più complicata. Abbiamo cache dei dati. Non abbiamo cache L1 delle istruzioni, ma c'è L1 dei dati. La L2 invece è globale, non c'è suddivisione tra quella delle istruzioni e dei dati. RDRAM saltato (ci sarebbero 3 lucidi su RDRAM). C'è RDRAM che permette di avere accesso molto più rapido: grande successo fino a qualche anno fa. Oggi ce l'ha meno perchè protetta da copyright. RDRAM stanno scomparendo. Multiprocessori Quello interessante è che in realtà quello che stiamo dicendo sui processori più moderni ha poco a che fare con aspetti concettuali. Ci sono degli aggiustamenti, ma non novità straordinarie. Riduzione dimensione transistori. Ma in termini concettuali siamo un po' arrivati in fondo. In un certo senso la ricerca si sta orientando sempre di più sul problema dei consumi. Se oggi uscisse con un sistema di 10 o 12h sbaraglierebbe tutto (con un peso di 1, 2 o 3 Kg al massimo). Ecco dove si concentrano molti sforzi. Nuovi batterie a gas con ricarica molto complicata. Non c'è ancora la Soluzione. Ci sono novità in termini architetturali, ma che non incidono in maniera così clamorosa come ROB, renaming e fuori ordine. C'è quello che chiamiamo dual processor: raddoppio puro e semplice del processore. Poi c'è il dual-core, quelli che hanno cache in comune. Ma avere cache in comune non è così semplice, perchè pone grossi problemi. Tra gli aspetti possibili di multiprocessor esiste il multi-threads (nell'ambito di un sistema si possono avere flussi paralleli, salvo poi reintegrarsi al termine). In generale suddivisione che tanto più possibile, quanto più compilatore è intelligente. Esistono supporti di tipo architetturale. Hyperthreading: Pentium 4 è rimasto sostanzialmente il medesimo (unica macchina), nella quale siamo in grado di fare correre in maniera APPARENTEMENTE parallela (perchè se c'è un unico processore, c'è un unico flusso), ma se queste istruzioni corrispondono a due thread e se siamo in grado di differenziare questi 2 thread. Possiamo avere anche thread bloccato e in questa condizione potremmo avere pipeline bloccata. Possiamo quindi dare controllo ad altro thread. E' chiaro che due thread diversi sono apparentemente due processi diversi. Su porzioni di programma diversi. XEON: abbiamo delle unità funzionali (per esempio fetch, unità di decodifica, cache, ..) condivise, poi abbiamo set dei registri che contraddistinguono singolo thread. All'interno del nostro processore siamo costretti a replicare buona parte dei registri. Dobbiamo replicare registri stadio per stadio (quando esce da uno stadio, quei registri devono essere duplicati). Ricordiamo che una pipeline è per sua natura una serie di circuiti combinatori separati da una serie di registri. Ovvio che quei registri devono essere duplicati. Qui c'è disamina di questi elementi, per esempio abbiamo elementi duplicati che non sono necessariamente i registri. Per esempio duplicazione del TLB diversi. Avere due thread non vuol dire che i due thread appartengano a stesso processo (in questo caso stesso TLB). Un thread non è legato ad un solo processo. La macchina si accorge che ci sono questi due thread e li mette in esecuzione. Qui iniziano due thread paralleli. Stessa operazione riferita a processi diversi. Qui le code sono differenziate, la trace cache è ovviamente differenziata: ogni thread ha sua sequenza di operazioni. Qui caratteristiche Xeon hyperthreading. Anche registri di renaming duplicati. Non sono duplicate unità funzionali. Saranno dati nel ROB che ci dicono se quei dati appartengono ad un thread o all'altro. Si può avere il dubbio se più efficiente una gestione multihreading o multiprocessore. Multiprocessore è infinitamente più efficiente. In multithreading ci SEMBRA che due thread lavorino in parallelo, ma unità funzionali sono singoli. In caso di multiprocessore abbiamo parallelismo reale. C'è però forte differenza in termine di complessità e consumi. Hyperthreading richiede aumento numero dei transistori dal 5 al 10%. Con aumento di efficienza del 25%. Con rapporto costo/prestazioni favorevole. Specialmente di fronte a processori che consumano tanto come XEON, il raddoppio del processore comportava grossi svantaggi in termini di consumo, al contrario di hyperthreading. Aumento consumi sullo stesso chip davvero ingestibili. Ecco che hyperthreading ha avuto ottimo successo. Oggi abbiamo multiprocessori sullo stesso chip, che hano 2 o 4 i quali vanno in hyperthreading. Non particolarmente appetibile però perchè si sa che efficienza del multiprocessore ha curva a ginocchio. Oltre certo numero di processori c'è problema di conflitto. Normalmente il numero massimo di processori (intesi strettamente connessi), non processori che comunicano tramite linee di trasmissione. Quelli si chiama lascamente interconnessi (ma sono più reti di processori, che multiprocessore). Oltre i 4 processori, un multiprocessori non va più tanto. Confine che abbiamo è powerall: potenza dissipata per unità di superficie. Oltre un certo livello temperatura sale disperatamente. Consumo dipende da numero di transistori e da frequenza di commutazione. Nella nostra tecnologia CMOS. Pentium Mobile Ecco risposta che non è avvenuta dopo Pentium 4, ma quasi in parallelo un po' dopo. Il primo processore che si è rivolto direttamente a laptop è Centrino. Integra wireless, ora alcuni WiMax. Questa tecnologia simile a P3 (in cui si aumentava velocità senza cambiare troppo architettura). Centrino ha quindi aumentato unità funzionali e avendo numero di stadi inferiore, si è deciso di aumentare numero porte di accesso alle unità funzionali. Quindi diminuendo velocità del clock si è cercato di aumentare accessi a unità funzionali. Qui non c'è trace cache (visto che non deriva da P4). Qui L1 integrata e L2. Caratteristiche: abbiamo un loop detector, cioè branch target buffer oltre ad info relative a destinazione ha ulteriore elemento che va a conteggiare numero di volte che salto è stato all'indietro (loop) e su questa base (con algoritmo ignoto) è stata migliorata efficienza del BTB, che è uno degli elementi vincenti in termini di efficienza. BTB molto migliorato ed efficienza migliorata del 20%. Micro fusione: Noi sappiamo che uno dei colli di bottiglia delle nostre macchine è decodifica. Sappiamo anche al decodificatore vengono destinate forte quantità di silicio. Ognuno dei decodificatori semplici produce una mircooperazione ogni clock al massimo. Questa quantità può essere insufficiente. In queste macchine è stata adottata una sorta di riassunto. E' possibile che negli stessi 128 bit è possibile inserire due microoperazioni. Ma questo nell'ambito dei decodificatori semplici vuol dire che in realtà ne producono due. Questo senza alterare alterare ROB. Notiamo che questa operazione si accoppia con macro fusione e che avviene nei processori più nuovi. In cui anche istruzioni di alto livello, possono essere nell'ambito della stessa istruzione contenerne 2. Queste migliorie funzionano in termini parziali. Microfusione siamo intorno a miglioramento del 5% per le intere e 10% per le FP. Ultima cosa interessante: c'è una ulteriore miglioria per cui quando i decodificatori decodificano salto a subroutine, fa due operazioni: scrive indirizzo di ritorno sullo stack e salta. Queste sono due microoperazioni. In caso di Centrino queste sono fuse in una sola. C'è poi discorso della disabilitazione selettiva delle unità. Quando alcune unità funzionali (o certi stadi della pipeline) non sono utilizzati vengono temporaneamente disabilitati e quindi riduzione consumi.

Nuovi sviluppi

[modifica | modifica sorgente]

Chip Multi Processor Ultimo a destra multiprocessore come inteso fino ad oggi: due processori separati, con ognuno le sue unità. Poi c'è con I/O condiviso, cioè accesso a bus condiviso. Lavorano con la L2 per conto loro. Poi c'è caso di cache secondaria condivisa. Sono le tipologie che oggi riscontriamo. Cache condivisa: Molti vantaggi e alcuni svantaggi. Vantaggi: facilità di trasmissione di parametri tra due processori senza intervento del bus. Ha un altro vantaggio: questa cache può (con politiche opportune) essere attribuita con proporzioni diverse ai due processori in funzione delle loro esigenze. Grande svantaggio: legato al fatto che normalmente le case costruttrici fanno progetto e vorrebbero usarlo in situazioni diverse. Usare stesso progetto anche per sistemi monoprocessore. Ma spezzare cache rende complicato fare il downgrade. Altro elemento di difficoltà è che questa cache deve essere a tecnologia molto efficiente perchè deve rispondere a due processori. Usare in questo caso write through non è così folle. Usare write through con accesso alla cache di secondo livello non è così penalizzante. Una politica write through ci dice che la L2 sarà sempre coerente con l'ultima L1 che ha scritto. Laddove ci sono cache c'è problema di coerenza, politiche basate su direttori o su MESI è comunque qualcosa che sottosta a questi sistemi. Qui abbiamo sistema che ci permette di colloquiare tra i due processori. Questo sistema è duplice sistema. Qui ci sono alcuni esempi di alcuni esempi che usano questa tecnica. Pag 5: questi sono due processori integrati su stesso chip. Questo tipo di implementazione richiede doppi piedini per colloquio verso bus. Maggiore complessità meccanica realizzativa è uno dei tanti elementi che contribuiscono e che incidono su resa. Prima processore viene testato come singolo chip, ma poi anche con altri. Futuro dell'Itanium molto indubbio. Questo è stato tentativo di eliminare costo che ci deriva dall'uso da un set di istruzioni retrocompatibile. Proviamo un set di istruzioni che massimizzi efficienza, senza retrocompatibilità. Pentium 4 dual core: Pentium D ed extreme. Queste cose sono riportate con caratteristiche per vedere il panorama in cui ci muoviamo negli ultimi anni. L'extreme edition ha la trace cache, mentre il pentium d non ha trace cache, quindi come al solito siamo in interregno in cui che cosa sia meglio è difficile da dire. Entrambe sufficientemente buone. Notiamo che extreme edition in grado di gestire hyperthreading. Questi sono dispositivi già disponibili. Questo è nostro processore: quantità molto elevata di cache. Cache di primo livello da 1MB e di secondo livello da 2MB. Notiamo che è socket 775 (correlato a numero di piedini). Notiamo che se abbiamo 700 piedini nel giro di pochi cm2 dobbiamo mettere tanti strati. Tecnologia dei circuiti stampati è problema clamoroso di progettazione dei sistemi. Due aspetti interessanti: questo processore ha possibilità di lavorare a 64 bit. Questo fa sì che se andiamo a 64 bit abbiamo 2 alla 64 indirizzi di memoria centrale. In questo caso caso salto di qualità in termini di parallelismo e di indirizzabilità. Fra le tante cose dell'estensione a 64 bit, abbiamo grande numero di registri architetturali. Registri visibili da parte del programmatore. Fino ad oggi AX, BX, CX, DX. Unico salto nel Pentium era stato fatto nei registri di segmento. Qui 8 registri addizionali. Va da sé che l'estensione del numero dei registri e il fatto che siano a 64 bit impone un forte aumento di registri RAT. Aumentano come dimensione e anche come numero. RAX è la versione a 64 bit dell'EAX, ... Torniamo indietro su alcune cose: sistema ha maggiore resistenza agli attacchi degli hacker sui buffer overlfow. Se per un buco di sistema in qualche maniera riesco a debordare, in qualche maniera se trovo via di fare questo, riesco ad inserire qualcosa di “cattivo” nello stack e se questo è codice eseguibile posso farlo tornare dove voglio. 90% dei virus fatti così. Se non sono sofisticato ho blocco della macchina, se sono più sofisticato inserisco istruzione. Nella versione 9 abbiamo 4 MB di memoria cache secondaria. Abbiamo anche virtualizzazione. Virtualizzatore: Strato hardware che permette di far coesistere sistemi operativi diversi. Non è in contrasto con hyperthreading. Questi due processori possono a loro volta in hyperthreading. E' quindi strato inferiore. Virtualizzatore ci fa vedere CPU virtuali diverse. Non nel senso di memoria virtuale, ma MACCHINE virtuali, vuol dire avere registri registri che vengono mappati da hardware a registri fisici. Nella CPU virtuale possiamo avere sistema operativo che può essere multitasking e multithread. Core: Archittetura Core è architettura di riferimento per quanto riguarda Intel. E' stato sviluppato in Israele. Questa architettura si rivolge ai bassi consumi. Primo nome interno e poi nome di marketing. Ci sono poche cose interessanti: tornati in situazioni a 5 porte (come in Pentium 6 e in Centrino). Abbiamo ROB superiore (più di 40). Sembra che sia differenziato in base ad applicazioni. Mi interessa segnalare che non esiste trace cache, architettura molto simile a P6 e che abbiamo. Esiste fusione delle microoperazioni. Questo fa sì che si possono generare fino a 8 (6 ma in realtà due microfuse). In termini pratici c'è una cosa interessante: c'è anche memory reorder buffer. Esiste la possibilità di alterare la sequenza anche negli accessi in memoria: se io devo fare n LOAD, posso anticipare terza load. Potrei avere accessi a dispositivi con velocità di accesso diversi. Il memory reorder buffer è una sorta di piccolo reorder buffer nel quale sono esaminati gli indirizzi di riferimento delle nostre microoperazioni e nei quali si può ottenere anticipazione di certe operazioni già disponibili. In funzione di quello che c'è all'interno.