Come fa il computer a evitare che i processi si intralcino tra loro?

Lo scheduler del kernel si prende cura di dividere i processi nel tempo. Il proprio sistema operativo deve dividerli anche riguardo lo spazio, per evitare che non sconfinino oltre la porzione di memoria di lavoro loro assegnata. Anche se si assume che tutti i programmi provino ad essere cooperativi, non si vorrebbe che un bug in uno di loro sia capace di corrompere anche gli altri. Le operazioni compiute dal proprio sistema operativo per risolvere questo problema sono chiamate gestione della memoria.

Ogni processo del vostro repertorio ha la propria area di memoria, come luogo dal quale eseguire il proprio codice e dove immagazzinare le variabili e i risultati. Potete pensare a questo insieme come formato da un segmento di codice, di sola lettura (che contiene le istruzioni del processo), e da un segmento dati scrivibile (che contiene tutte le variabili immagazzinate dal processo). Il segmento dati è sempre unico per ogni processo, mentre nel caso due processi usino lo stesso codice Unix automaticamente fa in modo che condividano un unico segmento codice, come misura di efficienza.

Memoria virtuale: la versione semplice

L'efficienza è importante, perché la memoria è costosa. A volte non ce n'è abbastanza per contenere per intero tutti i programmi che il computer sta eseguendo, specialmente se si usa un grosso programma come un server X. Per ovviare a questo problema, Unix usa una tecnica chiamata memoria virtuale. Non cerca di tenere in memoria tutto il codice e i dati di un processo. Tiene piuttosto caricato solo un working set relativamente piccolo; il resto dello stato del processo viene lasciato in una speciale area di spazio swapsul proprio disco fisso.

Notare che in passato quel A volte dell'ultimo paragrafo era un Quasi sempre, perchè la dimensione della memoria era tipicamente ridotta rispetto alla dimensione dei programmi in esecuzione, quindi il ricorso allo swap era frequente. Oggi la memoria è molto meno costosa e persino i computer di fascia bassa ne hanno molta. Sui moderni computer monoutente con 64MB di memoria e oltre è possibile eseguire X e un insieme tipico di programmi senza neppure ricorrere allo swap dopo che sono stati inizialmente caricati nel core.

Memoria virtuale: la versione dettagliata

In realtà, nella precedente sezione le cose sono state un po' semplificate. Certo, i programmi vedono la maggior parte della propria memoria come un unico grande banco opaco di indirizzi più grande della memoria fisica, e lo swap su disco è usato per mantenere questa illusione. Ma il proprio hardware in realtà contiene almeno cinque tipi differenti di memoria, e le differenze tra loro possono avere una grande importanza quando i programmi devono essere ottimizzati per la massima velocità. Per capire realmente quello che succede all'interno del proprio computer, si dovrebbe imparare come funzionano tutte queste memorie.

I cinque tipi di memoria sono questi: registri del processore, cache interna (o su chip), cache esterna (o fuori dal chip), memoria principale, e disco. Il motivo per cui ci sono così tanti tipi di memoria è semplice; la velocità costa denaro. Ho elencato questi tipi di memoria in ordine crescente rispetto al tempo di accesso e ordine decrescente di costo. La memoria del registro è la più veloce e la più costosa e può effetuare circa un bilione di accessi casuali al secondo, mentre il disco è la memoria più lenta e di bassa qualità e può effettuare 100 accessi casuali al secondo.

Di seguito c'è un elenco completo che riflette le velocità per un computer desktop tipico precedente agli anni 2000. Mentre la velocità e la capacità aumenteranno e il prezzo diminuirà, ci si può aspettare che questi rapporti rimangano abbastanza costanti — e sono questi rapporti che formano la gerarchia della memoria.

Disco

Dimensione: 13000MB Accesso: 100KB/sec

Memoria principale

Dimensione: 256MB Accesso: 100M/sec

Cache esterna

Dimensione: 512KB Accesso: 250M/sec

Cache interna

Dimensione: 32KB Accesso: 500M/sec

Processore

Dimensione: 28 bytes Accesso: 1000M/sec

Non possiamo produrre tutto con i tipi di memoria più veloci. Potrebbe essere un modo troppo dispendioso; e anche se si potesse, la memoria veloce è volatile. Ciò significa che abbandona i propri dati quando si spegne il computer. Così i computer devono avere i dischi rigidi o altri tipi di memoria non volatile che mantengono i dati quando si spegne l'alimentazione. E qui c'è un enorme disparità tra la velocità del processore e la velocità dei dischi. Nel mezzo della gerachia dei tre livelli di memoria (cache interna, cache esterna, e memoria principale) fondamentalmente esiste per superare questo dislivello.

Linux e gli altri Unix hanno una caratteristica chiamata memoria virtuale. Questo significa che il sistema operativo si comporta come se disponesse di molta più memoria principale ripetto a quella che ha realmente. La propria memoria fisica principale reale si comporta come un insieme impostato di finestre o cache su di uno spazio molto ampio di memoria "virtuale", la maggior parte della quale in qualsiasi momento è attualmente immagazzinata sul disco in una zona speciale chiamata l'area di swap. Non visibile dai programmi utente, il SO muove blocchi di dati (chiamati "pagine") tra la memoria e il disco per mantenere questa illusione. Il risultato finale è che la propria memoria virtuale è più grande ma non così più lenta rispetto alla memoria reale.

