Italian

Warning

In caso di dubbi sulla correttezza del contenuto di questa traduzione, l’unico riferimento valido è la documentazione ufficiale in inglese. Per maggiori informazioni consultate le avvertenze.

Tipologie di blocco e le loro istruzioni

Introduzione

Il kernel fornisce un certo numero di primitive di blocco che possiamo dividere in tre categorie:

  • blocchi ad attesa con sospensione

  • blocchi locali per CPU

  • blocchi ad attesa attiva

Questo documento descrive questi tre tipi e fornisce istruzioni su come annidarli, ed usarli su kernel PREEMPT_RT.

Categorie di blocchi

Blocchi ad attesa con sospensione

I blocchi ad attesa con sospensione possono essere acquisiti solo in un contesti dov’è possibile la prelazione.

Diverse implementazioni permettono di usare try_lock() anche in altri contesti, nonostante ciò è bene considerare anche la sicurezza dei corrispondenti unlock(). Inoltre, vanno prese in considerazione anche le varianti di debug di queste primitive. Insomma, non usate i blocchi ad attesa con sospensioni in altri contesti a meno che proprio non vi siano alternative.

In questa categoria troviamo:

  • mutex

  • rt_mutex

  • semaphore

  • rw_semaphore

  • ww_mutex

  • percpu_rw_semaphore

Nei kernel con PREEMPT_RT, i seguenti blocchi sono convertiti in blocchi ad attesa con sospensione:

  • local_lock

  • spinlock_t

  • rwlock_t

Blocchi locali per CPU

  • local_lock

Su kernel non-PREEMPT_RT, le funzioni local_lock gestiscono le primitive di disabilitazione di prelazione ed interruzioni. Al contrario di altri meccanismi, la disabilitazione della prelazione o delle interruzioni sono puri meccanismi per il controllo della concorrenza su una CPU e quindi non sono adatti per la gestione della concorrenza inter-CPU.

Blocchi ad attesa attiva

  • raw_spinlcok_t

  • bit spinlocks

Nei kernel non-PREEMPT_RT, i seguenti blocchi sono ad attesa attiva:

  • spinlock_t

  • rwlock_t

Implicitamente, i blocchi ad attesa attiva disabilitano la prelazione e le funzioni lock/unlock hanno anche dei suffissi per gestire il livello di protezione:

_bh()

disabilita / abilita bottom halves (interruzioni software)

_irq()

disabilita / abilita le interruzioni

_irqsave/restore()

salva e disabilita le interruzioni / ripristina ed attiva le interruzioni

Semantica del proprietario

Eccetto i semafori, i sopracitati tipi di blocchi hanno tutti una semantica molto stringente riguardo al proprietario di un blocco:

Il contesto (attività) che ha acquisito il blocco deve rilasciarlo

I semafori rw_semaphores hanno un’interfaccia speciale che permette anche ai non proprietari del blocco di rilasciarlo per i lettori.

rtmutex

I blocchi a mutua esclusione RT (rtmutex) sono un sistema a mutua esclusione con supporto all’ereditarietà della priorità (PI).

Questo meccanismo ha delle limitazioni sui kernel non-PREEMPT_RT dovuti alla prelazione e alle sezioni con interruzioni disabilitate.

Chiaramente, questo meccanismo non può avvalersi della prelazione su una sezione dove la prelazione o le interruzioni sono disabilitate; anche sui kernel PREEMPT_RT. Tuttavia, i kernel PREEMPT_RT eseguono la maggior parte delle sezioni in contesti dov’è possibile la prelazione, specialmente in contesti d’interruzione (anche software). Questa conversione permette a spinlock_t e rwlock_t di essere implementati usando rtmutex.

semaphore

La primitiva semaphore implementa un semaforo con contatore.

I semafori vengono spesso utilizzati per la serializzazione e l’attesa, ma per nuovi casi d’uso si dovrebbero usare meccanismi diversi, come mutex e completion.

