doc:it_IT: first translation for locking/

To begin with:
    - locking/index.rst
    - locking/lockdep-design.rst
    - locking/lockstat.rst
    - locking/lockturture.rst
    - locking/locktypes.rst

And RCU/torture.rst to avoid broken references.

Signed-off-by: Federico Vaga <federico.vaga@vaga.pv.it>
Signed-off-by: Jonathan Corbet <corbet@lwn.net>
Link: https://lore.kernel.org/r/20240106233820.30454-1-federico.vaga@vaga.pv.it
This commit is contained in:
Federico Vaga 2024-01-07 00:38:20 +01:00 committed by Jonathan Corbet
parent 91a3d6be99
commit 6151b9c8f2
10 changed files with 2058 additions and 1 deletions

View File

@ -318,7 +318,7 @@ Suppose that a previous kvm.sh run left its output in this directory::
tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28 tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28
Then this run can be re-run without rebuilding as follow: Then this run can be re-run without rebuilding as follow::
kvm-again.sh tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28 kvm-again.sh tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28

View File

@ -0,0 +1,19 @@
.. SPDX-License-Identifier: GPL-2.0
.. _it_rcu_concepts:
===============
Concetti su RCU
===============
.. toctree::
:maxdepth: 3
torture
.. only:: subproject and html
Indici
======
* :ref:`genindex`

View File

