AOS - 17 - KERNEL MEMORY-MANAGEMENT IV
Lecture Info
Data:
Sito corso: link
Progresso unità: 4/5
Argomenti:
Homework #3
Core map
Memory Organization in UMA
Free lists
Buddy Allocator
Memory Management in NUMA
Mem-Policy
1 Homework #3: VTPMO
TODO.
2 Memory Management at Steady State
Per riuscire ad allocare e deallocare memoria in modo flessibile, dopo aver inizializzato e modificato la kernel page table, dobbiamo fare il setup di altre due strutture dati, che sono la core-map e le free lists.
La core-map è una struttura dati che mantiene informazioni sullo
stato della memoria fisica del sistema. In particolare è un array
di struct page
, o mem_map_t
. A ciascun frame sono, in particolare,
associate tre informazioni:
struct list_head list
, collega la entry ad altre entry dell'array per formare delle free lists.atomic_t count
, che contiene un contatore atomico per sapere quante pagine logiche sono state mappate in quel frame;unsigned long flags
, che contiene varie informazioni sul frame. I possibili valori per il campo flags sono i seguenti,La funzione
free_area_init()
viene utilizzata per inizializzare la core-map. Inizialmente tutti i frame hanno il bitPG_reserved
. Per togliere questo bit, e quindi per poter utilizzare i frame, si esegue la funzionemem_init()
.
3 Memory Organization in UMA Architectures
In una architettura UMA
, o NUMA
unaware, o single NUMA
zone, la
memoria fisica è suddivisa in tre zone, che sono definite in
include/linux/mmzone.h
e sono
ZONA_DMA (codice 0): Utilizzata per i dispositivi DMA, che storicamente non potevano indirizzare zone arbitrarie di memoria.
ZONE_NORMAL (codice 1): Zona mappa direttamente con le pagine del kernel.
ZONE_HIGHMEM (codice 2): Zona utilizzata per page cache e per le pagine dello user. In questa zona non si garantisce il direct mapping.
Ogni zona di memoria ha la sua free list.
4 Free Lists
Una free list è una struttura dati che ci permette di astrarre
rispetto alla visione della memoria offerta della core-map. In
particolare tramite le free lists siamo in grado di costruirci
particolari visioni della memoria. Le informazioni sulle free list
sono mantenute all'interno della struttura dati pg_data_t
, definita
nel file include/linux/mmzone.h
.
typedef struct pglist_data { // Array di zone_t. In particolare per ogni zona della memoria // abbiamo una struttura zone_t, che ci permette di vedere quella // zona di memoria in modi diversi. zone_t node_zones[MAX_NR_ZONES]; // ... // Pointer alla core-map che utilizziamo per fornire le visioni // della memoria ad un più alto livello. struct page *node_mem_map; // ... // Size della core-map che stiamo gestendo. unsigned long node_size; } pg_data_t;
La struttura invece zone_t
è definita sempre nel file
include/linux/mmzone.h
come segue
typedef struct zone_struct { spinlock_t lock; // Questo array ci permette di vedere in MAX_ORDER modi diversi lo // stato della memoria. Ciascun modo diverso rappresenta una // particolare free list. Questo meccanismo forma il cosiddetto // buddy allocator system. free_area_t free_area[MAX_ORDER]; struct page *zone_mem_map; } zone_t;
Notiamo che il lock serve per rendere le operazioni sulla memoria safe rispetto alla concorrenza. Presente anche nelle ultime versioni.
Osservazione: Lo studio di algoritmi non bloccanti per fare questo tipo di operazioni è un campo di ricerca importante.
4.1 Buddy Allocator System
Il buddy allocator system ci permette di vedere la memoria libera in modi diversi. In particolare posso vedere la memoria al livello dei singoli frame, oppure a livello di coppie di frame, oppure a livello di 4 frame liberi, e via dicendo. Ovviamente, se ho due frame, o li vedo come un blocco da due frame, oppure come due blocchi da un frame. Non posso mischiare queste viste, in quanto altrimenti dichiarerei che un frame è libero due volte.
La struttura free_area_t
structure è definita come segue
typedef struct free_area_struct { struct list_head list; unsigned int *map; // Buddies fragmentation state } free_area_t;
L'array free_area[] della struttura zone_t
contiene quindi
Un puntatore al primo frame associato a blocchi di un certo ordine.
Un pointer ad una bitmap che contiene informazioni di frammentazione della memora rispetto a quel particolare ordine.
Quando facciamo una allocazione o deallocazione, prendendo il lock, andiamo a bloccare tutte le viste della memoria.
5 Dealing With NUMA machines
Nelle architetture NUMA-aware per differenziare i vari nodi NUMA
utilizziamo la struttura struct pglist_data
. Questa struttura non è
altro che una lista, o un array, di pg_data_t
, e viene utilizzata
anche in caso di macchina UMA. Ciascuna struttura pg_data_t
gestisce
la visione di un particolare nodo NUMA.
Ogni nodo NUMA può avere il suo buddy allocator, e a seconda di quale API utilizziamo, possiamo specificare su quale nodo NUMA vogliamo eseguire le nostre operazioni.
6 Releasing Boot Memory
La funzione free_all_bootmem_core(pg_data_t *pgdat)
viene utilizzata
per rilasciare le pagine di memoria che erano state marcate come
boot memory only. Tale funziona utilizza inernamente la funzione
__free_page(page)
, che è la prima API del buddy allocator utilizzata
dal kernel.
7 Memory Allocation Contexts
Le allocazioni di memoria possono avvenire per due ragioni principali, che sono
Process context: In questo caso l'allocazione della memoria è legata alle attività di uno specifico processo. Questo tipo di allocazione permette quindi di portare il thread che richiede la memoria in stato di wait, per aspettare che l'allocazione può essere eseguita.
Interrupt: In questo caso l'allocazione della memoria è richiesta da un interrupt handler, e dunque non posso far aspettare l'handler. Devo quindi gestire l'allocazione in modo immediato, andando eventualmente a ritornare un fallimento, se non ho memoria disponibile.
8 Buddy-System API
Dopo il processo di start-up, la memoria può essere gestita tramite
delle particolari API, che gestiscono le strutture dati menzionate
precedentemente. I prototipi per queste API sono presenti in
<linux/malloc.h>
.
L'API di base è formata dalle seguenti funzioni,
get_zeroed_page(): Rimuove un frame dalla free list, azzera il frame scelto, e ritorna il suo indirizzo virtuale.
unsigned long get_zeroed_page(int flags);
__get_free_page(): Rimuove un frame dalla free list e ritorna il suo indirizzo virtuale.
unsigned long __get_free_page(int flags);
__get_free_pages(): Rimuove un blocco continuo di frame a seconda dell'order dato dalla free list e ritorna l'indirizzo virtuale del primo frame.
unsigned long __get_free_pages(int flags, unsigned long order);
free_page(): Inserisce il frame associato a quell'indirizzo virtuale all'interno della free list.
void free_page(unsigned long addr);
free_pages(): Inserisce un blocco di frames di un certo ordine all'interno della free list. Se sbagliando l'ordine, tra l'allocazione e la deallocazione, possiamo corrompere varie configurazioni del kernel, ed avere gravi errori nel sistema.
void free_pages(unsigned long addr, unsigned long order);
Le flags utilizzate sono le seguenti:
GFP_ATOMIC, la chiamata non può portare a situazioni bloccanti tipo sleep(). Questa flag viene utilizzata per allocazioni in contesti generati da interrupt.
GFP_USER, GFP_BUFFER, GFP_KERNEL, in questi casi invece la chiamata può portare a situazioni bloccanti.
Tutte le API del buddy-system ritornano indirizzi virtuali di pagine mappate in modo diretto. Conoscendo gli indirizzi virtuali possiamo quindi scoprire direttamente la posizione in memoria fisica dei relativi frames.
Dato poi che tutte le strutture dati core del sistema operativo vengono allocate dal buddy-system, ne segue che sono tutte allocate in modo diretto.
9 Mem-Policy
Dato che in una architettura NUMA abbiamo più buddy allocators, uno
per ogni nodo NUMA, come facciamo a stabilire quale buddy-allocator
utilizzare quando chiamiamo una API tipo free_pages()
? Questo
problema viene risolto attraverso il concetto di mem_policy.
Ogni thread in esecuzione ha delle meta-informazioni associate. Tra queste meta-informazioni troviamo anche la mem_policy, che specifica il nodo NUMA in cui le operazioni di memoria devono essere effettuate. Le API più esterne quindi utilizzano la mem policy per chiamare delle API più interne. In particolare per allocare delle pagine, l'API più interna è la
struct page *alloc_page_nodes(int nid, unsigned int flags, unsigned int order);
Notiamo che questa API prende un parametro extra, il parametro nid, che specifica il nodo NUMA all'interno della fare eseguire l'allocazione e quindi specifica il buddy-allocator da utilizzare.
Il valore della mem policy di default è la FIRST-TOUCH, che consiste nell'utilizzare il nodo NUMA associato alla CPU dove il thread sta girando.
9.1 Mem-Policy API
Per gestire la mempolicy il kernel ci offre la seguente API in <numaif.h>
set_mempolicy(): Utilizzata per settare la mem policy a livello del processo.
int set_mempolicy(int mode, unsigned long *nodemask, unsigned long maxnode);
A seconda della modalità, e a seconda dei nodi numa utilizzati (specificati tramite una bitmap), abbiamo una diversa gestione della memoria. La modalità può avere i seguenti valori:
MPOL_DEFAULT.
MPOL_BIND: utilizza solamente quei nodi numa.
MPOL_INTERLEAVE: round robin tra i numa nodes specificati nella bitmask.
MPOL_PREFERRED:pesca prima nei nodi numa specificati dalla bitmap, ma utilizza anche altri nodi in caso di necessità.
mbind(): Utilizzata per settare la NUMA mem policy a livello di indirizzi logici.
int mbind(void *addr, unsigned long len, int mode, unsigned long *nodemask, unsigned long maxnode, unsigned flags);
move_pages(): Utilizzata per muovere delle specifiche pagine di un processo ai nodi NUMA specificati. Il risultato dell'operazione viene indicato nella locazione di memoria puntata da status.
long move_pages(int pid, unsigned long count, void **pages, const int *nodes, int *status, int flags);
9.2 NUMA/numa-test.c
In questo esempio abbiamo quanto importante è ottimizzare l'utilizzo dei nodi NUMA in sistemi performanti.