AOS - 18 - KERNEL MEMORY-MANAGEMENT V


Lecture Info

  • Data: [2019-11-04 lun]

  • Sito corso: link

  • Slides: AOS - 3 KERNEL LEVEL MEMORY MANAGEMENT

  • Progresso unità: 5/5

  • Argomenti:

    • Quicklists

    • SLAB allocators

    • Kernel page remapping and TLB

  • Introduzione: Nella scorsa lezione avevamo introdotto il buddy system per l'allocazione e la deallocazione di pagine di memoria fisica (frames) tramite uno schema di direct-mapping. Il buddy system però, per garantire la sincronizzazione della visione della memoria tra CPU-core diversi, utilizza dei meccanismi di spinlock. Questo rende l'allocaziona e la deallocazione molto dispendiosa, specialmente per zone di memoria che vengono richieste e rilasciate molto frequentemente.

1 Quicklists

Il buddy system non scala rispetto al numero di CPU-core che possiamo ospitare su un dato NUMA node. Per migliorare questo problema sono stati introdotti degli allocatori di più alto livello. Inizialmente questi allocatori venivano utilizzati solamente per specifiche strutture dati, come ad esempio le page tables. Attualmente sono disponibili delle API per allocare e deallocare della memoria in modo veloce e flessibile.

Queste API funzionano utilizzando le API dei buddy allocators per fare del "pre-reserving" della memoria. In particolare si ottengono delle pagine logiche dal buddy allocators e queste pagine vengono gestite non più al livello del buddy allocators ma ad un più alto livello. Queste free list prendono il nome di quicklists.

Per le allocazioni delle page tables sono presenti delle API che utilizzano questo concetto di quicklists, che sono

  pgd_alloc(), pgd_free()
  pmd_alloc(), pmd_free()
  pte_alloc(), pte_free()

In linux la struttura della quicklist è definita nel file include/linux/quicklist.h . Se la quicklist è piena, il buddy system viene utilizzato per aggiornare lo stato della quicklist.


1.1 Quicklist Implementation

Notiamo che queste quicklists sono specifiche per i vari CPU-core. L'idea principale di una quicklist è che, essendo specifica per un CPU-core, il sistema può allocare e deallocare memoria senza eseguire operazioni di sincronizzazione, in quanto le informazioni sono viste da un singolo CPU-core.

Per implementare questo sistema quindi il puntatore alla quicklist deve risiedere in una per-CPU memory. Così facendo in maniera automatica ogni CPU-core può andare a prendere della memoria dalla sua quicklist.


1.2 Quicklist API

L'API interna per fare allocazione utilizzando una quicklist è la quicklist_alloc() , ed è implementata come segue

static inline void *quicklist_alloc(int nr, gfp_t flags, ...)
{
  struct quicklist *q;
  void **p = NULL;

  // quicklist contains a list of lists, thus we use nr to get a
  // particular list.
  //
  // NOTE: Since this block of code in which we manipulate the
  // quicklist pointed by q is not atomic, we have to make sure that
  // during its execution we always refer to the same pointer. In
  // particular we have to make sure that during this code we will not
  // get de-scheduled and begin execution in another CPU-core.
  //
  // TODO: understand how this is done later, when we'll do task
  // management. 
  q = &get_cpu_var(quicklist)[nr];
  p = q->page; // Buffer to deliver

  // Get first page from quicklist if it exists
  if( likely(p) )
    {
      q->page = p[0];
      p[0] = NULL;
      q->nr_pages--;
    }
  
  // NOTE: Ends the section started with get_cpu_var(). From this
  // point forward we can be de-scheduled into any other CPU-cores. 
  put_cpu_var(quicklist);

  if( likely (p) )
    return p;

  // Use buddy-system API in case quicklist is empty.
  p = (void *) __get_free_page(flags | __GFP_ZERO);
  return p;
}

2 Logical to Physical Addr Translation

The following macros can be used to translate between logical and physical addresses for directly mapped memory pages.

// in include/asm-i386/io.h
virt_to_phys(unsigned int addr);
phys_to_virt(unsigned int addr);

// in generic kernel versions
__pa(virtual_addr);
__va(physical_addr); 

3 BUDDY-VS-PER-CPU-QUICK-LIST/allocators.c