semaphore e PREEMPT_RT

I kernel PREEMPT_RT non cambiano l’implementazione di semaphore perché non hanno un concetto di proprietario, dunque impediscono a PREEMPT_RT d’avere l’ereditarietà della priorità sui semafori. Un proprietario sconosciuto non può ottenere una priorità superiore. Di consequenza, bloccarsi sui semafori porta all’inversione di priorità.

rw_semaphore

Il blocco rw_semaphore è un meccanismo che permette più lettori ma un solo scrittore.

Sui kernel non-PREEMPT_RT l’implementazione è imparziale, quindi previene l’inedia dei processi scrittori.

Questi blocchi hanno una semantica molto stringente riguardo il proprietario, ma offre anche interfacce speciali che permettono ai processi non proprietari di rilasciare un processo lettore. Queste interfacce funzionano indipendentemente dalla configurazione del kernel.

rw_semaphore e PREEMPT_RT

I kernel PREEMPT_RT sostituiscono i rw_semaphore con un’implementazione basata su rt_mutex, e questo ne modifica l’imparzialità:

Dato che uno scrittore rw_semaphore non può assicurare la propria priorità ai suoi lettori, un lettore con priorità più bassa che ha subito la prelazione continuerà a trattenere il blocco, quindi porta all’inedia anche gli scrittori con priorità più alta. Per contro, dato che i lettori possono garantire la propria priorità agli scrittori, uno scrittore a bassa priorità che subisce la prelazione vedrà la propria priorità alzata finché non rilascerà il blocco, e questo preverrà l’inedia dei processi lettori a causa di uno scrittore.

local_lock

I local_lock forniscono nomi agli ambiti di visibilità delle sezioni critiche protette tramite la disattivazione della prelazione o delle interruzioni.

Sui kernel non-PREEMPT_RT le operazioni local_lock si traducono nell’abilitazione o disabilitazione della prelazione o le interruzioni.

local_lock(&llock)

preempt_disable()

local_unlock(&llock)

preempt_enable()

local_lock_irq(&llock)

local_irq_disable()

local_unlock_irq(&llock)

local_irq_enable()

local_lock_irqsave(&llock)

local_irq_save()

local_unlock_irqrestore(&llock)

local_irq_restore()

Gli ambiti di visibilità con nome hanno due vantaggi rispetto alle primitive di base:

  • Il nome del blocco permette di fare un’analisi statica, ed è anche chiaro su cosa si applichi la protezione cosa che invece non si può fare con le classiche primitive in quanto sono opache e senza alcun ambito di visibilità.

  • Se viene abilitato lockdep, allora local_lock ottiene un lockmap che permette di verificare la bontà della protezione. Per esempio, questo può identificare i casi dove una funzione usa preempt_disable() come meccanismo di protezione in un contesto d’interruzione (anche software). A parte questo, lockdep_assert_held(&llock) funziona come tutte le altre primitive di sincronizzazione.

local_lock e PREEMPT_RT

I kernel PREEMPT_RT sostituiscono local_lock con uno spinlock_t per CPU, quindi ne cambia la semantica:

  • Tutte le modifiche a spinlock_t si applicano anche a local_lock

L’uso di local_lock

I local_lock dovrebbero essere usati su kernel non-PREEMPT_RT quando la disabilitazione della prelazione o delle interruzioni è il modo più adeguato per gestire l’accesso concorrente a strutture dati per CPU.

Questo meccanismo non è adatto alla protezione da prelazione o interruzione su kernel PREEMPT_RT dato che verrà convertito in spinlock_t.

raw_spinlock_t e spinlock_t

raw_spinlock_t

I blocco raw_spinlock_t è un blocco ad attesa attiva su tutti i tipi di kernel, incluso quello PREEMPT_RT. Usate raw_spinlock_t solo in sezioni critiche nel cuore del codice, nella gestione delle interruzioni di basso livello, e in posti dove è necessario disabilitare la prelazione o le interruzioni. Per esempio, per accedere in modo sicuro lo stato dell’hardware. A volte, i raw_spinlock_t possono essere usati quando la sezione critica è minuscola, per evitare gli eccessi di un rtmutex.