@ -0,0 +1,369 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
=============================================
Le operazioni RCU per le verifiche *torture*
=============================================
CONFIG_RCU_TORTURE_TEST
=======================
L'opzione CONFIG_RCU_TORTURE_TEST è disponibile per tutte le implementazione di
RCU. L'opzione creerà un modulo rcutorture che potrete caricare per avviare le
verifiche. La verifica userà printk() per riportare lo stato, dunque potrete
visualizzarlo con dmesg (magari usate grep per filtrare "torture"). Le verifiche
inizieranno al caricamento, e si fermeranno alla sua rimozione.
I parametri di modulo hanno tutti il prefisso "rcutortute.", vedere
Documentation/admin-guide/kernel-parameters.txt.
Rapporto
========
Il rapporto sulle verifiche si presenta nel seguente modo::
rcu-torture:--- Start of test: nreaders=16 nfakewriters=4 stat_interval=30 verbose=0 test_no_idle_hz=1 shuffle_interval=3 stutter=5 irqreader=1 fqs_duration=0 fqs_holdoff=0 fqs_stutter=3 test_boost=1/0 test_boost_interval=7 test_boost_duration=4
rcu-torture: rtc: (null) ver: 155441 tfle: 0 rta: 155441 rtaf: 8884 rtf: 155440 rtmbe: 0 rtbe: 0 rtbke: 0 rtbre: 0 rtbf: 0 rtb: 0 nt: 3055767
rcu-torture: Reader Pipe: 727860534 34213 0 0 0 0 0 0 0 0 0
rcu-torture: Reader Batch: 727877838 17003 0 0 0 0 0 0 0 0 0
rcu-torture: Free-Block Circulation: 155440 155440 155440 155440 155440 155440 155440 155440 155440 155440 0
rcu-torture:--- End of test: SUCCESS: nreaders=16 nfakewriters=4 stat_interval=30 verbose=0 test_no_idle_hz=1 shuffle_interval=3 stutter=5 irqreader=1 fqs_duration=0 fqs_holdoff=0 fqs_stutter=3 test_boost=1/0 test_boost_interval=7 test_boost_duration=4
Sulla maggior parte dei sistemi questo rapporto si produce col comando "dmesg |
grep torture:". Su configurazioni più esoteriche potrebbe essere necessario
usare altri comandi per visualizzare i messaggi di printk(). La funzione
printk() usa KERN_ALERT, dunque i messaggi dovrebbero essere ben visibili. ;-)
La prima e l'ultima riga mostrano i parametri di module di rcutorture, e solo
sull'ultima riga abbiamo il risultato finale delle verifiche effettuate che può
essere "SUCCESS" (successo) or "FAILURE" (insuccesso).
Le voci sono le seguenti:
* "rtc": L'indirizzo in esadecimale della struttura attualmente visibile dai
lettori.
* "ver": Il numero di volte dall'avvio che il processo scrittore di RCU ha
cambiato la struttura visible ai lettori.
* "tfle": se non è zero, indica la lista di strutture "torture freelist" da
mettere in "rtc" è vuota. Questa condizione è importante perché potrebbe
illuderti che RCU stia funzionando mentre invece non è il caso. :-/
* "rta": numero di strutture allocate dalla lista "torture freelist".
* "rtaf": il numero di allocazioni fallite dalla lista "torture freelist" a
causa del fatto che fosse vuota. Non è inusuale che sia diverso da zero, ma è
un brutto segno se questo numero rappresenta una frazione troppo alta di
"rta".
* "rtf": il numero di rilasci nella lista "torture freelist"
* "rtmbe": Un valore diverso da zero indica che rcutorture crede che
rcu_assign_pointer() e rcu_dereference() non funzionino correttamente. Il
valore dovrebbe essere zero.
* "rtbe": un valore diverso da zero indica che le funzioni della famiglia
rcu_barrier() non funzionano correttamente.
* "rtbke": rcutorture è stato capace di creare dei kthread real-time per forzare
l'inversione di priorità di RCU. Il valore dovrebbe essere zero.
* "rtbre": sebbene rcutorture sia riuscito a creare dei kthread capaci di
forzare l'inversione di priorità, non è riuscito però ad impostarne la
priorità real-time al livello 1. Il valore dovrebbe essere zero.
* "rtbf": Il numero di volte che è fallita la promozione della priorità per
risolvere un'inversione.
* "rtb": Il numero di volte che rcutorture ha provato a forzare l'inversione di
priorità. Il valore dovrebbe essere diverso da zero Se state verificando la
promozione della priorità col parametro "test_bootst".
* "nt": il numero di volte che rcutorture ha eseguito codice lato lettura
all'interno di un gestore di *timer*. Questo valore dovrebbe essere diverso da
zero se avete specificato il parametro "irqreader".
* "Reader Pipe": un istogramma dell'età delle strutture viste dai lettori. RCU
non funziona correttamente se una qualunque voce, dalla terza in poi, ha un
valore diverso da zero. Se dovesse succedere, rcutorture stampa la stringa
"!!!" per renderlo ben visibile. L'età di una struttura appena creata è zero,
diventerà uno quando sparisce dalla visibilità di un lettore, e incrementata
successivamente per ogni periodo di grazia; infine rilasciata dopo essere
passata per (RCU_TORTURE_PIPE_LEN-2) periodi di grazia.
L'istantanea qui sopra è stata presa da una corretta implementazione di RCU.
Se volete vedere come appare quando non funziona, sbizzarritevi nel romperla.
;-)
* "Reader Batch": un istogramma di età di strutture viste dai lettori, ma
conteggiata in termini di lotti piuttosto che periodi. Anche qui dalla terza
voce in poi devono essere zero. La ragione d'esistere di questo rapporto è che
a volte è più facile scatenare un terzo valore diverso da zero qui piuttosto
che nella lista "Reader Pipe".
* "Free-Block Circulation": il numero di strutture *torture* che hanno raggiunto
un certo punto nella catena. Il primo numero dovrebbe corrispondere
strettamente al numero di strutture allocate; il secondo conta quelle rimosse
dalla vista dei lettori. Ad eccezione dell'ultimo valore, gli altri
corrispondono al numero di passaggi attraverso il periodo di grazia. L'ultimo
valore dovrebbe essere zero, perché viene incrementato solo se il contatore
della struttura torture viene in un qualche modo incrementato oltre il
normale.
Una diversa implementazione di RCU potrebbe fornire informazioni aggiuntive. Per
esempio, *Tree SRCU* fornisce anche la seguente riga::
srcud-torture: Tree SRCU per-CPU(idx=0): 0(35,-21) 1(-4,24) 2(1,1) 3(-26,20) 4(28,-47) 5(-9,4) 6(-10,14) 7(-14,11) T(1,6)
Questa riga mostra lo stato dei contatori per processore, in questo caso per
*Tree SRCU*, usando un'allocazione dinamica di srcu_struct (dunque "srcud-"
piuttosto che "srcu-"). I numeri fra parentesi sono i valori del "vecchio"
contatore e di quello "corrente" per ogni processore. Il valore "idx" mappa
questi due valori nell'array, ed è utile per il *debug*. La "T" finale contiene
il valore totale dei contatori.
Uso su specifici kernel
=======================
A volte può essere utile eseguire RCU torture su un kernel già compilato, ad
esempio quando lo si sta per mettere in proeduzione. In questo caso, il kernel
dev'essere compilato con CONFIG_RCU_TORTUE_TEST=m, cosicché le verifiche possano
essere avviate usano modprobe e terminate con rmmod.
Per esempio, potreste usare questo script::
#!/bin/sh
modprobe rcutorture
sleep 3600
rmmod rcutorture
dmesg | grep torture:
Potete controllare il rapporto verificando manualmente la presenza del marcatore
di errore "!!!". Ovviamente, siete liberi di scriverne uno più elaborato che
identifichi automaticamente gli errori. Il comando "rmmod" forza la stampa di
"SUCCESS" (successo), "FAILURE" (fallimento), o "RCU_HOTPLUG". I primi due sono
autoesplicativi; invece, l'ultimo indica che non son stati trovati problemi in
RCU, tuttavia ci sono stati problemi con CPU-hotplug.
Uso sul kernel di riferimento
=============================
Quando si usa rcutorture per verificare modifiche ad RCU stesso, spesso è
necessario compilare un certo numero di kernel usando configurazioni diverse e
con parametri d'avvio diversi. In questi casi, usare modprobe ed rmmod potrebbe
richiedere molto tempo ed il processo essere suscettibile ad errori.
Dunque, viene messo a disposizione il programma
tools/testing/selftests/rcutorture/bin/kvm.sh per le architetture x86, arm64 e
powerpc. Di base, eseguirà la serie di verifiche elencate in
tools/testing/selftests/rcutorture/configs/rcu/CFLIST. Ognuna di queste verrà
eseguita per 30 minuti in una macchina virtuale con uno spazio utente minimale
fornito da un initrd generato automaticamente. Al completamento, gli artefatti
prodotti e i messaggi vengono analizzati alla ricerca di errori, ed i risultati
delle esecuzioni riassunti in un rapporto.
Su grandi sistemi, le verifiche di rcutorture posso essere velocizzare passano a
kvm.sh l'argomento --cpus. Per esempio, su un sistema a 64 processori, "--cpus
43" userà fino a 43 processori per eseguire contemporaneamente le verifiche. Su
un kernel v5.4 per eseguire tutti gli scenari in due serie, riduce il tempo
d'esecuzione da otto ore a un'ora (senza contare il tempo per compilare sedici
kernel). L'argomento "--dryrun sched" non eseguirà verifiche, piuttosto vi
informerà su come queste verranno organizzate in serie. Questo può essere utile
per capire quanti processori riservare per le verifiche in --cpus.
Non serve eseguire tutti gli scenari di verifica per ogni modifica. Per esempio,
per una modifica a Tree SRCU potete eseguire gli scenari SRCU-N e SRCU-P. Per
farlo usate l'argomento --configs di kvm.sh in questo modo: "--configs 'SRCU-N
SRCU-P'". Su grandi sistemi si possono eseguire più copie degli stessi scenari,
per esempio, un hardware che permette di eseguire 448 thread, può eseguire 5
istanze complete contemporaneamente. Per farlo::
kvm.sh --cpus 448 --configs '5*CFLIST'
Oppure, lo stesso sistema, può eseguire contemporaneamente 56 istanze dello
scenario su otto processori::
kvm.sh --cpus 448 --configs '56*TREE04'
O ancora 28 istanze per ogni scenario su otto processori::
kvm.sh --cpus 448 --configs '28*TREE03 28*TREE04'
Ovviamente, ogni esecuzione utilizzerà della memoria. Potete limitarne l'uso con
l'argomento --memory, che di base assume il valore 512M. Per poter usare valori
piccoli dovrete disabilitare le verifiche *callback-flooding* usando il
parametro --bootargs che vedremo in seguito.
A volte è utile avere informazioni aggiuntive di debug, in questo caso potete
usare il parametro --kconfig, per esempio, ``--kconfig
'CONFIG_RCU_EQS_DEBUG=y'``. In aggiunta, ci sono i parametri --gdb, --kasan, and
kcsan. Da notare che --gdb vi limiterà all'uso di un solo scenario per
esecuzione di kvm.sh e richiede di avere anche un'altra finestra aperta dalla
quale eseguire ``gdb`` come viene spiegato dal programma.
Potete passare anche i parametri d'avvio del kernel, per esempio, per
controllare i parametri del modulo rcutorture. Per esempio, per verificare
modifiche del codice RCU CPU stall-warning, usate ``bootargs
'rcutorture.stall_cpu=30``. Il programma riporterà un fallimento, ossia il
risultato della verifica. Come visto in precedenza, ridurre la memoria richiede
la disabilitazione delle verifiche *callback-flooding*::
kvm.sh --cpus 448 --configs '56*TREE04' --memory 128M \
--bootargs 'rcutorture.fwd_progress=0'
A volte tutto quello che serve è una serie completa di compilazioni del kernel.
Questo si ottiene col parametro --buildonly.
Il parametro --duration sovrascrive quello di base di 30 minuti. Per esempio,
con ``--duration 2d`` l'esecuzione sarà di due giorni, ``--duraction 5min`` di
cinque minuti, e ``--duration 45s`` di 45 secondi. L'ultimo può essere utile per
scovare rari errori nella sequenza d'avvio.
Infine, il parametro --trust-make permette ad ogni nuova compilazione del kernel
di riutilizzare tutto il possibile da quelle precedenti. Da notare che senza il
parametro --trust-make, i vostri file di *tag* potrebbero essere distrutti.
Ci sono altri parametri più misteriosi che sono documentati nel codice sorgente
dello programma kvm.sh.
Se un'esecuzione contiene degli errori, il loro numero durante la compilazione e
all'esecuzione verranno elencati alla fine fra i risultati di kvm.sh (che vi
consigliamo caldamente di reindirizzare verso un file). I file prodotti dalla
compilazione ed i risultati stampati vengono salvati, usando un riferimento
temporale, nelle cartella tools/testing/selftests/rcutorture/res. Una cartella
di queste cartelle può essere fornita a kvm-find-errors.sh per estrarne gli
errori. Per esempio::
tools/testing/selftests/rcutorture/bin/kvm-find-errors.sh \
tools/testing/selftests/rcutorture/res/2020.01.20-15.54.23
Tuttavia, molto spesso è più conveniente aprire i file direttamente. I file
riguardanti tutti gli scenari di un'esecuzione di trovano nella cartella
principale (2020.01.20-15.54.23 nell'esempio precedente), mentre quelli
specifici per scenario si trovano in sotto cartelle che prendono il nome dello
scenario stesso (per esempio, "TREE04"). Se un dato scenario viene eseguito più
di una volta (come abbiamo visto con "--configs '56*TREE04'"), allora dalla
seconda esecuzione in poi le sottocartelle includeranno un numero di
progressione, per esempio "TREE04.2", "TREE04.3", e via dicendo.
Il file solitamente più usato nella cartella principale è testid.txt. Se la
verifica viene eseguita in un repositorio git, allora questo file conterrà il
*commit* sul quale si basano le verifiche, mentre tutte le modifiche non
registrare verranno mostrate in formato diff.
I file solitamente più usati nelle cartelle di scenario sono:
.config
Questo file contiene le opzioni di Kconfig
Make.out
Questo file contiene il risultato di compilazione per uno specifico scenario
console.log
Questo file contiene il risultato d'esecuzione per uno specifico scenario.
Questo file può essere esaminato una volta che il kernel è stato avviato,
ma potrebbe non esistere se l'avvia non è fallito.
vmlinux
Questo file contiene il kernel, e potrebbe essere utile da esaminare con
programmi come pbjdump e gdb
Ci sono altri file, ma vengono usati meno. Molti sono utili all'analisi di
rcutorture stesso o dei suoi programmi.
Nel kernel v5.4, su un sistema a 12 processori, un'esecuzione senza errori
usando gli scenari di base produce il seguente risultato::
SRCU-N ------- 804233 GPs (148.932/s) [srcu: g10008272 f0x0 ]
SRCU-P ------- 202320 GPs (37.4667/s) [srcud: g1809476 f0x0 ]
SRCU-t ------- 1122086 GPs (207.794/s) [srcu: g0 f0x0 ]
SRCU-u ------- 1111285 GPs (205.794/s) [srcud: g1 f0x0 ]
TASKS01 ------- 19666 GPs (3.64185/s) [tasks: g0 f0x0 ]
TASKS02 ------- 20541 GPs (3.80389/s) [tasks: g0 f0x0 ]
TASKS03 ------- 19416 GPs (3.59556/s) [tasks: g0 f0x0 ]
TINY01 ------- 836134 GPs (154.84/s) [rcu: g0 f0x0 ] n_max_cbs: 34198
TINY02 ------- 850371 GPs (157.476/s) [rcu: g0 f0x0 ] n_max_cbs: 2631
TREE01 ------- 162625 GPs (30.1157/s) [rcu: g1124169 f0x0 ]
TREE02 ------- 333003 GPs (61.6672/s) [rcu: g2647753 f0x0 ] n_max_cbs: 35844
TREE03 ------- 306623 GPs (56.782/s) [rcu: g2975325 f0x0 ] n_max_cbs: 1496497
CPU count limited from 16 to 12
TREE04 ------- 246149 GPs (45.5831/s) [rcu: g1695737 f0x0 ] n_max_cbs: 434961
TREE05 ------- 314603 GPs (58.2598/s) [rcu: g2257741 f0x2 ] n_max_cbs: 193997
TREE07 ------- 167347 GPs (30.9902/s) [rcu: g1079021 f0x0 ] n_max_cbs: 478732
CPU count limited from 16 to 12
TREE09 ------- 752238 GPs (139.303/s) [rcu: g13075057 f0x0 ] n_max_cbs: 99011
Ripetizioni
===========
Immaginate di essere alla caccia di un raro problema che si verifica all'avvio.
Potreste usare kvm.sh, tuttavia questo ricompilerebbe il kernel ad ogni
esecuzione. Se avete bisogno di (diciamo) 1000 esecuzioni per essere sicuri di
aver risolto il problema, allora queste inutili ricompilazioni possono diventare
estremamente fastidiose.
Per questo motivo esiste kvm-again.sh.
Immaginate che un'esecuzione precedente di kvm.sh abbia lasciato i suoi
artefatti nella cartella::
tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28
Questa esecuzione può essere rieseguita senza ricompilazioni::
kvm-again.sh tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28
Alcuni dei parametri originali di kvm.sh possono essere sovrascritti, in
particolare --duration e --bootargs. Per esempio::
kvm-again.sh tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28 \
--duration 45s
rieseguirebbe il test precedente, ma solo per 45 secondi, e quindi aiutando a
trovare quel raro problema all'avvio sopracitato.
Esecuzioni distribuite
======================
Sebbene kvm.sh sia utile, le sue verifiche sono limitate ad un singolo sistema.
Non è poi così difficile usare un qualsiasi ambiente di sviluppo per eseguire
(diciamo) 5 istanze di kvm.sh su altrettanti sistemi, ma questo avvierebbe
inutili ricompilazioni del kernel. In aggiunta, il processo di distribuzione
degli scenari di verifica per rcutorture sui sistemi disponibili richiede
scrupolo perché soggetto ad errori.
Per questo esiste kvm-remote.sh.
Se il seguente comando funziona::
ssh system0 date
e funziona anche per system1, system2, system3, system4, e system5, e tutti
questi sistemi hanno 64 CPU, allora potere eseguire::
kvm-remote.sh "system0 system1 system2 system3 system4 system5" \
--cpus 64 --duration 8h --configs "5*CFLIST"
Questo compilerà lo scenario di base sul sistema locale, poi lo distribuirà agli
altri cinque sistemi elencati fra i parametri, ed eseguirà ogni scenario per
otto ore. Alla fine delle esecuzioni, i risultati verranno raccolti, registrati,
e stampati. La maggior parte dei parametri di kvm.sh possono essere usati con
kvm-remote.sh, tuttavia la lista dei sistemi deve venire sempre per prima.
L'argomento di kvm.sh ``--dryrun scenarios`` può essere utile per scoprire
quanti scenari potrebbero essere eseguiti in gruppo di sistemi.
Potete rieseguire anche una precedente esecuzione remota come abbiamo già fatto
per kvm.sh::
kvm-remote.sh "system0 system1 system2 system3 system4 system5" \
tools/testing/selftests/rcutorture/res/2022.11.03-11.26.28-remote \
--duration 24h
In questo caso, la maggior parte dei parametri di kvm-again.sh possono essere
usati dopo il percorso alla cartella contenente gli artefatti dell'esecuzione da
ripetere.

View File

@ -10,6 +10,18 @@ Utilità di base
symbol-namespaces symbol-namespaces
Primitive di sincronizzazione
=============================
Come Linux impedisce che tutto si verifichi contemporaneamente. Consultate
Documentation/translations/it_IT/locking/index.rst per maggiorni informazioni
sul tema.
.. toctree::
:maxdepth: 1
../RCU/index
.. only:: subproject and html .. only:: subproject and html
Indices Indices

View File

@ -91,6 +91,7 @@ interfacciarsi con il resto del kernel.
:maxdepth: 1 :maxdepth: 1
core-api/index core-api/index
Sincronizzazione nel kernel <locking/index>
Strumenti e processi per lo sviluppo Strumenti e processi per lo sviluppo
==================================== ====================================

View File

@ -0,0 +1,20 @@
.. SPDX-License-Identifier: GPL-2.0
================
Sincronizzazione
================
.. toctree::
:maxdepth: 1
locktypes
lockdep-design
lockstat
locktorture
.. only:: subproject and html
Indici
======
* :ref:`genindex`

View File

@ -0,0 +1,678 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
Validatore di sincronizzazione durante l'esecuzione
===================================================
Classi di blocchi
-----------------
L'oggetto su cui il validatore lavora è una "classe" di blocchi.
Una classe di blocchi è un gruppo di blocchi che seguono le stesse regole di
sincronizzazione, anche quando i blocchi potrebbero avere più istanze (anche
decine di migliaia). Per esempio un blocco nella struttura inode è una classe,
mentre ogni inode sarà un'istanza di questa classe di blocco.
Il validatore traccia lo "stato d'uso" di una classe di blocchi e le sue
dipendenze con altre classi. L'uso di un blocco indica come quel blocco viene
usato rispetto al suo contesto d'interruzione, mentre le dipendenze di un blocco
possono essere interpretate come il loro ordine; per esempio L1 -> L2 suggerisce
che un processo cerca di acquisire L2 mentre già trattiene L1. Dal punto di
vista di lockdep, i due blocchi (L1 ed L2) non sono per forza correlati: quella
dipendenza indica solamente l'ordine in cui sono successe le cose. Il validatore
verifica permanentemente la correttezza dell'uso dei blocchi e delle loro
dipendenze, altrimenti ritornerà un errore.
Il comportamento di una classe di blocchi viene costruito dall'insieme delle sue
istanze. Una classe di blocco viene registrata alla creazione della sua prima
istanza, mentre tutte le successive istanze verranno mappate; dunque, il loro
uso e le loro dipendenze contribuiranno a costruire quello della classe. Una
classe di blocco non sparisce quando sparisce una sua istanza, ma può essere
rimossa quando il suo spazio in memoria viene reclamato. Per esempio, questo
succede quando si rimuove un modulo, o quando una *workqueue* viene eliminata.
Stato
-----
Il validatore traccia l'uso cronologico delle classi di blocchi e ne divide
l'uso in categorie (4 USI * n STATI + 1).
I quattro USI possono essere:
- 'sempre trattenuto nel contesto <STATO>'
- 'sempre trattenuto come blocco di lettura nel contesto <STATO>'
- 'sempre trattenuto con <STATO> abilitato'
- 'sempre trattenuto come blocco di lettura con <STATO> abilitato'
gli `n` STATI sono codificati in kernel/locking/lockdep_states.h, ad oggi
includono:
- hardirq
- softirq
infine l'ultima categoria è:
- 'sempre trattenuto' [ == !unused ]
Quando vengono violate le regole di sincronizzazione, questi bit di utilizzo
vengono presentati nei messaggi di errore di sincronizzazione, fra parentesi
graffe, per un totale di `2 * n` (`n`: bit STATO). Un esempio inventato::
modprobe/2287 is trying to acquire lock:
(&sio_locks[i].lock){-.-.}, at: [<c02867fd>] mutex_lock+0x21/0x24
but task is already holding lock:
(&sio_locks[i].lock){-.-.}, at: [<c02867fd>] mutex_lock+0x21/0x24
Per un dato blocco, da sinistra verso destra, la posizione del bit indica l'uso
del blocco e di un eventuale blocco di lettura, per ognuno degli `n` STATI elencati
precedentemente. Il carattere mostrato per ogni bit indica:
=== ===========================================================================
'.' acquisito con interruzioni disabilitate fuori da un contesto d'interruzione
'-' acquisito in contesto d'interruzione
'+' acquisito con interruzioni abilitate
'?' acquisito in contesto d'interruzione con interruzioni abilitate
=== ===========================================================================
Il seguente esempio mostra i bit::
(&sio_locks[i].lock){-.-.}, at: [<c02867fd>] mutex_lock+0x21/0x24
||||
||| \-> softirq disabilitati e fuori da un contesto di softirq
|| \--> acquisito in un contesto di softirq
| \---> hardirq disabilitati e fuori da un contesto di hardirq
\----> acquisito in un contesto di hardirq
Per un dato STATO, che il blocco sia mai stato acquisito in quel contesto di
STATO, o che lo STATO sia abilitato, ci lascia coi quattro possibili scenari
mostrati nella seguente tabella. Il carattere associato al bit indica con
esattezza in quale scenario ci si trova al momento del rapporto.
+---------------+---------------+------------------+
| | irq abilitati | irq disabilitati |
+---------------+---------------+------------------+
| sempre in irq | '?' | '-' |
+---------------+---------------+------------------+
| mai in irq | '+' | '.' |
+---------------+---------------+------------------+
Il carattere '-' suggerisce che le interruzioni sono disabilitate perché
altrimenti verrebbe mostrato il carattere '?'. Una deduzione simile può essere
fatta anche per '+'
I blocchi inutilizzati (ad esempio i mutex) non possono essere fra le cause di
un errore.
Regole dello stato per un blocco singolo
----------------------------------------
Avere un blocco sicuro in interruzioni (*irq-safe*) significa che è sempre stato
usato in un contesto d'interruzione, mentre un blocco insicuro in interruzioni
(*irq-unsafe*) significa che è sempre stato acquisito con le interruzioni
abilitate.
Una classe softirq insicura è automaticamente insicura anche per hardirq. I
seguenti stati sono mutualmente esclusivi: solo una può essere vero quando viene
usata una classe di blocco::
<hardirq-safe> o <hardirq-unsafe>
<softirq-safe> o <softirq-unsafe>
Questo perché se un blocco può essere usato in un contesto di interruzioni
(sicuro in interruzioni), allora non può mai essere acquisito con le
interruzioni abilitate (insicuro in interruzioni). Altrimenti potrebbe
verificarsi uno stallo. Per esempio, questo blocco viene acquisito, ma prima di
essere rilasciato il contesto d'esecuzione viene interrotto nuovamente, e quindi
si tenterà di acquisirlo nuovamente. Questo porterà ad uno stallo, in
particolare uno stallo ricorsivo.
Il validatore rileva e riporta gli usi di blocchi che violano queste regole per
blocchi singoli.
Regole per le dipendenze di blocchi multipli
--------------------------------------------
La stessa classe di blocco non deve essere acquisita due volte, questo perché
potrebbe portare ad uno blocco ricorsivo e dunque ad uno stallo.
Inoltre, due blocchi non possono essere trattenuti in ordine inverso::
<L1> -> <L2>
<L2> -> <L1>
perché porterebbe ad uno stallo - chiamato stallo da blocco inverso - in cui si
cerca di trattenere i due blocchi in un ciclo in cui entrambe i contesti
aspettano per sempre che l'altro termini. Il validatore è in grado di trovare
queste dipendenze cicliche di qualsiasi complessità, ovvero nel mezzo ci
potrebbero essere altre sequenze di blocchi. Il validatore troverà se questi
blocchi possono essere acquisiti circolarmente.
In aggiunta, le seguenti sequenze di blocco nei contesti indicati non sono
permesse, indipendentemente da quale che sia la classe di blocco::
<hardirq-safe> -> <hardirq-unsafe>
<softirq-safe> -> <softirq-unsafe>
La prima regola deriva dal fatto che un blocco sicuro in interruzioni può essere
trattenuto in un contesto d'interruzione che, per definizione, ha la possibilità
di interrompere un blocco insicuro in interruzioni; questo porterebbe ad uno
stallo da blocco inverso. La seconda, analogamente, ci dice che un blocco sicuro
in interruzioni software potrebbe essere trattenuto in un contesto di
interruzione software, dunque potrebbe interrompere un blocco insicuro in
interruzioni software.
Le suddette regole vengono applicate per qualsiasi sequenza di blocchi: quando
si acquisiscono nuovi blocchi, il validatore verifica se vi è una violazione
delle regole fra il nuovo blocco e quelli già trattenuti.
Quando una classe di blocco cambia stato, applicheremo le seguenti regole:
- se viene trovato un nuovo blocco sicuro in interruzioni, verificheremo se
abbia mai trattenuto dei blocchi insicuri in interruzioni.
- se viene trovato un nuovo blocco sicuro in interruzioni software,
verificheremo se abbia trattenuto dei blocchi insicuri in interruzioni
software.
- se viene trovato un nuovo blocco insicuro in interruzioni, verificheremo se
abbia trattenuto dei blocchi sicuri in interruzioni.
- se viene trovato un nuovo blocco insicuro in interruzioni software,
verificheremo se abbia trattenuto dei blocchi sicuri in interruzioni
software.
(Di nuovo, questi controlli vengono fatti perché un contesto d'interruzione
potrebbe interrompere l'esecuzione di qualsiasi blocco insicuro portando ad uno
stallo; questo anche se lo stallo non si verifica in pratica)
Eccezione: dipendenze annidate sui dati portano a blocchi annidati
------------------------------------------------------------------
Ci sono alcuni casi in cui il kernel Linux acquisisce più volte la stessa
istanza di una classe di blocco. Solitamente, questo succede quando esiste una
gerarchia fra oggetti dello stesso tipo. In questi casi viene ereditato
implicitamente l'ordine fra i due oggetti (definito dalle proprietà di questa
gerarchia), ed il kernel tratterrà i blocchi in questo ordine prefissato per
ognuno degli oggetti.
Un esempio di questa gerarchia di oggetti che producono "blocchi annidati" sono
i *block-dev* che rappresentano l'intero disco e quelli che rappresentano una
sua partizione; la partizione è una parte del disco intero, e l'ordine dei
blocchi sarà corretto fintantoche uno acquisisce il blocco del disco intero e
poi quello della partizione. Il validatore non rileva automaticamente questo
ordine implicito, perché queste regole di sincronizzazione non sono statiche.
Per istruire il validatore riguardo a questo uso corretto dei blocchi sono stati
introdotte nuove primitive per specificare i "livelli di annidamento". Per
esempio, per i blocchi a mutua esclusione dei *block-dev* si avrebbe una
chiamata simile a::
enum bdev_bd_mutex_lock_class
{
BD_MUTEX_NORMAL,
BD_MUTEX_WHOLE,
BD_MUTEX_PARTITION
};
mutex_lock_nested(&bdev->bd_contains->bd_mutex, BD_MUTEX_PARTITION);
In questo caso la sincronizzazione viene fatta su un *block-dev* sapendo che si
tratta di una partizione.
Ai fini della validazione, il validatore lo considererà con una - sotto - classe
di blocco separata.
Nota: Prestate estrema attenzione che la vostra gerarchia sia corretta quando si
vogliono usare le primitive _nested(); altrimenti potreste avere sia falsi
positivi che falsi negativi.
Annotazioni
-----------
Si possono utilizzare due costrutti per verificare ed annotare se certi blocchi
devono essere trattenuti: lockdep_assert_held*(&lock) e
lockdep_*pin_lock(&lock).
Come suggerito dal nome, la famiglia di macro lockdep_assert_held* asseriscono
che un dato blocco in un dato momento deve essere trattenuto (altrimenti, verrà
generato un WARN()). Queste vengono usate abbondantemente nel kernel, per
esempio in kernel/sched/core.c::
void update_rq_clock(struct rq *rq)
{
s64 delta;
lockdep_assert_held(&rq->lock);
[...]
}
dove aver trattenuto rq->lock è necessario per aggiornare in sicurezza il clock
rq.
L'altra famiglia di macro è lockdep_*pin_lock(), che a dire il vero viene usata
solo per rq->lock ATM. Se per caso un blocco non viene trattenuto, queste
genereranno un WARN(). Questo si rivela particolarmente utile quando si deve
verificare la correttezza di codice con *callback*, dove livelli superiori
potrebbero assumere che un blocco rimanga trattenuto, ma livelli inferiori
potrebbero invece pensare che il blocco possa essere rilasciato e poi
riacquisito (involontariamente si apre una sezione critica). lockdep_pin_lock()
restituisce 'struct pin_cookie' che viene usato da lockdep_unpin_lock() per
verificare che nessuno abbia manomesso il blocco. Per esempio in
kernel/sched/sched.h abbiamo::
static inline void rq_pin_lock(struct rq *rq, struct rq_flags *rf)
{
rf->cookie = lockdep_pin_lock(&rq->lock);
[...]
}
static inline void rq_unpin_lock(struct rq *rq, struct rq_flags *rf)
{
[...]
lockdep_unpin_lock(&rq->lock, rf->cookie);
}
I commenti riguardo alla sincronizzazione possano fornire informazioni utili,
tuttavia sono le verifiche in esecuzione effettuate da queste macro ad essere
vitali per scovare problemi di sincronizzazione, ed inoltre forniscono lo stesso
livello di informazioni quando si ispeziona il codice. Nel dubbio, preferite
queste annotazioni!
Dimostrazione di correttezza al 100%
------------------------------------
Il validatore verifica la proprietà di chiusura in senso matematico. Ovvero, per
ogni sequenza di sincronizzazione di un singolo processo che si verifichi almeno
una volta nel kernel, il validatore dimostrerà con una certezza del 100% che
nessuna combinazione e tempistica di queste sequenze possa causare uno stallo in
una qualsiasi classe di blocco. [1]_
In pratica, per dimostrare l'esistenza di uno stallo non servono complessi
scenari di sincronizzazione multi-processore e multi-processo. Il validatore può
dimostrare la correttezza basandosi sulla sola sequenza di sincronizzazione
apparsa almeno una volta (in qualunque momento, in qualunque processo o
contesto). Uno scenario complesso che avrebbe bisogno di 3 processori e una
sfortunata presenza di processi, interruzioni, e pessimo tempismo, può essere
riprodotto su un sistema a singolo processore.
Questo riduce drasticamente la complessità del controllo di qualità della
sincronizzazione nel kernel: quello che deve essere fatto è di innescare nel
kernel quante più possibili "semplici" sequenze di sincronizzazione, almeno una
volta, allo scopo di dimostrarne la correttezza. Questo al posto di innescare
una verifica per ogni possibile combinazione di sincronizzazione fra processori,
e differenti scenari con hardirq e softirq e annidamenti vari (nella pratica,
impossibile da fare)
.. [1]
assumendo che il validatore sia corretto al 100%, e che nessun altra parte
del sistema possa corromperne lo stato. Assumiamo anche che tutti i percorsi
MNI/SMM [potrebbero interrompere anche percorsi dove le interruzioni sono
disabilitate] sono corretti e non interferiscono con il validatore. Inoltre,
assumiamo che un hash a 64-bit sia unico per ogni sequenza di
sincronizzazione nel sistema. Infine, la ricorsione dei blocchi non deve
essere maggiore di 20.
Prestazione
-----------
Le regole sopracitate hanno bisogno di una quantità **enorme** di verifiche
durante l'esecuzione. Il sistema sarebbe diventato praticamente inutilizzabile
per la sua lentezza se le avessimo fatte davvero per ogni blocco trattenuto e
per ogni abilitazione delle interruzioni. La complessità della verifica è
O(N^2), quindi avremmo dovuto fare decine di migliaia di verifiche per ogni
evento, il tutto per poche centinaia di classi.
Il problema è stato risolto facendo una singola verifica per ogni 'scenario di
sincronizzazione' (una sequenza unica di blocchi trattenuti uno dopo l'altro).
Per farlo, viene mantenuta una pila dei blocchi trattenuti, e viene calcolato un
hash a 64-bit unico per ogni sequenza. Quando la sequenza viene verificata per
la prima volta, l'hash viene inserito in una tabella hash. La tabella potrà
essere verificata senza bisogno di blocchi. Se la sequenza dovesse ripetersi, la
tabella ci dirà che non è necessario verificarla nuovamente.
Risoluzione dei problemi
------------------------
Il massimo numero di classi di blocco che il validatore può tracciare è:
MAX_LOCKDEP_KEYS. Oltrepassare questo limite indurrà lokdep a generare il
seguente avviso::
(DEBUG_LOCKS_WARN_ON(id >= MAX_LOCKDEP_KEYS))
Di base questo valore è 8191, e un classico sistema da ufficio ha meno di 1000
classi, dunque questo avviso è solitamente la conseguenza di un problema di
perdita delle classi di blocco o d'inizializzazione dei blocchi. Di seguito una
descrizione dei due problemi:
1. caricare e rimuovere continuamente i moduli mentre il validatore è in
esecuzione porterà ad una perdita di classi di blocco. Il problema è che ogni
caricamento crea un nuovo insieme di classi di blocco per tutti i blocchi di
quel modulo. Tuttavia, la rimozione del modulo non rimuove le vecchie classi
(vedi dopo perché non le riusiamo). Dunque, il continuo caricamento e
rimozione di un modulo non fa altro che aumentare il contatore di classi fino
a raggiungere, eventualmente, il limite.
2. Usare array con un gran numero di blocchi che non vengono esplicitamente
inizializzati. Per esempio, una tabella hash con 8192 *bucket* dove ognuno ha
il proprio spinlock_t consumerà 8192 classi di blocco a meno che non vengano
esplicitamente inizializzati in esecuzione usando spin_lock_init() invece
dell'inizializzazione durante la compilazione con __SPIN_LOCK_UNLOCKED().
Sbagliare questa inizializzazione garantisce un esaurimento di classi di
blocco. Viceversa, un ciclo che invoca spin_lock_init() su tutti i blocchi li
mapperebbe tutti alla stessa classe di blocco.
La morale della favola è che dovete sempre inizializzare esplicitamente i
vostri blocchi.
Qualcuno potrebbe argomentare che il validatore debba permettere il riuso di
classi di blocco. Tuttavia, se siete tentati dall'argomento, prima revisionate
il codice e pensate alla modifiche necessarie, e tenendo a mente che le classi
di blocco da rimuovere probabilmente sono legate al grafo delle dipendenze. Più
facile a dirsi che a farsi.
Ovviamente, se non esaurite le classi di blocco, la prossima cosa da fare è
quella di trovare le classi non funzionanti. Per prima cosa, il seguente comando
ritorna il numero di classi attualmente in uso assieme al valore massimo::
grep "lock-classes" /proc/lockdep_stats
Questo comando produce il seguente messaggio::
lock-classes: 748 [max: 8191]
Se il numero di assegnazioni (748 qui sopra) aumenta continuamente nel tempo,
allora c'è probabilmente un problema da qualche parte. Il seguente comando può
essere utilizzato per identificare le classi di blocchi problematiche::
grep "BD" /proc/lockdep
Eseguite il comando e salvatene l'output, quindi confrontatelo con l'output di
un'esecuzione successiva per identificare eventuali problemi. Questo stesso
output può anche aiutarti a trovare situazioni in cui l'inizializzazione del
blocco è stata omessa.
Lettura ricorsiva dei blocchi
-----------------------------
Il resto di questo documento vuole dimostrare che certi cicli equivalgono ad una
possibilità di stallo.
Ci sono tre tipi di bloccatori: gli scrittori (bloccatori esclusivi, come
spin_lock() o write_lock()), lettori non ricorsivi (bloccatori condivisi, come
down_read()), e lettori ricorsivi (bloccatori condivisi ricorsivi, come
rcu_read_lock()). D'ora in poi, per questi tipi di bloccatori, useremo la
seguente notazione:
W o E: per gli scrittori (bloccatori esclusivi) (W dall'inglese per
*Writer*, ed E per *Exclusive*).
r: per i lettori non ricorsivi (r dall'inglese per *reader*).
R: per i lettori ricorsivi (R dall'inglese per *Reader*).
S: per qualsiasi lettore (non ricorsivi + ricorsivi), dato che entrambe
sono bloccatori condivisi (S dall'inglese per *Shared*).
N: per gli scrittori ed i lettori non ricorsivi, dato che entrambe sono
non ricorsivi.
Ovviamente, N equivale a "r o W" ed S a "r o R".
Come suggerisce il nome, i lettori ricorsivi sono dei bloccatori a cui è
permesso di acquisire la stessa istanza di blocco anche all'interno della
sezione critica di un altro lettore. In altre parole, permette di annidare la
stessa istanza di blocco nelle sezioni critiche dei lettori.
Dall'altro canto, lo stesso comportamento indurrebbe un lettore non ricorsivo ad
auto infliggersi uno stallo.
La differenza fra questi due tipi di lettori esiste perché: quelli ricorsivi
vengono bloccati solo dal trattenimento di un blocco di scrittura, mentre quelli
non ricorsivi possono essere bloccati dall'attesa di un blocco di scrittura.
Consideriamo il seguente esempio::
TASK A: TASK B:
read_lock(X);
write_lock(X);
read_lock_2(X);
L'attività A acquisisce il blocco di lettura X (non importa se di tipo ricorsivo
o meno) usando read_lock(). Quando l'attività B tenterà di acquisire il blocco
X, si fermerà e rimarrà in attesa che venga rilasciato. Ora se read_lock_2() è
un tipo lettore ricorsivo, l'attività A continuerà perché gli scrittori in
attesa non possono bloccare lettori ricorsivi, e non avremo alcuno stallo.
Tuttavia, se read_lock_2() è un lettore non ricorsivo, allora verrà bloccato
dall'attività B e si causerà uno stallo.
Condizioni bloccanti per lettori/scrittori su uno stesso blocco
---------------------------------------------------------------
Essenzialmente ci sono quattro condizioni bloccanti:
1. Uno scrittore blocca un altro scrittore.
2. Un lettore blocca uno scrittore.
3. Uno scrittore blocca sia i lettori ricorsivi che non ricorsivi.
4. Un lettore (ricorsivo o meno) non blocca altri lettori ricorsivi ma potrebbe
bloccare quelli non ricorsivi (perché potrebbero esistere degli scrittori in
attesa).
Di seguito le tabella delle condizioni bloccanti, Y (*Yes*) significa che il
tipo in riga blocca quello in colonna, mentre N l'opposto.
+---+---+---+---+
| | W | r | R |
+---+---+---+---+
| W | Y | Y | Y |
+---+---+---+---+
| r | Y | Y | N |
+---+---+---+---+
| R | Y | Y | N |
+---+---+---+---+
(W: scrittori, r: lettori non ricorsivi, R: lettori ricorsivi)
Al contrario dei blocchi per lettori non ricorsivi, quelli ricorsivi vengono
trattenuti da chi trattiene il blocco di scrittura piuttosto che da chi ne
attende il rilascio. Per esempio::
TASK A: TASK B:
read_lock(X);
write_lock(X);
read_lock(X);
non produce uno stallo per i lettori ricorsivi, in quanto il processo B rimane
in attesta del blocco X, mentre il secondo read_lock() non ha bisogno di
aspettare perché si tratta di un lettore ricorsivo. Tuttavia, se read_lock()
fosse un lettore non ricorsivo, questo codice produrrebbe uno stallo.
Da notare che in funzione dell'operazione di blocco usate per l'acquisizione (in
particolare il valore del parametro 'read' in lock_acquire()), un blocco può
essere di scrittura (blocco esclusivo), di lettura non ricorsivo (blocco
condiviso e non ricorsivo), o di lettura ricorsivo (blocco condiviso e
ricorsivo). In altre parole, per un'istanza di blocco esistono tre tipi di
acquisizione che dipendono dalla funzione di acquisizione usata: esclusiva, di
lettura non ricorsiva, e di lettura ricorsiva.
In breve, chiamiamo "non ricorsivi" blocchi di scrittura e quelli di lettura non
ricorsiva, mentre "ricorsivi" i blocchi di lettura ricorsivi.
I blocchi ricorsivi non si bloccano a vicenda, mentre quelli non ricorsivi sì
(anche in lettura). Un blocco di lettura non ricorsivi può bloccare uno
ricorsivo, e viceversa.
Il seguente esempio mostra uno stallo con blocchi ricorsivi::
TASK A: TASK B:
read_lock(X);
read_lock(Y);
write_lock(Y);
write_lock(X);
Il processo A attende che il processo B esegua read_unlock() so Y, mentre il
processo B attende che A esegua read_unlock() su X.
Tipi di dipendenze e percorsi forti
-----------------------------------
Le dipendenze fra blocchi tracciano l'ordine con cui una coppia di blocchi viene
acquisita, e perché vi sono 3 tipi di bloccatori, allora avremo 9 tipi di
dipendenze. Tuttavia, vi mostreremo che 4 sono sufficienti per individuare gli
stalli.
Per ogni dipendenza fra blocchi avremo::
L1 -> L2
Questo significa che lockdep ha visto acquisire L1 prima di L2 nello stesso
contesto di esecuzione. Per quanto riguarda l'individuazione degli stalli, ci
interessa sapere se possiamo rimanere bloccati da L2 mentre L1 viene trattenuto.
In altre parole, vogliamo sapere se esiste un bloccatore L3 che viene bloccato
da L1 e un L2 che viene bloccato da L3. Dunque, siamo interessati a (1) quello
che L1 blocca e (2) quello che blocca L2. Di conseguenza, possiamo combinare
lettori ricorsivi e non per L1 (perché bloccano gli stessi tipi) e possiamo
combinare scrittori e lettori non ricorsivi per L2 (perché vengono bloccati
dagli stessi tipi).
Con questa semplificazione, possiamo dedurre che ci sono 4 tipi di rami nel
grafo delle dipendenze di lockdep:
1) -(ER)->:
dipendenza da scrittore esclusivo a lettore ricorsivo. "X -(ER)-> Y"
significa X -> Y, dove X è uno scrittore e Y un lettore ricorsivo.
2) -(EN)->:
dipendenza da scrittore esclusivo a bloccatore non ricorsivo.
"X -(EN)->" significa X-> Y, dove X è uno scrittore e Y può essere
o uno scrittore o un lettore non ricorsivo.
3) -(SR)->:
dipendenza da lettore condiviso a lettore ricorsivo. "X -(SR)->"
significa X -> Y, dove X è un lettore (ricorsivo o meno) e Y è un
lettore ricorsivo.
4) -(SN)->:
dipendenza da lettore condiviso a bloccatore non ricorsivo.
"X -(SN)-> Y" significa X -> Y , dove X è un lettore (ricorsivo
o meno) e Y può essere o uno scrittore o un lettore non ricorsivo.
Da notare che presi due blocchi, questi potrebbero avere più dipendenza fra di
loro. Per esempio::
TASK A:
read_lock(X);
write_lock(Y);
...
TASK B:
write_lock(X);
write_lock(Y);
Nel grafo delle dipendenze avremo sia X -(SN)-> Y che X -(EN)-> Y.
Usiamo -(xN)-> per rappresentare i rami sia per -(EN)-> che -(SN)->, allo stesso
modo -(Ex)->, -(xR)-> e -(Sx)->
Un "percorso" in un grafo è una serie di nodi e degli archi che li congiungono.
Definiamo un percorso "forte", come il percorso che non ha archi (dipendenze) di
tipo -(xR)-> e -(Sx)->. In altre parole, un percorso "forte" è un percorso da un
blocco ad un altro attraverso le varie dipendenze, e se sul percorso abbiamo X
-> Y -> Z (dove X, Y, e Z sono blocchi), e da X a Y si ha una dipendenza -(SR)->
o -(ER)->, allora fra Y e Z non deve esserci una dipendenza -(SN)-> o -(SR)->.
Nella prossima sezione vedremo perché definiamo questo percorso "forte".
Identificazione di stalli da lettura ricorsiva
----------------------------------------------
Ora vogliamo dimostrare altre due cose:
Lemma 1:
Se esiste un percorso chiuso forte (ciclo forte), allora esiste anche una
combinazione di sequenze di blocchi che causa uno stallo. In altre parole,
l'esistenza di un ciclo forte è sufficiente alla scoperta di uno stallo.
Lemma 2:
Se non esiste un percorso chiuso forte (ciclo forte), allora non esiste una
combinazione di sequenze di blocchi che causino uno stallo. In altre parole, i
cicli forti sono necessari alla rilevazione degli stallo.
Con questi due lemmi possiamo facilmente affermare che un percorso chiuso forte
è sia sufficiente che necessario per avere gli stalli, dunque averli equivale
alla possibilità di imbattersi concretamente in uno stallo. Un percorso chiuso
forte significa che può causare stalli, per questo lo definiamo "forte", ma ci
sono anche cicli di dipendenze che non causeranno stalli.
Dimostrazione di sufficienza (lemma 1):
Immaginiamo d'avere un ciclo forte::
L1 -> L2 ... -> Ln -> L1
Questo significa che abbiamo le seguenti dipendenze::
L1 -> L2
L2 -> L3
...
Ln-1 -> Ln
Ln -> L1
Ora possiamo costruire una combinazione di sequenze di blocchi che causano lo
stallo.
Per prima cosa facciamo sì che un processo/processore prenda L1 in L1 -> L2, poi
un altro prende L2 in L2 -> L3, e così via. Alla fine, tutti i Lx in Lx -> Lx+1
saranno trattenuti da processi/processori diversi.
Poi visto che abbiamo L1 -> L2, chi trattiene L1 vorrà acquisire L2 in L1 -> L2,
ma prima dovrà attendere che venga rilasciato da chi lo trattiene. Questo perché
L2 è già trattenuto da un altro processo/processore, ed in più L1 -> L2 e L2 ->
L3 non sono -(xR)-> né -(Sx)-> (la definizione di forte). Questo significa che L2
in L1 -> L2 non è un bloccatore non ricorsivo (bloccabile da chiunque), e L2 in
L2 -> L3 non è uno scrittore (che blocca chiunque).
In aggiunta, possiamo trarre una simile conclusione per chi sta trattenendo L2:
deve aspettare che L3 venga rilasciato, e così via. Ora possiamo dimostrare che
chi trattiene Lx deve aspettare che Lx+1 venga rilasciato. Notiamo che Ln+1 è
L1, dunque si è creato un ciclo dal quale non possiamo uscire, quindi si ha uno
stallo.
Dimostrazione della necessità (lemma 2):
Questo lemma equivale a dire che: se siamo in uno scenario di stallo, allora
deve esiste un ciclo forte nel grafo delle dipendenze.
Secondo Wikipedia[1], se c'è uno stallo, allora deve esserci un ciclo di attese,
ovvero ci sono N processi/processori dove P1 aspetta un blocco trattenuto da P2,
e P2 ne aspetta uno trattenuto da P3, ... e Pn attende che il blocco P1 venga
rilasciato. Chiamiamo Lx il blocco che attende Px, quindi P1 aspetta L1 e
trattiene Ln. Quindi avremo Ln -> L1 nel grafo delle dipendenze. Similarmente,
nel grafo delle dipendenze avremo L1 -> L2, L2 -> L3, ..., Ln-1 -> Ln, il che
significa che abbiamo un ciclo::
Ln -> L1 -> L2 -> ... -> Ln
, ed ora dimostriamo d'avere un ciclo forte.
Per un blocco Lx, il processo Px contribuisce alla dipendenza Lx-1 -> Lx e Px+1
contribuisce a quella Lx -> Lx+1. Visto che Px aspetta che Px+1 rilasci Lx, sarà
impossibile che Lx in Px+1 sia un lettore e che Lx in Px sia un lettore
ricorsivo. Questo perché i lettori (ricorsivi o meno) non bloccano lettori
ricorsivi. Dunque, Lx-1 -> Lx e Lx -> Lx+1 non possono essere una coppia di
-(xR)-> -(Sx)->. Questo è vero per ogni ciclo, dunque, questo è un ciclo forte.
Riferimenti
-----------
[1]: https://it.wikipedia.org/wiki/Stallo_(informatica)
[2]: Shibu, K. (2009). Intro To Embedded Systems (1st ed.). Tata McGraw-Hill