Questo running example ci permette di analizzare la differenza in performance tra l'utilizzo di quicklists e l'utilizzo del buddy system. Il kernel utilizzato è 3.x , in quanto nei nuovi kernel è difficile misurare la differenza in performance.

// Used to allocate memory. If LOCAL is defined, then we use the
// quicklists, otherwise we use the buddy system.
asmlinkage void* get_buffer(void){
  void* the_addr = NULL;
#ifdef LOCAL
  int *x;
  int cpu_id;

  // NOTE: This is used to understand if the CPU-core that has
  // started the module entered already once this function. Note
  // that since we are using a static variable, we are not able
  // to run this program un multiple cores, because after one
  // core has entered the function, all other cores will see
  // set=1, even though they have not yet initialized the
  // variable me with the proper value.
  static int set = 0;

  if(set == 0){
    x = this_cpu_ptr(&me);
    *x = smp_processor_id();
    set = 1;
  }
  cpu_id = this_cpu_read(me);
  the_addr = lists[cpu_id];
  lists[cpu_id] = *((void**)the_addr);

#else
  // Use the buddy system. 
  the_addr = __get_free_pages(GFP_KERNEL,0);
#endif
  DEBUG
    printk("%s: get_buffer called - allocation returned address %p\n",MODNAME,the_addr);
  if(the_addr == NULL){
    return NULL;
  }     

  return the_addr;
}

4 SLUB Allocator

Il kernel linux offre infine degli allocatori a grana più piccola di una pagina. In particolare le APIs principali sono definite in <linux/malloc.h> , e sono riportate a seguire. Notiamo che tutti i buffer ottenuti da questo sottosistema sono cache alligned. Idealmente dovrebbero essere utilizzati per allocare taglie di memoria piccola.

  • kmalloc(): Equivalente ad una malloc() a livello applicativo.

    void *kmalloc(size_t size, gfp_t flags);
    
  • kfree(): Equivalente ad una free() a livello applicativo.

    void *kfree(void *obj);
    
  • kzalloc(): Ci assicura che la memoria viene azzerata.

    void *kzalloc(size_t size, gfp_t flags);
    
  • kmalloc_node(): Le funzione viste prima sono NUMA-unaware, e si affidano, tramite l'utilizzo delle mem-policy, a delle API di livello più basso, tra cui troviamo la seguente.

    void *kmalloc_node(size_t size, int flags, int node);
    

Se vogliamo allocare memoria di un size più grande, possiamo invece utilizzare le seguenti APIs.

  • vmalloc(): Alloca della memoria di un dato size. La memoria allocata non necessariamente è continua. La funzione ritorna l'indirizzo virtuale della pagina.

    void *vmalloc(unsigned long size);
    
  • vfree(): Libera zone di memoria allocate con una vmalloc()

    void vreee(void *addr);
    

Notiamo che l'utilizzo della vmalloc può cambiare la kernel page table, e quindi cambia lo stato della TLB. In generale le pagine mappate tramite vmalloc non sono necessariamente direct-mapped.

La tabella seguente riporta le differenze principali tra una vmalloc() e una kmalloc()

5 Kernel Page and TLB State

Notiamo che la kernel page table ha una natura "globale", nel senso che ogni core può utilizzare la stessa kernel page table. Quando eseguiamo servizi come vmalloc() e vfree() , che possono potenzialmente modificare il contenuto della kernel page table, ci dobbiamo quindi porre la seguente domanda:

Quando un CPU-core modifica il contenuto di una page table, il firmware è in grado di capire che la modifica sta avendo luogo in modo da modificare il contenuto della TLB? Detto altrimenti, qual è il livello di automazione nella gestione dello stato della TLB?

Nelle architetture moderne l'automazione di riallineamento della TLB rispetto lo stato della page table è solo parziale. Per esempio in x86 il firmware cambia automaticamente lo stato della TLB solamente quando il registro CR3 viene modificato. Questa modifica però viene fatta solamente nel CPU-core che ha modificato il registro CR3. Se al posto di cambiare page table ne vogliamo solamente modificare il contenuto, aggiornando comunque la TLB, possiamo

  1. Modificare la page table.

  2. Riscrivere in CR3 lo stesso valore di prima.

Questo aggiornamento però verrà eseguito solamente su un solo CPU-core.

In generale quindi se l'architetture non supporta questo tipo di automazione sta software del kernel il compito di gestire l'allineamento dello stato del TLB rispetto allo stato della page table.


