Transcription
Questo video è stato sponsorizzato da Brilliant. Una caratteristica distintiva di un computer per scopi generali è la sua capacità di essere programmato per eseguire una vasta gamma di compiti e ciò comporta l'interazione con tutti i tipi di periferiche o, come le chiamiamo in informatica, dispositivi di I/O. Oggi impareremo le basi di come una CPU comunica con così tanti dispositivi diversi e i vari metodi utilizzati per renderlo possibile. Ciao amici, mi chiamo George e questo è core dumped. Prima di iniziare, voglio solo menzionare che questo video include molte astrazioni e spiegazioni semplificate per rendere l'argomento più facile da capire per più persone. Quindi, se hai già familiarità con questo argomento e hai qualche feedback costruttivo, sentiti libero di condividerlo nei commenti. Il termine dispositivi di I/O sta per dispositivi di input output. Ha senso. Il sistema può utilizzare periferiche come una tastiera o un mouse per ricevere input dall'utente e dispositivi come uno schermo o altoparlanti per inviare informazioni all'utente. La domanda principale a cui cercheremo di rispondere oggi è: come comunica la CPU, il cervello del computer, con tutti questi dispositivi? Nello stesso modo in cui le CPU comunicano con la memoria attraverso un bus, un insieme di fili che interconnettono i loro pin, è logico pensare che anche un dispositivo di I/O comunichi con la CPU attraverso una connessione fisica cablata. Molte persone pensano che la CPU utilizzi questa connessione per gestire i dispositivi a livello micro. Ma fin dai primi giorni dell'informatica, gli ingegneri hanno imparato che lasciare che la CPU controllasse direttamente le periferiche non era generalmente una buona idea. E la ragione è semplice. La CPU è progettata per funzionare a frequenze estremamente elevate. Eseguendo milioni o addirittura miliardi di operazioni al secondo. I dispositivi di I/O, al contrario, sono molto più lenti. Pensa a una stampante. Stampare una singola pagina può richiedere secondi. Immagina se la CPU dovesse controllare ogni piccolo motore, rullo e movimento dell'ugello in una stampante, spingendo la carta in avanti, allineando la cartuccia d'inchiostro e decidendo l'istante esatto per rilasciare l'inchiostro. Dall'inizio alla fine della stampa di una pagina, la CPU avrebbe sprecato innumerevoli secondi a fare un lavoro che non contribuisce in alcun modo al suo vero scopo, l'esecuzione di programmi. E mentre le stampanti sono un esempio estremo, lo stesso problema esiste anche con dispositivi semplici come le tastiere. Ecco una rapida occhiata a come funzionavano alcune vecchie tastiere per computer. Per semplificare, concentriamoci solo su alcuni tasti. Come può la CPU sapere quando e quale tasto viene premuto? Dare a ogni tasto il suo filo dedicato direttamente alla CPU sarebbe uno spreco, richiedendo tanti fili quante sono le chiavi. Invece, una tastiera è organizzata come una matrice di righe e colonne. Un filo per riga, un filo per colonna. Quello che molti vecchi computer facevano era attivare una riga e una colonna alla volta, quindi alternare rapidamente attraverso l'intera matrice. Pensala come la scansione di ogni intersezione, una dopo l'altra. Raccomando di impostare il video a 60 fotogrammi al secondo per la parte seguente in modo da poter apprezzare più facilmente l'animazione. Se questo schema di scansione viene eseguito abbastanza velocemente, circuiti aggiuntivi possono rilevare quando viene premuto un tasto. Ad ogni intersezione riga-colonna, è collegato un gate AND. Un ingresso proviene da un filo di riga e l'altro ingresso proviene da un interruttore collegato a un filo di colonna. Quando premi un tasto, ad esempio, la lettera G, quell'interruttore si chiude. A prima vista, potrebbe sembrare che la pressione del tasto non cambi immediatamente nulla. Ma ricorda, un computer opera incredibilmente velocemente. Molto prima che l'utente rilasci il tasto, il ciclo di scansione raggiungerà la riga e la colonna per quel tasto. Poiché l'interruttore è chiuso, l'uscita del gate AND cambia, segnalando che il tasto è premuto. Se combiniamo le uscite di tutti i gate AND con un circuito OR, otteniamo una singola linea di pressione del tasto attiva se viene premuto un tasto, spenta se nessuno lo è. Questo filo può essere utilizzato per far sapere alla CPU quando viene premuto un tasto. E poiché ogni tasto corrisponde a una riga-colonna unica intersezione, la CPU può capire esattamente quale tasto è premuto controllando quale coppia di fili è attiva in quel momento. Abbastanza intelligente, vero? Ora, potresti chiederti perché diavolo sto parlando di questo. Ebbene, sto solo cercando di fare un punto qui. All'inizio, questo sembra un sistema perfettamente ragionevole. Ma nota qualcosa di importante. Affinché funzioni, la CPU stessa deve continuare a commutare quei fili di riga e colonna più e più volte. Nelle CPU più vecchie, questo lavoro extra aveva effettivamente un impatto misurabile sulle prestazioni. E certo, le CPU moderne sono abbastanza veloci da gestirlo. Ma chiediti, dovrebbe davvero sprecare cicli su questo invece di fare ciò per cui è destinata? Infatti, ci sono questi piccoli processori utili chiamati microcontrollori. Sono progettati specificamente per interagire con l'hardware e possono occuparsi esattamente dello stesso lavoro mentre la CPU rimane concentrata sull'esecuzione dei programmi. In questo esempio, quando viene premuto un tasto, il microcontrollore, abbastanza potente da gestire il circuito di scansione, rileva la pressione del tasto e semplicemente passa il risultato alla CPU attraverso una connessione fisica che chiameremo bus di controllo. E questa idea non è limitata alle tastiere. Se hai mai aperto un vecchio disco rigido, noterai che non è solo un disco rotante con una testina di lettura/scrittura. C'è un'intera scheda logica piena di chip dedicati al controllo dei motori, al posizionamento della testina, alla gestione della meccanica e persino alla crittografia e decrittografia dei dati in modo che la CPU non debba farlo. A proposito, se hai imparato qualcosa fino a questo punto del video, premi il pulsante mi piace. Aiuta molto. Quando un programma esegue del codice come questo, la CPU non esegue istruzioni per far girare il disco e muovere la testina per cercare il file. Invece, invia un comando attraverso il bus di controllo chiedendo al controller del disco di trovare data.txt. Una volta inviata la richiesta, la CPU può continuare a svolgere altre attività. Nel frattempo, il controller gestisce la meccanica della rotazione del disco e del movimento della testina. Quando il file viene trovato, il controller restituisce l'indirizzo in cui si trova. Successivamente, se il programma vuole leggere, diciamo, quattro byte da quel file, la CPU invia una richiesta dicendo al dispositivo di leggere quattro byte a partire dall'indirizzo precedentemente restituito. Ancora una volta, il controller del disco fa il lavoro effettivo di posizionare la testina e leggere i dati mentre la CPU è libera di continuare a eseguire altre istruzioni. Una volta che le informazioni richieste sono pronte, il controller le restituisce alla CPU tramite il bus. È così che dovrebbe funzionare ogni dispositivo di I/O. Invece che la CPU gestisca il dispositivo a livello micro, invia semplicemente richieste e il controller del dispositivo le esegue. Quindi, prima lezione imparata oggi. La CPU non controlla i dispositivi di I/O. Comunica con loro. Successivamente, vedremo come viene effettivamente implementato il bus di controllo in hardware. La parte difficile. Non preoccuparti però, potrebbe sembrare complesso, ma con alcune animazioni chiare, avrà perfettamente senso. Nello stesso modo, Brilliant ha perfettamente senso. Se vuoi imparare nuovi concetti, se sei una persona che ama imparare cose nuove e acquisire competenze reali, ma senti che scorrere infinitamente PDF non è il modo giusto, allora Brilliant è per te. Brilliant è una piattaforma di apprendimento basata sulla risoluzione attiva dei problemi. Ogni lezione è progettata per farti giocare con le idee utilizzando un approccio basato sui primi principi che ti aiuta a comprendere i concetti dalle fondamenta. Rimani motivato con un mix perfetto di sfide coinvolgenti, serie giornaliere e funzionalità competitive che rendono l'apprendimento gratificante. La parte migliore è che Brilliant è disponibile sul tuo telefono. Quindi, invece di scorrere passivamente i PDF, puoi imparare un po' ogni giorno, ovunque tu sia. Corsi come "thinking in code" e "programming with variables" ti aiutano a sviluppare la mentalità di un programmatore insegnandoti a scomporre problemi complessi in parti logiche e gestibili e ti aiutano anche a sviluppare un'intuizione per la logica informatica, mentre impari a progettare e debuggare programmi reali. Per imparare gratuitamente su Brilliant, vai su brilliant.org/corumpt, scansiona il codice QR sullo schermo o fai clic sul link nella descrizione. Brilliant ha anche offerto ai nostri spettatori uno sconto del 20% su un abbonamento annuale premium, che ti dà accesso giornaliero illimitato a tutto su Brilliant. E ora torniamo al video. Come ho detto, il bus di controllo viene utilizzato per inviare sia comandi che dati. Ma come viene effettivamente implementato questo bus in hardware? Quando si progetta l'I/O, ci sono due cose a cui dobbiamo pensare. Il metodo di comunicazione, come la CPU parla fisicamente con i dispositivi, e la tecnica di trasferimento dati, come e quando i dati si muovono. I dispositivi di I/O sono più complicati della memoria perché ci sono più modi per implementare entrambi, e in pratica, l'hardware può utilizzare qualsiasi combinazione di essi. La buona notizia è che ogni metodo è in realtà abbastanza semplice da capire. Quindi, iniziamo con il metodo di comunicazione. Poiché i dispositivi di I/O hanno i loro microcontrollori, vengono forniti anche con una qualche forma di memoria integrata. Questa può essere semplice come latch, flip-flop o piccoli file di registri all'interno della logica del controller. E per dispositivi più complessi, potrebbe includere anche chip DRAM dedicati. La memoria mappata I/O funziona esponendo quella memoria integrata, o almeno parte di essa, in modo che la CPU possa leggerla e scriverla come se fosse solo un'altra parte della memoria principale. Nello stesso modo in cui ai processi viene assegnato uno spazio di indirizzamento, l'approccio di memoria mappata assegna ai dispositivi di I/O le proprie regioni di memoria specifiche all'interno dello spazio di indirizzamento del sistema. Dal punto di vista della CPU, comunicare con un dispositivo non è diverso dal leggere e scrivere variabili in memoria. Quindi può utilizzare le sue normali istruzioni di caricamento, memorizzazione e spostamento per parlare con i dispositivi, il che mantiene l'architettura semplice. Ad esempio, se un programma vuole inviare una richiesta HTTP, la CPU può passare il messaggio alla scheda di rete copiandolo nello spazio di memoria mappato a quella scheda utilizzando istruzioni di spostamento standard. Naturalmente, è ancora necessaria una connessione fisica tra la CPU e i dispositivi. È necessaria una certa circuiteria aggiuntiva per indicare se un indirizzo di memoria passato attraverso il bus degli indirizzi appartiene alla memoria principale o a un dispositivo di I/O. Ma questa è solo logica di base costruita con alcuni registri e comparatori. Quindi è economica e semplice. Infatti, abbiamo già visto come un circuito può verificare se un indirizzo sul bus rientra in un certo intervallo. E quella logica può essere combinata con demultiplexer per instradare i segnali nella giusta direzione. Ma nota che separare il bus di controllo dal bus di memoria principale è solo il primo passo. Quel bus deve ancora essere instradato al dispositivo corretto per ogni operazione di I/O. Tornerò su questo punto più tardi. Per ora, parliamo dell'altro metodo di comunicazione principale noto come I/O isolato. In questo metodo, i dispositivi non sono affatto mappati in memoria. Invece, sono collegati tramite un bus dedicato completamente separato dal bus di memoria, da cui il nome. La comunicazione con i dispositivi avviene tramite un protocollo personalizzato. Il grande vantaggio è che le operazioni di memoria e I/O non possono interferire tra loro, il che rende il sistema più adatto al parallelismo. Ma lo svantaggio è che la CPU ora necessita di istruzioni speciali come "in" e "out" per parlare con i dispositivi su questo bus. Ciò significa più circuiti all'interno della CPU, il che rende il chip più grande, più costoso da produrre e meno efficiente dal punto di vista energetico. In breve, la memoria mappata I/O tratta i dispositivi come se fossero parte della memoria, assegnando a ciascun dispositivo la propria regione di memoria. L'I/O isolato, d'altra parte, tratta i dispositivi come componenti separati con i propri percorsi dedicati, dando a ciascuno uno slot virtuale chiamato porta, essenzialmente l'equivalente di un indirizzo di memoria. A proposito, è per questo che l'I/O isolato è spesso indicato come I/O mappato su porta. Per quanto riguarda le architetture, differiscono nel metodo che utilizzano. L'architettura x86, ad esempio, supporta entrambi gli approcci, mentre ARM utilizza solo la memoria mappata I/O, un ottimo esempio del perché x86 è considerata un'architettura CISC mentre ARM è classificata come RISC, evidenziando le loro differenze in complessità ed efficienza energetica. Ora, qualcosa che molti di voi potrebbero chiedersi è se la CPU invia una richiesta a un dispositivo di I/O e poi passa ad altro, come fa a sapere quando il dispositivo è pronto a rispondere? Ci sono due soluzioni principali. La prima, chiamata I/O programmato o polling, assegna un registro di stato a ciascun dispositivo. Per impostazione predefinita, questo registro è contrassegnato come non pronto. Quando il dispositivo ha dati disponibili, sia perché la CPU li ha richiesti sia perché si è verificato un evento, come una pressione di tasto o un pacchetto di rete in arrivo, il registro passa a pronto. La CPU quindi controlla ripetutamente o esegue il polling di questo registro finché non vede che il dispositivo è pronto. A quel punto, può iniziare a interagire con il dispositivo. Questo metodo è semplice ma inefficiente. La CPU spreca cicli eseguendo costantemente istruzioni aggiuntive solo per controllare i flag di stato. Quindi, c'è un modo migliore per farlo noto come I/O guidato da interrupt. Invece di costringere la CPU a controllare un registro di stato, il dispositivo utilizza un filo speciale per inviare un segnale di interrupt quando è pronto. Come abbiamo imparato negli episodi precedenti, questo segnale interrompe l'attività corrente della CPU e salta l'esecuzione a un gestore di interrupt. Nel caso dell'I/O, quel gestore contiene il codice necessario per servire il dispositivo che ha attivato l'interrupt. Ciò rende il sistema molto più reattivo perché la CPU reagisce immediatamente agli eventi di input o output senza perdere tempo a fare polling. L'I/O guidato da interrupt è quasi sempre preferito, anche se richiede supporto hardware per gli interrupt. Fortunatamente, quasi tutte le CPU moderne li supportano. E poiché il polling può sempre essere implementato via software, ogni CPU in pratica supporta entrambi i metodi. In pratica, quando si lavora con dispositivi di I/O, specialmente nei moderni sistemi per scopi generali, troverai quasi sempre la memoria mappata I/O combinata con l'I/O guidato da interrupt. E questo è tutto. Ora conosciamo le basi di come le CPU interagiscono con i dispositivi di I/O. Ma prima di concludere, devo chiarire alcune cose perché so che alcuni di voi le segnaleranno nei commenti se non lo faccio. Quando ho spiegato la memoria mappata I/O in precedenza, ho utilizzato un modello primitivo semplificato. La verità è che i dispositivi di I/O non sono memoria, e costringerli a comportarsi come memoria è possibile, come mostrerò tra un po', ma non è sempre l'ideale. Nei progetti moderni, la connessione fisica che ho mostrato è solitamente implementata come un bus I/O dedicato personalizzato. In questo modo, i dispositivi non devono accettare o rispondere alle operazioni di memoria. Possono utilizzare il proprio protocollo di comunicazione più vicino all'I/O isolato invece di essere costretti a comportarsi come memoria sul bus principale. La logica di routing e conversione per questo bus è gestita all'interno di un componente storicamente noto come memory controller hub. Se hai abbastanza anni, potresti ricordare che fino alla metà degli anni 2000, tutte le schede madri per PC avevano uno zoccolo per CPU più due chip principali saldati sulla scheda: il northbridge e il southbridge. Su questa vecchia scheda madre, puoi vedere che non c'era un bus che andava direttamente alla memoria. Invece, l'unico bus che usciva dalla CPU era collegato al northbridge. Questo era noto come front-side bus. Il compito principale del northbridge era quello di fungere da memory controller hub, instradando quel bus sia al bus di memoria che al bus PCI Express. Per chi non lo sapesse, gli slot PCI Express vengono utilizzati per collegare dispositivi di I/O ad alta larghezza di banda come schede grafiche e schede di rete. Un bus interno veniva anche instradato al southbridge, che lo scomponeva in interfacce a bassa velocità come PCI, quella vecchia porta parallela che molti di voi probabilmente non riconoscerebbero nemmeno. Il buon vecchio Universal Serial Bus o USB, SATA per le connessioni dei dischi rigidi e altre periferiche. Infatti, se guardi attentamente questa scheda madre, puoi vedere le tracce che collegano il southbridge direttamente al pannello I/O. In breve, il North Bridge gestiva connessioni ad alta velocità come RAM e GPU, mentre il South Bridge gestiva I/O più lenti. Ora, se hai assemblato un PC negli ultimi anni o anche solo guardato altri farlo, potresti aver notato qualcosa di strano. Sulle schede madri moderne, non c'è affatto un northbridge. Invece, vedrai tracce che vanno direttamente dallo zoccolo della CPU agli slot di memoria e altre che vanno direttamente agli slot PCI Express. E su questa particolare scheda madre che sto mostrando, noterai che alcune tracce vanno persino direttamente dalla CPU al pannello I/O. Quindi, cosa sta succedendo qui? A prima vista, questo sembra sospettosamente simile al metodo di I/O isolato di cui abbiamo parlato in precedenza, dove i dispositivi hanno il proprio bus dedicato che proviene direttamente dalla CPU. Ma quello che è realmente successo è che le CPU sono diventate molto più veloci e il vecchio design northbridge-southbridge è diventato un collo di bottiglia, specialmente per le operazioni di memoria. Il motivo per cui ora vedi bus di memoria e I/O uscire direttamente dalla CPU è che tutta la logica di routing e traduzione precedentemente gestita dal northbridge e persino parti del southbridge sono state integrate nella CPU stessa. La funzionalità rimanente del southbridge, insieme ai pochi compiti del northbridge che non sono ancora stati spostati nella CPU, ora risiedono in un singolo chip saldato sulla scheda, noto semplicemente come chipset. Sulle schede madri moderne, puoi trovarlo sotto un dissipatore di calore dove si trovava il vecchio southbridge. Ed è per questo che oggi è così comune vedere bus di controllo I/O provenire direttamente dalla CPU. Con il memory controller hub completamente integrato nel processore, tutto il routing e la traduzione del protocollo sono ora gestiti internamente. L'unica domanda rimasta è perché mappare i dispositivi di I/O tramite indirizzi di memoria se in realtà non si comportano come memoria. Se stiamo già progettando una complessa logica di traduzione all'interno del memory controller hub, perché non implementare invece l'I/O isolato? So che può sembrare strano e controintuitivo, ma ci sono grandi vantaggi in questo design. Per capire perché, dobbiamo tornare a un problema comune fino agli anni '80. All'epoca, i computer domestici stavano prendendo piede. Potevi acquistare macchine da Apple, Commodore, Atari, IBM e molti altri. Il problema, ogni marchio era il proprio piccolo ecosistema. Una volta scelto uno, eri bloccato. Software e hardware di un computer semplicemente non funzionavano con un altro. All'inizio, questo potrebbe sembrare un problema di architetture CPU diverse, ma non era così. Ad esempio, l'Apple 2 e il Commodore PET, entrambi rilasciati nel 1977, utilizzavano esattamente la stessa CPU, il MOS 6502. E quando dico esattamente la stessa CPU, intendo letteralmente. Potevi scambiare i chip della CPU tra i due computer, e avrebbero comunque funzionato normalmente. Eppure, se prendevi un floppy disk con un programma scritto per il Commodore PET, quel programma non funzionava sull'Apple 2 e viceversa. Gli sviluppatori dovevano riscrivere e distribuire versioni separate del loro software per ogni piattaforma. Infatti, il MOS 6502, una delle CPU più popolari di tutti i tempi, si trovava su molti computer e console diversi nei primi anni '80, nessuno dei quali era compatibile tra loro. Perché? Perché all'epoca la comunicazione periferica si basava su pura memoria mappata I/O. Ogni dispositivo di I/O era mappato a una specifica regione di memoria. Ma ogni produttore decideva il proprio layout. E il vero problema era che questa mappatura era cablata sulla scheda madre. Non poteva essere riconfigurata via software. Il risultato, la stessa istruzione che faceva visualizzare un colore sullo schermo a un computer poteva far suonare un suono a un altro. Un'istruzione per inviare un comando al lettore di cassette su un sistema poteva sovrascrivere la memoria del kernel su un altro, corrompendo dati importanti. Un programma scritto per un modello spesso non aveva senso su un altro, e istruzioni senza senso di solito significavano un crash immediato. Questo spiega anche perché non potevi semplicemente collegare, diciamo, un'unità disco di una marca a un computer di un'altra. I dispositivi di I/O di marche diverse avevano la stessa funzionalità, ma ognuno era implementato in modo completamente diverso. Ora, immagina se quell'approccio esistesse ancora oggi. E se nel 2025 comprassi una GPU e un SSD, ma non potessi usarli insieme perché le loro regioni di memoria si sovrapponessero? O se comprassi un mouse ASUS che funzionasse solo su schede madri ASUS perché ogni produttore mappava i dispositivi in modo diverso. Questo incubo è esattamente il motivo per cui i sistemi moderni utilizzano ancora la memoria mappata I/O come astrazione, ma la nascondono dietro bus dedicati standardizzati come PCI Express o USB. Non è immediatamente ovvio perché ciò richiederebbe un bus personalizzato. Potresti pensare, il problema non avrebbe potuto essere risolto semplicemente evitando dispositivi cablati e rendendo i dispositivi indirizzabili dinamicamente? In teoria, sì. Ma ciò richiederebbe un protocollo di handshake. In modo che il dispositivo e il computer potessero negoziare quale regione di memoria utilizzare. E una volta introdotto un handshake, hai già creato un canale di comunicazione personalizzato. A quel punto, ha più senso utilizzare quel bus personalizzato per tutta la comunicazione. E questa è la mia migliore risposta senza addentrarmi in un sacco di dettagli tecnici che probabilmente annoierebbero la maggior parte delle persone. Questo approccio misto che utilizza la memoria mappata I/O ma la traduce in bus di comunicazione personalizzati offre chiari vantaggi per gli utenti comuni. Questo è enorme. Significa che puoi acquistare periferiche sapendo che le differenze di marca non ti escluderanno. E per gli sviluppatori, è altrettanto prezioso perché il modello sottostante è ancora la memoria mappata I/O. Scrivere software che interagisce con dispositivi come i driver rimane relativamente semplice. Può essere fatto nel modo familiare di creare, scrivere e leggere variabili in memoria invece di trattare direttamente segnali elettrici, pin e interrupt hardware. Prima di concludere questo video, voglio solo menzionare che i protocolli hardware come PCI Express, SATA o USB definiscono solo come le informazioni si muovono sul bus. I segnali elettrici, i formati dei pacchetti, le temporizzazioni, la direzione in cui viaggiano i dati su ciascun filo. Ma la CPU ha ancora bisogno di sapere cosa inviare attraverso quel bus e come interpretare ciò che ritorna. Il motivo per cui quasi tutte le periferiche necessitano di driver è perché ogni dispositivo parla la propria lingua. E il driver agisce come traduttore tra il sistema operativo e l'hardware, dicendo alla CPU esattamente quali comandi inviare, come formattare i dati e come interpretare le risposte inviate e ricevute attraverso il bus hardware. Tratteremo i driver con maggiori dettagli in un futuro episodio. Quindi, assicurati di iscriverti perché non vorrai perdertelo. Infine, per i miei spettatori iscritti a Codecrafters, voglio solo menzionare che questo mese stanno organizzando un concorso di tastiere tra tutti coloro che completano almeno una sfida. Non stanno sponsorizzando questo messaggio. Penso sinceramente che le loro sfide siano un ottimo modo per imparare. Inoltre, se usi il link nella descrizione, aiuta anche a supportare il canale. Concluderò qui. Ci sono tre grandi lezioni che spero porterai via da questo video. E se hai imparato qualcosa di nuovo, premi il pulsante mi piace. È gratuito e aiuta davvero gli altri a scoprire questo contenuto. Grazie per aver guardato e ci vediamo nel prossimo.