INDIRIZZAMENTO DELLA MEMORIA

Transcript

INDIRIZZAMENTO DELLA MEMORIA
INDIRIZZAMENTO DELLA MEMORIA
I programmatori si riferiscono all’indirizzo di memoria come al modo di accedere al contenuto di una cella
di memoria. Quando si tratta di processori 80x86, dobbiamo distinguere tre tipi di indirizzo:
Indirizzo logico – usato nelle istruzioni in linguaggio macchina per specificare l’indirizzo di un
operando o di una istruzione. Questo tipo rappresenta la base dell’architettura a segmenti del
80x86 che costringe i programmatori MS-DOS e Windows a dividere i programmi in segmenti .
Ogni indirizzo logico è formato da un segmento e da un offset (o spiazzamento) che indica la
distanza tra l’inizio del segmento e la posizione attuale.
Indirizzo lineare o virtuale – un intero a 32 bit senza segno che può indirizzare fino a 4 GB di
locazioni di memoria. Viene di solito rappresentato in notazione esadecimale; l'intervallo di
valori va da 0x00000000 a 0xffffffff.
Indirizzo fisico – usato per indirizzare le celle nei chip di memoria. Corrisponde al segnale
elettrico inviato dai contatti del microprocessore al bus di memoria. Viene rappresentato da un
intero senza segno a 32 o 36 bit.
L’unità di gestione della memoria (Memory Management Unit – MMU) trasforma un indirizzo logico in uno
lineare per mezzo di un circuito hardware chiamato unità di segmentazione; un secondo circuito chiamato
unità di paginazione trasforma l’indirizzo lineare in fisico.
Nei sistemi multiprocessore, tutte le CPU condividono di solito la stessa memoria; ciò significa che ai chip di
RAM possono accedere contemporaneamente e in modo indipendente. Poiché le operazioni di lettura e
scrittura su un chip devono avvenire in modo seriale, un circuito chiamato arbitro della memoria è inserito
fra il bus ed ogni chip di RAM. Il suo compito è garantire l’accesso al chip se libero e rinviarlo se occupato
dalla richiesta di un’altra CPU. Anche i sistemi uniprocessore usano l’arbitro della memoria, poiché
includono processori specializzati, i controller DMA, che operano contemporaneamente alla CPU. Nel caso
dei sistemi multiprocessore, la struttura dell’arbitro è più complessa a causa del maggior numero di
ingressi. Il Pentium dual-core, ad esempio, ha un arbitro con due porte di ingresso per ogni chip e richiede
messaggi di sincronizzazione tra le due CPU prima del tentativo di uso del bus in comune. Dal punto di vista
del programmatore, la gestione dell’arbitro è trasparente perché viene svolta da circuiti hardware.
Segmentazione hardware
A partire dal 386, i microprocessori Intel compiono la traduzione degli indirizzi in due modi diversi, in
modalità reale e protetta. Nel prossimo paragrafo verrà trattata la modalità protetta. La modalità reale
esiste principalmente per mantenere la compatibilità con i modelli più vecchi e per consentire l’avviamento
del sistema operativo (bootstrap).
Selettori e registri di segmento
Selettore di segmento
indice
15
TI = Table Indicator
TI
3
RPL
2
RPL= Requestor Privilege Level
1
0 bit
Un indirizzo logico è diviso in due parti: un identificatore di segmento e un offset che indica la posizione
relativa entro il segmento. L’identificatore è un campo di 16 bit chiamato selettore di segmento, mentre
l’offset è un campo di 32 bit. Il selettore viene descritto più avanti.
Per ottenere più velocemente il selettore di segmento, il processore fornisce registri di segmento, il cui
scopo è di memorizzare il selettore di segmento: sono i registri cs, ss, ds, es, fs, gs. Anche se sono solo sei,
un programma può utilizzarli per scopi differenti, salvandone il contenuto in memoria per poi memorizzarlo
di nuovo.
Tre dei sei registri hanno finalità specifiche:
cs – registro del segmento del codice, punta al segmento che contiene le istruzioni del programma
ss – registro del segmento dello stack, punta al segmento che contiene lo stack del programma corrente
ds – registro del segmento dei dati, punta al segmento che contiene i dati globali e statici.
Gli altri tre registri sono di uso generale e possono riferirsi a segmenti generici. Il registro cs ha un’altra
importante funzione: include un campo di 2 bit che specifica il Livello di Privilegio Corrente (Current
Privilege Level – CPL) della CPU. Il valore 0 indica il privilegio maggiore, mentre 3 quello inferiore. Linux usa
solo i livello 0 e 3, che corrispondono rispettivamente alla modalità del kernel (Kernel Mode) e alla modalità
utente (User Mode).
Descrittori di segmento
Ogni segmento è rappresentato da un Descrittore di Segmento di 8 byte che ne descrive le caratteristiche. I
descrittori sono memorizzati nella Global Descriptor Table (GDT) o nella Local Descriptor Table (LDT).
Descrittore di segmento dati
Base(24-31)
G
B
0
A
V
L
Limit(16-19)
1
D
P
L
S
=
1
Type
Base (16-23)
Base(0-15)
Limit(0-15)
1
D
P
L
S
=
1
Type
Base (16-23)
Base(0-15)
Limit(0-15)
D
P
L
S
=
0
Type
Base(16-23)
Base(0-15)
Limit(0-15)
Descrittore di segmento codice
Base(24-31)
G
D
0
A
V
L
Limit(16-19)
Descrittore di segmento di sistema
Base(24-31)
63
G
0
Limit(16-19)
1
bit
0
Di solito esiste una sola GDT, mentre ogni processo può avere la propria LDT se ne ha bisogno per creare
altri segmenti oltre a quelli contenuti nella GDT. L’indirizzo e la dimensione della GDT e della LDT corrente
sono memorizzati nei registri di controllo gdtr e ldtr. Il significato dei campi dei descrittori è il seguente:
Base – contiene l’indirizzo lineare del primo byte del segmento.
2
G – flag di granularità; se è azzerato, la dimensione del segmento è espressa in byte, altrimenti è in multipli
di 4096 bytes.
Limit – contiene l’offset dell’ultima locazione di memoria del segmento; rappresenta la lunghezza del
segmento. Quando G = 0, la dimensione può andare da 1 byte a 1 MB; altrimenti può andare da 4 K a 4 GB.
S – flag di sistema; se è azzerato, indica un segmento di sistema che contiene strutture dati importanti
come la LDT; se è settato, indica un segmento codice o dati normali.
Type – caratterizza il tipo di segmento e i diritti di accesso
DPL – Livello di Privilegio del Descrittore (Descriptor Privilege Level): usato per limitare gli accessi al
segmento. Rappresenta il livello minimo di privilegio della CPU per accedere al segmento. Quindi un
segmento con DPL uguale a 0 è accessibile solo quando CPL è 0, cioè in modalità del kernel, mentre un
segmento con DPL uguale a 3 è accessibile con qualunque valore di CPL.
P – flag presente: è uguale a 0 se il segmento non è attualmente caricato in memoria. Linux imposta sempre
a 1 questo flag, perché non usa mai lo swapping su disco di un intero segmento.
D o B – a seconda che il segmento contenga codice o dati. Il significato è piuttosto diverso nei due casi, ma
il byte è impostato a 1 se gli indirizzi usati come offset sono di 32 bit, ed è azzerato se sono di 16 bit (per
maggiori dettagli consultare la documentazione Intel).
AVL – può essere usato dal sistema operativo, ma è ignorato da Linux.
Ci sono diversi tipi di segmenti e di conseguenza diversi tipi di Descrittori di Segmento. Di seguito sono
elencati quelli più usati da Linux.
Descrittore di Segmento Codice - si riferisce ad un segmento di codice; può essere incluso sia nella GDT che
nella LDT. Ha il flag S attivato (non è di sistema).
Descrittore di Segmento Dati – si riferisce ad un segmento dati; può essere incluso sia nella GDT che nella
LDT. Ha il flag S attivato. I segmenti che contengono lo stack sono implementati con questo tipo.
Descrittore di Segmento di Stato del Task (TSSD) – si riferisce a un Task State Segment (TSS), cioè a un
segmento usato per salvare il contenuto dei registri del processore; è presente solo nella GDT. Il valore del
campo Type è 11 o 9, a seconda se il processo corrispondente è attualmente in esecuzione su una CPU
oppure no. Il flag S è azzerato.
Descrittore di Tabella del Descrittore Locale (Local Descriptor Table Descriptor – LDTD) – si riferisce a un
segmento che contiene una LDT; può apparire solo nella GDT. Il campo Type ha valore 2. Il flag S è
azzerato. Di seguito viene spiegato come il processore 80x86 è in grado di decidere se un descrittore è
memorizzato nella GDT o nella LDT di un processo.
Accesso rapido ai descrittori di segmento
Gli indirizzi logici sono composti da un Selettore di Segmento a 16 bit e da un offset a 32 bit, e i registri di
segmento memorizzano solo il Selettore di Segmento. Per velocizzare la traduzione di indirizzi logici in
lineari, il processore 80x86 è dotato di un registro addizionale non programmabile – che non può essere
impostato dal programmatore – per ognuno dei sei registri di segmento programmabili. Ogni registro non
programmabile contiene gli 8 byte del Descrittore di Segmento corrispondente . Ogni volta che un Selettore
3
è caricato in un registro di segmento, il relativo Descrittore viene caricato nel registro non programmabile
corrispondente. Di conseguenza la traduzione degli indirizzi logici che si riferiscono a quel segmento può
avvenire senza accedere alla GDT o alla LDT presenti in memoria. L’accesso alle tabelle è necessario solo
quando cambia il contenuto dei registri di segmento.
Ogni Selettore di Segmento contiene questi tre campi:
index – identifica la entry del Descrittore nella GDT o nella LDT
TI – Indicatore di Tabella (Table Indicator): specifica se il Descrittore è incluso nella GDT (TI = 0) o nella LDT
(TI = 1).
RPL – Requestor Privilege Level: specifica il CPL della CPU quando il selettore è caricato nel registro cs; può
essere usato per ridurre selettivamente il livello di privilegio quando si accede ai segmenti dati (per
maggiori dettagli consultare la documentazione Intel).
Poiché un Descrittore di segmento è lungo 8 byte, il suo indirizzo relativo entro la GDT o la LDT viene
calcolato moltiplicando il campo indice del Selettore (13 byte) per 8. Per esempio, se la GDT si trova
all’indirizzo 0x00020000 (valore memorizzato nel registro gdtr) e l'indice vale 2, l’indirizzo del descrittore
corrispondente sarà 0x00020000 + (2 x 8), ossia 0x00020010.
La prima entry della GDT ha sempre valore 0. Questo assicura che l’indirizzo logico con un Selettore di
Segmento nullo viene considerato non valido, causando così un’eccezione del processore. Il numero
massimo di descrittori nella GDT è 8.191 (2 13 – 1).
Unità di segmentazione
Per tradurre un indirizzo logico nel corrispondente indirizzo lineare, l’unità di segmentazione compie le
seguenti operazioni:
1 - esamina il campo TI del Selettore per determinare quale tabella contiene il Descrittore. Questo campo
indica se esso è presente nella GDT (in questo caso l’unità di segmentazione ottiene l’indirizzo lineare di
base della GDT dal registro gdtr) o nella LDT attiva (in questo caso l’indirizzo è ottenuto dal registro ldtr);
2 -calcola l’indirizzo del Descrittore in base al campo Indice del selettore. L’Indice viene moltiplicato per 8
(la dimensione del Descrittore) e il risultato viene aggiunto al contenuto del registro gdtr o ldtr;
3 -aggiunge l’offset al campo Base del Descrittore, ottenendo così l’indirizzo lineare.
Da notare che, grazie ai registri non programmabili associati a quelli di segmento, le prime due operazioni
sono necessarie solo se il registro di segmento ha cambiato valore.
Segmentazione in Linux
La segmentazione è stata introdotta nei microprocessori 80x86 per incoraggiare i programmatori a dividere
le loro applicazioni in entità correlate logicamente, come subroutine o aree dati globali e locali. Linux però
usa la segmentazione in modo molto limitato. Infatti segmentazione e paginazione sono in qualche modo
ridondanti, perché entrambe possono venire usate per separare lo spazio degli indirizzi fisici dei processi: la
segmentazione può assegnare un intervallo di indirizzi lineari differente ad ogni processo, mentre la
4
paginazione può mappare lo stesso intervallo di indirizzi lineari in spazi diversi di indirizzi fisici. Linux
preferisce la paginazione per questi motivi:
- la gestione della memoria è più semplice quando tutti i processi usano lo stesso valore dei registri di
segmento, cioè quando condividono lo stesso intervallo di indirizzi lineari;
- uno degli obiettivi di progetto di Linux è la portabilità in un vasto insieme di architetture; in particolare i
sistemi RISC hanno un limitato supporto alla segmentazione.
La versione 2.6 di Linux usa la segmentazione solo quando richiesto dall’architettura 80x86. Tutti i processi
in modalità utente impiegano lo stesso paio di segmenti per istruzioni e dati: user code segment e user
data segment rispettivamente. Allo stesso modo tutti i processi in modalità del kernel usano lo stesso paio
di segmenti codice e dati: kernel code segment e kernel data segment. La tabella riporta i valori dei campi
del Descrittore per questi segmenti:
Segmento
user code
user data
kernel code
kernel data
Base
0x00000000
0x00000000
0x00000000
0x00000000
G
1
1
1
1
Limit
0xfffff
0xfffff
0xfffff
0xfffff
S
1
1
1
1
Type
10
2
10
2
DPL
3
3
0
0
D/B
1
1
1
1
P
1
1
1
1
I Selettori di Segmento corrispondenti sono definiti dalle macro __USER_CS, __USER_DS, __KERNEL_CS,
__KERNEL_DS. Per indirizzare il segmento codice del kernel, viene caricato nel registro di segmento cs il
valore definito dalla macro __KERNEL_CS..
Da notare che tutti gli indirizzi associati ai segmenti in questione vanno da 0 al limite di 2 32 – 1. Questo
significa che tutti i processi, sia in modalità del kernel che in modalità utente, possono usare gli stessi
indirizzi logici.
Un’altra importante conseguenza è che in Linux gli indirizzi logici coincidono con quelli lineari; ciò significa
anche che il valore del campo offset di un indirizzo logico coincide sempre con il valore del corrispondente
indirizzo lineare.
Come detto in precedenza, il CPL del processore indica se esso si trova in modalità del kernel o in modalità
utente, e viene specificato dal campo RPL del Selettore di Segmento contenuto nel registro cs. Quando il
valore di CPL cambia, alcuni registri di segmento devono essere aggiornati. Per esempio, quando CPL = 3, ds
deve contenere il Selettore del segmento dati utente, ma quando CPL = 0 deve contenere il Selettore del
segmento dati del kernel. Lo stesso vale per ss, che deve riferirsi allo stack della modalità utente nel primo
caso, e allo stack della modalità del kernel nel secondo.
Quando salva un puntatore ad una istruzione o a una struttura dati, il kernel non ha bisogno di
memorizzare il Selettore dell’indirizzo logico, perché il registro ss contiene il Selettore di segmento
corrente. Ad esempio, quando il kernel invoca una funzione, esegue una istruzione call specificando solo il
componente offset del suo indirizzo logico; è implicitamente indicato che il Selettore è quello contenuto
nel registro cs. Poiché esiste un solo segmento del tipo “eseguibile in modalità kernel”, cioè __KERNEL_CS,
è sufficiente caricare questo valore in cs quando la CPU passa in modalità del kernel.
Oltre ai quattro descritti, Linux usa pochi altri segmenti specializzati che vengono descritti insieme alla GDT.
5
La GDT di Linux
In sistemi uniprocessore, c’è un’unica GDT, mentre quelli multiprocessore ne hanno una per ogni CPU.
Tutte le GDT sono contenute nell’array cpu_gdt_table, mentre gli indirizzi e le dimensioni delle stesse sono
memorizzate nell’array cpu_gdt_descr. Questi simboli sono definiti nel file arch/i386/kernel/head.S dei
sorgenti del kernel.
GDT Linux
null
reserved
reserved
reserved
not used
not used
TLS#1
TLS#2
TLS#3
reserved
reserved
reserved
kernel code
kernel data
user code
user data
0x0
0x33
0x3b
0x43
0x60 __KERNEL_CS
0x68 __KERNEL_DS
0x73 __USER_CS
0x7b __USER_DS
TSS
LDT
PNPBIOS 32 bit code
PNPBIOS 16 bit code
PNPBIOS 16 bit code
PNPBIOS 16 bit code
PNPBIOS 16 bit code
APMBIOS 32 bit code
APMBIOS 16 bit code
APMBIOS data
not used
not used
not used
not used
not used
Double fault TSS
0x80
0x88
0x90
0x98
0xa0
0xa8
0xb0
0xb8
0xc0
0xc8
0xf8
Dal file /arch/i386/kernel/head.S
/*
* The Global Descriptor Table contains 28
*/
.align PAGE_SIZE_asm
ENTRY(cpu_gdt_table)
.quad 0x0000000000000000
/* NULL
.quad 0x0000000000000000
/* 0x0b
.quad 0x0000000000000000
/* 0x13
.quad 0x0000000000000000
/* 0x1b
.quad 0x0000000000000000
/* 0x20
.quad 0x0000000000000000
/* 0x28
.quad 0x0000000000000000
/* 0x33
.quad 0x0000000000000000
/* 0x3b
.quad 0x0000000000000000
/* 0x43
.quad 0x0000000000000000
/* 0x4b
.quad 0x0000000000000000
/* 0x53
.quad 0x0000000000000000
/* 0x5b
.quad
.quad
.quad
.quad
0x00cf9a000000ffff
0x00cf92000000ffff
0x00cffa000000ffff
0x00cff2000000ffff
/*
/*
/*
/*
0x60
0x68
0x73
0x7b
6
quadwords, per-CPU.
descriptor */
reserved */
reserved */
reserved */
unused */
unused */
TLS entry 1 */
TLS entry 2 */
TLS entry 3 */
reserved */
reserved */
reserved */
kernel 4GB code at 0x00000000 */
kernel 4GB data at 0x00000000 */
user 4GB code at 0x00000000 */
user 4GB data at 0x00000000 */
.quad 0x0000000000000000
.quad 0x0000000000000000
/* 0x80 TSS descriptor */
/* 0x88 LDT descriptor */
/* Segments used for calling PnP BIOS */
.quad 0x00c09a0000000000
/* 0x90 32-bit code */
.quad 0x00809a0000000000
/* 0x98 16-bit code */
.quad 0x0080920000000000
/* 0xa0 16-bit data */
.quad 0x0080920000000000
/* 0xa8 16-bit data */
.quad 0x0080920000000000
/* 0xb0 16-bit data */
/*
* The APM segments have byte granularity and their
* and limits are set at run time.
*/
.quad 0x00409a0000000000
/* 0xb8 APM CS
code
.quad 0x00009a0000000000
/* 0xc0 APM CS 16 code
.quad 0x0040920000000000
/* 0xc8 APM DS
data
.quad
.quad
.quad
.quad
.quad
.quad
0x0000000000000000
0x0000000000000000
0x0000000000000000
0x0000000000000000
0x0000000000000000
0x0000000000000000
/*
/*
/*
/*
/*
/*
0xd0
0xd8
0xe0
0xe8
0xf0
0xf8
-
bases
*/
(16 bit) */
*/
unused */
unused */
unused */
unused */
unused */
GDT entry 31: double-fault TSS */
Ogni GDT include 18 descrittori di segmento e 14 entry nulle, non usate o riservate. Esse sono inserite per
fare si che i descrittori normalmente utilizzati insieme siano mantenuti nella stessa linea di 32 byte della
cache hardware.
- I 18 descrittori puntano ai seguenti segmenti:
- I 4 fondamentali descritti in precedenza
- Un Task State Segment (TSS) diverso per ogni processore. Lo spazio di indirizzi lineari di un TSS è un
piccolo sottoinsieme di quello del segmento dati del kernel. I TSS sono memorizzati in sequenza nell’array
init_tss; in particolare il campo Base del descrittore TSS per la CPU numero n punta alla posizione n
dell’array init_tss. Il campo G è azzerato e Limit vale 0xeb, per cui il segmento è lungo 236 bytes. Type vale
9 o 11 e DPL è posto a 0 poiché i processi in modalità utente non possono accedere ai TSS.
- Un segmento che contiene la LDT locale, di solito condivisa tra tutti i processi.
- 3 Thread-Local Storage (TLS): è un sistema che consente alle applicazioni multithread di impiegare fino a
tre segmenti di dati locali per ogni thread. Le chiamate di sistema set_thread_area() e get_thread_area()
creano e rilasciano i TLS per il processo in esecuzione.
- 3 segmenti connessi al sistema Advanced Power Management (APM): il codice del BIOS fa uso dei
segmenti, per cui quando il driver Linux APM invoca le funzioni del BIOS per interrogare o definire lo stato
dei dispositivi APM può fare uso degli appositi segmenti codice e dati.
-5 segmenti correlati ai servizi Plug and Play (PnP) del BIOS. Vale il caso precedente.
- Un segmento speciale TSS usato dal kernel per gestire le eccezioni “Double fault”.
7
Tutte le GDT dei sistemi multiprocessore hanno le medesime entry, tranne che in pochi casi. Ogni
processore ha un proprio TSS, per cui la relativa entry nella GDT differisce dalle altre. Inoltre alcune entry
possono dipendere dal processo che la CPU sta eseguendo (LDT e TLS). Infine in alcuni casi un processore
può modificare temporaneamente una entry nella propria copia della GDT, ad esempio quando chiama una
procedura APM del BIOS.
La LDT di Linux
La maggior parte delle applicazioni in modalità utente non usano la LDT, per cui il kernel ne definisce una
comune a tutti i processi, contenuta nell’array default_ldt. Questo array contiene 5 entry, ma solo 2 sono
effettivamente usate dal kernel: una call gate per gli eseguibili BCS e una per gli eseguibili Solaris/x86.
Le call gate sono metodi forniti dai processori 80x86 per cambiare il livello di privilegio della CPU quando
chiama una funzione predefinita; per i particolari consultare la documentazione Intel
In qualche caso però un processo può richiedere la propria LDT, ad esempio Wine che esegue applicazioni
Windows orientate ai segmenti. La chiamata di sistema modify_ldt() permette di fare ciò. Ogni LDT privata
richiede anche il proprio segmento; viene di conseguenza modificata la entry della LDT nella copia della
GDT che si riferisce alla CPU in questione.
Le applicazioni in modalità utente possono allocare nuovi segmenti per mezzo di modify_ldt(); il kernel
tuttavia non li utilizza mai e non tiene traccia dei corrispondenti Descrittori, poiché sono inclusi nella LDT
del processo.
Paginazione hardware
L’unità di paginazione traduce gli indirizzi lineari in indirizzi fisici. Uno dei compiti chiave dell’unità è di
controllare il tipo di accesso richiesto confrontandolo con i diritti di accesso dell’indirizzo. Se l'accesso alla
memoria non è valido, genera una eccezione di “Page Fault” (errore di pagina).
Per ragioni di efficienza gli indirizzi lineari sono raggruppati in intervalli di lunghezza fissa chiamati pagine:
indirizzi consecutivi appartenenti ad una pagina sono mappati in indirizzi fisici contigui. In questo modo il
kernel può definire l’indirizzo e i diritti di accesso di una pagina intera e non dei singoli indirizzi lineari. Il
termine “pagina” indica sia l’insieme di indirizzi lineari sia i dati contenuti in essi.
L’unità di paginazione considera la RAM suddivisa in frame di pagina di lunghezza fissa (a volte chiamati
pagine fisiche). Ogni frame contiene una pagina, per cui le loro dimensioni coincidono. Un frame è una
parte della memoria, quindi un’area di memorizzazione. E’ importante distinguere la pagina dal frame di
pagina: la prima è un blocco di dati che può essere memorizzato in un frame di pagina o su disco.
Le strutture che mappano gli indirizzi lineari in indirizzi fisici sono dette tabelle di pagina (page tables); si
trovano nella memoria principale e devono essere inizializzate dal kernel prima che venga abilitata l'unità
di paginazione. A partire dal 386, tutti i processori 80x86 supportano la paginazione; essa viene abilitata
attivando il flag PG del registro di controllo cr0. Quando PG = 0 gli indirizzi lineari sono interpretati come
indirizzi fisici.
Paginazione regolare
8
A partire dal 386, l’unità di paginazione dei processori Intel utilizza pagine di 4 KB.
I 32 bit di un indirizzo lineare sono divisi in tre campi:
Directory, i 10 bit più significativi
Table, i 10 intermedi
Offset, i 12 meno significativi.
La traduzione degli indirizzi lineari è realizzata in due fasi, ognuna basata su una tabella di traduzione. La
prima è chiamata Page Directory (PD), e la seconda Page Table (PT)1.
Obiettivo di questo schema a tre livelli è quello di ridurre la richiesta di RAM per la PT di ogni processo. Se
fosse usato un unico livello di tabelle, occorrerebbero fino a 2 20 entry (e con entry di 4 byte, 4 MB di RAM)
per rappresentare la PT di ogni processo, se ognuno usasse uno spazio di indirizzi di 4 GB, anche se non tutti
gli indirizzi nell’intervallo vengono usati. Lo schema a due livelli riduce l’impegno di memoria richiedendo la
PT solo per le regioni di memoria attualmente in uso.
Ogni processo in attività deve avere una PD, ma non è necessario allocare subito la RAM per tutte le PT, è
più efficiente allocare RAM per una PT solo quando il processo la richiede.
L’indirizzo fisico della PD è memorizzato nel registro di controllo cr3. Il campo Directory dell’indirizzo lineare
indica l’entry nella PD che punta alla PT appropriata. Il campo Table indica l’entry nella PT che contiene
l’indirizzo fisico del frame di pagina. Il campo Offset determina la posizione relativa entro il frame di pagina.
Poiché è lungo 12 bit, ogni pagina ha la dimensione di 4096 byte.
I campi Directory e Table sono entrambi di 10 bit, quindi PD e PT contengono fino a 1024 entry. Una PD può
indirizzare 1024 x 1024 x 4096 = 232 celle di memoria, come ci si può aspettare da indirizzi a 32 bit.
Le entry delle PD e PT hanno la stessa struttura e sono formate dai seguenti campi:
Flag Present: se è attivato, la pagina è contenuta nella memoria principale; se è 0, la pagina non è in
memoria e i rimanenti bit della entry possono essere usati dal sistema operativo per i propri scopi. Se la
entry della PD o della PT richiesta per la traduzione di un indirizzo ha il flag Present azzerato, l’unità di
paginazione memorizza l’indirizzo nel registro cr2 e genera l’eccezione 14: Page Fault.
Campo contenente i 20 bit più significativi dell’indirizzo fisico di un frame di pagina: poiché ogni frame di
pagina può contenere 4 KB, il suo indirizzo fisico deve essere un multiplo di 4096, per cui i 12 bit meno
significativi dell’indirizzo sono uguali a 0. Se il campo si riferisce a una PD, il frame contiene una PT; se si
riferisce a una PT, il frame contiene una pagina di dati.
Flag Accessed: è attivato ogni volta che l’unità di paginazione indirizza il frame di pagina corrispondente.
Questo flag può essere usato dal sistema operativo per lo swapping delle pagine. L’unità di paginazione non
resetta mai questo flag; lo deve fare il sistema operativo.
Flag Dirty: si applica solo alle entry della PT. Viene attivato ogni volta che è compiuta una operazione di
scrittura sul frame di pagina. Come per il flag Accessed, può essere usato dal sistema operativo per lo
swapping delle pagine. L’unità di paginazione non resetta mai questo flag; lo deve fare il sistema operativo.
Flag Read/Write: contiene i diritti di accesso (lettura/scrittura o lettura) della pagina o della PT.
1 Il termine page table minuscolo identifica ogni pagina che contiene la mappatura di indirizzi lineari in fisici, mentre il
termine Page Table in maiuscolo indica la pagina all’ultimo livello delle tabelle di pagina.
9
Flag User/Supervisor: contiene il livello di privilegio richiesto per accedere alla pagina o alla PT.
Flag PCD e PWT: controlla il modo in cui la pagina o la PT è gestita dalla cache hardware.
Flag Page Size: si applica solo alle entry della PD. Se è attivato, l’entry si riferisce a frame di pagina di 2 o 4
MB.
Flag Global: si applica solo alle entry di PT. Questo flag è stato introdotto nel Pentium Pro per evitare che
pagine usate di frequente vengano eliminate dalla cache TLB (Translation Lookaside Buffer). E’ attivo solo
se è settato il flag PGE (Page Global Enable) nel registro cr4
Paginazione estesa
A partire dal Pentium è stata introdotta la paginazione estesa che consente di avere pagine di 4 MB invece
che 4 KB. La paginazione estesa è usata per tradurre ampi intervalli di indirizzi lineari; in questi casi il kernel
può eseguire l’operazione senza l’intervento di PT, risparmiando così memoria e preservando le entry di
TLB.
La paginazione estesa è abilitata attivando il flag Page Size in una entry di PD. In questo caso l’unità di
paginazione divide i 32 bit di un indirizzo lineare in due campi:
Directory: i 10 bit più significativi
Offset: i restanti 22 bit.
Le entry della PD non cambiano rispetto alla paginazione normale eccetto che:
-
Il flag Page Size è settato
-
Solo i 10 bit più significativi dei 20 dell’indirizzo fisico hanno un significato. Questo perché ogni
indirizzo fisico è allineato a 4 MB, per cui i 22 bit meno significativi sono azzerati.
La paginazione estesa coesiste con quella regolare e viene abilitata settando il flag PSE del registro cr4.
Sistema di protezione hardware
L’unità di paginazione usa un sistema di protezione diverso dall’unità di segmentazione. Mentre i processori
80x86 ammettono 4 livelli di privilegio per un segmento, solo 2 sono associati alle pagine e alle PT, dato che
i privilegi sono controllati dal flag User/Supervisor. Quando il flag è azzerato, la pagina può essere
indirizzata solo se il CPL è inferiore a 3 (cioè il processore è in modalità del kernel). Quando il flag è
impostato a 1, la pagina può sempre essere indirizzata.
In più, invece dei tre diritti di accesso (lettura, scrittura ed esecuzione) associati ai segmenti, solo due
(lettura e scrittura) sono associati alle pagine. Se il flag Read/Write di una PD o PT è impostato a 0, la PT o la
pagina corrispondente possono essere solo lette; altrimenti la pagina può essere letta e scritta 2.
Un esempio di paginazione regolare
2 Il Pentium 4 ha un flag NX (No eXecute) in ogni entry di PT a 64 bit (la paginazione estesa deve essere abilitata. Linux
2.6.11 supporta questa caratteristica hardware).
10
Il kernel assegna ad un processo in esecuzione l’intervallo di indirizzi tra 0x20000000 e 0x2003ffff. Questo
spazio comprende 64 pagine. Non interessa l’indirizzo fisico dei frame di pagina che contengono le pagine;
alcune di esse possono non essere ancora in memoria. Interessano solo i restanti campi delle entry della PT.
I 10 bit più significativi sono interpretati dall’unità di paginazione come campi Directory. L’indirizzo inizia
con un 2 seguito da zeri, per cui i 10 bit hanno tutti lo stesso valore 0x80 (128 decimale). Il campo Directory
in tutti gli indirizzi punta alla 129° entry della PD del processo. Essa contiene l’indirizzo fisico della PT. Se al
processo non sono assegnati altri indirizzi, tutte le restanti 1023 entry della PD sono riempite con 0.
I valori assunti dai 10 bit intermedi (campo Table) vanno da 0 a 0x03f, cioè 63 decimale. Quindi solo le
prime 64 entry della PT sono valide; le restanti 960 sono riempite con 0.
Se il processo richiede di leggere l’indirizzo lineare 0x20021406:
-
Il campo Directory (0X080) viene usato per selezionare la entry 0x080 nella PD, che punta a PT;
-
Il campo Table (0x21) viene usato per selezionare la entry numero 0x21 nella PT che punta al
frame di pagina;
-
Il campo Offset (0x406) viene usato per selezionare il byte 0x406 nel frame di pagina.
Se il flag Present della 0x21esima entry della PT è azzerato, la pagina non è presente in memoria; in questo
caso l’unità di paginazione origina l’eccezione Page Fault. La stessa eccezione viene generata se il processo
tenta di accedere a un indirizzo al di fuori dell’intervallo 0x20000000 0x2003ffff, poiché le corrispondenti
entry della PT sono poste a 0 e il flag Present è azzerato.
Estensione degli indirizzi fisici (Physical Address Extension – PAE)
La quantità di memoria di un processore è limitata dal numero di pin di indirizzo connessi al bus di
memoria. I vecchi processori Intel, a partire dal 386, usano indirizzi fisici a 32 bit. In teoria su questi sistemi
si possono installare fino a 4 GB di memoria; in pratica, a causa delle richieste di spazio di indirizzamento
dei processi in modalità utente, il kernel può indirizzare direttamente solo 1 GB di RAM.
I server che gestiscono migliaia di processi contemporaneamente richiedono anche più di 4 GB di memoria,
e la spinta all’aumento della RAM è continua. Intel ha soddisfatto questa richiesta aumentando il numero di
pin di indirizzo nei processori da 32 a 36. A partire dal Pentium Pro ora i processori possono indirizzare fino
a 236 = 64 GB di RAM. Questo intervallo di indirizzi però può essere sfruttato solo introducendo un nuovo
meccanismo di paginazione che traduca gli indirizzi lineari a 32 bit in indirizzi fisici a 36 bit.
Con il Pentium Pro, Intel ha introdotto la Physical Address Extension (PAE). Un altro sistema, la Page Size
Extension (PSE-36) venne introdotta col Pentium III, ma Linux non lo usa.
PAE viene attivata impostando a 1 il flag PAE nel registro di controllo cr4. Il flag Page Size (PS) nella entry
della PD abilita il formato di pagina a 2 MB (quando PAE è attiva). Per supportare questo sistema Intel ha
cambiato il meccanismo di paginazione.
-
I 64 GB di RAM sono divisi in 2 24 frame di pagina e il campo indirizzo fisico della PT è stato
ampliato da 20 a 24 bit. Poiché una entry di PT in PAE deve includere 12 bit di flag e 24 bit di
indirizzo fisico per un totale di 36 bit, la sua dimensione è stata portata da 32 a 64 bit. Di
conseguenza una PT di 4 KB contiene 512 entry invece di 1024.
11
-
E’ stato introdotto un nuovo livello di tabelle di pagina chiamato Page Directory Pointer Table
(PDPT) che comprende 4 entry di 64 bit.
-
Il registro di controllo cr3 contiene un indirizzo base a 27 bit della PDPT. Poiché le PDPT sono
contenute nei primi 4 GB di RAM e sono allineate a un multiplo di 32 byte (2 5), 27 bit sono
sufficienti.
-
Quando vengono mappati gli indirizzi lineari a pagine di 4 KB (il flag PS nella PD è azzerato), i 32
bit di un indirizzo lineare sono interpretati in questo modo:
cr3 punta alla PDPT
i bit 31 – 30 puntano a una delle 4 entry della PDPT
i bit 29 – 21 puntano a una delle 512 entry della PD
i bit 20 – 12 puntano a una delle 512 entry della PT
i bit 11 – 0 rappresentano l’offset della pagina di 4 KB
-
Quando vengono mappati gli indirizzi lineari a pagine di 2 MB (il flag PS nella PD è settato a 1), i
32 bit dell’indirizzo lineare sono interpretati in questo modo:
cr3 punta alla PDPT
bit 31 – 30 puntano a una delle 4 entry della PDPT
i bit 29 – 21 puntano a una delle 512 entry della PD
i bit 20 – 0 rappresentano l’offset della pagina di 2 MB
Per riassumere, una volta che cr3 è settato, è possibile indirizzare fino a 4 GB di RAM. Se si vuole indirizzare
più RAM, si deve inserire un nuovo valore in cr3 o cambiare il contenuto della PDPT. Il problema principale
è che gli indirizzi lineari sono ancora di 32 bit. Questo costringe i programmatori del kernel a riutilizzare gli
stessi indirizzi lineari per mappare diverse aree di RAM. Chiaramente PAE non amplia lo spazio di indirizzi di
un processo perché riguarda solo gli indirizzi fisici. Inoltre solo il kernel può variare le tabelle di pagina per
cui un processo in modalità utente non può usare più di 4 GB di indirizzi. D’altra parte PAE permette al
kernel di gestire fino a 64 GB di RAM e quindi di incrementare significativamente il numero di processi del
sistema.
Paginazione nell’architettura a 64 bit
La paginazione a due livelli, comunemente usata dai sistemi a 32 bit, non è sufficiente per quelli a 64 bit,
come spiegato di seguito. La dimensione di pagina è 4 KB. Poiché 1 KB copre un intervallo di 2 10 indirizzi, 4
KB copre 212 indirizzi, per cui il campo Offset è di 12 bit. Restano 52 bit da dividere trai campi Table e
Directory. Se si usano soltanto 48 dei 64 bit (con uno spazio di indirizzamento di 256 TB), restano 48 – 12 =
36 bit da dividere tra Table e Directory. Se vengono divisi in due campi di 18 bit ciascuno, sia la PD che la PT
dovrebbero contenere 218 (ovvero 256.000) entry ciascuna.
Per questo motivo i sistemi di paginazione a 64 bit usano livelli addizionali. Il loro numero dipende dal tipo
di processore. In tabella sono elencate le caratteristiche di alcune piattaforme supportate da Linux.
piattaforma
dimensione di pagina
bit di indirizzo
12
livelli di paginazione
divisione dell’indirizzo
alpha
ia64
ppc64
sh64
x86_64
8 KB
4 KB
4 KB
4 KB
4 KB
43
39
41
41
48
3
3
3
3
4
10 + 10 + 10 + 13
9 + 9 + 9 + 12
10 + 10 + 9 + 12
10 + 10 + 9 + 12
9 + 9 + 9 + 12
Linux adotta un sistema di paginazione comune che soddisfa i sistemi hardware supportati.
Cache hardware
Gli attuali processori hanno frequenze di clock dell’ordine dei gigahertz, mentre le RAM dinamiche (DRAM)
hanno tempi di accesso dell’ordine delle centinaia di cicli di clock. Ciò significa che la CPU viene molto
rallentata quando esegue istruzioni che comportano letture di operandi o salvataggi di dati in memoria.
Le memorie cache sono state introdotte per ridurre la differenza tra le velocità di CPU e RAM. Si basano sul
noto principio di località, che riguarda sia i programmi che le strutture dati. A causa della struttura ciclica
dei programmi e dell’impaccamento dei dati correlati entro gli array lineari, indirizzi vicini a quelli usati di
recente hanno una elevata probabilità di essere richiesti a breve. Ha senso perciò introdurre una piccola ma
veloce unità di memoria che contiene codice e dati usati di recente. Per questo fine è stata introdotta una
nuova unità chiamata linea. La linea è formata da poche dozzine di byte contigui trasferiti in blocco dalla
lenta DRAM alla veloce RAM statica (SRAM) posta sul chip del processore che implementa la cache.
La cache è divisa in un sottoinsieme di linee. A un estremo, la cache può essere a mappatura diretta se una
linea della memoria principale è sempre memorizzata nella stessa locazione della cache. All'altro estremo
può essere pienamente associativa se ogni linea della memoria può essere memorizzata in ogni locazione
della cache. La maggior parte è associativa di grado N se ogni linea di memoria può essere memorizzata in
una di N linee di cache.
La cache è inserita fra l'unità di paginazione e la memoria principale. Comprende una memoria cache
hardware e un controller. La cache contiene le linee di memoria; il controller contiene un array di entry,
una per ogni linea di cache. Ogni entry include un tag e alcuni flag che descrivono lo stato della linea di
cache. Il tag consiste di alcuni bit che permettono al controller di riconoscere la locazione di memoria
attualmente mappata nella linea. I bit dell'indirizzo fisico sono divisi in tre gruppi: i più significativi
corrispondono al tag, quelli di mezzo all'indice del subset del controller e i meno significativi all'offset entro
la linea.
Quando accede ad una cella di memoria della RAM, la CPU estrae l'indice del subset dall'indirizzo fisico e
confronta il tag di tutte le linee nel subset con i bit di ordine maggiore dell'indirizzo. Se la CPU trova una
linea con il tag che corrisponde ai bit di ordine maggiore dell'indirizzo, si ha una corrispondenza (cache hit),
altrimenti una cache miss.
Quando si ha una cache hit, il controller si comporta in modo diverso a seconda del tipo di accesso. Per una
operazione di lettura, il controller seleziona i dati dalla linea e li trasferisce a un registro della CPU; la
memoria non viene interrogata e la CPU risparmia tempo, che è poi lo scopo per cui la cache è stata
inventata. Per una operazione di scrittura, il controller può scegliere tra due strategie: write-through e
write-back. Nella prima il controller scrive sempre sia sulla RAM che sulla linea di cache. Nella seconda, che
comporta maggior efficienza immediata, solo la linea di cache viene aggiornata, mentre la RAM rimane
invariata. Dopo una operazione write-back, naturalmente anche la RAM viene aggiornata. Il controller
scrive nella RAM solo quando la CPU esegue una istruzione che richiede un flush della cache oppure
quando riceve un segnale di FLUSH hardware (di solito dopo una cache miss).
13
Quando si verifica una cache miss, la linea di cache viene scritta in memoria, se necessario, e la linea
corretta viene caricata dalla RAM nella cache.
I sistemi multiprocessore hanno una cache hardware separata per ogni processore e necessitano di circuiti
hardware addizionali per sincronizzare i contenuti. L'aggiornamento richiede più tempo: quando una CPU
modifica la sua cache deve controllare se gli stessi dati sono contenuti in altre cache; se così, deve
notificare alle altre CPU di aggiornare i propri valori. Questo processo è detto cache snooping.
Fortunatamente questo avviene a livello hardware e non interessa il kernel.
La tecnologia della cache è in rapida evoluzione. Il primo Pentium conteneva una singola cache on-chip
chiamata L1. Modelli più recenti includono cache on-chip più grandi e più lente dette L2, L3, ecc. La
consistenza fra i vari livelli è implementata a livello hardware. Linux ne ignora i dettagli e considera solo
l'esistenza di una unica cache.
Il flag CD nel registro cr0 abilita o disabilita il circuito della cache. Il flag NW nello stesso registro specifica se
adottare la strategia write-through oppure write-back.
Un'altra caratteristica della cache del Pentium è che permette al sistema operativo di associare una diversa
politica di gestione della cache per ogni frame di pagina. A questo scopo ogni entry di PD e PT include due
flag: PCD (Page Cache Disable) che abilita/disabilita la cache e PWT(Page Write-Through) che specifica la
strategia da adottare. Linux azzera PCD e PWT in tutte le entry di PD e PT; di conseguenza la cache è
sempre abilitata per tutti i frame di pagina e viene adottata la strategia write-back per le operazioni di
scrittura.
Translation Lookaside Buffers (TLB)
Oltre alla cache di uso generale, i processori 80x86 includono anche una cache chiamata TLB per velocizzare
la traduzione degli indirizzi lineari. Quando uno di questi viene usato per la prima volta, il suo
corrispondente fisico è determinato con un accesso lento alla PT in RAM. Viene però memorizzato in una
entry di TLB, in modo da avere una traduzione veloce in un futuro accesso.
Nei sistemi multiprocessore ogni CPU ha una TLB locale. Le TLB non richiedono sincronizzazione tra le CPU,
dato che diversi processi in differenti CPU possono associare lo stesso indirizzo lineare a diversi indirizzi
fisici. Quando il registro cr3 viene modificato, l'hardware annulla tutte le entry delle TLB locali, dato che
viene ora usato un nuovo set di tabelle di pagina e le TLB puntano a dati ormai vecchi.
14
PAGINAZIONE IN LINUX
Linux adotta un modello di paginazione che vale sia per le architetture a 32 che a 64 bit; queste ultime
infatti richiedono un maggior numero di livelli di paginazione rispetto ai 32 bit. Dalla versione 2.6.10 il
modello di Linux prevede 3 livelli di paginazione, mentre dalla 2.6.11 è stato adottato un modello a 4 livelli
così denominati:
•
Page Global Directory (PGD)
•
Page Upper Directory (PUD)
•
Page Middle Directory (PMD)
•
Page Table (PT).
La PGD include gli indirizzi di varie PUD, ognuna delle quali punta a diverse PMD; ognuna di queste ultime
contiene l'indirizzo di varie PT che puntano ai frame di pagina. Perciò l'indirizzo lineare viene diviso in vari
campi, fino a 5; la loro dimensione dipende dall'architettura del computer.
Per sistemi a 32 bit senza PAE, due livelli sono sufficienti, per cui Linux elimina i campi PUD e PMD
assegnando loro una dimensione di 0 bit. Mantiene comunque la posizione delle relative tabelle nella
sequenza di puntatori, in modo che lo stesso codice possa valere sia per i 32 che per i 64 bit; lo fa
semplicemente assegnando a PUD e PMD 1 sola entry che mappa la corrispondente voce della PGD.
Per sistemi a 32 bit con PAE abilitata, vengono usati 3 livelli; PGD corrisponde alla PDPT descritta in
precedenza, la PUD viene eliminata, la PMD corrisponde alla PD, e la PT corrisponde a sé stessa. Per le
architetture a 64 bit vengono usati 3 o 4 livelli a seconda dell'hardware usato.
La gestione dei processi in Linux si basa molto sulla paginazione. Infatti essa consente:
–
di assegnare un diverso spazio di indirizzi fisici ad ogni processo, assicurando una efficace
protezione contro gli errori di indirizzamento;
–
di distinguere tra pagine (di dati) e frame di pagina (nella memoria fisica). Ciò permette di
memorizzare la stessa pagina in un frame in memoria, salvarla su disco e poi caricarla in un frame
differente. Questi sono gli ingredienti base del sistema di memoria virtuale.
Nella spiegazione successiva, si farà riferimento all'hardware del 80x86. Quando avviene una
commutazione di processo, Linux salva il registro di controllo cr3 nel descrittore del processo
precedentemente in esecuzione, poi carica in cr3 il valore memorizzato nel descrittore del processo che
deve essere eseguito; per cui quando quest'ultimo viene avviato dalla CPU, la unità di paginazione trova il
corretto set di tabella di pagina.
La mappatura degli indirizzi è un procedimento meccanico, anche se complesso, e si basa su varie macro e
funzioni che forniscono al kernel le informazioni di cui ha bisogno.
Campi degli indirizzi lineari
Le macro seguenti facilitano la gestione delle tabelle:
•
PAGE_SHIFT: lunghezza in bit del campo Offset. Restituisce il valore 12 nei sistemi 80x86.
Rappresenta il logaritmo in base 2 della dimensione della Pagina (212 = 4 KB). Viene usata da
15
PAGE_SIZE per restituire la dimensione della pagina. PAGE_MASK dà il valore 0xfffff000 usato per
mascherare i bit dell'Offset.
•
PMD_SHIFT: lunghezza in bit della somma dei campi Offset e Table; in altre parole è il logaritmo
della dimensione dell'area mappata da una entry di PMD. Questa dimensione viene fornita dalla
macro PMD_SIZE. La PMD_MASK è usata per mascherare i due campi nell'indirizzo lineare. Con PAE
disabilitata PMD_SHIFT vale 22 (12 Offset + 10 Table), PMD_SIZE vale 2 22 = 4MB e PMD_MASK vale
0xffc00000. Con PAE abilitata i valori sono rispettivamente: 21 (12 Offset + 9 Table), 2 21 = 2 MB,
0xffe00000.
•
PUD_SHIFT: logaritmo della dimensione dell'area mappata da una entry della PUD. PUD_SIZE
fornisce l'ampiezza dell'area mappata da una entry della PUD. PUD_MASK maschera i bit dei campi
Offset, Table, Middle Air. Nei processori 80x86 PUD_SHIFT = PMD_SHIFT e PUD_SIZE = 4 MB oppure
2 MB.
•
PGDIR_SHIFT: logaritmo della dimensione dell'area che può mappare una entry della PGD.
PGDIR_SIZE fornisce il valore di questa dimensione. PGDIR_MASK maschera i bit dei campi Offset,
Table, Middle Air e Upper Air. Con PAE disabilitata, PGDIR_SHIFT vale 22, PGDIR_SIZE vale 2 22 = 4
MB e PGDIR_MASK vale 0xffc00000. Quando PAE è abilitata, i valori sono rispettivamente 30 (12
Offset + 9 Table + 9 Middle Air), 230 = 1 GB e 0xc0000000.
•
PTRS_PER_PTE, PTRS_PER_PMD, PTRS_PER_PUD, PTRS_PER_PGD: calcolano il numero di entry per
le tabelle. Con PAE disabilitato valgono rispettivamente: 1024, 1, 1, 1024; con PAE abilitato: 512,
512, 1, 4.
Aspetto della memoria fisica
Durante la fase di inizializzazione il kernel deve costruire una mappa della memoria fisica che indichi quale
intervallo di indirizzi il kernel può usare e quali no (o perché mappano memoria condivisa di I/O per
dispositivi hardware o perché contengono dati del BIOS).
Il kernel considera riservati i frame di pagina:
•
che ricadono nell'intervallo non disponibile
•
che contengono codice o strutture dati inizializzate del kernel.
Una pagina contenuta in un frame riservato non può essere assegnata dinamicamente o soggetta a
swapping su disco.
Come regola generale, il kernel viene caricato in RAM a partire dall'indirizzo fisico 0x0010000, cioè dal
secondo MB. L'occupazione di memoria dipende da come è stato configurato: tipicamente occupa meno di
3 MB. Il motivo di questa collocazione risiede nelle particolarità dell'architettura PC:
•
il frame di pagina 0 è usato dal BIOS per memorizzare la configurazione hardware del sistema
rilevata durante il Power-On Self-Test (POST). In alcuni laptop il BIOS scrive dati in questa zona
anche dopo l'inizializzazione.
•
l'intervallo tra 0x000a0000 e 0x000fffff è riservato alle routine del BIOS e per la mappatura della
memoria interna delle schede grafiche ISA. E' noto come il “buco” tra 640 KB e 1 MB e non è
disponibile per il kernel.
16
•
altri frame entro il primo MB possono essere riservati da specifici modelli di computer. Ad esempio
Think-pad mappa il frame 0xa0 nel 0x9f.
All'inizio della fase di boot, il kernel interroga il BIOS e ottiene la dimensione della memoria fisica. In
modelli recenti invoca anche una procedura del BIOS per costruire una lista di indirizzi fisici e dei
corrispondenti tipi di memoria. In seguito esegue la funzione machine_specific_memory_setup(), che
costruisce la mappa degli indirizzi fisici sulla base della lista del BIOS, se disponibile; altrimenti segue un
modello standard prudenziale: tutti i frame con numeri da 0x9f (LOWMEMSIZE()) a 0x100 (HIGH_MEMORY)
sono marcati come riservati.
Esempio di mappa degli indirizzi fisici in un sistema con 128 MB di RAM
inizio
fine
tipo
0x00000000
0x0009ffff
disponibile
0x000f0000
0x000fffff
riservato
0x00100000
0x07feffff
disponibile
0x07ff0000
0x07ff2fff
dati ACPI
0x07ff3000
0x07ffffff
NVS ACPI
0xffff0000
0xffffffff
riservato
L'intervallo tra 0x07ff0000 e 0x07ff2fff contiene informazioni sui componenti hardware scritte dal BIOS
nella fase POST; il kernel poi copia queste informazioni in una propria struttura dati e considera il frame
usabile. L'intervallo 0x07ff3000 – 0x07ffffff è riservato alla mappatura dei chip ROM dei dispositivi
hardware. L'intervallo che inizia da 0xffff0000 è riservato al chip ROM del BIOS. Da notare che il BIOS può
non fornire indicazioni su alcuni intervalli (come 0x000a0000 – 0x000effff); per sicurezza, Linux li considera
non utilizzabili.
Il kernel può non vedere tutta la memoria riportata dal BIOS; ad esempio, se non è compilato con in
supporto a PAE, può indirizzare solo 4 GB, anche se la memoria fisica è maggiore. La funzione
setup_memory() viene chiamata subito dopo machine_specific_memory_setup(); analizza la tabella delle
regioni di memoria e inizializza alcune variabili che descrivono lo stato della memoria fisica:
Descrizione
Variabile
num_physpages
il più alto numero di frame di pagina utilizzabile
totalram_pages
numero totale di frame utlizzabili
min_low_pfn
numero del primo frame utilizzabile dopo l'immagine del kernel
max_pfn
numero dell'ultimo frame utilizzabile
max_low_pfn
numero dell'ultimo frame mappato direttamente dal kernel (low memory)
totalhigh_pages
numero totale di frame non mappati direttamente dal kernel (high memory)
highstart_pfn
numero del primo frame non mappato direttamente dal kernel
highend_pfn
numero dell'ultimo frame non mappato direttamente dal kernel
17
Per evitare di caricare il kernel in pagine non contigue, Linux preferisce evitare il primo MB; chiaramente le
pagine non riservate dall'architettura PC saranno usate per memorizzare pagine assegnate dinamicamente.
La figura mostra come sono utilizzati i primi 3 MB (768 frame di pagina)
0
1
non disp.
0x9f
disponibile
0x100
non disp.
0x2ff
disponibile
codice kernel
_text
Initializedkernel data
_etext
uninitializeddisponibile
kernel data
_edata
_end
Il simbolo _text che corrisponde all'indirizzo 0x00100000, identifica il primo byte del kernel; il simbolo
_etext identifica l'ultimo byte. I dati del kernel sono divisi in due gruppi: inizializzati, che vanno da _etext a
_edata, e non inizializzati che terminano a _end. Questi simboli non sono definiti nel codice sorgente di
Linux, ma sono prodotti durante la compilazione (i loro indirizzi sono presenti nel file System.map).
Tabelle di pagina dei processi
Lo spazio degli indirizzi di un processo è diviso in due parti:
•
da 0x00000000 a 0xbfffffff può essere indirizzato sia in modalità del kernel che utente
•
da 0xc0000000 a 0xffffffff può essere indirizzato solo in modalità del kernel
Un processo in modalità utente opera con indirizzi inferiori a 0xc0000000; in modalità del kernel opera su
indirizzi superiori a 0xc0000000; in qualche caso, comunque, il kernel deve avere accesso allo spazio di
indirizzi della modalità utente per acquisire o memorizzare dati. La macro PAGE_OFFSET restituisce il valore
0xc0000000.
Il contenuto delle prime entry della PGD che mappa gli indirizzi inferiori a 0xc0000000 (le prime 768 entry
con PAE disabilitato o le prime tre con PAE abilitato) dipende dal processo. Le altre entry sono comuni a
tutti i processi e sono uguali alle corrispondenti entry della PGD principale del kernel.
Tabelle di pagina del kernel
Il kernel mantiene un insieme di tabelle di pagina per il proprio uso basate sulla master kernel Page Global
Directory (MKPGD). Dopo l'inizializzazione del sistema questo set di pagine non viene adoperato da nessun
processo o thread del kernel; piuttosto, le entry finali della MKPGD sono il modello di quelle corrispondenti
della PGD dei processi regolari.
L'inizializzazione delle tabelle di pagina del kernel è un procedimento a due fasi. Subito prima del
caricamento in memoria dell'immagine del kernel, la CPU è ancora in modalità reale, e la paginazione non è
attivata.
Nella prima fase, il kernel crea un limitato spazio di indirizzi che comprende i propri segmenti codice e dati,
la PT iniziale e 128 KB per alcune strutture dinamiche. Questo spazio minimale è sufficiente per caricare in
RAM il kernel e inizializzare le sue strutture principali.
Nella seconda fase il kernel sfrutta tutta la memoria disponibile e inizializza le tabelle di pagina.
18
Tabelle di Pagina provvisorie del kernel
Una PGD provvisoria è inizializzata staticamente durante la compilazione del kernel, mentre le PT
provvisorie sono inizializzate dalla funzione assembly startup_32(), definita in arch/i386/kernel/head.S.
Non verranno più menzionate le PUD e le PMD, in quanto sono equiparate a entry della PGD, dato che il
supporto a PAE non è ancora attivo in questo stadio.
La PGD provvisoria è contenuta nella variabile swapper_pg_dir. Le PT provvisorie sono memorizzate a
partire da pg0, subito dopo il valore _end. Per semplificare, si può ipotizzare che tutto lo spazio iniziale di
indirizzamento del kernel cada nei primi 8 MB di RAM, per cui per indirizzare 8 MB occorrono due PT.
L'obiettivo in questa prima fase è indirizzare facilmente gli 8 MB sia in modalità reale che protetta. Perciò il
kernel deve mappare i due intervalli tra 0x00000000 e 0x007fffff e tra 0xc0000000 e 0xc07fffff negli indirizzi
fisici tra 0x00000000 e 0x007fffff. In altre parole il kernel può indirizzare i primi 8 MB o con indirizzi lineari
identici a quelli fisici o con 8 MB di indirizzi lineari a partire da 0xc0000000.
Tutte le entry della swapper_page_dir sono riempite con zeri, tranne la 0, 1, 0x300 (768 decimale) e 0x301
(769): queste vengono inizializzate in questo modo:
•
nel campo indirizzo delle entry 0 e 0x300 viene inserito l'indirizzo fisico di pg0, in quello delle entry
1 e 0x301, l'indirizzo fisico del frame successivo a pg0
•
i flag Present, Read/Write e User/Supervisor sono settati in tutte le 4 entry
•
i flag Accessed, Dirty, PCD, PWD, Page Size sono azzerati in tutte le 4 entry
La funzione startup_32() abilita anche l'unità di paginazione, caricando l'indirizzo di swapper_pg_dir nel
registro cr3 e settando il flag PG nel registro cr0, come mostrato nel frammento di codice equivalente:
movl $swapper_pg_dir-0xc0000000, %eax
movl $eax, %cr3
movl %cr0, %eax
orl $0x80000000, %eax
movl %eax, %cr0
Tabella definitiva delle pagine del kernel quando la RAM è meno di 896 MB
La mappatura finale deve trasformare gli indirizzi lineari a partire da 0xc0000000 in indirizzi fisici a partire
da 0. La macro __pa converte un indirizzo lineare che parte da PAGE_OFFSET nell'indirizzo fisico
corrispondente, mentre la macro __va opera la conversione inversa.
La PGD è ancora memorizzata in swapper_pg_dir. Viene inizializzata dalla funzione paging_init() che esegue
le azioni seguenti:
1 – chiama pagetable_init() per creare le entry della PT
2 – scrive l'indirizzo fisico di swapper_pg_dir nel registro cr3
3 – se il kernel è compilato con il supporto a PAE, setta il flag PAE nel registro cr4
4 – chiama __flush_tlb_all() per invalidare tutte le voci di TLB.
19
Le azioni compiute da pagetable_init() dipendono dalla quantità di RAM e dal modello di CPU. Nel caso più
semplice, il computer ha meno di 896 MB di RAM (gli ultimi 128 MB di indirizzi lineari sono riservati a
particolari tipi di mappatura come “Fix mapped linear addresses”; al kernel restano perciò 1 GB – 128 MB =
896 MB), gli indirizzi a 32 bit sono sufficienti a indirizzare tutta la RAM e non c'è bisogno di PAE.
La PGD della swapper_pg_dir viene reinizializzata per mezzo di un ciclo simile al seguente:
pgd = swapper_pg_dir + pgd_index(PAGE_OFFSET); /* 768 */
phys_addr = 0x00000000;
while (phys_addr < (max_low_pfn * PAGE_SIZE)) {
pmd = one_md_table_init(pgd); /* returns pgd itself */
set_pmd(pmd, __pmd(phys_addr | pgprot_val(__pgprot(0x1e3))));
/* 0x1e3 == Present, Accessed, Dirty, Read/Write, Page Size, Global */
phys_addr += PTRS_PER_PTE * PAGE_SIZE; /* 0x40000000 */
++pgd;
}
Il flag User/Supervisor è azzerato, quindi i processi in modalità utente non possono accedere allo spazio del
kernel; il flag Page Size è settato, quindi il kernel può usare pagine di 4 MB.
La mappatura dei primi 8 MB è richiesta nella fase di inizializzazione: quando non è più necessaria, il kernel
azzera le tabelle di pagina chiamando la funzione zap_low_mappings().
Questa descrizione non esaurisce tutte le operazioni; in realtà il kernel imposta anche le entry della PT che
corrispondono a gli indirizzi lineari a mappatura fissa (fix-mapped linear addresses).
Tabella definitiva delle pagine del kernel quando la RAM è tra 896 MB e 4 GB
In questo caso la RAM non può essere interamente mappata nello spazio degli indirizzi lineari del kernel.
Linux nella fase di inizializzazione mappa perciò solo una finestra di 896 MB. Se un programma richiede
altre parti della RAM, altri intervalli di indirizzi lineari devono essere mappati nella RAM richiesta. Ciò
significa cambiare le entry di alcune pagine (rimappatura dinamica). Per inizializzare la PGD il kernel usa lo
stesso codice del caso precedente.
Tabella definitiva delle pagine del kernel quando la RAM è superiore a 4 GB
In questo caso si verificano queste condizioni:
•
la CPU supporta la PAE
•
la RAM supera i 4 GB
•
il kernel è compilato con il supporto a PAE.
Anche se la PAE usa indirizzi fisici a 36 bit, gli indirizzi lineari sono ancora a 32 bit. Come nel caso
precedente, Linux mappa una finestra di 896 MB; il resto della RAM rimane non mappato e viene gestito
con la rimappatura dinamica. La differenza è che viene usato un modello a tre livelli di pagine e la PGD è
inizializzata con un ciclo differente.
Il kernel inizializza le prime tre entry della PGD con l'indirizzo di una pagina vuota (empty_zero_page).La
quarta è inizializzata con l'indirizzo della PMD (pmd) allocata chiamando la funzione
20
alloc_bootmem_low_pages(). Le prime 448 entry nella PMD (64 delle 512 sono riservate alla “allocazione di
memoria non-contigua”) sono riempite con gli indirizzi fisici dei primi 896 MB di RAM. Tutte le CPU che
supportano PAE supportano anche le pagine di 2 MB; come prima, quando è possibile, Linux le usa per
ridurre il numero delle Tabelle di Pagina.
La quarta entry della PGD viene poi copiata nella prima, per copiare la mappatura della memoria fisica
inferiore nei primi 896 MB di spazio degli indirizzi lineari. Questa azione è richiesta per completare
l'inizializzazione dei sistemi SMP; quando non è più necessaria, il kernel chiama zap_low_mappings() per
ripulire le entry delle tabelle.
Indirizzi lineari a mappatura fissa
La parte iniziale del quarto gigabyte dello spazio di indirizzi lineari del kernel mappa la memoria fisica del
sistema. Però almeno 128 MB sono disponibili per implementare l'allocazione della memoria non-contigua
e gli indirizzi lineari a mappatura fissa.
Un indirizzo lineare a mappatura fissa (FMLA) è un indirizzo lineare costante come 0xffffc000, il cui
corrispondente indirizzo fisico non deve essere l'indirizzo lineare meno 0xc0000000, ma piuttosto un
indirizzo fisico definito in modo arbitrario. Perciò ogni FMLA mappa un frame di pagina di memoria fisica. Il
kernel li usa al posto di variabili puntatore che non cambiano il loro valore.
FMLA sono concettualmente simili agli indirizzi lineari che mappano i primi 896 MB di RAM. Comunque un
FMLA può mappare un qualunque indirizzo fisico, mentre la mappatura dei primi 896 MB è lineare.
Rispetto ai puntatori, gli FMLA sono più efficienti. Infatti dereferenziare un puntatore richiede un accesso di
memoria in più che dereferenziare un indirizzo costante immediato. In più, controllare il valore di una
variabile prima di dereferenziare è una buona pratica di programmazione, che non è richiesta per gli
indirizzi.
Ogni FMLA è rappresentato da un indice intero definito nella struttura
enum fixed_addresses {
FIX_HOLE,
FIX_VSYSCALL,
FIX_APIC_BASE,
FIX_IO_APIC_BASE_O,
[…]
__end_of_fixed_addresses
};
Gli FMLA sono posti alla fine del quarto gigabyte di indirizzi lineari. La funzione fix_to virt calcola l'indirizzo
lineare costante a partire dall'indice
inline unsigned long fix_to_virt(const unsigned int idx) {
if(idx >= __end_of_fixed_addresses)
__this_fixmap_does_not_exist();
return (0xfffff000UL – (idx << PAGE_SHIFT));
}
Una funzione del kernel può chiamare fix_to_virt(FIX_IOAPIC_BASE_O); dato che la funzione è dichiarata
“inline”, il compilatore non genera una chiamata di funzione ma inserisce il codice nella funzione
21
chiamante. Il controllo sul valore dell'indice non viene mai compiuto a runtime. Infatti FIX_IOAPIC_BASE_O
è una costante uguale a 3 e il compilatore può eliminare la condizione if dato che essa risulta falsa. Al
contrario, se la condizione è vera oppure l'argomento di fix_to_virt() non è una costante, il compilatore
genera un errore nella fase di linking perché il simbolo __this_fixmap_does_not_exist() non è definito da
nessuna parte. Eventualmente, il compilatore calcola 0xfffff000 – (3 << PAGE_SHIFT) e sostituisce alla
funzione il valore 0xfffc000.
Per associare un indirizzo fisico a un FMLA il kernel usa le macro set_fixmap(idx,phys) e
set_fixmap_nocache(idx,phys). Entrambe inizializzano la entry dell PT che corrisponde a fix_to_virt(idx) con
phys; la seconda funzione in più disabilita la cache settando il flag PCD. Invece clear_fixmap(idx) rimuove la
mappatura tra idx e phys.
Gestione della cache hardware e di TLB
Cache e TLB hanno un ruolo importante nell'aumentare le prestazioni dei computer. Gli sviluppatori del
kernel usano varie tecniche per ridurre il numero di cache miss.
Gestione della cache hardware
La cache è indirizzata per linee; la macro L1_CACHE_BYTES fornisce la dimensione in byte di una linea di
cache. Nel Pentium 4 il valore è 128; sui modelli precedenti 32.
Per ottimizzare la percentuale di cache hit, il kernel considera l'architettura nel prendere le seguenti
decisioni:
–
i campi delle strutture usati più di frequente sono posti a offset inferiori nella struttura, in modo
che vengano memorizzati nella stessa linea di cache
–
quando deve allocare un numeroso insieme di strutture, il kernel tenta di memorizzare ognuna di
esse in modo da usare uniformemente ogni linea di cache.
La sincronizzazione della cache è fatta automaticamente dal microprocessore 80x86, per cui il kernel non
compie nessuna azione di flush.
Gestione di TLB
I processori non possono sincronizzare la propria cache TLB, poiché è il kernel a decidere se la mappatura di
indirizzi non è più valida. Linux 2.6 offre molti metodi di flush che possono essere applicati a seconda del
tipo di cambiamento della tabella delle pagine (flush_tlb_all,
flush_tlb_kernel_page, flush_tlb,
flush_tlb_mm, flush_tlb_range, flush_tlb_pgtables, flush_tlb_page).
Ogni microprocessore di solito offre un insieme di istruzioni assembly molto più ristretto per invalidare un
TLB; per questo aspetto, una delle architetture più flessibili è Ultra SPARC. Intel ha solo due tecniche:
–
tutti i modelli di Pentium compiono il flush automatico delle entry TLB relative a pagine non globali
quando un valore viene memorizzato nel registro cr3
–
nel Pentium Pro e nei modelli successivi, l'istruzione invlpg invalida una singola entry TLB che
mappa un dato indirizzo.
Le seguenti macro sfruttano questi metodi hardware e sono la base per implementare le funzioni
indipendenti dall'architettura elencate all'inizio del paragrafo:
22
__flush_tlb():
riscrive cr3 con il proprio valore. Usato da flush_tlb, flush_tlb_mm, flush_tlb_range.
__flush_tlb_global():
azzera il flag PGE nel registro cr4, riscrive c3 con il proprio valore e resetta PGE.
Usato per flush_tlb_all, flush_tlb_kernel_range.
__flush_tlb_single(addr):
esegue l'istruzione assembly invlpg con il parametro addr. Usata da
flush_tlb_page.
Non viene citata la funzione flush_tlb_pgtables; nell'architettura 80x86 non deve essere fatto nulla quando
una tabella di pagina viene scollegata dalla tabella da cui viene indirizzata, per cui la funzione è vuota.
I metodi di flush vengono estesi in modo semplice ai sistemi multiprocessore; la funzione manda un
interrupt interprocessore che costringe le altre CPU a eseguire le funzioni di invalidazione di TLB.
In generale, una commutazione di processo (process switch) comporta il cambiamento delle tabelle di
pagina attive. Le entry di TLB devono venire azzerate; questo avviene automaticamente quando il kernel
scrive l'indirizzo della nuova PGD nel registro cr3. Il kernel riesce ad evitare il flush nei seguenti casi:
–
quando avviene la commutazione tra processi che usano lo stesso set di tabelle di pagina
–
quando avviene la commutazione tra processi normali e thread del kernel; infatti questi ultimi non
hanno un proprio set di tabelle di pagina, ma usano quelli dei processi in esecuzione sulla CPU al
momento della commutazione.
Vi sono altri casi in cui il kernel deve annullare alcune entry di TLB; ad esempio quando assegna un frame di
pagina ad un processo in modalità utente e memorizza il suo indirizzo in una entry di PT, deve invalidare le
voci di TLB che si riferiscono a quell'indirizzo lineare. Nei sistemi multiprocessore deve invalidare anche le
TLB delle CPU che usano la stessa tabella di pagina.
Per evitare operazioni inutili, in sistemi multiprocessore viene usata la tecnica “lazy TLB mode”. L'idea di
fondo è la seguente: se diverse CPU usano le stesse tabelle di pagina e una entry di TLB va invalidata su
tutte, il flush può essere rinviato nelle CPU che eseguono thread del kernel. Infatti i thread usano le tabelle
del processo in esecuzione in quel momento, e non è necessario invalidare le voci di TLB dato che essi non
accedono allo spazio degli indirizzi della modalità utente. Per inciso, il metodo flush_tlb_all non usa la
modalità “lazy”; viene chiamata quando il kernel modifica una tabella dello spazio di indirizzi in modalità del
kernel.
Quando una CPU esegue un thread del kernel, passa in “lazy TLB mode”. Quando viene inviato un segnale
di cancellazione per qualche entry, la CPU non lo esegue, ma memorizza che il processo usa un set di
tabelle non valido. Appena nella CPU avviene una commutazione di processo con un diverso set di tabelle
di pagina, l'hardware esegue il flush delle entry di TLB e il kernel pone la CPU in modalità non-lazy. Invece
se la commutazione riguarda un processo che usa lo stesso set di tabelle del precedente, le cancellazioni di
entry differite vanno eseguite; questo avviene eseguendo il flush di tutte le entry non globali di TLB.
Per implementare la modalità “lazy” occorrono ulteriori strutture dati. La variabile cpu_tlbstate è un array
static di NR_CPUS strutture (di default il valore è 32, cioè il massimo numero di CPU per sistema)
contenenti un campo active_mm che punta al descrittore di memoria del processo corrente, e un flag state
che può assumere due valori: TLBSTATE_OK (modalità non lazy) e TLBSTATE_LAZY. In aggiunta, ogni
descrittore di memoria include un campo cpu_vm_mask che contiene gli indici delle CPU che dovrebbero
ricevere gli interrupt interprocessore legati al flush. Questo campo è significativo solo se il descrittore di
memoria appartiene ad un processo in esecuzione.
23
Quando una CPU esegue un thread del kernel, il kernel setta il flag state a TLBSTATE_LAZY; cpu_vm_mask
contiene gli indici di tutte le CPU del sistema, inclusa la presente. Quando un'altra CPU vuole annullare le
entry relative ad un certo set di tabelle, invia un Interrupt a tutte le CPU il cui indice è incluso in
cpu_vm_mask.
Quando una CPU riceve l'interrupt e verifica che esso riguarda il set di tabelle usato dal processo che sta
eseguendo, controlla che il flag state sia a TLBSTATE_LAZY. In questo caso il kernel non esegue il flushing e
toglie l'indice di questa CPU dal campo cpu_vm_mask. Ne consegue che:
–
finché la CPU rimane in modalità “lazy”, non riceve altri interrupt di flush
–
se avviene la commutazione con un altro processo che usa lo stesso set di tabelle, il kernel chiama
__flush_tlb() per cancellare tutte le entry non globali di TLB.
24