Memoria virtuale
Nella pagina dedicata ai processi e ai thread abbiamo lasciato il sistema operativo sul palco, impegnato nel suo spettacolo più difficile, quello di tenere in scena più processi contemporaneamente, ciascuno convinto di possedere la macchina per intero, e in questa pagina scendiamo nel piano sottostante di quel palcoscenico per rispondere alla domanda che allora abbiamo apposta lasciato sospesa, cioè dove abitano davvero tutti quegli abitanti, come fa il sistema a concedere a ognuno di loro una memoria privata e protetta quando la memoria vera, quella costruita con i chip e saldata sulla scheda madre, è una sola e finisce sempre troppo presto.
Il problema che qui risolviamo ha tre facce che conviene guardare una alla volta, perché insieme spiegano quasi ogni scelta progettuale che incontreremo tra poco; la prima faccia è che i programmi crescono più in fretta della memoria, e già negli anni sessanta gli ingegneri si trovarono applicazioni scientifiche che non stavano in nessuna memoria esistente, mentre oggi un sistema operativo moderno pretende da solo diversi gigabyte prima ancora di far nascere il tuo primo processo; la seconda faccia è che i processi sono molti e la loro somma supera quasi sempre la memoria disponibile, perché se ogni programma chiede il suo spazio e il sistema ne ospita una dozzina, la somma delle richieste supera di regola la somma delle celle fisiche; la terza faccia è la protezione, cioè l'esigenza che un processo distratto o malintenzionato resti lontano dalla memoria dei suoi vicini, come promesso nella pagina precedente, e questa separazione deve valere anche quando tutti condividono la stessa memoria fisica.
Per avvicinare l'idea con calma, immagina un disegnatore tecnico il cui tavolo da disegno sia più piccolo del foglio che deve riprodurre, così che il foglio intero non possa mai stare tutto disteso in superficie, eppure il disegnatore lavora sereno, perché il foglio intero esiste, arrotolato in un rullo appeso alla parete, mentre la sola porzione su cui la matita sta lavorando in quel momento è quella distesa davanti a lui, e viene spostata con un gesto automatico ogni volta che il disegno richiede una zona diversa; il tavolo da disegno è la memoria fisica, il foglio nel rullo è il programma, e la magia che permette al disegnatore di comportarsi come se il foglio intero fosse disteso davanti a lui è la memoria virtuale, il tema di tutta questa pagina.
La storia ci insegna che il bisogno venne prima della soluzione, perché nei primi decenni i programmi che eccedevano la memoria venivano spezzati a mano dal programmatore in pezzetti chiamati overlay, caricati e scaricati da un piccolo caposquadra software con il sudore della fronte, un lavoro noioso e fragile che pochi sapevano fare bene; la svolta arrivò nel 1961 per merito di Fotheringham, che pensò di affidare l'intero compito al computer, e da quell'idea nacque la memoria virtuale, la cui idea di fondo è che ogni programma possieda il proprio spazio di indirizzi, cioè la propria mappa privata di celle numerate, spezzata in pezzi chiamati pagine, ciascuna una striscia contigua di indirizzi, mappata sulla memoria fisica senza che tutte le pagine debbano trovarsi in memoria nello stesso istante per far funzionare il programma.
In un certo senso la memoria virtuale generalizza la vecchia idea dei registri base e limite, che risistemavano solo due pezzi del programma, perché qui l'intero spazio di indirizzi viene ricollocato a piccoli pezzi, e la tecnica che quasi tutti i sistemi usano per farlo si chiama paginazione, nella quale lo spazio di indirizzi virtuali viene tagliato in unità di misura fissa chiamate pagine, mentre la memoria fisica viene tagliata con la stessa forbice in blocchi chiamati page frame, termine inglese che traduciamo come blocchi fisici, e pagine e blocchi hanno in genere la stessa misura, che nei sistemi comuni è di quattromilanovantasei byte, benché nella storia si siano usate pagine da cinquecentododici byte fino a un gigabyte, e l'architettura x86-64 ammetta oggi pagine da quattro kilobyte, da due megabyte e da un gigabyte da mescolare a piacere.
Il cuore della paginazione è una struttura dati che ci accompagnerà per tutta la pagina, la tabella delle pagine, cioè un lungo elenco di corrispondenze, uno per ogni pagina virtuale, dove ogni riga dice in quale blocco fisico quella pagina si trova in questo momento, oppure dice che la pagina non è in memoria affatto; matematicamente parlando la tabella è una funzione che prende il numero della pagina virtuale e restituisce il numero del blocco fisico, e dentro ogni riga convivono alcuni campi che vale la pena conoscere per nome, perché torneranno utili più avanti. Il campo più importante è il numero del blocco fisico, che è lo scopo di tutta la traduzione, affiancato da un bit di presenza che vale uno quando la pagina è davvero in memoria e zero quando la riga descrive una pagina assente, dai bit di protezione che dicono se la pagina ammette lettura, scrittura ed esecuzione, dal bit di modifica che l'hardware alza da solo ogni volta che qualcuno scrive nella pagina, segnandola come sporca, e dal bit di riferimento che si alza invece sia in lettura che in scrittura e che dice al sistema quali pagine sono vive; nota bene che l'indirizzo del disco dove vive la pagina assente non sta nella tabella, perché la tabella custodisce solo ciò che serve all'hardware, mentre le questioni da biblioteca rimangono nei tavoli interni del sistema operativo.
Vediamo ora chi esegue la traduzione, cioè l'unità di gestione della memoria, la
memory management unit o MMU, un pezzo di silicio che vive dentro lo stesso chip del
processore e che riceve ogni indirizzo virtuale prima che questo tocchi il bus della memoria;
quando il programma chiede con l'istruzione MOV REG,0 il contenuto della cella
zero, l'indirizzo virtuale zero va alla MMU, la MMU riconosce che la pagina zero, quella che
copre le celle da zero a quattromilanovantacinque, è mappata sul blocco fisico due, che
copre le celle da ottomilanuncentonovantadue a dodicimiladuecentoottantasette, e mette
sull'autobus l'indirizzo ottomilanuncentonovantadue, mentre la memoria, che non sa nulla
dell'esistenza della MMU, esegue la lettura come qualsiasi altra; la traduzione funziona così
in fretta perché l'indirizzo virtuale viene tagliato in due campi di bit, con i bit alti che
formano il numero di pagina e i bit bassi che formano lo spiazzamento dentro la pagina, e la
scelta di pagini grandi quanto potenze di due rende il taglio istantaneo, dato che i confini
dei campi coincidono con i confini dei bit; l'indirizzo virtuale ottomilanuncentonovantasei,
che cade nella pagina virtuale due con spiazzamento quattro, manda la MMU a leggere la riga
numero due della tabella, dove trova il blocco sei, e l'indirizzo fisico nasce concatenando il
blocco sei con lo spiazzamento quattro, cioè ventiquattromilacinquecentoottanta, senza
addizioni né moltiplicazioni.
Resta il caso in cui la riga della tabella abbia il bit di presenza a zero, e allora accade ciò che dà il nome a tutto il capitolo, il page fault, cioè il fallimento di pagina; quando il programma tocca l'indirizzo trentaduemilasettecentoottanta, che è il byte dodicesimo della pagina virtuale otto, la MMU scopre la riga vuota e scatena una trappola, un segnale d'emergenza che consegna il processore al sistema operativo; il sistema sceglie un blocco fisico poco usato, ne scrive il contenuto sul disco se la pagina che ospitava era sporca, va a prendere dal disco la pagina richiesta, aggiorna la tabella con la nuova corrispondenza, e infine fa ripartire l'istruzione che era fallita, che questa volta trova tutto al suo posto.
L'analogia che ti chiediamo di tenere per tutta la pagina è quella del bibliotecario che va in magazzino, perché tu, lettore seduto al tavolo di consultazione, rappresenti il processo, il tavolo rappresenta la memoria fisica, le scaffalature della sala rappresentano i blocchi liberi e il magazzino rappresenta il disco; quando il volume che ti serve non è in sala, il bibliotecario annota la tua richiesta, scompare nel magazzino, torna col volume, lo pone sul tavolo cacciando via il meno consultato degli altri, e tu riprendi la lettura esattamente dalla riga in cui ti eri fermato, senza aver mai saputo del viaggio; il processo vive la stessa esperienza, perché il page fault è trasparente, parola elegante che significa che il programma non si accorge di nulla e continua a credere di abitare in una memoria intera e tutta sua.
Questo modo di portare le pagine in memoria soltanto quando vengono chieste ha un nome, demand paging, paginazione su richiesta, e il primo avvio di un processo ne dà la dimostrazione, perché al primo accesso la memoria del processo è vuota, la prima istruzione provoca subito un fallimento, seguono altri fallimenti per le variabili globali e per la pila, e dopo qualche istante il processo possiede quasi tutte le pagine che gli servono e smette di disturbare; il prezzo di ogni viaggio in magazzino resta però alto, dato che eseguire un'istruzione richiede qualche nanosecondo mentre leggere una pagina dal disco ne richiede milioni, all'incirca dieci millisecondi, ed ecco rientrare in scena il multitasking della pagina precedente, perché mentre un processo aspetta il suo volume il processore viene consegnato a un altro processo, e la sala di consultazione resta sempre piena di lavoro.
La traduzione avviene a ogni accesso alla memoria, e se ogni accesso dovesse prima leggere la riga della tabella nella memoria fisica, ogni lettura ne costerebbe due, dimezzando la velocità della macchina, quindi i progettisti si sono inventati un ripiano segreto chiamato TLB, sigla di translation lookaside buffer, una piccola memoria associativa dentro la MMU che confronta il numero di pagina con tutte le sue poche voci, da otto a duecentocinquantasei, contemporaneamente e in un colpo solo; l'osservazione che rende la cosa possibile è che i programmi toccano pochissime pagine con grande insistenza, e infatti il ciclo principale di un programma reale abita poche pagine di codice, i dati correnti occupano poche pagine di dati, e la pila ne occupa altre due, così una TLB modesta cattura quasi sempre l'indirizzo senza disturbare la tabella.
Quando la pagina non è nella TLB si parla di mancata traduzione, la MMU compie allora una passeggiata nella tabella delle pagine, mette il risultato nella TLB espellendone una voce vecchia, e riparte, e se anche la tabella dice che la pagina è assente comincia il viaggio in magazzino; vale la pena distinguere le sfumature, perché c'è la mancata traduzione mite, in cui la pagina è in memoria e basta aggiornare la TLB con una decina di istruzioni, la mancata traduzione dura, in cui serve davvero il disco, e persino un fallimento minore, quando la pagina è già in memoria per conto di un altro processo e basta aggiornare le tabelle senza leggere nulla; l'unico caso in cui il programma ha davvero sbagliato è l'indirizzo invalido, che il sistema premia con il segnale di violazione di segmento e la fine del processo, mentre in ogni altro caso l'hardware e il sistema operativo riparano tutto da soli, pagando soltanto in prestazioni.
Un secondo problema di misura nasce quando lo spazio di indirizzi diventa grande, perché con indirizzi a trentadue bit e pagine da quattro kilobyte le pagine sono un milione e ogni processo vorrebbe la sua tabella da un milione di righe, così le tabelle vengono organizzate a più livelli; nell'esempio classico l'indirizzo viene diviso in due pezzi da dieci bit più lo spiazzamento, il primo pezzo indicizza una tabella madre di milleventiquattro righe dove ogni riga punta a una tabella figlia, e il secondo pezzo indicizza la tabella figlia per trovare il blocco, con il trucco che le tabelle figlie vengono create solo dove servono, così un processo che abita dodici megabyte in tre fasce e lascia un vuoto enorme in mezzo usa appena quattro tabelle anziché un milione di righe; la storia dei processori reali è una scala sempre più ripida, perché l'80386 del 1985 usava due livelli, il Pentium Pro aggiunse un terzo livello allungando le voci a sessantaquattro bit, e quando AMD portò la famiglia x86 a sessantaquattro bit si arrivò a quattro livelli da cinquecentododici voci ciascuno, che bastano per indirizzare duecentocinquantasei terabyte e che i progettisti giudicarono sufficienti per un pezzo; un'altra strada, percorsa da PowerPC, UltraSPARC e Itanium, sono le tabelle rovesciate, in cui c'è una voce per ogni blocco fisico invece che per ogni pagina virtuale, struttura compatta ma difficile da consultare, perché la traduzione richiede una ricerca in tutta la tabella, problema risolto con una tabella di hash che accorcia le catene e con la TLB che memorizza le coppie trovate.
Arriviamo ora al cuore pratico della pagina, perché quando il fallimento scopre che tutti i blocchi fisici sono occupati, il sistema operativo deve scegliere una vittima da cacciare via, e se la pagina vittima è sporca va prima riscritta sul disco mentre se è pulita la copia sul disco è già aggiornata e il blocco può essere semplicemente abbandonato; scegliere a caso funziona male, perché espellere una pagina molto usata significa ritrovarsela richiesta due istanti dopo, con un altro viaggio in magazzino, e prima della scelta vera e propria occorre decidere il regno di caccia, dato che la vittima può essere cercata solo fra le pagine del processo che ha fallito, strategia detta locale, che equivale a dare a ogni processo una frazione fissa di memoria, oppure fra le pagine di chiunque, strategia detta globale, che assegna i blocchi in modo dinamico a chi serve; gli algoritmi globali funzionano meglio in generale, soprattutto quando le dimensioni dell'insieme di lavoro cambiano durante la vita del processo, e per regolare l'assegnazione esiste persino un algoritmo che guarda la frequenza dei fallimenti, dando blocchi in più ai processi che soffrono e togliendoli a chi fallisce troppo di rado.
Il miglior algoritmo possibile si descrive in una frase e si dimostra impossibile nell'altra, perché la regola ottima dice di espellere la pagina il cui prossimo uso è più lontano nel futuro, così da rimandare il più possibile il prossimo fallimento, e i computer, come le persone, amano rimandare i guai; il guaio è che al momento del fallimento il sistema non conosce il futuro, esattamente come chi deve scegliere lo job più corto prima di saperne la durata, e l'algoritmo resta un riferimento da laboratorio, dato che su un simulatore si può registrare la sequenza dei riferimenti di una prima esecuzione e poi misurare quanto ci si avvicina all'ottimo, e se un sistema reale si ferma a un punto percentuale dall'ideale, cercare algoritmi migliori ripaga al massimo quel punto percentuale.
L'algoritmo che si avvicina all'ottimo senza guardare il futuro si chiama LRU, sigla di least recently used, il meno recentemente usato, e si fonda sull'osservazione che le pagine usate intensamente nelle ultime istruzioni saranno probabilmente usate di nuovo presto, mentre le pagine inutilizzate da tempi lunghi resteranno inutilizzate ancora; la regola dice quindi di espellere la pagina ferma da più tempo, ed è eccellente, ma realizzarla alla lettera costa caro, perché servirebbe una lista aggiornata a ogni singolo accesso alla memoria, oppure un contatore a sessantaquattro bit incrementato a ogni istruzione e scritto nella riga di ogni pagina toccata, con una scansione completa della tabella a ogni fallimento, hardware possibile in teoria che pochissime macchine hanno mai posseduto.
Nella pratica si usano approssimazioni furbe, e la prima è l'algoritmo NFU, sigla di not frequently used, che tiene un contatore software per pagina e a ogni scatto d'orologio vi somma il bit di riferimento, espellendo alla fine la pagina col contatore più basso, trucco che però soffre della memoria dell'elefante, perché i contatori non dimenticano nulla e le pagine della prima fase di un compilatore restano in vetta anche quando quella fase è finita da un pezzo; la correzione si chiama invecchiamento, e consiste nello scorrere i contatori verso destra di un bit a ogni scatto e nell'aggiungere il bit di riferimento a sinistra, così gli usi recenti pesano più degli usi antichi, e con otto bit, che coprono circa centosessanta millisecondi di storia, la memoria torna a dimenticare con grazia.
Gli altri algoritmi pratici si dispongono attorno a questo tema come varianti, e tutti si possono raccontare in prosa; l'algoritmo NRU divide le pagine in quattro classi secondo i bit di riferimento e di modifica ed espelle a caso dalla classe più bassa non vuota, la politica FIFO, primo entrato primo uscito, espelle la pagina caricata da più tempo come un supermercato che togliesse dagli scaffali il prodotto in vendita da più anni, con il rischio di disfarsi della farina solo perché vecchia, la seconda chance ispeziona il bit di riferimento della più vecchia e le concede la grazia se è stata usata di recente, l'algoritmo dell'orologio dispone i blocchi in cerchio con una lancetta che gira e ferma la pagina il cui bit di riferimento è a zero, e il WSClock, che fonde orologio e insieme di lavoro, è oggi fra i più usati nei sistemi reali.
Il paragone che fa respirare tutti questi algoritmi è la libreria di casa, dove i libri del lavoro corrente stanno sulla scrivania, quelli dell'anno scorso sullo scaffale, quelli intonsi da un decennio in cantina, e quando lo spazio manca a chi va prima in cantina, poi tra i libri fermi da più tempo sullo scaffale, mai tra quelli che tieni in mano in questi giorni; l'algoritmo LRU dice che il libro che ti servirà domani è quasi certamente uno di quelli che hai aperto ieri, l'invecchiamento dice che conta più l'ultima settimana che l'ultimo decennio, e la seconda chance è il gesto di togliere un volume dallo scaffale, accorgersi che ha la pagina segnata, e rimetterlo in fondo alla fila.
Per capire quanti libri meritano la scrivania serve un concetto che Denning ha chiamato working set, insieme di lavoro, cioè l'insieme delle pagine che un processo sta usando davvero in un certo periodo, definibile come le pagine toccate dagli ultimi accessi alla memoria o, nella pratica, quelle toccate negli ultimi decimi di secondo di tempo virtuale, contando solo il tempo in cui il processo ha avuto il processore; i programmi mostrano una località di riferimento, con le fasi di calcolo che toccano sempre le stesse poche pagine, come le passate di un compilatore che lavorano ognuna sulla sua porzione di dati, e se l'intero working set sta in memoria il processo corre quasi senza fallimenti finché la fase cambia.
Se invece il working set non ci sta, il processo cade nel thrashing, il battimento frenetico, e allora il disastro si racconta bene con lo studente che sfoglia troppo in fretta, immagina un ragazzo il cui banco sia così piccolo che i quaderni della materia in corso non ci stanno tutti, e che a ogni riga di appunti debba scendere in cantina a prendere il quaderno della pagina successiva, rimettendo giù quello appena usato; l'intero pomeriggio passa per le scale e pochissime righe arrivano davvero sul quaderno, perché eseguire un'istruzione costa nanosecondi mentre il viaggio in cantina ne costa milioni, e un programma che fallisce ogni poche istruzioni marcia a un paio d'istruzioni ogni dieci millisecondi, un ritmo che trasforma il computer più veloce del mondo in un corriere perpetuo.
Quando la somma dei working set di tutti i processi supera la memoria, nessun algoritmo di sostituzione e nessuna politica di assegnazione salvano la situazione, e la medicina si chiama controllo del carico, cioè l'allontanamento temporaneo di processi interi sul disco, con le loro pagine e i loro blocchi, finché i rimanenti respirano; per ridurre i danni quando un processo ritorna, molti sistemi praticano anche il prepaging, portando in memoria tutto l'insieme di lavoro prima di rimettere il processo in corsa, così da evitare la scossa iniziale di fallimenti che altrimenti accompagnerebbe ogni ritorno.
La paginazione premia anche l'economia, perché in un sistema con tanti utenti è normale che più processi eseguano lo stesso programma o le stesse librerie, e tenere in memoria due copie identiche di pagine di sola lettura sarebbe uno spreco, così i processi possono condividere le pagine del codice, ciascuno con la sua tabella che punta agli stessi blocchi fisici; serve cura amministrativa, dato che se un processo termina bisogna scoprire se le pagine sono ancora in uso prima di liberarle, e servono strutture dati che conteggino i prestiti, ma il risparmio è immenso.
La condivisione più quotidiana è quella delle librerie condivise, chiamate DLL, sigla di dynamic link library, in ambiente Windows, dove invece di copiare dentro ogni eseguibile i megabyte delle librerie grafiche e di sistema, il collegatore inserisce un piccolo delegato che lega la chiamata alla libreria vera al momento dell'esecuzione; la libreria viene caricata una volta sola, pagina per pagina e solo se e quando serve, e la sorpresa è che tutti i programmi che la usano ricevono gratis la correzione di un errore, perché sostituire il file della libreria con una versione aggiustata basta a guarire ogni programma al prossimo avvio; perché la libreria possa stare a indirizzi diversi nei processi diversi va compilata con indirizzi relativi, codice cioè indipendente dalla posizione, come un mobile su ruote che si adatta a ogni stanza.
Il trucco più elegante della famiglia è però la scrittura su copia, il copy-on-write, e il suo teatro è la chiamata di sistema che crea un nuovo processo, che in UNIX si chiama fork, dopo la quale padre e figlio condividono tutte le pagine marcate come sola lettura; finché entrambi si limitano a leggere la condivisione dura e costa zero copie, ma appena uno dei due tenta di scrivere la protezione scatta, il sistema intercetta la trappola, copia solo la pagina colpevole e concede a ciascuno dei due la sua copia privata in lettura e scrittura, e il paragone è la fotocopia che si fa solo quando serve, perché dieci colleghi possono leggere lo stesso modulo appeso alla bacheca senza problemi, e il giorno in cui uno deve appuntarci le sue correzioni quel collega copia il solo foglio che deve annotare e scrive sulla copia, mentre gli altri nove continuano a leggere l'originale; grazie a questo rinvio le pagine mai modificate, che sono di solito tutte le pagine di codice, non vengono mai copiate, e la creazione di un processo, che sembrava dovesse duplicare una montagna di byte, finisce per costare quasi nulla.
Vediamo ora come questa architettura vive nei sistemi operativi veri, e il capitolo del libro distingue quattro momenti in cui il sistema tocca la paginazione; quando un processo nasce, il sistema crea e inizializza la sua tabella, gli riserva spazio nell'area di scambio sul disco e prepara le copie di testo e dati da cui attingere, spesso usando direttamente il file eseguibile come magazzino; quando un processo viene scelto per correre, la MMU viene riprogrammata con la sua tabella e la TLB viene svuotata dei ricordi del processo precedente; quando arriva un fallimento, il sistema individua l'indirizzo colpevole dai registri, verifica che l'accesso sia legittimo, trova o libera un blocco, ordina la lettura dal disco, aggiorna la tabella e riporta il program counter all'istruzione fallita per rieseguirla; quando un processo muore, il sistema libera tabella, blocchi e spazio sul disco, con la cautela di controllare che nessuna pagina condivisa sia ancora in prestito.
Due dettagli di mestiere meritano una menzione, perché mostrano dove la pagina si intreccia col resto della macchina; il primo è che un'istruzione può fallire a metà, dopo aver letto un operando e prima dell'altro, e riavviarla richiede di sapere dove è cominciata, problema che alcune CPU risolvono con un registro segreto che copia il program counter appena prima di ogni istruzione, lasciando alle altre il compito di rammendare; il secondo è che le pagine usate per i trasferimenti diretti in memoria dai dispositivi vanno bloccate, inchiodate, perché se il blocco venisse assegnato a un'altra pagina mentre il controllore scrive, metà dei dati finirebbe dove deve e metà sopra un ospite indifeso.
Nei sistemi che usi ogni giorno queste scelte si concretizzano in due filosofie vicine ma riconoscibili; Linux preferisce una partizione di swap grezza, un'area del disco senza file system dove gli indirizzi sono semplici numeri di blocco, e tiene traccia dell'area di scambio di ogni processo nella sua tabella di processo, mentre Windows usa uno o più file preallocati dentro il file system normale, il famoso pagefile.sys, e in entrambi i casi il testo dei programmi, che è di sola lettura, può essere semplicemente abbandonato quando serve spazio e riletto dal file eseguibile alla prossima richiesta, con le librerie condivise che godono della stessa cortesia.
I progettisti affrontano infine la domanda architetturale di chi debba decidere, e una risposta storica affascinante è quella del sistema Mach, che divise la gestione della memoria fra un gestore di basso livello legato all'hardware, un gestore dei fallimenti nel cuore del sistema e un paginatore esterno che girava come un processo qualunque, separando la politica dal meccanismo a prezzo di qualche attraversamento di confine; sulla storia dei modelli di memoria resta la nota di fine capitolo, con MULTICS che aveva inventato segmenti paginati e la prima TLB, l'x86 che li portò avanti per trent'anni, e nessun sistema moderno che li usasse davvero, tant'è che i processori a sessantaquattro bit hanno tolto la segmentazione vera, e oggi i sistemi reali funzionano tutti con paginazione semplice e indirizzi piatti.
Che senso ha tutto questo per chi vuole capire la propria macchina e un giorno costruirsela da solo; il senso è che la memoria virtuale è l'astrazione che fa credere a ogni processo di abitare una memoria grande, intera e privata, mentre la verità è fatta di pezzi sparsi, pagine in prestito, copie condivise e scaffali sul disco, e questa illusione ben tenuta permette insieme la protezione reciproca, la condivisione parsimoniosa e il multitasking che danno alla macchina la sua voce; capire la paginazione significa capire come poche pagine di silicio fingano di essere un mondo intero per ciascun programma, ed è il pezzo di ingegneria che, dopo il processo, più di ogni altro rende possibile il sistema operativo moderno.
Ora tocca a te, perché questa pagina si capisce davvero soltanto vivendola sul proprio computer, quindi appendi l'immagine del disegnatore alla parete della mente e prova questi esperimenti con calma, tutti al sicuro dentro i tuoi permessi, ognuno dei quali tocca con mano un pezzo del meccanismo che abbiamo raccontato.
Per cominciare, guarda lo stato della memoria del tuo sistema; su Linux apri il terminale
e scrivi free -h, e leggi la riga della memoria con la sua colonna totale, quella
usata e quella libera, poi guarda anche la riga dello swap, che è il magazzino, e confronta i
numeri con quello che credevi di avere installato; su Windows apri il Task Manager, entra
nella scheda Prestazioni e scegli Memoria, dove vedrai la memoria fisica in uso, quella
disponibile e in basso la dimensione del file di paging, cioè il magazzino di Windows, e
annota i tre numeri prima di aprire altro.
Poi metti sotto osservazione i fallimenti di pagina; su Linux scrivi ps -o pid,comm,minflt,majflt per vedere ogni processo con i suoi fallimenti minori, quelli
risolti senza viaggi in cantina, e i suoi fallimenti maggiori, quelli che hanno chiesto il
disco, poi apri un programma pesante come un browser con tante schede e riesegui lo
stesso comando, osservando come i fallimenti maggiori salgano proprio nei primi istanti di
vita di un processo, e con vmstat 1 puoi persino contare i viaggi di andata e
ritorno dal magazzino nelle colonne si e so; su Windows apri il Monitor risorse e osserva
la colonna degli errori di pagina degli processi mentre apri e chiudi i programmi.
Se vuoi sentire il brivido del thrashing senza far male a nulla, prova a mettere sotto
pressione la memoria; su Linux, dopo aver installato stress-ng con il gestore dei pacchetti
della tua distribuzione, scrivi stress-ng --vm 2 --vm-bytes 2G --timeout 60s e
tieni d'occhio vmstat 1, perché con due processi che pretendono due gigabyte
ciascuno le colonne si e so schizzeranno in alto e la macchina diventerà appiccicosa per
qualche secondo, esattamente lo studente sulle scale di cui sopra, e allo scadere dei
sessanta secondi tutto tornerà normale, quindi salva prima il lavoro aperto e non aver
paura, il capitolo ha già fatto questa esperienza per te.
Osserva poi dove sta il magazzino e qualche manopola di regia; su Linux swapon --show elenca le partizioni e i file usati come swap, e cat /proc/sys/vm/overcommit_memory rivela quanto il kernel sia generoso nel promettere
memoria che non c'è ancora, mentre grep -i huge /proc/meminfo mostra le
pagine grandi, quelle pensate per risparmiare spazio nella TLB, la stessa economia che il
capitolo racconta quando discute la dimensione delle pagine; su Windows il file di paging
vive nella radice del disco di sistema, e la sua dimensione si regola nelle impostazioni di
prestazioni sotto memoria virtuale, e confrontando le due filosofie ritrovi proprio la scelta
del capitolo, la partizione grezza da una parte e i file preallocati dentro il file system
dall'altra.
Chiudi il giro con due esercizi più ragionati; il primo usa pmap -x seguito dal
numero di un processo qualsiasi, che trovi con ps, e ti mostra la sua memoria
virtuale enorme accanto alla residente ben più piccola, il tavolo da disegno accanto al
foglio del disegnatore, e top nelle sue colonne VIRT e RES racconta la stessa
cosa per tutti i processi insieme; il secondo si fa con carta e penna, inventando una
macchina a sedici bit con pagine da quattro kilobyte e trentadue kilobyte di memoria fisica,
compilando una tabella di sedici righe per otto blocchi come quella del libro, traducendo a
mano gli indirizzi zero, ottomilanuncentonovantadue e trentaduemilasettecentoottanta, e
decidendo poi da solo quale pagina espelleresti e perché, come farebbe un algoritmo vero.
Il modulo attinge alla parte centrale del capitolo dedicato alla gestione della memoria del libro Modern Operating Systems di Andrew S. Tanenbaum e Herbert Bos, precisamente alle sezioni dalla memoria virtuale fino al riepilogo finale, passando per gli algoritmi di sostituzione delle pagine, l'insieme di lavoro e le questioni di implementazione con le note su Linux e Windows, e trovi il volume nella cartella books/ col nome Modern Operating Systems 4th Edition--Andrew Tanenbaum.pdf, dove le figure della tabella delle pagine e dell'orologio meritano di essere studiate con la calma che abbiamo cercato di tenere anche qui.
La memoria che hai imparato a spartire e proteggere in questa pagina svanisce però nel silenzio appena togli la corrente, perché il tavolo del disegnatore, per quanto ben organizzato, non custodisce nulla di definitivo, ed è giunto il momento di salire al piano dove la memoria diventa permanente e i documenti trovano la loro dimora stabile, continua verso la pagina successiva, dove i file e i file system ti aspettano con le loro cartelle, i loro nomi e le loro gerarchie.