View File

@ -0,0 +1,230 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
=======================
Statistiche sui blocchi
=======================
Cosa
====
Come suggerisce il nome, fornisce statistiche sui blocchi.
Perché
======
Perché, tanto per fare un esempio, le contese sui blocchi possono influenzare
significativamente le prestazioni.
Come
====
*Lockdep* ha punti di collegamento nelle funzioni di blocco e inoltre
mappa le istanze di blocco con le relative classi. Partiamo da questo punto
(vedere Documentation/translations/it_IT/locking/lockdep-design.rst).
Il grafico sottostante mostra la relazione che intercorre fra le
funzioni di blocco e i vari punti di collegamenti che ci sono al loro
interno::
__acquire
|
lock _____
| \
| __contended
| |
| <wait>
| _______/
|/
|
__acquired
|
.
<hold>
.
|
__release
|
unlock
lock, unlock - le classiche funzioni di blocco
__* - i punti di collegamento
<> - stati
Grazie a questi punti di collegamento possiamo fornire le seguenti statistiche:
con-bounces
- numero di contese su un blocco che riguarda dati di un processore
contentions
- numero di acquisizioni di blocchi che hanno dovuto attendere
wait time
min
- tempo minimo (diverso da zero) che sia mai stato speso in attesa di
un blocco
max
- tempo massimo che sia mai stato speso in attesa di un blocco
total
- tempo totale speso in attesa di un blocco
avg
- tempo medio speso in attesa di un blocco
acq-bounces
- numero di acquisizioni di blocco che riguardavano i dati su un processore
acquisitions
- numero di volte che un blocco è stato ottenuto
hold time
min
- tempo minimo (diverso da zero) che sia mai stato speso trattenendo un blocco
max
- tempo massimo che sia mai stato speso trattenendo un blocco
total
- tempo totale di trattenimento di un blocco
avg
- tempo medio di trattenimento di un blocco
Questi numeri vengono raccolti per classe di blocco, e per ogni stato di
lettura/scrittura (quando applicabile).
Inoltre, questa raccolta di statistiche tiene traccia di 4 punti di contesa
per classe di blocco. Un punto di contesa è una chiamata che ha dovuto
aspettare l'acquisizione di un blocco.
Configurazione
--------------
Le statistiche sui blocchi si abilitano usando l'opzione di configurazione
CONFIG_LOCK_STAT.
Uso
---
Abilitare la raccolta di statistiche::
# echo 1 >/proc/sys/kernel/lock_stat
Disabilitare la raccolta di statistiche::
# echo 0 >/proc/sys/kernel/lock_stat
Per vedere le statistiche correnti sui blocchi::
( i numeri di riga non fanno parte dell'output del comando, ma sono stati
aggiunti ai fini di questa spiegazione )
# less /proc/lock_stat
01 lock_stat version 0.4
02-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
03 class name con-bounces contentions waittime-min waittime-max waittime-total waittime-avg acq-bounces acquisitions holdtime-min holdtime-max holdtime-total holdtime-avg
04-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
05
06 &mm->mmap_sem-W: 46 84 0.26 939.10 16371.53 194.90 47291 2922365 0.16 2220301.69 17464026916.32 5975.99
07 &mm->mmap_sem-R: 37 100 1.31 299502.61 325629.52 3256.30 212344 34316685 0.10 7744.91 95016910.20 2.77
08 ---------------
09 &mm->mmap_sem 1 [<ffffffff811502a7>] khugepaged_scan_mm_slot+0x57/0x280
10 &mm->mmap_sem 96 [<ffffffff815351c4>] __do_page_fault+0x1d4/0x510
11 &mm->mmap_sem 34 [<ffffffff81113d77>] vm_mmap_pgoff+0x87/0xd0
12 &mm->mmap_sem 17 [<ffffffff81127e71>] vm_munmap+0x41/0x80
13 ---------------
14 &mm->mmap_sem 1 [<ffffffff81046fda>] dup_mmap+0x2a/0x3f0
15 &mm->mmap_sem 60 [<ffffffff81129e29>] SyS_mprotect+0xe9/0x250
16 &mm->mmap_sem 41 [<ffffffff815351c4>] __do_page_fault+0x1d4/0x510
17 &mm->mmap_sem 68 [<ffffffff81113d77>] vm_mmap_pgoff+0x87/0xd0
18
19.............................................................................................................................................................................................................................
20
21 unix_table_lock: 110 112 0.21 49.24 163.91 1.46 21094 66312 0.12 624.42 31589.81 0.48
22 ---------------
23 unix_table_lock 45 [<ffffffff8150ad8e>] unix_create1+0x16e/0x1b0
24 unix_table_lock 47 [<ffffffff8150b111>] unix_release_sock+0x31/0x250
25 unix_table_lock 15 [<ffffffff8150ca37>] unix_find_other+0x117/0x230
26 unix_table_lock 5 [<ffffffff8150a09f>] unix_autobind+0x11f/0x1b0
27 ---------------
28 unix_table_lock 39 [<ffffffff8150b111>] unix_release_sock+0x31/0x250
29 unix_table_lock 49 [<ffffffff8150ad8e>] unix_create1+0x16e/0x1b0
30 unix_table_lock 20 [<ffffffff8150ca37>] unix_find_other+0x117/0x230
31 unix_table_lock 4 [<ffffffff8150a09f>] unix_autobind+0x11f/0x1b0
Questo estratto mostra le statistiche delle prime due classi di
blocco. La riga 01 mostra la versione dell'output - la versione
cambierà ogni volta che cambia il formato. Le righe dalla 02 alla 04
rappresentano l'intestazione con la descrizione delle colonne. Le
statistiche sono mostrate nelle righe dalla 05 alla 18 e dalla 20
alla 31. Queste statistiche sono divise in due parti: le statistiche,
seguite dai punti di contesa (righe 08 e 13) separati da un divisore.
Le righe dalla 09 alla 12 mostrano i primi quattro punti di contesa
registrati (il codice che tenta di acquisire un blocco) e le righe
dalla 14 alla 17 mostrano i primi quattro punti contesi registrati
(ovvero codice che ha acquisito un blocco). È possibile che nelle
statistiche manchi il valore *max con-bounces*.
Il primo blocco (righe dalla 05 alla 18) è di tipo lettura/scrittura e quindi
mostra due righe prima del divisore. I punti di contesa non corrispondono alla
descrizione delle colonne nell'intestazione; essi hanno due colonne: *punti di
contesa* e *[<IP>] simboli*. Il secondo gruppo di punti di contesa sono i punti
con cui si contende il blocco.
La parte interna del tempo è espressa in us (microsecondi).
Quando si ha a che fare con blocchi annidati si potrebbero vedere le
sottoclassi di blocco::
32...........................................................................................................................................................................................................................
33
34 &rq->lock: 13128 13128 0.43 190.53 103881.26 7.91 97454 3453404 0.00 401.11 13224683.11 3.82
35 ---------
36 &rq->lock 645 [<ffffffff8103bfc4>] task_rq_lock+0x43/0x75
37 &rq->lock 297 [<ffffffff8104ba65>] try_to_wake_up+0x127/0x25a
38 &rq->lock 360 [<ffffffff8103c4c5>] select_task_rq_fair+0x1f0/0x74a
39 &rq->lock 428 [<ffffffff81045f98>] scheduler_tick+0x46/0x1fb
40 ---------
41 &rq->lock 77 [<ffffffff8103bfc4>] task_rq_lock+0x43/0x75
42 &rq->lock 174 [<ffffffff8104ba65>] try_to_wake_up+0x127/0x25a
43 &rq->lock 4715 [<ffffffff8103ed4b>] double_rq_lock+0x42/0x54
44 &rq->lock 893 [<ffffffff81340524>] schedule+0x157/0x7b8
45
46...........................................................................................................................................................................................................................
47
48 &rq->lock/1: 1526 11488 0.33 388.73 136294.31 11.86 21461 38404 0.00 37.93 109388.53 2.84
49 -----------
50 &rq->lock/1 11526 [<ffffffff8103ed58>] double_rq_lock+0x4f/0x54
51 -----------
52 &rq->lock/1 5645 [<ffffffff8103ed4b>] double_rq_lock+0x42/0x54
53 &rq->lock/1 1224 [<ffffffff81340524>] schedule+0x157/0x7b8
54 &rq->lock/1 4336 [<ffffffff8103ed58>] double_rq_lock+0x4f/0x54
55 &rq->lock/1 181 [<ffffffff8104ba65>] try_to_wake_up+0x127/0x25a
La riga 48 mostra le statistiche per la seconda sottoclasse (/1) della
classe *&irq->lock* (le sottoclassi partono da 0); in questo caso,
come suggerito dalla riga 50, ``double_rq_lock`` tenta di acquisire un blocco
annidato di due spinlock.
Per vedere i blocco più contesi::
# grep : /proc/lock_stat | head
clockevents_lock: 2926159 2947636 0.15 46882.81 1784540466.34 605.41 3381345 3879161 0.00 2260.97 53178395.68 13.71
tick_broadcast_lock: 346460 346717 0.18 2257.43 39364622.71 113.54 3642919 4242696 0.00 2263.79 49173646.60 11.59
&mapping->i_mmap_mutex: 203896 203899 3.36 645530.05 31767507988.39 155800.21 3361776 8893984 0.17 2254.15 14110121.02 1.59
&rq->lock: 135014 136909 0.18 606.09 842160.68 6.15 1540728 10436146 0.00 728.72 17606683.41 1.69
&(&zone->lru_lock)->rlock: 93000 94934 0.16 59.18 188253.78 1.98 1199912 3809894 0.15 391.40 3559518.81 0.93
tasklist_lock-W: 40667 41130 0.23 1189.42 428980.51 10.43 270278 510106 0.16 653.51 3939674.91 7.72
tasklist_lock-R: 21298 21305 0.20 1310.05 215511.12 10.12 186204 241258 0.14 1162.33 1179779.23 4.89
rcu_node_1: 47656 49022 0.16 635.41 193616.41 3.95 844888 1865423 0.00 764.26 1656226.96 0.89
&(&dentry->d_lockref.lock)->rlock: 39791 40179 0.15 1302.08 88851.96 2.21 2790851 12527025 0.10 1910.75 3379714.27 0.27
rcu_node_0: 29203 30064 0.16 786.55 1555573.00 51.74 88963 244254 0.00 398.87 428872.51 1.76
Per cancellare le statistiche::
# echo 0 > /proc/lock_stat