spinlock_t

Il significato di spinlock_t cambia in base allo stato di PREEMPT_RT.

Sui kernel non-PREEMPT_RT, spinlock_t si traduce in un raw_spinlock_t ed ha esattamente lo stesso significato.

spinlock_t e PREEMPT_RT

Sui kernel PREEMPT_RT, spinlock_t ha un’implementazione dedicata che si basa sull’uso di rt_mutex. Questo ne modifica il significato:

  • La prelazione non viene disabilitata.

  • I suffissi relativi alla interruzioni (_irq, _irqsave / _irqrestore) per le operazioni spin_lock / spin_unlock non hanno alcun effetto sullo stato delle interruzioni della CPU.

  • I suffissi relativi alle interruzioni software (_bh()) disabilitano i relativi gestori d’interruzione.

    I kernel non-PREEMPT_RT disabilitano la prelazione per ottenere lo stesso effetto.

    I kernel PREEMPT_RT usano un blocco per CPU per la serializzazione, il che permette di tenere attiva la prelazione. Il blocco disabilita i gestori d’interruzione software e previene la rientranza vista la prelazione attiva.

A parte quanto appena discusso, i kernel PREEMPT_RT preservano il significato di tutti gli altri aspetti di spinlock_t:

  • Le attività che trattengono un blocco spinlock_t non migrano su altri processori. Disabilitando la prelazione, i kernel non-PREEMPT_RT evitano la migrazione. Invece, i kernel PREEMPT_RT disabilitano la migrazione per assicurarsi che i puntatori a variabili per CPU rimangano validi anche quando un’attività subisce la prelazione.

  • Lo stato di un’attività si mantiene durante le acquisizioni del blocco al fine di garantire che le regole basate sullo stato delle attività si possano applicare a tutte le configurazioni del kernel. I kernel non-PREEMPT_RT lasciano lo stato immutato. Tuttavia, la funzionalità PREEMPT_RT deve cambiare lo stato se l’attività si blocca durante l’acquisizione. Dunque, salva lo stato attuale prima di bloccarsi ed il rispettivo risveglio lo ripristinerà come nell’esempio seguente:

    task->state = TASK_INTERRUPTIBLE
     lock()
       block()
         task->saved_state = task->state
         task->state = TASK_UNINTERRUPTIBLE
         schedule()
                                        lock wakeup
                                          task->state = task->saved_state
    

    Altri tipi di risvegli avrebbero impostato direttamente lo stato a RUNNING, ma in questo caso non avrebbe funzionato perché l’attività deve rimanere bloccata fintanto che il blocco viene trattenuto. Quindi, lo stato salvato viene messo a RUNNING quando il risveglio di un non-blocco cerca di risvegliare un’attività bloccata in attesa del rilascio di uno spinlock. Poi, quando viene completata l’acquisizione del blocco, il suo risveglio ripristinerà lo stato salvato, in questo caso a RUNNING:

    task->state = TASK_INTERRUPTIBLE
     lock()
       block()
         task->saved_state = task->state
         task->state = TASK_UNINTERRUPTIBLE
         schedule()
                                        non lock wakeup
                                          task->saved_state = TASK_RUNNING
    
                                        lock wakeup
                                          task->state = task->saved_state
    

    Questo garantisce che il vero risveglio non venga perso.

rwlock_t

Il blocco rwlock_t è un meccanismo che permette più lettori ma un solo scrittore.

Sui kernel non-PREEMPT_RT questo è un blocco ad attesa e per i suoi suffissi si applicano le stesse regole per spinlock_t. La sua implementazione è imparziale, quindi previene l’inedia dei processi scrittori.

rwlock_t e PREEMPT_RT

