Memoria virtuale e Sistema operativo
In riferimento all’architettura x86 e al
sistema operativo Linux
Premessa
Questa NON è una lezione di un corso di Sistemi Operativi, né tantomeno di Linux
Lo scopo della lezione è capire come le potenzialità dell’architettura x86 vengono usate in pratica
Ci riferiremo all’architettura a 32 bit
Certi dettagli del funzionamento di Linux verranno trascurati o semplificati allo scopo di cui sopra
Chi leggesse la letteratura su Linux, dovrebbe confrontarsi con molto materiale aggiuntivo che qui non è stato preso in
considerazione
Architettura x86
e Linux
Memoria virtuale x86
Segmentazione e paginazione
La paginazione è opzionale, la segmentazione è ineliminabile
La segmentazione mette in atto un sistema di protezione raffinato.
Ma viene usata?
Memoria virtuale x86
Segmentazione e paginazione
La paginazione è opzionale, la segmentazione è ineliminabile
La segmentazione x86 mette in atto un sistema di protezione raffinato.
Ma viene usata?
Sostanzialmente NO!
Linux (derivato da Unix) usa un modello lineare piatto
Così fan tutti (Windows, Solaris, ..)
Modello piatto
Come si fa (versione a 32 bit)
Paginazione (modello piatto)
L’indirizzo lineare prodotto dall’unità di segmentazione viene interpretato come indirizzo virtuale
Su macchine a 32 bit
Lo spazio virtuale si riduce a 4 GB (in pratica l’indirizzo è dato dal solo OFFSET)
La memoria fisica può arrivare a 4 GB
Dal PentiumPro la memoria fisica può arrivare fino a 64 GB
(tramite il PAE –
physical address extension). Non considereremo
questo caso
Linux
Due sole modalità di funzionamento:
User mode (PL=3) o Kernel mode (PL=0)
Tutti i processi vedono il medesimo spazio (virtuale) di 4GB,
suddiviso in spazio kernel e spazio user. La memoria (virtuale) è suddivisa corrispondentemente
Un processo di utente è
normalmente in modo User. Se fa una chiamata al SO passa in modo Kernel
Linux
Due sole modalità di funzionamento:
User mode o Kernel mode
Tutti i processi vedono il medesimo spazio (virtuale) di 4GB,
suddiviso in spazio kernel e spazio user. La memoria (virtuale) di ogni singolo processo è suddivisa corrispondentemente
Un processo di utente è
normalmente in modo User. Se fa una chiamata al SO passa in modo Kernel
Mappato nella parte bassa della memoria
Memoria Fisica
Parte della memoria fisica è usata per scopi
predefiniti
Legacy Estensioni
Memoria Fisica
Parte della memoria fisica è usata per scopi
predefiniti
Legacy Estensioni
Di norma il kernel (il codice del kernel) si mappa dalla posizione 0x100000, cioè dopo il primo megabyte di
memoria fisica
Incidentalmente: una Motherboard
Northbridge dirige il traffico
I 4 segmenti di Linux
Un segmento di codice e uno dati per il modo User
Un segmento di codice e uno dati per il modo Kernel
Ci sono altri segmenti per un totale di 18 con funzioni accessorie
Uno di questi è un TSS (uno solo!), vedremo avanti come è usato
I 4 segmenti di Linux
Un segmento di codice e uno dati per il modo User
Un segmento di codice e uno dati per il modo Kernel
Attenzione ai livelli di privilegio DPL determina user/kernel Type
2: Data Read/write 10: Codice Execute/Read
I 4 segmenti di Linux
Un segmento di codice e uno dati per il modo User
Un segmento di codice e uno dati per il modo Kernel
Granularita: Pagina (4K) Spazio complessivo 4G
Normale segmento
(codice o dati) Sempre presente
Indirizzi a 32 bit
E le vecchie, care GDT e LDT ?
Viene usata solo GDT. Essa contiene
I descrittori dei 4 segmenti visti
Pochi altri descrittori (in totale fino a 18) molti dei quali sono semplicemente vuoti.
Tra questi c’è il descrittore di una “default_LDT” che il kernel non usa ma che può essere usata da processi che richiedano una LDT
Alcuni descrittori per i thread e per il PnP (BIOS)
Un TSS
Si ricorda che la IDT ( Interrupt Descriptor Table ) non è parte del sistema dei segmenti
La “IDT non è un segmento”; l’accesso è in modo trasparente via il registro di CPU IDTR (non visibile al programmatore in modo protetto)
Programmi,
codice oggetto
e memoria
Programmi e memoria
Un compilatore genera codice per uno spazio comunque “virtuale”
C e C++ (Gnu) per Linux
organizzano il programma in
“segmenti” come nello schema
Text: codice
Data: dati inizializzati (statici)
BSS: dati non inizializzati (statici)
Heap: memoria dinamica (malloc())
Stack: variabili dinamiche
Codice oggetto (ELF – Executable and Linkable Format )
ELF: Uno dei possibili formati
Contiene la traduzione del programma sorgente e info per il linker e loader
Gli header danno
informazioni a linker e al
loader su cosa c’è entro il file
Microsoft ha diversi formati PE ( Portable Executable ): EXE, DLL, OBJ, SYS
File ELF versione linkable
Linker
Text Data
BSS
Text Data BSS
Text Data
BSS
Comporta riallocazione/riassegnazione degli indirizzi
2 viste
Le info in ELF header permettono due viste (2 tipi di file) differenti:
(a) file linkable ; (b) file eseguibile.
Section header table: specifica la struttura delle sezioni (tabelle, nomi ecc.)
Program header table: specifica (alla funzione exec() del sistema operativo) come creare il process image
File linkable File executable
L’ordine degli header e delle altre parti non è predefinito.
Questo è uno dei possibili ordini.
File eseguibile
Esempio di possibile file eseguibile
File eseguibile
Dimensione del segmento
Indirizzo virtuale del segmento
Un programma memorizzato su disco è una entità
“passiva”
Offset del segmento
rispetto all’inizio del file
File eseguibile
Queste informazioni stanno nel Program header
p_vaddr
p_filesz p_offset
Process Image
Confini di pagina
.text
.data .text
Immagine Eseguibile
I segmenti del processo immagine devono partire da multipli di 4KB (assumendo pagine di 4KB), ovvero a confini multipli di 0x1000.
Process image: dislocazione nello spazio virtuale
.data .data
I due segmenti sono separati perché ciascuno deve partire da un confine di pagina.
Le parti di sementi diversi che stanno nella stessa pagina vengono duplicate
In questo modo i due segmenti possono avere diritti di accesso diversi (cosa non possibile se fine Text e inizio Data fossero stati nella stessa pagina)
Process image
(corrispondente al precedente modulo eseguibile)Process Image
Process image
(corrispondente al precedente modulo eseguibile)Process Image
Il processo immagine non viene costruito come nuovo file (vedi seguente)
Il sistema operativo, in base al file
eseguibile, determina dove sono allocati i segmenti nello spazio virtuale.
Descrizione del process image
Process Image
Caricamento del programma:
copiare logicamente un segmento del file su un segmento della
memoria virtuale.
Attenzione: il caricamento fisico può
avvenire in un secondo tempo quando viene fatto il primo riferimento a un segmento
La memoria (virtuale di Linux)
Questo confine è fisso
La Memoria
Virtuale di Linux
Modello di traduzione Linux
Sarebbe a tre livelli
(cr3 è il registro dell’architettura x86)
Usato sulle
macchine a 64 bit
Sulle macchine a 32 bit
Usa la traduzione convenzionale (X86)
Non c’è la MPD
Questa si chiama ancora Page Global Directory (PGD)
Dove sta il kernel
Nella configurazione standard al kernel è riservato l’ultimo dei 4 GB dello spazio degli indirizzi (virtuali)
In memoria fisica il kernel è stabilmente allocato nella parte bassa, a partire dalla posizione 0x100000 (ovvero dopo il primo MB di memoria)
Una tipica configurazione può richiedere meno di 2 MB
Il primo MB di memoria fisica non viene usato perché una parte è
ricoperta dal BIOS (ROM).
Altre parti del primo MB servono a usi specifici. (I page frame corrispondenti non vengono usati)
Memoria fisica
Nota: C0000000 0 (l’effettivo inizio del codice kernel è a c0100000, che si mappa dopo il primo MB fisico)
Avvio del kernel (1)
Il BIOS esegue una serie di test e inizializza l’hardware
Legge il boot sector (primo settore del disco, detto anche Master Boot Record - MBR,
Prima parte del GRUB),
Lo copia in RAM a partire dall’indirizzo 7c00 e gli passa il
controllo, determinando a sua volta il caricamento della seconda
parte del boot loader (presenta la schermata con cui si sceglie il
sistema operativo da caricare)
Avvio del kernel (2)
Vengono preparate la GDT e la IDT (temporanee)
Viene effettuato il passaggio al modo protetto, ma senza attivare la paginazione
Viene caricato e decompresso il kernel, e viene effettuato il salto al punto di entrata (0x100000 (in realtà 0x100100))
Viene effettuata l’inizializzazione finale di GDT, IDT
Vengono costruite le tabelle e la mappatura di memoria (pure temporanee)
Viene attivata la paginazione (fino a questo punto gli indirizzi lineari generati corrispondevano agli indirizzi fisici)
Viene portato EIP nello spazio virtuale (da questo momento gli indirizzi lineari generati sono virtuali)
Viene creato il processo con PID = 1
Reale/virtuale ?????
Il kernel viene compilato a partire da PAGE_OFFSET
Gli indirizzi (assoluti) di posizioni entro il kernel sono quindi superiori a 0xc0000000
Ma il kernel viene caricato nella parte bassa della memoria fisica
Come fa allora a funzionare ?????
Questa posizione si chiama PAGE_OFFSET (PAGE_OFFSET = 0xc0000000)
A regime
Quando il kernel è ormai istallato e la paginazione è attiva non ci sono (ovviamente) problemi: provvede in modo automatico il meccanismo di traduzione degli indirizzi
PMT
Memoria Fisica
1MB
A regime
Quando il kernel è ormai istallato e la paginazione è attiva non ci sono (ovviamente) problemi: provvede in modo automatico il meccanismo di traduzione degli indirizzi
PMT
Memoria Fisica
1MB
Ma durante la fase iniziale, quando ancora la MMU non è in funzione, come fa un programma compilato per stare oltre c0000000 a funzionare
INVARIATO entro il primo GB ??????
Un po’ di codice (parte iniziale del kernel)
PAGE_OFFSET EQU c0000000H
C0000000 ORG PAGE_OFFSET ;Prima posizione :::
C0001000 V DW 2011 ;var inizializ :::
C0001400 JMP L1 ;un salto
::: ; (relativo)
C0001420 L1: :::
:::
C0001430 JMP L2 ;un altro salto
::: ; (assoluto)
c0001680 L2: :::
..la sua codifica
Alla variabile V viene assegnato (come indirizzo) il valore che corrisponde a PAGE_OFFSET+lo scostamento che le compete; lo stesso a L1 e L2
Cioè numeri superiori a 0xc0000000
Ma nell’architettura x86:
Il salto a L1 viene codificato come salto relativo (a EIP)
un salto a non oltre 127 (128) posizioni in avanti (indietro) viene codificato come salto relativo
Il salto a L2 viene codificato come assoluto
Carichiamo il programma da 0x100000
100000 :::
101000 (V) 2011 :::
101400 JMPR 20 :::
101420 (L1) :::
:::
101430 JMPA c0001680 :::
c0001680 (L2)
Contenuto di memoria
Nota: JMPR e JMPA stanno per salto relativo e assoluto
rispettivamente
Carichiamo il programma da 0x100000
100000 :::
101000 (V) 2011 :::
101400 JMPR 20 :::
101420 L1: :::
:::
101430 JMPA c0001680 :::
c0001680 L2:
Contenuto di memoria
101400 EIP
E supponiamo che EIP punti alla
posizione che contiene il salto (relativo) a L1
Carichiamo il programma da 0x100000
100000 :::
101000 (V) 2011 :::
101400 JMPR 20 :::
101420 L1: :::
:::
101430 JMPA c0001680 :::
c0001680 L2:
Contenuto di memoria
101400 EIP
E supponiamo che EIP punti alla
posizione che contiene il salto (relativo) a L1
Il salto si compie: EIP viene aggiornato a 101420
Il programma esegue correttamente anche se si trova a un indirizzo
diverso da quello per cui è stato compilato
Portiamoci in 101430
100000 :::
101000 (V) 2011 :::
101400 JMPR 20 :::
101420 L1: :::
:::
101430 JMPA c0001680 :::
c0001680 L2:
Contenuto di memoria
101430 EIP
Portiamoci in 101430
100000 :::
101000 (V) 2011 :::
101400 JMPR 20 :::
101420 L1: :::
:::
101430 JMPA c0001680 :::
c0001680 L2:
Contenuto di memoria
101430 EIP
Ovvero “salta nello spazio virtuale” del kernel
Il salto (assoluto) aggiorna EIP con il valore codificato nell’istruzione
Portiamoci in 101430
100000 :::
101000 (V) 2011 :::
101400 JMPR 20 :::
101420 L1: :::
:::
101430 JMPA c0001680 :::
c0001680 L2:
Contenuto di memoria
101430 EIP
Ovvero “salta nello spazio virtuale” del kernel
Il salto (assoluto) aggiorna EIP con il valore codificato nell’istruzione
Dopo l’attivazione della paginazione basta un salto come questo a
passare allo spazio virtuale
Vediamolo sul serio (GAS)
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
NB: all’inizio di questo codice la CPU è già in modo protetto (eax, ecc..), ma con mappatura disabilitata
swapper_pg_dir è il nome della posizione a cui si trova la tabella
PGD
Vediamolo sul serio (GAS)
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
NB: all’inizio di questo codice la CPU è già in modo protetto (eax, ecc..), ma con mappatura disabilitata
Siccome il kernel viene mappato a partire da 0, la differenza è l’indirizzo assoluto (a
partire da 0) a cui si trova PGD
Vediamolo sul serio (GAS)
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
Abilita la paginazione
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
Quei due salti (preliminare)
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
Questo è un salto relativo
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
Quei due salti (preliminare)
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
Questa carica in eax $1f (un indirizzo assoluto) ovvero il corrispondente indirizzo nello spazio virtuale del compilatore
Salta all’indirizzo contenuto in eax (salto indiretto)
…ma la PMT ??
Il passaggio allo spazio virtuale presuppone che ci sia la PMT (ovvero la PGD e le PT). Dove sono ?? Come ci sono state messe??
Anzitutto si assume che il kernel vada a occupare i primi 8 MB di memoria fisica (anche se è scritto in modo che effettivamente il codice parta dopo il primo MB)
Per indirizzare 8 MB ci vogliono 2 PT
In compilazione viene costruita una PGD vuota eccetto che per i due elementi che punteranno alle due PT
swapper_pg_dir corrisponde a PGD (che necessariamente viene presa negli 8 MB fisici in cui è il kernel e quindi negli 8 MB virtuali corrispondenti )
Le variabili pg0 e pg1 tengono i puntatori a PT0 e PT1 (esse pure prese necessariamente nei medesimi 8 MB)
Le due PT vengono riempite dal kernel stesso all’avvio (cioè non
a tempo di compilazione), prima di passare al modo virtuale
…segue
Il kernel deve mappare c0000000 su 0 (cioè la prima
pagina virtuale del kernel sulla prima pagina fisica usata)
c 0 0 0 0 0 0 0
1100 0000 0000 0000 0000 0000 0000 0000
10 bit 12 bit
10 bit
0 0
300
Posizione 0 in PT Posizione 300 in
PGD
dunque
Nell’elemento 300 di PGD deve andarci l’indirizzo di PT0 (contenuto nella variabile pg0 del kernel)
In PT0 ci vanno 1024 elementi che indirizzati attraverso il secondo campo che portano a 1024 tabelle (coprendo i primi 4MB dello spazio fisico)
All’elemento 301 di PGD deve andarci l’indirizzo di PT1 (contenuto nella variabile pg1 del kernel)
In PT1 ci vanno 1024 elementi che indirizzati attraverso il
secondo campo che portano a 1024 tabelle (coprendo i secondi
4MB fisico)
Riguardiamolo meglio
/*
* Enable paging
*/
3:
mov $swapper_pg_dir - PAGE_OFFSET,%eax
mov %eax,%cr3
/* set the page table pointer. */mov %cr0,%eax
or $0x80000000,%eax
mov %eax,%cr0
/* ..and set paging (PG) bit */jmp 1f
/* flush the prefetch-queue */1:
mov $1f,%eax
jmp *%eax
/* make sure eip is relocated */1:
mov $swapper_pg_dir-PAGE_OFFSET,%eax
swapper_pg_dir è la variabile nello spazio virtuale del kernel che corrisponde alla prima posizione di PGD
La differenza è lo scostamento di swapper_pg_dir rispetto alla base dello spazio kernel, ma anche rispetto a 0 (memoria reale)
Dunque l’effetto del mov è portare in eax l’indirizzo di memoria fisica su cui è mappata swapper_pg_dir (cioè PGD)
Quando EIP viene portato nello spazio kernel tutto è predisposto per la mappatura dei primi 8 MB di memoria fisica kernel sui primi 8 MB di memoria fisica
Successivamente le tabelle vengono riempite in modo definitivo
Lo stato delle cose all’avvio della traduzione
1K c0000000 Spazio virtuale
kernel
cr3
301 300
PGD
PT0 Puntatore alla pagina fisica 1023
Puntatore alla pagina fisica 0
0
… Lo stato delle cose all’avvio della traduzione
c0000000 Spazio virtuale
kernel
cr3
301 300
PGD
PT0
PT0 realizza la marcatura dei primi 4 MB
… Lo stato delle cose all’avvio della traduzione
c0000000 Spazio virtuale
kernel
PT1 cr3
301 300
PGD
PT1 realizza la marcatura dei secondi 4 MB
Ma c’è ancora un dettaglio importante !!!
Se non si prendono provvedimenti questa sequenza non funziona
mov %eax,%cr0
jmp 1f ;salto relativo
1:
mov $1f,%eax
jmp *%eax ;salto indiretto al kern 1:
Perché non funziona?
Che provvedimenti prendere?
….Ma c’è ancora un dettaglio importante
Dopo l’istruzione mov %eax,%cr0 la MMU è
abilitata, ma l’indirizzo in EIP è rimasto nel campo degli 8 MB bassi (perché il kernel qui si trova)
Fino ad ora era un indirizzo lineare che veniva preso come indirizzo fisico
Ora il medesimo indirizzo lineare è diventato virtuale e quindi subisce la traduzione
Dunque occorre anche la mappatura dagli 8 MB bassi dello spazio degli indirizzi agli 8 MB bassi della memoria fisica
Ciò viene ottenuto con altre due tabelle (PTB0 e PTB1) i cui descrittori stanno in PGD(0) e PGD(1)
Conclusione: in questa fase vengono create 4 PT (che
potranno essere modificate in seguito)
….Ma c’è ancora un dettaglio importante
Dopo l’istruzione mov %eax,%cr0 la MMU è
abilitata, ma l’indirizzo in EIP è rimasto nel campo degli 8 MB bassi (perché il kernel qui si trova)
Prima era un indirizzo lineare che veniva preso come indirizzo fisico
Ora il medesimo indirizzo lineare è diventato virtuale e quindi subisce la traduzione
Dunque occorre mappare gli (equivalenti) 8 MB bassi dello
spazio degli indirizzi virtuali sugli 8 MB bassi della memoria fisica
Ciò viene ottenuto con altre due tabelle (PTB0 e PTB1) i cui descrittori stanno in PGD(0) e PGD(1)
Conclusione: in questa fase occorrono 4 PT
(che potranno essere modificate in seguito)
Non è finita. Va bene per i salti, ma per i dati ???
La variabile V va in 101000
Ma l’istruzione mov V,%eax, è assemblata come mov 0xc0001000,%eax
Dunque l’istruzione non darebbe risultato corretto (se eseguita prima dell’abilitazione della paginazione)
Per dare risultato corretto V deve essere indirizzata in modo
“ relativo” rispetto alla base del blocco in cui si trova; ciò richiede:
Che in un registro venga caricato il valore della posizione in cui cade PAGE_OFFSET (nel caso specifico 100000)
Che a questo venga aggiunta la differenza V-PAGE_OFFSET
Che l’indirizzamento di V avvenga in modo indiretto attraverso questo registro
Va bene per i salti, ma per i dati ???
La variabile V va in 101000
Ma l’istruzione mov eax,V è assemblata come
mov eax, c0001000
Dunque l’istruzione non darebbe risultato corretto
Per dare risultato corretto V deve essere indirizzata in modo “ relativo” rispetto alla base del blocco in cui si trova; ciò richiede:
Che in un registro venga caricato il valore della posizione in cui cade PAGE_OFFSET (nel caso specifico 0x100000)
Che a questo venga aggiunta la differenza V-PAGE_OFFSET
Che l’indirizzamento di V avvenga in modo indiretto attraverso
questo registro
…ad esempio
PAGE_OFFSET EQU c0000000H
C0000000 org PAGE_OFFSET ;Prima posizione mov $,%ebx
:::
C0001000 V dw 2011 ;var inizializ :::
mov [%ebx + (V-PAGE_OFFSET)], %eax
Carica in EBX il contenuto del program counter (ovvero EIP).
Se questa istruzione è alla posizione XXXXXH carica XXXXXH
…ad esempio
PAGE_OFFSET EQU c0000000H
C0000000 org PAGE_OFFSET ;Prima posizione mov $,%ebx
:::
C0001000 V dw 2011 ;var inizializ :::
mov [%ebx + (V-PAGE_OFFSET)], %eax
Apparentemente carica in EAX il contenuto della posizione 0xC0001000. Ma siccome è attiva la traduzione degli indirizzi l’indirizzo viene mappato sullo spazio in cui è stato caricato il kernel, ovvero carica in EAX il contenuto di V
It’s magic!
Il kernel viene compilato per risiedere nell’ultimo dei 4 GB dello spazio virtuale, ma viene allocato nella parte bassa della memoria fisica
All’atto del caricamento in memoria del kernel la macchina è il modo reale (è un 8086)
Il kernel passa inizialmente al modo protetto (da questo momento non è più un 8086), ma senza attivare la paginazione
Fino all’abilitazione della paginazione tutti gli indirizzi (lineari) generati dal kernel ricadono nel campo di indirizzi ricoperto in memoria fisica
Subito dopo l’attivazione della paginazione un’istruzione rialloca EIP
Ma prima c’è un momento in cui EIP non è ancora “riallocato” è come se fosse in uno spazio virtuale nei primi 8 MB
Dopo la riallocazione gli indirizzi generati dal kernel sono nel suo spazio virtuale
Tabelle e strutture dati, manipolate nello spazio reale fino alla riallocazione, è come se venissero “trasferite” nello spazio virtuale
Vediamo un po’
Usiamo il comando cat /proc/<pid>/maps
cat comando unix che legge file specificati come parametri
proc è un file virtuale che contiene informazioni sui processi
pid indentifica il processo
maps chiede la mappa dell’occupazione in memoria (virtuale)
Come terzo elemento si sono altre possibilità (status, meminfo, cpuinfo, ..)
Nel seguito facciamo vedere cosa viene mostrato dopo aver dato due comandi bash (command processor) e
cat stesso e dopo aver avviato tre task gnometrics,
solitario e sudoko
bash e cat (sono comandi di shell)
bash
cat
Pid di bash
bash , cat (sono comandi di shell)
bash
cat
I txt partono dallo stesso indirizzo
gnometrics, solitario
Caricati di seguito e mantenuti attivi
sudoko
Come si vede nello spazio virtuale il segmento txt parte
sempre da 8048000
TASK
Come vengono realizzati
(questa parte è facoltativa)
Un task (processo)
E’ descritto nel kernel attraverso un process descriptor , denominato task_struct (circa 1,7 KB su macchine 32 bit) . Esso contiene una gran quantità di info, tra cui:
PID (process ID)
Stato del processo
Priorità
Puntatore al task che lo precede e al task che lo segue nella process list (che contiene tutti i task)
Puntatori ad altre strutture che descrivono file, e altro. Tra questi c’è mm il puntatore alla struttura mm_struct che, a sua volta, contiene puntatori a descrittori di aree di memoria
Puntatori, che individuano altre strutture, per esempio le mappe di memoria, i file associati, ecc.
Non vengono usati i meccanismi di gestione automatica del TSS (salvataggio ripristino task) di cui è dotata la logica delle CPU x86
Task list
Schema di descrizione della memoria
..più precisamente
Mappa della memoria
Mappa della memoria (virtuale di un task)
Occhio a questo: è il puntatore alla tabella di primo livello della PMT che in precedenza è stato indicato come swapper_pg_dir
Avvio di un processo
Quando viene avviato un nuovo processo gli viene assegnato uno spazio virtuale
Tale assegnazione è da intendersi come la copiatura logica dei
segmenti da file (ELF) a segmenti nella memoria virtuale, cioè le tabelle viste in precedenza
Non implica che venga immediatamente copiata una pagina fisica.
Ipoteticamente il sistema potrebbe non caricare nemmeno una pagina di codice
Quel che si richiede è che venga costruita la PMT, congruente con l’allocazione nello spazio virtuale, in modo che:
Quando il kernel passa il controllo al processo (saltando alla prima istruzione nello spazio virtuale del processo) viene generato un indirizzo che viene
tradotto attraverso la PMT
Se la pagina non c’è, ne consegue un page fault al quale fa seguito il caricamento della pagina, ecc..
(Ovviamente conviene caricare un po’ di pagine prima di dare il via)
Traduzione indirizzi
Se lavora a 3 livelli
Altri aspetti
della gestione della
memoria
Facciamo il punto
Lo spazio virtuale del kernel (a parte il kernel stesso e le strutture dati statiche) è sostanzialmente impiegato per la mappatura della memoria
Di norma il kernel (effettivo) sta a partire dalla posizione 0x100000 (secondo MB di RAM)
Le pagine di memoria fisica occupate dal codice del kernel e dalle strutture dati statiche del kernel (definite al tempo di compilazione) sono riservate (non possono essere allocate dinamicamente né possono essere swapped ) su disco.
Approssimativamente si tratta dei primi 2 MB fisici
Parte del primo MB fisico è riservata al BIOS (ROM+RAM)
Le restanti parti del primo MB possono essere usate dinamicamente
Le altre aree di RAM sono utilizzate dinamicamente dal meccanismo di paginazione
(Ovviamente ci stiamo sempre riferendo a Linux su x86)
Alcune complicazioni
Il kernel ha uno spazio di indirizzi di 1GB
Gli ultimi 128 KB sono riservati per mappature di I/O, restano 896 MB di indirizzamento mappabile in RAM
Se la RAM è minore di 896 MB, essa può essere mappata tutta sul kernel (meglio: lo spazio virtuale del kernel è non minore dello spazio fisico)
Per anni lo spazio massimo di memoria fisica che poteva essere manipolato dal kernel era quello che derivava da una tale
mappatura dello spazio virtuale
Se la RAM è tra 896 MB e 4 GB, o addirittura superiore a 4GB (PAE), lo spazio virtuale è minore dello spazio fisico; nello spazio virtuale del kernel non c’è posto per la mappatura di tutte le
pagine fisiche
Il kernel non può direttamente manipolare nel suo spazio virtuale gli oggetti che si trovino nello spazio fisico (oltre 896 MB)
High/low memory
Il sistema distingue tra low (sotto 896) and high (sopra 896) memory.
Low memory è quella per la quale esistono corrispondenti indirizzi logici nello spazio del kernel
High memory è quella per la quale non esistono indirizzi logici nello spazio del kernel
Le strutture dati del kernel vengono tendenzialmente tenute in pagine di low memory; mentre high memory tende a essere
utilizzata per mapparci gli spazi logici dei processi utente (3 GB, gestiti con normale traduzione)
Come viene gestita la high memory ?
Zone
La memoria fisica viene vista come suddivisa in “zone”:
ZONE_DMA: è un’area di 16 MB nella parte bassa (è quella su cui opera il DMA); le pagine in questa zona vengono usate per il
ZONE_NORMAL: tra 16 MB e A memory zone is composed of page frames or physical pages, which means that a page frame is allocated from a particular memory zone. Three memory
zones exist in Linux: ZONE_DMA (used for DMA page frames),
ZONE_NORMAL (non-DMA pages with virtual mapping), and
ZONE_HIGHMEM (pages whose addresses are not contained in
the virtual address space).
Descrittore di pagina
Il kernel tiene un descrittore di pagina (struct page) per ogni page frame. Un descrittore contiene:
Un insieme di flag
Un contatore count
Puntatori per l’impiego in varie liste (tra cui una lista circolare contenente tutti i descrittori di pagina, la lista LRU, …)
I descrittori sono tenuti in un vettore globale mem_map
Un descrittore occupa meno di 64 byte
1 MB (256 pagine da 4 KB) richiede circa 256*64 = 4 * 4 KB, ovvero circa 4 page frame per il suo mem_map
1 GB richiede 4 K page frame (ovvero 16 MB)
(grande spreco di memoria !!)
…flag
Indicatori individuali dello stato della pagina contenuti in un parola (32 bit). Tra di essi:
PG_locked: pagina è bloccata (coinvolta in una operazione di I/O
PG_dirty: pagina modificata
PG_lru: pagina nella lista delle pagine attive o inattive
PG_active : la pagina è nella lista delle pagine attive
PG_reserved: page frame riservato al kernel o non usabile (p.e.
ROM); non può subire azioni di swapping
.
…count
E’ il contatore di uso della pagina. Dà la stagionatura ( aging ) della pagina
Se vale 0 il page frame corrispondente è libero e può essere usato per qualunque processo o per lo stesso kernel. Se il suo valore è maggiore di 0 il frame è assegnato a un processo o contiene
(qualche struttura) dati del kernel
Quando una pagina viene allocata gli viene dato il valore 3
Ogni volta che viene toccata(*) il contatore è incrementato di 3 fino a un massimo di 20
Ogni volta che gira il kernel swap daemon (kswapd) decrementa di 1
Se il contatore giunge a zero la pagina diventa swappable (a meno che non appartenga al novero delle pagine riservate.
(i numeri precedenti sono quelli di default, possono essere variati)
(*) Ovvero quando viene osservata come toccata tra un page fault e il
successivo
…liste, puntatori
Tutti i descrittori di pagina stanno in una doppia lista circolare
Il campo virtual dà l’indirizzo virtuale su cui si mappa la pagina nello spazio kernel.
Per le pagine in low memory (sempre mappate): l’indirizzo virtuale della corrispondente pagina nello spazio del kernel
Per le pagine in high memory (possono non essere mappate): 0 se la pagina non è mappata, l’indirizzo virtuale nel kernel se è mappata
Per indirizzare oltre i primi 4GB fisici deve essere attivo PAE
La mappatura high memory viene effettuata attraverso una PT speciale (il cui indirizzo è nella variabile
pkmap_page_table)
Swapping
Dove viene copiato il contenuto di una pagina dirty che viene rimossa per fare posto?
Non nella sua posizione originale su disco (modificherebbe il process image che non sarebbe più quello) !!
Deve essere salvata da qualche altra parte, in modo che all’occorrenza venga ricaricata in memoria
C’è un’ area di swap (un file) sul quale viene copiata la pagina sporca per essere eventualmente riportata in memoria se serve ancora
Il corrispondente elemento di PMT tiene traccia che la pagina è nell’area di swap (in che punto entro il file), per poterla riprendere all’occorrenza
La gestione è affidata al kernel swap daemon
Altre questioni
Interruzioni (Linux)
Risposta attraverso gate in IDT ( Interrupt Descriptor Table ) con passaggio all’associato Interrupt Handler
Nell’architettura x86 ci sono tre tipi di gate che possono stare nella IDT: Task, Interrupt e Trap, con funzionalità simili ma diverse
Task Gate: contiene il selettore del TSS che deve rimpiazzare quello corrente (task switch). Linux NON adopra i TSS (usa task_struct come TCB) e quindi i task gate NON vengono usati
In modo x64 i Task gate non vengono più supportati dalla logica di CPU !!
Né è supportato il task switching !!!
Interrupt e trap gate (architettura x86)
Interrupt Gate: contiene il selettore del segmento e l’offset a cui saltare ( cioè l’indirizzo dell’handler ).
Se l’handler è a un livello più privilegiato del programma interrotto si ha uno stack switch:
SS e ESP vengono salvati sullo stack del livello privilegiato (è il caso di un processo user interrotto con chiamata all’handler in kernel)
Vengono salvati EFLAGS, CS e EIP
Viene azzerato IF (disabilitazione sistema interruzione)
Se l’handler è allo stesso livello del programma interrotto:
Vengono salvati EFLAGS, CS e EIP (sullo stack dell’interrotto)
Viene azzerato IF (disabilitazione sistema interruzione)
Trap Gate: simile a all’interrupt gate, eccetto che non
azzera IF
Uso gate (Linux)
Terminologia leggermente differente
Interrupt Gate: esattamente lo stesso significato architetturale
Sempre a livello 0 (DPL=0): inaccessibile ai task utente. Tutti gli interrupt handler sono raggiunti attraverso queste porte
Trap Gate: Significato di Trap architetturale
A livello 0 (DPL=0): inaccessibile ai task utente. La maggior parte degli handler delle eccezioni usano queste
System Gate: Significato di Trap architetturale, ma
A livello User (DPL=3): le relative eccezioni possono essere
attivate da processi utente. Esse corrispondono alle eccezioni 3,
4, 5 e 128 (INT 3 è per il breakpoint),
Risposta alle interruzioni
Una interruzione fa saltare all’interrupt handler
appropriato attraverso la porta di interruzione (in IDT)
Il passaggio attraverso la porta di interruzione determina automaticamente il salvataggio di EFLAG, CS, EIP, SS e ESP
NB non coinvolge il Task secondo la logica x86
In pratica non viene usato il TSS
In passato su usava il meccanismo di switching hardware
dell’architettura x86, dalla versione Linux 2.4 il salvataggio/ripristino è fatto a furia di PUSH/POP
Vengono salvati sullo stack tutti i registri che la logica di CPU
non salva automaticamente (EAX, EBX, …, ESI, EBP, ..)
...tuttavia
Un TSS è necessario per 2 motivi
Quando c’è un passaggio da user a kernel mode l’indirizzo dello stack del kernel viene preso dal TSS
Nel TSS ci sono SS e ESP dei livelli 0, 1 e 2
Quando uno task utente tenta di effettuare operazioni di I/O, la logica di CPU fa un test di protezione basato sull’ I/O Privilege Level (IOPL, contenuto in EFLAGS) e sull’ I/O permission bit map (contenuta nel TSS)
In sistemi multiprocessore (fisici o logici) c’è un TSS per
processore
GDT e LDT
Il Kernel non usa LDT, ma solo GDT
Viene definita una LDT di default
Di norma usata da tutti i processi utenti
Per poter far girare sw che usa le LDT (Wine) c’è la system call modify_ldt( ) con la quale un processo utente può crearsi una sua LDT che va a rimpiazzare quella di default in GDT
A che scopo ??
Conclusioni
L’architettura x86 ha un raffinato sistema di protezione a livelli
Linux rinuncia alla protezione a livelli e si limita ai soli spazio supervisore e spazio utente
Non usa i meccanismi di salvataggio/attivazione dei task della logica x86
Su macchine a 32 bit la segmentazione consentirebbe uno spazio virtuale di 64 TB
Linux usa un modello piatto, limitando lo spazio virtuale a soli 4 GB, con complicazioni notevoli quando lo spazio fisico è di 64GB