View File

@ -0,0 +1,181 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
============================================
Funzionamento del test *Kernel Lock Torture*
============================================
CONFIG_LOCK_TORTURE_TEST
========================
L'opzione di configurazione CONFIG_LOCK_TORTURE_TEST fornisce un
modulo kernel che esegue delle verifiche che *torturano* le primitive di
sincronizzazione del kernel. Se dovesse servire, il modulo kernel,
'locktorture', può essere generato successivamente su un kernel che
volete verificare. Periodicamente le verifiche stampano messaggi tramite
``printk()`` e che quindi possono essere letti tramite ``dmesg`` (magari
filtrate l'output con ``grep "torture"``). La verifica inizia quando
il modulo viene caricato e termina quando viene rimosso. Questo
programma si basa sulle modalità di verifica di RCU tramite rcutorture.
Questa verifica consiste nella creazione di un certo numero di thread
del kernel che acquisiscono un blocco e lo trattengono per una certa
quantità di tempo così da simulare diversi comportamenti nelle sezioni
critiche. La quantità di contese su un blocco può essere simulata
allargando la sezione critica e/o creando più thread.
Parametri del modulo
====================
Questo modulo ha i seguenti parametri:
Specifici di locktorture
------------------------
nwriters_stress
Numero di thread del kernel che stresseranno l'acquisizione
esclusiva dei blocchi (scrittori). Il valore di base è il
doppio del numero di processori attivi presenti.
nreaders_stress
Numero di thread del kernel che stresseranno l'acquisizione
condivisa dei blocchi (lettori). Il valore di base è lo stesso
di nwriters_stress. Se l'utente non ha specificato
nwriters_stress, allora entrambe i valori corrisponderanno
al numero di processori attivi presenti.
torture_type
Tipo di blocco da verificare. Di base, solo gli spinlock
verranno verificati. Questo modulo può verificare anche
i seguenti tipi di blocchi:
- "lock_busted":
Simula un'incorretta implementazione del
blocco.
- "spin_lock":
coppie di spin_lock() e spin_unlock().
- "spin_lock_irq":
coppie di spin_lock_irq() e spin_unlock_irq().
- "rw_lock":
coppie di rwlock read/write lock() e unlock().
- "rw_lock_irq":
copie di rwlock read/write lock_irq() e
unlock_irq().
- "mutex_lock":
coppie di mutex_lock() e mutex_unlock().
- "rtmutex_lock":
coppie di rtmutex_lock() e rtmutex_unlock().
Il kernel deve avere CONFIG_RT_MUTEXES=y.
- "rwsem_lock":
coppie di semafori read/write down() e up().
Generici dell'ambiente di sviluppo 'torture' (RCU + locking)
------------------------------------------------------------
shutdown_secs
Numero di secondi prima che la verifica termini e il sistema
venga spento. Il valore di base è zero, il che disabilita
la possibilità di terminare e spegnere. Questa funzionalità
può essere utile per verifiche automatizzate.
onoff_interval
Numero di secondi fra ogni tentativo di esecuzione di
un'operazione casuale di CPU-hotplug. Di base è zero, il
che disabilita la funzionalità di CPU-hotplug. Nei kernel
con CONFIG_HOTPLUG_CPU=n, locktorture si rifiuterà, senza
dirlo, di effettuare una qualsiasi operazione di
CPU-hotplug indipendentemente dal valore specificato in
onoff_interval.
onoff_holdoff
Numero di secondi da aspettare prima di iniziare le
operazioni di CPU-hotplug. Normalmente questo verrebbe
usato solamente quando locktorture è compilato come parte
integrante del kernel ed eseguito automaticamente all'avvio,
in questo caso è utile perché permette di non confondere
l'avvio con i processori che vanno e vengono. Questo
parametro è utile sono se CONFIG_HOTPLUG_CPU è abilitato.
stat_interval
Numero di secondi fra una stampa (printk()) delle
statistiche e l'altra. Di base, locktorture riporta le
statistiche ogni 60 secondi. Impostando l'intervallo a 0
ha l'effetto di stampare le statistiche -solo- quando il
modulo viene rimosso.
stutter
Durata della verifica prima di effettuare una pausa di
eguale durata. Di base "stutter=5", quindi si eseguono
verifiche e pause di (circa) cinque secondi.
L'impostazione di "stutter=0" fa si che la verifica
venga eseguita continuamente senza fermarsi.
shuffle_interval
Il numero di secondi per cui un thread debba mantenere
l'affinità con un sottoinsieme di processori, di base è
3 secondi. Viene usato assieme a test_no_idle_hz.
verbose
Abilita le stampe di debug, via printk(). Di base è
abilitato. Queste informazioni aggiuntive sono per la
maggior parte relative ad errori di alto livello e resoconti
da parte dell'struttura 'torture'.
Statistiche
===========
Le statistiche vengono stampate secondo il seguente formato::
spin_lock-torture: Writes: Total: 93746064 Max/Min: 0/0 Fail: 0
(A) (B) (C) (D) (E)
(A): tipo di lock sotto verifica -- parametro torture_type.
(B): Numero di acquisizione del blocco in scrittura. Se si ha a che fare
con una primitiva di lettura/scrittura apparirà di seguito anche una
seconda voce "Reads"
(C): Numero di volte che il blocco è stato acquisito
(D): Numero minimo e massimo di volte che un thread ha fallito
nell'acquisire il blocco
(E): valori true/false nel caso di errori durante l'acquisizione del blocco.
Questo dovrebbe dare un riscontro positivo -solo- se c'è un baco
nell'implementazione delle primitive di sincronizzazione. Altrimenti un
blocco non dovrebbe mai fallire (per esempio, spin_lock()).
Ovviamente lo stesso si applica per (C). Un semplice esempio è il tipo
"lock_busted".
Uso
===
Il seguente script può essere utilizzato per verificare i blocchi::
#!/bin/sh
modprobe locktorture
sleep 3600
rmmod locktorture
dmesg | grep torture:
L'output può essere manualmente ispezionato cercando il marcatore d'errore
"!!!". Ovviamente potreste voler creare degli script più elaborati che
verificano automaticamente la presenza di errori. Il comando "rmmod" forza la
stampa (usando printk()) di "SUCCESS", "FAILURE", oppure "RCU_HOTPLUG". I primi
due si piegano da soli, mentre l'ultimo indica che non stati trovati problemi di
sincronizzazione, tuttavia ne sono stati trovati in CPU-hotplug.
Consultate anche: Documentation/translations/it_IT/RCU/torture.rst

View File

@ -0,0 +1,547 @@
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
.. _it_kernel_hacking_locktypes:
========================================
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.