La maggiore o minore lentezza della memoria virtuale rispetto a quella fisica dipende da come gli algoritmi dello swapping del sistema operativo corrispondono al modo in cui i programmi usano la memoria virtuale. Fortunatamente, le letture e scritture in memoria che sono vicine nel tempo tendono anche a raggrupparsi nello spazio di memoria. Questa tendenza è chiamata località, o più formalmente località di riferimento — ed è una cosa buona. Se i riferimenti di memoria fossero allocati nello spazio virtuale in modo casuale, si dovrebbe effettuare una'operazione di lettura e scrittura sul disco per ogni nuovo riferimento e la memoria virtuale potrebbe essere lenta come una memoria su disco. Ma siccome i programmi hanno effettivamente una forte località, il sistema operativo può svolgere relativamente pochi swap per ogni riferimento.

È stato trovato per esperienza che il metodo più efficace per un'ampia classe di modelli per l'uso della memoria è molto semplice; è chiamato algoritmo LRU o least recently used (usato meno di recente) . Il sistema della memoria virtuale prende i blocchi del disco dentro i suoi working set secondo le sue necessità. Quando esso è eseguito fuori dalla memoria fisica per il working set, scarica il blocco usato meno di recente. Tutti gli Unix, e la maggior parte degli altri sistemi operativi aventi la memoria virtuale, usano variazioni minori sull'LRU.

La memoria virtuale è il primo anello di congiunzione tra la velocità del disco e quella del processore. Essa è gestita esplicitamente dal SO. Ma c'è ancora un divario considerevole tra la velocità della memoria fisica principale e la velocità a cui un processore può accedere ai suo registro di memoria. La cache esterna e quella interna si occupano di questo, usando una tecnica simile a quella della memoria virtuale come ho descritto precedentemente.

Così come la memoria fisica principale si comporta come un insieme di finestre o cache sull'area swap del disco, allo stesso modo la cache esterna agisce come delle finestre sulla memoria principale. La cache esterna è veloce (250M accessi per sec, invece di 100M) e più piccola. L'hardware (specificatamente, il controller della memoria del proprio computer) fa l'operazione LRU nella cache esterna su blocchi di dati scaricati dalla memoria principale. Per ragioni storiche, l'unità della cache swapping è chiamata linea invece che pagina.

Ma questo non è tutto. La cache interna dà l'incremento definitivo della velocità effettiva attraverso porzioni di memoria della cache esterna. Essa è ancora più rapida e piccola — infatti, risiede sul chip del processore.

Se si vogliono creare dei programmi realmente veloci, è utile conoscere questi dettagli. I propri programmi diventano più veloci quando hanno una forte località di referenza dato che questo permette un miglior funzionamento della cache. Tuttavia il modo più facile di rendere i programmi più veloci è quello di renderli più piccoli. Se un programma non è rallentato da una gran quantità di I/O del disco o aspetta degli eventi della rete, normalmente verrà eseguito alla velocità della cache più piccola nella quale il programma può risiedere.

Se non è possibile ridimensionare il programma, qualche sforzo per adattare le porzioni che dipendono criticamente dalla velocità in modo che abbiano una forte località può dare buoni risultati. I dettagli sulle tecniche per realizzare questo adattamento vanno oltre lo scopo di questo tutorial; nel momento in cui se ne avrà bisogno, si avrà preso abbastanza confidenza con qualche compilatore per scoprirne molti da soli.

Unità della gestione della memoria

Anche quando si dispone di una memoria fisica abbastanza grande da evitare lo swapping, la parte del sistema operativo chiamata gestore della memoria mantiene un importante ruolo da svolgere. Deve garantire che i programmi possano modificare soltanto il proprio segmento dati; deve cioè impedire che del codice difettoso o malizioso in un programma rovini i dati di altri programmi. A questo scopo tiene una tabella dei segmenti di dati e di codice. La tabella è aggiornata non appena un processo richiede più memoria oppure libera memoria (quest'ultimo caso si verifica di solito all'uscita dal programma).

Questa tabella è usata per passare comandi a una parte specializzata dell'hardware sottostante chiamata MMU o unità di gestione della memoria. I chip dei processori moderni hanno MMU incorporate. La MMU ha la capacità speciale di porre dei delimitatori attorno alle aree di memoria, in modo che un riferimento che sconfina venga rifiutato e faccia scattare uno speciale interrupt.

Se non si ha mai visto un messaggio di Unix del tipo Segmentation fault, core dumped o qualcosa di simile, questo è esattamente quello che è successo; un tentativo da parte del programma in esecuzione di accedere alla memoria (core) al di fuori dal proprio segmento ha fatto scattare un interrupt fatale. Questo rivela un bug nel codice del programma; il core dump si trascina dietro costituisce una informazione diagnostica che ha lo scopo di aiutare il programmatore nell'individuazione del problema.

C'è un altro modo per proteggere i processi l'uno dall'altro, oltre alla limitazione della memoria a cui possono accedere. Si dovrebbe anche poter controllare il loro accesso ai file in modo che un programma difettoso o maligno non possa rovinare parti critiche del sistema. È per questo motivo che Unix ha i permessi sui file che vedremo in dettaglio in seguito.