Sui kernel PREEMPT_RT rwlock_t ha un’implementazione dedicata che si basa sull’uso di rt_mutex. Questo ne modifica il significato:

  • Tutte le modifiche fatte a spinlock_t si applicano anche a rwlock_t.

  • Dato che uno scrittore rw_semaphore non può assicurare la propria priorità ai suoi lettori, un lettore con priorità più bassa che ha subito la prelazione continuerà a trattenere il blocco, quindi porta all’inedia anche gli scrittori con priorità più alta. Per contro, dato che i lettori possono garantire la propria priorità agli scrittori, uno scrittore a bassa priorità che subisce la prelazione vedrà la propria priorità alzata finché non rilascerà il blocco, e questo preverrà l’inedia dei processi lettori a causa di uno scrittore.

Precisazioni su PREEMPT_RT

local_lock su RT

Sui kernel PREEMPT_RT Ci sono alcune implicazioni dovute alla conversione di local_lock in un spinlock_t. Per esempio, su un kernel non-PREEMPT_RT il seguente codice funzionerà come ci si aspetta:

local_lock_irq(&local_lock);
raw_spin_lock(&lock);

ed è equivalente a:

raw_spin_lock_irq(&lock);

Ma su un kernel PREEMPT_RT questo codice non funzionerà perché local_lock_irq() si traduce in uno spinlock_t per CPU che non disabilita né le interruzioni né la prelazione. Il seguente codice funzionerà su entrambe i kernel con o senza PREEMPT_RT:

local_lock_irq(&local_lock);
spin_lock(&lock);

Un altro dettaglio da tenere a mente con local_lock è che ognuno di loro ha un ambito di protezione ben preciso. Dunque, la seguente sostituzione è errate:

func1()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock_1, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_1, flags);
}

func2()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock_2, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_2, flags);
}

func3()
{
  lockdep_assert_irqs_disabled();
  access_protected_data();
}

Questo funziona correttamente su un kernel non-PREEMPT_RT, ma su un kernel PREEMPT_RT local_lock_1 e local_lock_2 sono distinti e non possono serializzare i chiamanti di func3(). L’assert di lockdep verrà attivato su un kernel PREEMPT_RT perché local_lock_irqsave() non disabilita le interruzione a casa della specifica semantica di spinlock_t in PREEMPT_RT. La corretta sostituzione è:

func1()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}

func2()
{
  local_irq_save(flags);    -> local_lock_irqsave(&local_lock, flags);
  func3();
  local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}

func3()
{
  lockdep_assert_held(&local_lock);
  access_protected_data();
}

spinlock_t e rwlock_t

Ci sono alcune conseguenze di cui tener conto dal cambiamento di semantica di spinlock_t e rwlock_t sui kernel PREEMPT_RT. Per esempio, sui kernel non PREEMPT_RT il seguente codice funziona come ci si aspetta:

local_irq_disable();
spin_lock(&lock);

ed è equivalente a:

spin_lock_irq(&lock);

Lo stesso vale per rwlock_t e le varianti con _irqsave().

Sui kernel PREEMPT_RT questo codice non funzionerà perché gli rtmutex richiedono un contesto con la possibilità di prelazione. Al suo posto, usate spin_lock_irq() o spin_lock_irqsave() e le loro controparti per il rilascio. I kernel PREEMPT_RT offrono un meccanismo local_lock per i casi in cui la disabilitazione delle interruzioni ed acquisizione di un blocco devono rimanere separati. Acquisire un local_lock àncora un processo ad una CPU permettendo cose come un’acquisizione di un blocco con interruzioni disabilitate per singola CPU.

Il tipico scenario è quando si vuole proteggere una variabile di processore nel contesto di un thread:

struct foo *p = get_cpu_ptr(&var1);

spin_lock(&p->lock);
p->count += this_cpu_read(var2);