5.1 Types of TLB Events

A seconda della scala:

  • Global: quando vengono modificate entry relative a indirizzi virtuali accessibili da tutti i CPU-core in real-time-concurrency. (memoria kernel)

  • Local: quando vengono modificate entry relative a indirizzi virtuali accessibile in time-sharing concurrency. (memoria user)

A seconda della tipologia:

  • Virtual to physical address remapping

  • Virtual address access rule modification (read only vs write access)


5.2 TLB Flush Costs

Nelle architetture moderne per riuscire a reallinare lo stato della TLB allo stato della page table l'idea è quella di invalidare il contenuto della TLB. Invalidare il contenuto della TLB, ovviamente, porta ad una perdita di performance. In particolare troviamo i seguenti costi:

  • Costi Diretti:

    1. La latenza necessaria al protocollo utilizzato per invalidare le entry della TLB. Questa varia a seconda del protocollo utilizzato, che può essere selective o non-selective. In x86, quando riscriviamo C3, stiamo invalidando tutte le entry della TLB.

    2. La latenza per coordinate i vari CPU-core in caso di un global TLB flush. In x86 il firmware non offre metodi per coordinare automaticamente i vari CPU-core a fare il flush della TLB. L'architettura permette solamente ad un singolo core di flushare la propria TLB. La coordinazione sarà quindi svolta dal software.

  • Costi Indiretti:

    1. Il refill della TLB, dopo aver flushata. Questo costo dipende da vari fattori, tra cui quante entry ho invalidato.


5.3 Linux TLB Flush APIs

Linux offre varie funzioni per flushare la TLB, tra cui troviamo le seguenti

  • flush_tbl_all: Questa funzione flusha l'intero contenuto della TLB su tutti i processori in esecuzione nel sistema. Una volta completa, le modifiche alla page table sono rese globali.

    void flush_tb_all(void)
    

    Viene utilizzata quando si modifica la kernel page table, in quanto ha una natura "globale" all'interno del sistema.

    É l'operazione di flush più costosa.

    vmalloc() e vfree() possono eseguire questo tipo di flush in caso di modifiche alla kernel page table. Come abbiamo già detto, x86 non offre una operazione livello macchina per fare questa operazione. L'operazione è quindi implementata utilizzando un meccanismo più generale di coordinamento tra CPU-cores.

  • flush_tlb_mm(): Questa API fa il flush delle entry della TLB che riguardano la porzione user-space del richiesto mm (memory map) context.

    void flush_tlb_mm(struct mm_struct *mm)
    

    Viene utilizzata quando ho applicazioni multi-threaded che girano su diversi CPU-core. Viene utilizzata anche durante la duplicazione di uno spazio di memoria con una dup_mmap() (utilizzata durante la fork() ) oppure dopo una exit_mmap() .

    Questa API lavora su un contesto locale ma richiede comunque la coordinazione dei vari CPU-cores. Ai vari CPU-cores viene passata la struttura mm, e i vari CPU-cores verificano se il loro CR3 sta puntando alla page table modificata.

  • flush_tlb_range(): Questa API permette di flushare le entry della memory map mm che entrano in un certo intervallo.

    void flush_tlb_range(struct mm_struct *mm, unsigned long start, unsigned long end)
    
  • flush_tlb_page(): Questa API è la più selettiva, e permette di flushare una singola pagina all'interno della TLB. x86 offre l'istruzione INVLPG m per invalidare la TLB entry per la pagina che contiene l'indirizzo di memoria m.

    void flush_tlb_page(struct vm_area_struct *vma, unsigned long addr)
    
  • flush_tlb_pgtables(): In alcune architetture, come Sparc64 , esistono delle cache apposite per le page table stesse. Questa API offerta da linux permette quindi di flushare determinate entry nelle cache delle page table, se l'architettura permette di farlo.

    void flush_tlb_pgtables(struct mm_struct *mm, unsigned long start, unsigned long end)
    
  • update_mm_cache(): Alcune architetture permettono il preloading delle entries della TLB ( ARM Cortex ). Questa API permette di fare un pre-loading di entry nella TLB. Questo costrutto può essere utile per motivi di performance, in quanto diminuisce la latenza e la varianza per l'accesso alla memoria.

    void update_mm_cache(struct vm_area_struct *vma, unsigned long addr, pte_t pte)