Questo codice è corretto su un kernel non-PREEMPT_RT, ma non lo è su un PREEMPT_RT. La modifica della semantica di spinlock_t su PREEMPT_RT non permette di acquisire p->lock perché, implicitamente, get_cpu_ptr() disabilita la prelazione. La seguente sostituzione funzionerà su entrambe i kernel:

struct foo *p;

migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);

La funzione migrate_disable() assicura che il processo venga tenuto sulla CPU corrente, e di conseguenza garantisce che gli accessi per-CPU alle variabili var1 e var2 rimangano sulla stessa CPU fintanto che il processo rimane prelabile.

La sostituzione con migrate_disable() non funzionerà nel seguente scenario:

func()
{
  struct foo *p;

  migrate_disable();
  p = this_cpu_ptr(&var1);
  p->val = func2();

Questo non funziona perché migrate_disable() non protegge dal ritorno da un processo che aveva avuto il diritto di prelazione. Una sostituzione più adatta per questo caso è:

func()
{
  struct foo *p;

  local_lock(&foo_lock);
  p = this_cpu_ptr(&var1);
  p->val = func2();

Su un kernel non-PREEMPT_RT, questo codice protegge dal rientro disabilitando la prelazione. Su un kernel PREEMPT_RT si ottiene lo stesso risultato acquisendo lo spinlock di CPU.

raw_spinlock_t su RT

Acquisire un raw_spinlock_t disabilita la prelazione e possibilmente anche le interruzioni, quindi la sezione critica deve evitare di acquisire uno spinlock_t o rwlock_t. Per esempio, la sezione critica non deve fare allocazioni di memoria. Su un kernel non-PREEMPT_RT il seguente codice funziona perfettamente:

raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);

Ma lo stesso codice non funziona su un kernel PREEMPT_RT perché l’allocatore di memoria può essere oggetto di prelazione e quindi non può essere chiamato in un contesto atomico. Tuttavia, si può chiamare l’allocatore di memoria quando si trattiene un blocco non-raw perché non disabilitano la prelazione sui kernel PREEMPT_RT:

spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);

bit spinlocks

I kernel PREEMPT_RT non possono sostituire i bit spinlock perché un singolo bit è troppo piccolo per farci stare un rtmutex. Dunque, la semantica dei bit spinlock è mantenuta anche sui kernel PREEMPT_RT. Quindi, le precisazioni fatte per raw_spinlock_t valgono anche qui.

In PREEMPT_RT, alcuni bit spinlock sono sostituiti con normali spinlock_t usando condizioni di preprocessore in base a dove vengono usati. Per contro, questo non serve quando si sostituiscono gli spinlock_t. Invece, le condizioni poste sui file d’intestazione e sul cuore dell’implementazione della sincronizzazione permettono al compilatore di effettuare la sostituzione in modo trasparente.

Regole d’annidamento dei tipi di blocchi

Le regole principali sono:

  • I tipi di blocco appartenenti alla stessa categoria possono essere annidati liberamente a patto che si rispetti l’ordine di blocco al fine di evitare stalli.

  • I blocchi con sospensione non possono essere annidati in blocchi del tipo CPU locale o ad attesa attiva

  • I blocchi ad attesa attiva e su CPU locale possono essere annidati nei blocchi ad attesa con sospensione.

  • I blocchi ad attesa attiva possono essere annidati in qualsiasi altro tipo.

Queste limitazioni si applicano ad entrambe i kernel con o senza PREEMPT_RT.

Il fatto che un kernel PREEMPT_RT cambi i blocchi spinlock_t e rwlock_t dal tipo ad attesa attiva a quello con sospensione, e che sostituisca local_lock con uno spinlock_t per CPU, significa che non possono essere acquisiti quando si è in un blocco raw_spinlock. Ne consegue il seguente ordine d’annidamento:

  1. blocchi ad attesa con sospensione

  2. spinlock_t, rwlock_t, local_lock

  3. raw_spinlock_t e bit spinlocks

Se queste regole verranno violate, allora lockdep se ne accorgerà e questo sia con o senza PREEMPT_RT.