요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. include:: ../disclaimer-ita.rst
.. c:namespace:: it_IT
:Original: :ref:`Documentation/kernel-hacking/locking.rst <kernel_hacking_lock>`
:Translator: Federico Vaga <federico.vaga@vaga.pv.it>
.. _it_kernel_hacking_lock:
==========================================
L'inaffidabile guida alla sincronizzazione
==========================================
:Author: Rusty Russell
Introduzione
============
Benvenuto, alla notevole ed inaffidabile guida ai problemi di sincronizzazione
(locking) nel kernel. Questo documento descrive il sistema di sincronizzazione
nel kernel Linux 2.6.
Dato il largo utilizzo del multi-threading e della prelazione nel kernel
Linux, chiunque voglia dilettarsi col kernel deve conoscere i concetti
fondamentali della concorrenza e della sincronizzazione nei sistemi
multi-processore.
Il problema con la concorrenza
==============================
(Saltatelo se sapete già cos'è una corsa critica).
In un normale programma, potete incrementare un contatore nel seguente modo:
::
contatore++;
Questo è quello che vi aspettereste che accada sempre:
.. table:: Risultati attesi
+------------------------------------+------------------------------------+
| Istanza 1 | Istanza 2 |
+====================================+====================================+
| leggi contatore (5) | |
+------------------------------------+------------------------------------+
| aggiungi 1 (6) | |
+------------------------------------+------------------------------------+
| scrivi contatore (6) | |
+------------------------------------+------------------------------------+
| | leggi contatore (6) |
+------------------------------------+------------------------------------+
| | aggiungi 1 (7) |
+------------------------------------+------------------------------------+
| | scrivi contatore (7) |
+------------------------------------+------------------------------------+
Questo è quello che potrebbe succedere in realtà:
.. table:: Possibile risultato
+------------------------------------+------------------------------------+
| Istanza 1 | Istanza 2 |
+====================================+====================================+
| leggi contatore (5) | |
+------------------------------------+------------------------------------+
| | leggi contatore (5) |
+------------------------------------+------------------------------------+
| aggiungi 1 (6) | |
+------------------------------------+------------------------------------+
| | aggiungi 1 (6) |
+------------------------------------+------------------------------------+
| scrivi contatore (6) | |
+------------------------------------+------------------------------------+
| | scrivi contatore (6) |
+------------------------------------+------------------------------------+
Corse critiche e sezioni critiche
---------------------------------
Questa sovrapposizione, ovvero quando un risultato dipende dal tempo che
intercorre fra processi diversi, è chiamata corsa critica. La porzione
di codice che contiene questo problema è chiamata sezione critica.
In particolar modo da quando Linux ha incominciato a girare su
macchine multi-processore, le sezioni critiche sono diventate uno dei
maggiori problemi di progettazione ed implementazione del kernel.
La prelazione può sortire gli stessi effetti, anche se c'è una sola CPU:
interrompendo un processo nella sua sezione critica otterremo comunque
la stessa corsa critica. In questo caso, il thread che si avvicenda
nell'esecuzione potrebbe eseguire anch'esso la sezione critica.
La soluzione è quella di riconoscere quando avvengono questi accessi
simultanei, ed utilizzare i *lock* per accertarsi che solo un'istanza
per volta possa entrare nella sezione critica. Il kernel offre delle buone
funzioni a questo scopo. E poi ci sono quelle meno buone, ma farò finta
che non esistano.
Sincronizzazione nel kernel Linux
=================================
Se dovessi darvi un suggerimento sulla sincronizzazione: **mantenetela
semplice**.
Siate riluttanti nell'introduzione di nuovi *lock*.
I due principali tipi di *lock* nel kernel: spinlock e mutex
------------------------------------------------------------
Ci sono due tipi principali di *lock* nel kernel. Il tipo fondamentale è lo
spinlock (``include/asm/spinlock.h``), un semplice *lock* che può essere
trattenuto solo da un processo: se non si può trattenere lo spinlock, allora
rimane in attesa attiva (in inglese *spinning*) finché non ci riesce.
Gli spinlock sono molto piccoli e rapidi, possono essere utilizzati ovunque.
Il secondo tipo è il mutex (``include/linux/mutex.h``): è come uno spinlock,
ma potreste bloccarvi trattenendolo. Se non potete trattenere un mutex
il vostro processo si auto-sospenderà; verrà riattivato quando il mutex
verrà rilasciato. Questo significa che il processore potrà occuparsi d'altro
mentre il vostro processo è in attesa. Esistono molti casi in cui non potete
permettervi di sospendere un processo (vedere
`Quali funzioni possono essere chiamate in modo sicuro dalle interruzioni?`_)
e quindi dovrete utilizzare gli spinlock.
Nessuno di questi *lock* è ricorsivo: vedere
`Stallo: semplice ed avanzato`_
I *lock* e i kernel per sistemi monoprocessore
----------------------------------------------
Per i kernel compilati senza ``CONFIG_SMP`` e senza ``CONFIG_PREEMPT``
gli spinlock non esistono. Questa è un'ottima scelta di progettazione:
quando nessun altro processo può essere eseguito in simultanea, allora
non c'è la necessità di avere un *lock*.
Se il kernel è compilato senza ``CONFIG_SMP`` ma con ``CONFIG_PREEMPT``,
allora gli spinlock disabilitano la prelazione; questo è sufficiente a
prevenire le corse critiche. Nella maggior parte dei casi, possiamo considerare
la prelazione equivalente ad un sistema multi-processore senza preoccuparci
di trattarla indipendentemente.
Dovreste verificare sempre la sincronizzazione con le opzioni ``CONFIG_SMP`` e
``CONFIG_PREEMPT`` abilitate, anche quando non avete un sistema
multi-processore, questo vi permetterà di identificare alcuni problemi
di sincronizzazione.
Come vedremo di seguito, i mutex continuano ad esistere perché sono necessari
per la sincronizzazione fra processi in contesto utente.
Sincronizzazione in contesto utente
-----------------------------------
Se avete una struttura dati che verrà utilizzata solo dal contesto utente,
allora, per proteggerla, potete utilizzare un semplice mutex
(``include/linux/mutex.h``). Questo è il caso più semplice: inizializzate il
mutex; invocate mutex_lock_interruptible() per trattenerlo e
mutex_unlock() per rilasciarlo. C'è anche mutex_lock()
ma questa dovrebbe essere evitata perché non ritorna in caso di segnali.
Per esempio: ``net/netfilter/nf_sockopt.c`` permette la registrazione
di nuove chiamate per setsockopt() e getsockopt()
usando la funzione nf_register_sockopt(). La registrazione e
la rimozione vengono eseguite solamente quando il modulo viene caricato
o scaricato (e durante l'avvio del sistema, qui non abbiamo concorrenza),
e la lista delle funzioni registrate viene consultata solamente quando
setsockopt() o getsockopt() sono sconosciute al sistema.
In questo caso ``nf_sockopt_mutex`` è perfetto allo scopo, in particolar modo
visto che setsockopt e getsockopt potrebbero dormire.
Sincronizzazione fra il contesto utente e i softirq
---------------------------------------------------
Se un softirq condivide dati col contesto utente, avete due problemi.
Primo, il contesto utente corrente potrebbe essere interroto da un softirq,
e secondo, la sezione critica potrebbe essere eseguita da un altro
processore. Questo è quando spin_lock_bh()
(``include/linux/spinlock.h``) viene utilizzato. Questo disabilita i softirq
sul processore e trattiene il *lock*. Invece, spin_unlock_bh() fa
l'opposto. (Il suffisso '_bh' è un residuo storico che fa riferimento al
"Bottom Halves", il vecchio nome delle interruzioni software. In un mondo
perfetto questa funzione si chiamerebbe 'spin_lock_softirq()').
Da notare che in questo caso potete utilizzare anche spin_lock_irq()
o spin_lock_irqsave(), queste fermano anche le interruzioni hardware:
vedere `Contesto di interruzione hardware`_.
Questo funziona alla perfezione anche sui sistemi monoprocessore: gli spinlock
svaniscono e questa macro diventa semplicemente local_bh_disable()
(``include/linux/interrupt.h``), la quale impedisce ai softirq d'essere
eseguiti.
Sincronizzazione fra contesto utente e i tasklet
------------------------------------------------
Questo caso è uguale al precedente, un tasklet viene eseguito da un softirq.
Sincronizzazione fra contesto utente e i timer
----------------------------------------------
Anche questo caso è uguale al precedente, un timer viene eseguito da un
softirq.
Dal punto di vista della sincronizzazione, tasklet e timer sono identici.
Sincronizzazione fra tasklet e timer
------------------------------------
Qualche volta un tasklet od un timer potrebbero condividere i dati con
un altro tasklet o timer
Lo stesso tasklet/timer
~~~~~~~~~~~~~~~~~~~~~~~
Dato che un tasklet non viene mai eseguito contemporaneamente su due
processori, non dovete preoccuparvi che sia rientrante (ovvero eseguito
più volte in contemporanea), perfino su sistemi multi-processore.
Differenti tasklet/timer
~~~~~~~~~~~~~~~~~~~~~~~~
Se un altro tasklet/timer vuole condividere dati col vostro tasklet o timer,
allora avrete bisogno entrambe di spin_lock() e
spin_unlock(). Qui spin_lock_bh() è inutile, siete già
in un tasklet ed avete la garanzia che nessun altro verrà eseguito sullo
stesso processore.
Sincronizzazione fra softirq
----------------------------
Spesso un softirq potrebbe condividere dati con se stesso o un tasklet/timer.
Lo stesso softirq
~~~~~~~~~~~~~~~~~
Lo stesso softirq può essere eseguito su un diverso processore: allo scopo
di migliorare le prestazioni potete utilizzare dati riservati ad ogni
processore (vedere `Dati per processore`_). Se siete arrivati
fino a questo punto nell'uso dei softirq, probabilmente tenete alla scalabilità
delle prestazioni abbastanza da giustificarne la complessità aggiuntiva.
Dovete utilizzare spin_lock() e spin_unlock() per
proteggere i dati condivisi.
Diversi Softirqs
~~~~~~~~~~~~~~~~
Dovete utilizzare spin_lock() e spin_unlock() per
proteggere i dati condivisi, che siano timer, tasklet, diversi softirq o
lo stesso o altri softirq: uno qualsiasi di essi potrebbe essere in esecuzione
su un diverso processore.
.. _`it_hardirq-context`:
Contesto di interruzione hardware
=================================
Solitamente le interruzioni hardware comunicano con un tasklet o un softirq.
Spesso questo si traduce nel mettere in coda qualcosa da fare che verrà
preso in carico da un softirq.
Sincronizzazione fra interruzioni hardware e softirq/tasklet
------------------------------------------------------------
Se un gestore di interruzioni hardware condivide dati con un softirq, allora
avrete due preoccupazioni. Primo, il softirq può essere interrotto da
un'interruzione hardware, e secondo, la sezione critica potrebbe essere
eseguita da un'interruzione hardware su un processore diverso. Questo è il caso
dove spin_lock_irq() viene utilizzato. Disabilita le interruzioni
sul processore che l'esegue, poi trattiene il lock. spin_unlock_irq()
fa l'opposto.
Il gestore d'interruzione hardware non ha bisogno di usare spin_lock_irq()
perché i softirq non possono essere eseguiti quando il gestore d'interruzione
hardware è in esecuzione: per questo si può usare spin_lock(), che è un po'
più veloce. L'unica eccezione è quando un altro gestore d'interruzioni
hardware utilizza lo stesso *lock*: spin_lock_irq() impedirà a questo
secondo gestore di interrompere quello in esecuzione.
Questo funziona alla perfezione anche sui sistemi monoprocessore: gli spinlock
svaniscono e questa macro diventa semplicemente local_irq_disable()
(``include/asm/smp.h``), la quale impedisce a softirq/tasklet/BH d'essere
eseguiti.
spin_lock_irqsave() (``include/linux/spinlock.h``) è una variante che
salva lo stato delle interruzioni in una variabile, questa verrà poi passata
a spin_unlock_irqrestore(). Questo significa che lo stesso codice
potrà essere utilizzato in un'interruzione hardware (dove le interruzioni sono
già disabilitate) e in un softirq (dove la disabilitazione delle interruzioni
è richiesta).
Da notare che i softirq (e quindi tasklet e timer) sono eseguiti al ritorno
da un'interruzione hardware, quindi spin_lock_irq() interrompe
anche questi. Tenuto conto di questo si può dire che
spin_lock_irqsave() è la funzione di sincronizzazione più generica
e potente.
Sincronizzazione fra due gestori d'interruzioni hardware
--------------------------------------------------------
Condividere dati fra due gestori di interruzione hardware è molto raro, ma se
succede, dovreste usare spin_lock_irqsave(): è una specificità
dell'architettura il fatto che tutte le interruzioni vengano interrotte
quando si eseguono di gestori di interruzioni.
Bigino della sincronizzazione
=============================
Pete Zaitcev ci offre il seguente riassunto:
- Se siete in un contesto utente (una qualsiasi chiamata di sistema)
e volete sincronizzarvi con altri processi, usate i mutex. Potete trattenere
il mutex e dormire (``copy_from_user(`` o ``kmalloc(x,GFP_KERNEL)``).
- Altrimenti (== i dati possono essere manipolati da un'interruzione) usate
spin_lock_irqsave() e spin_unlock_irqrestore().
- Evitate di trattenere uno spinlock per più di 5 righe di codice incluse
le chiamate a funzione (ad eccezione di quell per l'accesso come
readb()).
Tabella dei requisiti minimi
----------------------------
La tabella seguente illustra i requisiti **minimi** per la sincronizzazione fra
diversi contesti. In alcuni casi, lo stesso contesto può essere eseguito solo
da un processore per volta, quindi non ci sono requisiti per la
sincronizzazione (per esempio, un thread può essere eseguito solo su un
processore alla volta, ma se deve condividere dati con un altro thread, allora
la sincronizzazione è necessaria).
Ricordatevi il suggerimento qui sopra: potete sempre usare
spin_lock_irqsave(), che è un sovrainsieme di tutte le altre funzioni
per spinlock.
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
. IRQ Handler A IRQ Handler B Softirq A Softirq B Tasklet A Tasklet B Timer A Timer B User Context A User Context B
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
IRQ Handler A None
IRQ Handler B SLIS None
Softirq A SLI SLI SL
Softirq B SLI SLI SL SL
Tasklet A SLI SLI SL SL None
Tasklet B SLI SLI SL SL SL None
Timer A SLI SLI SL SL SL SL None
Timer B SLI SLI SL SL SL SL SL None
User Context A SLI SLI SLBH SLBH SLBH SLBH SLBH SLBH None
User Context B SLI SLI SLBH SLBH SLBH SLBH SLBH SLBH MLI None
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
Table: Tabella dei requisiti per la sincronizzazione
+--------+----------------------------+
| SLIS | spin_lock_irqsave |
+--------+----------------------------+
| SLI | spin_lock_irq |
+--------+----------------------------+
| SL | spin_lock |
+--------+----------------------------+
| SLBH | spin_lock_bh |
+--------+----------------------------+
| MLI | mutex_lock_interruptible |
+--------+----------------------------+
Table: Legenda per la tabella dei requisiti per la sincronizzazione
Le funzioni *trylock*
=====================
Ci sono funzioni che provano a trattenere un *lock* solo una volta e
ritornano immediatamente comunicato il successo od il fallimento
dell'operazione. Posso essere usate quando non serve accedere ai dati
protetti dal *lock* quando qualche altro thread lo sta già facendo
trattenendo il *lock*. Potrete acquisire il *lock* più tardi se vi
serve accedere ai dati protetti da questo *lock*.
La funzione spin_trylock() non ritenta di acquisire il *lock*,
se ci riesce al primo colpo ritorna un valore diverso da zero, altrimenti
se fallisce ritorna 0. Questa funzione può essere utilizzata in un qualunque
contesto, ma come spin_lock(): dovete disabilitare i contesti che
potrebbero interrompervi e quindi trattenere lo spinlock.
La funzione mutex_trylock() invece di sospendere il vostro processo
ritorna un valore diverso da zero se è possibile trattenere il lock al primo
colpo, altrimenti se fallisce ritorna 0. Nonostante non dorma, questa funzione
non può essere usata in modo sicuro in contesti di interruzione hardware o
software.
Esempi più comuni
=================
Guardiamo un semplice esempio: una memoria che associa nomi a numeri.
La memoria tiene traccia di quanto spesso viene utilizzato ogni oggetto;
quando è piena, l'oggetto meno usato viene eliminato.
Tutto in contesto utente
------------------------
Nel primo esempio, supponiamo che tutte le operazioni avvengano in contesto
utente (in soldoni, da una chiamata di sistema), quindi possiamo dormire.
Questo significa che possiamo usare i mutex per proteggere la nostra memoria
e tutti gli oggetti che contiene. Ecco il codice::
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/mutex.h>
#include <asm/errno.h>
struct object
{
struct list_head list;
int id;
char name[32];
int popularity;
};
/* Protects the cache, cache_num, and the objects within it */
static DEFINE_MUTEX(cache_lock);
static LIST_HEAD(cache);
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
/* Must be holding cache_lock */
static struct object *__cache_find(int id)
{
struct object *i;
list_for_each_entry(i, &cache, list)
if (i->id == id) {
i->popularity++;
return i;
}
return NULL;
}
/* Must be holding cache_lock */
static void __cache_delete(struct object *obj)
{
BUG_ON(!obj);
list_del(&obj->list);
kfree(obj);
cache_num--;
}
/* Must be holding cache_lock */
static void __cache_add(struct object *obj)
{
list_add(&obj->list, &cache);
if (++cache_num > MAX_CACHE_SIZE) {
struct object *i, *outcast = NULL;
list_for_each_entry(i, &cache, list) {
if (!outcast || i->popularity < outcast->popularity)
outcast = i;
}
__cache_delete(outcast);
}
}
int cache_add(int id, const char *name)
{
struct object *obj;
if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
return -ENOMEM;
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
mutex_lock(&cache_lock);
__cache_add(obj);
mutex_unlock(&cache_lock);
return 0;
}
void cache_delete(int id)
{
mutex_lock(&cache_lock);
__cache_delete(__cache_find(id));
mutex_unlock(&cache_lock);
}
int cache_find(int id, char *name)
{
struct object *obj;
int ret = -ENOENT;
mutex_lock(&cache_lock);
obj = __cache_find(id);
if (obj) {
ret = 0;
strcpy(name, obj->name);
}
mutex_unlock(&cache_lock);
return ret;
}
Da notare che ci assicuriamo sempre di trattenere cache_lock quando
aggiungiamo, rimuoviamo od ispezioniamo la memoria: sia la struttura
della memoria che il suo contenuto sono protetti dal *lock*. Questo
caso è semplice dato che copiamo i dati dall'utente e non permettiamo
mai loro di accedere direttamente agli oggetti.
C'è una piccola ottimizzazione qui: nella funzione cache_add()
impostiamo i campi dell'oggetto prima di acquisire il *lock*. Questo è
sicuro perché nessun altro potrà accedervi finché non lo inseriremo
nella memoria.
Accesso dal contesto utente
---------------------------
Ora consideriamo il caso in cui cache_find() può essere invocata
dal contesto d'interruzione: sia hardware che software. Un esempio potrebbe
essere un timer che elimina oggetti dalla memoria.
Qui di seguito troverete la modifica nel formato *patch*: le righe ``-``
sono quelle rimosse, mentre quelle ``+`` sono quelle aggiunte.
::
--- cache.c.usercontext 2003-12-09 13:58:54.000000000 +1100
+++ cache.c.interrupt 2003-12-09 14:07:49.000000000 +1100
@@ -12,7 +12,7 @@
int popularity;
};
-static DEFINE_MUTEX(cache_lock);
+static DEFINE_SPINLOCK(cache_lock);
static LIST_HEAD(cache);
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
@@ -55,6 +55,7 @@
int cache_add(int id, const char *name)
{
struct object *obj;
+ unsigned long flags;
if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
return -ENOMEM;
@@ -63,30 +64,33 @@
obj->id = id;
obj->popularity = 0;
- mutex_lock(&cache_lock);
+ spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
return 0;
}
void cache_delete(int id)
{
- mutex_lock(&cache_lock);
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
__cache_delete(__cache_find(id));
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
}
int cache_find(int id, char *name)
{
struct object *obj;
int ret = -ENOENT;
+ unsigned long flags;
- mutex_lock(&cache_lock);
+ spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
if (obj) {
ret = 0;
strcpy(name, obj->name);
}
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
return ret;
}
Da notare che spin_lock_irqsave() disabiliterà le interruzioni
se erano attive, altrimenti non farà niente (quando siamo già in un contesto
d'interruzione); dunque queste funzioni possono essere chiamante in
sicurezza da qualsiasi contesto.
Sfortunatamente, cache_add() invoca kmalloc() con
l'opzione ``GFP_KERNEL`` che è permessa solo in contesto utente. Ho supposto
che cache_add() venga chiamata dal contesto utente, altrimenti
questa opzione deve diventare un parametro di cache_add().
Esporre gli oggetti al di fuori del file
----------------------------------------
Se i vostri oggetti contengono più informazioni, potrebbe non essere
sufficiente copiare i dati avanti e indietro: per esempio, altre parti del
codice potrebbero avere un puntatore a questi oggetti piuttosto che cercarli
ogni volta. Questo introduce due problemi.
Il primo problema è che utilizziamo ``cache_lock`` per proteggere gli oggetti:
dobbiamo renderlo dinamico così che il resto del codice possa usarlo. Questo
rende la sincronizzazione più complicata dato che non avviene più in un unico
posto.
Il secondo problema è il problema del ciclo di vita: se un'altra struttura
mantiene un puntatore ad un oggetto, presumibilmente si aspetta che questo
puntatore rimanga valido. Sfortunatamente, questo è garantito solo mentre
si trattiene il *lock*, altrimenti qualcuno potrebbe chiamare
cache_delete() o peggio, aggiungere un oggetto che riutilizza lo
stesso indirizzo.
Dato che c'è un solo *lock*, non potete trattenerlo a vita: altrimenti
nessun altro potrà eseguire il proprio lavoro.
La soluzione a questo problema è l'uso di un contatore di riferimenti:
chiunque punti ad un oggetto deve incrementare il contatore, e decrementarlo
quando il puntatore non viene più usato. Quando il contatore raggiunge lo zero
significa che non è più usato e l'oggetto può essere rimosso.
Ecco il codice::
--- cache.c.interrupt 2003-12-09 14:25:43.000000000 +1100
+++ cache.c.refcnt 2003-12-09 14:33:05.000000000 +1100
@@ -7,6 +7,7 @@
struct object
{
struct list_head list;
+ unsigned int refcnt;
int id;
char name[32];
int popularity;
@@ -17,6 +18,35 @@
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
+static void __object_put(struct object *obj)
+{
+ if (--obj->refcnt == 0)
+ kfree(obj);
+}
+
+static void __object_get(struct object *obj)
+{
+ obj->refcnt++;
+}
+
+void object_put(struct object *obj)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
+ __object_put(obj);
+ spin_unlock_irqrestore(&cache_lock, flags);
+}
+
+void object_get(struct object *obj)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
+ __object_get(obj);
+ spin_unlock_irqrestore(&cache_lock, flags);
+}
+
/* Must be holding cache_lock */
static struct object *__cache_find(int id)
{
@@ -35,6 +65,7 @@
{
BUG_ON(!obj);
list_del(&obj->list);
+ __object_put(obj);
cache_num--;
}
@@ -63,6 +94,7 @@
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
+ obj->refcnt = 1; /* The cache holds a reference */
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
@@ -79,18 +111,15 @@
spin_unlock_irqrestore(&cache_lock, flags);
}
-int cache_find(int id, char *name)
+struct object *cache_find(int id)
{
struct object *obj;
- int ret = -ENOENT;
unsigned long flags;
spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
- if (obj) {
- ret = 0;
- strcpy(name, obj->name);
- }
+ if (obj)
+ __object_get(obj);
spin_unlock_irqrestore(&cache_lock, flags);
- return ret;
+ return obj;
}
Abbiamo incapsulato il contatore di riferimenti nelle tipiche funzioni
di 'get' e 'put'. Ora possiamo ritornare l'oggetto da cache_find()
col vantaggio che l'utente può dormire trattenendo l'oggetto (per esempio,
copy_to_user() per copiare il nome verso lo spazio utente).
Un altro punto da notare è che ho detto che il contatore dovrebbe incrementarsi
per ogni puntatore ad un oggetto: quindi il contatore di riferimenti è 1
quando l'oggetto viene inserito nella memoria. In altre versione il framework
non trattiene un riferimento per se, ma diventa più complicato.
Usare operazioni atomiche per il contatore di riferimenti
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In sostanza, :c:type:`atomic_t` viene usato come contatore di riferimenti.
Ci sono un certo numbero di operazioni atomiche definite
in ``include/asm/atomic.h``: queste sono garantite come atomiche su qualsiasi
processore del sistema, quindi non sono necessari i *lock*. In questo caso è
più semplice rispetto all'uso degli spinlock, benché l'uso degli spinlock
sia più elegante per casi non banali. Le funzioni atomic_inc() e
atomic_dec_and_test() vengono usate al posto dei tipici operatori di
incremento e decremento, e i *lock* non sono più necessari per proteggere il
contatore stesso.
::
--- cache.c.refcnt 2003-12-09 15:00:35.000000000 +1100
+++ cache.c.refcnt-atomic 2003-12-11 15:49:42.000000000 +1100
@@ -7,7 +7,7 @@
struct object
{
struct list_head list;
- unsigned int refcnt;
+ atomic_t refcnt;
int id;
char name[32];
int popularity;
@@ -18,33 +18,15 @@
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
-static void __object_put(struct object *obj)
-{
- if (--obj->refcnt == 0)
- kfree(obj);
-}
-
-static void __object_get(struct object *obj)
-{
- obj->refcnt++;
-}
-
void object_put(struct object *obj)
{
- unsigned long flags;
-
- spin_lock_irqsave(&cache_lock, flags);
- __object_put(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ if (atomic_dec_and_test(&obj->refcnt))
+ kfree(obj);
}
void object_get(struct object *obj)
{
- unsigned long flags;
-
- spin_lock_irqsave(&cache_lock, flags);
- __object_get(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ atomic_inc(&obj->refcnt);
}
/* Must be holding cache_lock */
@@ -65,7 +47,7 @@
{
BUG_ON(!obj);
list_del(&obj->list);
- __object_put(obj);
+ object_put(obj);
cache_num--;
}
@@ -94,7 +76,7 @@
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
- obj->refcnt = 1; /* The cache holds a reference */
+ atomic_set(&obj->refcnt, 1); /* The cache holds a reference */
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
@@ -119,7 +101,7 @@
spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
if (obj)
- __object_get(obj);
+ object_get(obj);
spin_unlock_irqrestore(&cache_lock, flags);
return obj;
}
Proteggere l'oggetto stesso
---------------------------
In questo esempio, assumiamo che gli oggetti (ad eccezione del contatore
di riferimenti) non cambino mai dopo la loro creazione. Se vogliamo permettere
al nome di cambiare abbiamo tre possibilità:
- Si può togliere static da ``cache_lock`` e dire agli utenti che devono
trattenere il *lock* prima di modificare il nome di un oggetto.
- Si può fornire una funzione cache_obj_rename() che prende il
*lock* e cambia il nome per conto del chiamante; si dirà poi agli utenti
di usare questa funzione.
- Si può decidere che ``cache_lock`` protegge solo la memoria stessa, ed
un altro *lock* è necessario per la protezione del nome.
Teoricamente, possiamo avere un *lock* per ogni campo e per ogni oggetto.
In pratica, le varianti più comuni sono:
- un *lock* che protegge l'infrastruttura (la lista ``cache`` di questo
esempio) e gli oggetti. Questo è quello che abbiamo fatto finora.
- un *lock* che protegge l'infrastruttura (inclusi i puntatori alla lista
negli oggetti), e un *lock* nell'oggetto per proteggere il resto
dell'oggetto stesso.
- *lock* multipli per proteggere l'infrastruttura (per esempio un *lock*
per ogni lista), possibilmente con un *lock* per oggetto.
Qui di seguito un'implementazione con "un lock per oggetto":
::
--- cache.c.refcnt-atomic 2003-12-11 15:50:54.000000000 +1100
+++ cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
@@ -6,11 +6,17 @@
struct object
{
+ /* These two protected by cache_lock. */
struct list_head list;
+ int popularity;
+
atomic_t refcnt;
+
+ /* Doesn't change once created. */
int id;
+
+ spinlock_t lock; /* Protects the name */
char name[32];
- int popularity;
};
static DEFINE_SPINLOCK(cache_lock);
@@ -77,6 +84,7 @@
obj->id = id;
obj->popularity = 0;
atomic_set(&obj->refcnt, 1); /* The cache holds a reference */
+ spin_lock_init(&obj->lock);
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
Da notare che ho deciso che il contatore di popolarità dovesse essere
protetto da ``cache_lock`` piuttosto che dal *lock* dell'oggetto; questo
perché è logicamente parte dell'infrastruttura (come
:c:type:`struct list_head <list_head>` nell'oggetto). In questo modo,
in __cache_add(), non ho bisogno di trattenere il *lock* di ogni
oggetto mentre si cerca il meno popolare.
Ho anche deciso che il campo id è immutabile, quindi non ho bisogno di
trattenere il lock dell'oggetto quando si usa __cache_find()
per leggere questo campo; il *lock* dell'oggetto è usato solo dal chiamante
che vuole leggere o scrivere il campo name.
Inoltre, da notare che ho aggiunto un commento che descrive i dati che sono
protetti dal *lock*. Questo è estremamente importante in quanto descrive il
comportamento del codice, che altrimenti sarebbe di difficile comprensione
leggendo solamente il codice. E come dice Alan Cox: “Lock data, not code”.
Problemi comuni
===============
Stallo: semplice ed avanzato
----------------------------
Esiste un tipo di baco dove un pezzo di codice tenta di trattenere uno
spinlock due volte: questo rimarrà in attesa attiva per sempre aspettando che
il *lock* venga rilasciato (in Linux spinlocks, rwlocks e mutex non sono
ricorsivi).
Questo è facile da diagnosticare: non è uno di quei problemi che ti tengono
sveglio 5 notti a parlare da solo.
Un caso un pochino più complesso; immaginate d'avere una spazio condiviso
fra un softirq ed il contesto utente. Se usate spin_lock() per
proteggerlo, il contesto utente potrebbe essere interrotto da un softirq
mentre trattiene il lock, da qui il softirq rimarrà in attesa attiva provando
ad acquisire il *lock* già trattenuto nel contesto utente.
Questi casi sono chiamati stalli (*deadlock*), e come mostrato qui sopra,
può succedere anche con un solo processore (Ma non sui sistemi
monoprocessore perché gli spinlock spariscano quando il kernel è compilato
con ``CONFIG_SMP``\ =n. Nonostante ciò, nel secondo caso avrete comunque
una corruzione dei dati).
Questi casi sono facili da diagnosticare; sui sistemi multi-processore
il supervisione (*watchdog*) o l'opzione di compilazione ``DEBUG_SPINLOCK``
(``include/linux/spinlock.h``) permettono di scovare immediatamente quando
succedono.
Esiste un caso più complesso che è conosciuto come l'abbraccio della morte;
questo coinvolge due o più *lock*. Diciamo che avete un vettore di hash in cui
ogni elemento è uno spinlock a cui è associata una lista di elementi con lo
stesso hash. In un gestore di interruzioni software, dovete modificare un
oggetto e spostarlo su un altro hash; quindi dovrete trattenete lo spinlock
del vecchio hash e di quello nuovo, quindi rimuovere l'oggetto dal vecchio ed
inserirlo nel nuovo.
Qui abbiamo due problemi. Primo, se il vostro codice prova a spostare un
oggetto all'interno della stessa lista, otterrete uno stallo visto che
tenterà di trattenere lo stesso *lock* due volte. Secondo, se la stessa
interruzione software su un altro processore sta tentando di spostare
un altro oggetto nella direzione opposta, potrebbe accadere quanto segue:
+---------------------------------+---------------------------------+
| CPU 1 | CPU 2 |
+=================================+=================================+
| Trattiene *lock* A -> OK | Trattiene *lock* B -> OK |
+---------------------------------+---------------------------------+
| Trattiene *lock* B -> attesa | Trattiene *lock* A -> attesa |
+---------------------------------+---------------------------------+
Table: Conseguenze
Entrambe i processori rimarranno in attesa attiva sul *lock* per sempre,
aspettando che l'altro lo rilasci. Sembra e puzza come un blocco totale.
Prevenire gli stalli
--------------------
I libri di testo vi diranno che se trattenete i *lock* sempre nello stesso
ordine non avrete mai un simile stallo. La pratica vi dirà che questo
approccio non funziona all'ingrandirsi del sistema: quando creo un nuovo
*lock* non ne capisco abbastanza del kernel per dire in quale dei 5000 *lock*
si incastrerà.
I *lock* migliori sono quelli incapsulati: non vengono esposti nei file di
intestazione, e non vengono mai trattenuti fuori dallo stesso file. Potete
rileggere questo codice e vedere che non ci sarà mai uno stallo perché
non tenterà mai di trattenere un altro *lock* quando lo ha già.
Le persone che usano il vostro codice non devono nemmeno sapere che voi
state usando dei *lock*.
Un classico problema deriva dall'uso di *callback* e di *hook*: se li
chiamate mentre trattenete un *lock*, rischiate uno stallo o un abbraccio
della morte (chi lo sa cosa farà una *callback*?).
Ossessiva prevenzione degli stalli
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Gli stalli sono un problema, ma non così terribile come la corruzione dei dati.
Un pezzo di codice trattiene un *lock* di lettura, cerca in una lista,
fallisce nel trovare quello che vuole, quindi rilascia il *lock* di lettura,
trattiene un *lock* di scrittura ed inserisce un oggetto; questo genere di
codice presenta una corsa critica.
corsa fra temporizzatori: un passatempo del kernel
--------------------------------------------------
I temporizzatori potrebbero avere dei problemi con le corse critiche.
Considerate una collezione di oggetti (liste, hash, eccetera) dove ogni oggetto
ha un temporizzatore che sta per distruggerlo.
Se volete eliminare l'intera collezione (diciamo quando rimuovete un modulo),
potreste fare come segue::
/* THIS CODE BAD BAD BAD BAD: IF IT WAS ANY WORSE IT WOULD USE
HUNGARIAN NOTATION */
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
timer_delete(&list->timer);
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Primo o poi, questo esploderà su un sistema multiprocessore perché un
temporizzatore potrebbe essere già partiro prima di spin_lock_bh(),
e prenderà il *lock* solo dopo spin_unlock_bh(), e cercherà
di eliminare il suo oggetto (che però è già stato eliminato).
Questo può essere evitato controllando il valore di ritorno di
timer_delete(): se ritorna 1, il temporizzatore è stato già
rimosso. Se 0, significa (in questo caso) che il temporizzatore è in
esecuzione, quindi possiamo fare come segue::
retry:
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
if (!timer_delete(&list->timer)) {
/* Give timer a chance to delete this */
spin_unlock_bh(&list_lock);
goto retry;
}
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Un altro problema è l'eliminazione dei temporizzatori che si riavviano
da soli (chiamando add_timer() alla fine della loro esecuzione).
Dato che questo è un problema abbastanza comune con una propensione
alle corse critiche, dovreste usare timer_delete_sync()
(``include/linux/timer.h``) per gestire questo caso.
Prima di rilasciare un temporizzatore dovreste chiamare la funzione
timer_shutdown() o timer_shutdown_sync() di modo che non venga più riarmato.
Ogni successivo tentativo di riarmare il temporizzatore verrà silenziosamente
ignorato.
Velocità della sincronizzazione
===============================
Ci sono tre cose importanti da tenere in considerazione quando si valuta
la velocità d'esecuzione di un pezzo di codice che necessita di
sincronizzazione. La prima è la concorrenza: quante cose rimangono in attesa
mentre qualcuno trattiene un *lock*. La seconda è il tempo necessario per
acquisire (senza contese) e rilasciare un *lock*. La terza è di usare meno
*lock* o di più furbi. Immagino che i *lock* vengano usati regolarmente,
altrimenti, non sareste interessati all'efficienza.
La concorrenza dipende da quanto a lungo un *lock* è trattenuto: dovreste
trattenere un *lock* solo il tempo minimo necessario ma non un istante in più.
Nella memoria dell'esempio precedente, creiamo gli oggetti senza trattenere
il *lock*, poi acquisiamo il *lock* quando siamo pronti per inserirlo nella
lista.
Il tempo di acquisizione di un *lock* dipende da quanto danno fa
l'operazione sulla *pipeline* (ovvero stalli della *pipeline*) e quant'è
probabile che il processore corrente sia stato anche l'ultimo ad acquisire
il *lock* (in pratica, il *lock* è nella memoria cache del processore
corrente?): su sistemi multi-processore questa probabilità precipita
rapidamente. Consideriamo un processore Intel Pentium III a 700Mhz: questo
esegue un'istruzione in 0.7ns, un incremento atomico richiede 58ns, acquisire
un *lock* che è nella memoria cache del processore richiede 160ns, e un
trasferimento dalla memoria cache di un altro processore richiede altri
170/360ns (Leggetevi l'articolo di Paul McKenney's `Linux Journal RCU
article <http://www.linuxjournal.com/article.php?sid=6993>`__).
Questi due obiettivi sono in conflitto: trattenere un *lock* per il minor
tempo possibile potrebbe richiedere la divisione in più *lock* per diverse
parti (come nel nostro ultimo esempio con un *lock* per ogni oggetto),
ma questo aumenta il numero di acquisizioni di *lock*, ed il risultato
spesso è che tutto è più lento che con un singolo *lock*. Questo è un altro
argomento in favore della semplicità quando si parla di sincronizzazione.
Il terzo punto è discusso di seguito: ci sono alcune tecniche per ridurre
il numero di sincronizzazioni che devono essere fatte.
Read/Write Lock Variants
------------------------
Sia gli spinlock che i mutex hanno una variante per la lettura/scrittura
(read/write): ``rwlock_t`` e :c:type:`struct rw_semaphore <rw_semaphore>`.
Queste dividono gli utenti in due categorie: i lettori e gli scrittori.
Se state solo leggendo i dati, potete acquisire il *lock* di lettura, ma
per scrivere avrete bisogno del *lock* di scrittura. Molti possono trattenere
il *lock* di lettura, ma solo uno scrittore alla volta può trattenere
quello di scrittura.
Se il vostro codice si divide chiaramente in codice per lettori e codice
per scrittori (come nel nostro esempio), e il *lock* dei lettori viene
trattenuto per molto tempo, allora l'uso di questo tipo di *lock* può aiutare.
Questi sono leggermente più lenti rispetto alla loro versione normale, quindi
nella pratica l'uso di ``rwlock_t`` non ne vale la pena.
Evitare i *lock*: Read Copy Update
--------------------------------------------
Esiste un metodo di sincronizzazione per letture e scritture detto
Read Copy Update. Con l'uso della tecnica RCU, i lettori possono scordarsi
completamente di trattenere i *lock*; dato che nel nostro esempio ci
aspettiamo d'avere più lettore che scrittori (altrimenti questa memoria
sarebbe uno spreco) possiamo dire che questo meccanismo permette
un'ottimizzazione.
Come facciamo a sbarazzarci dei *lock* di lettura? Sbarazzarsi dei *lock* di
lettura significa che uno scrittore potrebbe cambiare la lista sotto al naso
dei lettori. Questo è abbastanza semplice: possiamo leggere una lista
concatenata se lo scrittore aggiunge elementi alla fine e con certe
precauzioni. Per esempio, aggiungendo ``new`` ad una lista concatenata
chiamata ``list``::
new->next = list->next;
wmb();
list->next = new;
La funzione wmb() è una barriera di sincronizzazione delle
scritture. Questa garantisce che la prima operazione (impostare l'elemento
``next`` del nuovo elemento) venga completata e vista da tutti i processori
prima che venga eseguita la seconda operazione (che sarebbe quella di mettere
il nuovo elemento nella lista). Questo è importante perché i moderni
compilatori ed i moderni processori possono, entrambe, riordinare le istruzioni
se non vengono istruiti altrimenti: vogliamo che i lettori non vedano
completamente il nuovo elemento; oppure che lo vedano correttamente e quindi
il puntatore ``next`` deve puntare al resto della lista.
Fortunatamente, c'è una funzione che fa questa operazione sulle liste
:c:type:`struct list_head <list_head>`: list_add_rcu()
(``include/linux/list.h``).
Rimuovere un elemento dalla lista è anche più facile: sostituiamo il puntatore
al vecchio elemento con quello del suo successore, e i lettori vedranno
l'elemento o lo salteranno.
::
list->next = old->next;
La funzione list_del_rcu() (``include/linux/list.h``) fa esattamente
questo (la versione normale corrompe il vecchio oggetto, e non vogliamo che
accada).
Anche i lettori devono stare attenti: alcuni processori potrebbero leggere
attraverso il puntatore ``next`` il contenuto dell'elemento successivo
troppo presto, ma non accorgersi che il contenuto caricato è sbagliato quando
il puntatore ``next`` viene modificato alla loro spalle. Ancora una volta
c'è una funzione che viene in vostro aiuto list_for_each_entry_rcu()
(``include/linux/list.h``). Ovviamente, gli scrittori possono usare
list_for_each_entry() dato che non ci possono essere due scrittori
in contemporanea.
Il nostro ultimo dilemma è il seguente: quando possiamo realmente distruggere
l'elemento rimosso? Ricordate, un lettore potrebbe aver avuto accesso a questo
elemento proprio ora: se eliminiamo questo elemento ed il puntatore ``next``
cambia, il lettore salterà direttamente nella spazzatura e scoppierà. Dobbiamo
aspettare finché tutti i lettori che stanno attraversando la lista abbiano
finito. Utilizziamo call_rcu() per registrare una funzione di
richiamo che distrugga l'oggetto quando tutti i lettori correnti hanno
terminato. In alternative, potrebbe essere usata la funzione
synchronize_rcu() che blocca l'esecuzione finché tutti i lettori
non terminano di ispezionare la lista.
Ma come fa l'RCU a sapere quando i lettori sono finiti? Il meccanismo è
il seguente: innanzi tutto i lettori accedono alla lista solo fra la coppia
rcu_read_lock()/rcu_read_unlock() che disabilita la
prelazione così che i lettori non vengano sospesi mentre stanno leggendo
la lista.
Poi, l'RCU aspetta finché tutti i processori non abbiano dormito almeno
una volta; a questo punto, dato che i lettori non possono dormire, possiamo
dedurre che un qualsiasi lettore che abbia consultato la lista durante la
rimozione abbia già terminato, quindi la *callback* viene eseguita. Il vero
codice RCU è un po' più ottimizzato di così, ma questa è l'idea di fondo.
::
--- cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
+++ cache.c.rcupdate 2003-12-11 17:55:14.000000000 +1100
@@ -1,15 +1,18 @@
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/string.h>
+#include <linux/rcupdate.h>
#include <linux/mutex.h>
#include <asm/errno.h>
struct object
{
- /* These two protected by cache_lock. */
+ /* This is protected by RCU */
struct list_head list;
int popularity;
+ struct rcu_head rcu;
+
atomic_t refcnt;
/* Doesn't change once created. */
@@ -40,7 +43,7 @@
{
struct object *i;
- list_for_each_entry(i, &cache, list) {
+ list_for_each_entry_rcu(i, &cache, list) {
if (i->id == id) {
i->popularity++;
return i;
@@ -49,19 +52,25 @@
return NULL;
}
+/* Final discard done once we know no readers are looking. */
+static void cache_delete_rcu(void *arg)
+{
+ object_put(arg);
+}
+
/* Must be holding cache_lock */
static void __cache_delete(struct object *obj)
{
BUG_ON(!obj);
- list_del(&obj->list);
- object_put(obj);
+ list_del_rcu(&obj->list);
cache_num--;
+ call_rcu(&obj->rcu, cache_delete_rcu);
}
/* Must be holding cache_lock */
static void __cache_add(struct object *obj)
{
- list_add(&obj->list, &cache);
+ list_add_rcu(&obj->list, &cache);
if (++cache_num > MAX_CACHE_SIZE) {
struct object *i, *outcast = NULL;
list_for_each_entry(i, &cache, list) {
@@ -104,12 +114,11 @@
struct object *cache_find(int id)
{
struct object *obj;
- unsigned long flags;
- spin_lock_irqsave(&cache_lock, flags);
+ rcu_read_lock();
obj = __cache_find(id);
if (obj)
object_get(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ rcu_read_unlock();
return obj;
}
Da notare che i lettori modificano il campo popularity nella funzione
__cache_find(), e ora non trattiene alcun *lock*. Una soluzione
potrebbe essere quella di rendere la variabile ``atomic_t``, ma per l'uso
che ne abbiamo fatto qui, non ci interessano queste corse critiche perché un
risultato approssimativo è comunque accettabile, quindi non l'ho cambiato.
Il risultato è che la funzione cache_find() non ha bisogno di alcuna
sincronizzazione con le altre funzioni, quindi è veloce su un sistema
multi-processore tanto quanto lo sarebbe su un sistema mono-processore.
Esiste un'ulteriore ottimizzazione possibile: vi ricordate il codice originale
della nostra memoria dove non c'erano contatori di riferimenti e il chiamante
semplicemente tratteneva il *lock* prima di accedere ad un oggetto? Questo è
ancora possibile: se trattenete un *lock* nessuno potrà cancellare l'oggetto,
quindi non avete bisogno di incrementare e decrementare il contatore di
riferimenti.
Ora, dato che il '*lock* di lettura' di un RCU non fa altro che disabilitare
la prelazione, un chiamante che ha sempre la prelazione disabilitata fra le
chiamate cache_find() e object_put() non necessita
di incrementare e decrementare il contatore di riferimenti. Potremmo
esporre la funzione __cache_find() dichiarandola non-static,
e quel chiamante potrebbe usare direttamente questa funzione.
Il beneficio qui sta nel fatto che il contatore di riferimenti no
viene scritto: l'oggetto non viene alterato in alcun modo e quindi diventa
molto più veloce su sistemi molti-processore grazie alla loro memoria cache.
Dati per processore
-------------------
Un'altra tecnica comunemente usata per evitare la sincronizzazione è quella
di duplicare le informazioni per ogni processore. Per esempio, se volete
avere un contatore di qualcosa, potreste utilizzare uno spinlock ed un
singolo contatore. Facile e pulito.
Se questo dovesse essere troppo lento (solitamente non lo è, ma se avete
dimostrato che lo è devvero), potreste usare un contatore per ogni processore
e quindi non sarebbe più necessaria la mutua esclusione. Vedere
DEFINE_PER_CPU(), get_cpu_var() e put_cpu_var()
(``include/linux/percpu.h``).
Il tipo di dato ``local_t``, la funzione cpu_local_inc() e tutte
le altre funzioni associate, sono di particolare utilità per semplici contatori
per-processore; su alcune architetture sono anche più efficienti
(``include/asm/local.h``).
Da notare che non esiste un modo facile ed affidabile per ottenere il valore
di un simile contatore senza introdurre altri *lock*. In alcuni casi questo
non è un problema.
Dati che sono usati prevalentemente dai gestori d'interruzioni
--------------------------------------------------------------
Se i dati vengono utilizzati sempre dallo stesso gestore d'interruzioni,
allora i *lock* non vi servono per niente: il kernel già vi garantisce che
il gestore d'interruzione non verrà eseguito in contemporanea su diversi
processori.
Manfred Spraul fa notare che potreste comunque comportarvi così anche
se i dati vengono occasionalmente utilizzati da un contesto utente o
da un'interruzione software. Il gestore d'interruzione non utilizza alcun
*lock*, e tutti gli altri accessi verranno fatti così::
mutex_lock(&lock);
disable_irq(irq);
...
enable_irq(irq);
mutex_unlock(&lock);
La funzione disable_irq() impedisce al gestore d'interruzioni
d'essere eseguito (e aspetta che finisca nel caso fosse in esecuzione su
un altro processore). Lo spinlock, invece, previene accessi simultanei.
Naturalmente, questo è più lento della semplice chiamata
spin_lock_irq(), quindi ha senso solo se questo genere di accesso
è estremamente raro.
Quali funzioni possono essere chiamate in modo sicuro dalle interruzioni?
=========================================================================
Molte funzioni del kernel dormono (in sostanza, chiamano schedule())
direttamente od indirettamente: non potete chiamarle se trattenere uno
spinlock o avete la prelazione disabilitata, mai. Questo significa che
dovete necessariamente essere nel contesto utente: chiamarle da un
contesto d'interruzione è illegale.
Alcune funzioni che dormono
---------------------------
Le più comuni sono elencate qui di seguito, ma solitamente dovete leggere
il codice per scoprire se altre chiamate sono sicure. Se chiunque altro
le chiami dorme, allora dovreste poter dormire anche voi. In particolar
modo, le funzioni di registrazione e deregistrazione solitamente si
aspettano d'essere chiamante da un contesto utente e quindi che possono
dormire.
- Accessi allo spazio utente:
- copy_from_user()
- copy_to_user()
- get_user()
- put_user()
- kmalloc(GFP_KERNEL) <kmalloc>`
- mutex_lock_interruptible() and
mutex_lock()
C'è anche mutex_trylock() che però non dorme.
Comunque, non deve essere usata in un contesto d'interruzione dato
che la sua implementazione non è sicura in quel contesto.
Anche mutex_unlock() non dorme mai. Non può comunque essere
usata in un contesto d'interruzione perché un mutex deve essere rilasciato
dallo stesso processo che l'ha acquisito.
Alcune funzioni che non dormono
-------------------------------
Alcune funzioni possono essere chiamate tranquillamente da qualsiasi
contesto, o trattenendo un qualsiasi *lock*.
- printk()
- kfree()
- add_timer() e timer_delete()
Riferimento per l'API dei Mutex
===============================
.. kernel-doc:: include/linux/mutex.h
:internal:
.. kernel-doc:: kernel/locking/mutex.c
:export:
Riferimento per l'API dei Futex
===============================
.. kernel-doc:: kernel/futex/core.c
:internal:
.. kernel-doc:: kernel/futex/futex.h
:internal:
.. kernel-doc:: kernel/futex/pi.c
:internal:
.. kernel-doc:: kernel/futex/requeue.c
:internal:
.. kernel-doc:: kernel/futex/waitwake.c
:internal:
Approfondimenti
===============
- ``Documentation/locking/spinlocks.rst``: la guida di Linus Torvalds agli
spinlock del kernel.
- Unix Systems for Modern Architectures: Symmetric Multiprocessing and
Caching for Kernel Programmers.
L'introduzione alla sincronizzazione a livello di kernel di Curt Schimmel
è davvero ottima (non è scritta per Linux, ma approssimativamente si adatta
a tutte le situazioni). Il libro è costoso, ma vale ogni singolo spicciolo
per capire la sincronizzazione nei sistemi multi-processore.
[ISBN: 0201633388]
Ringraziamenti
==============
Grazie a Telsa Gwynne per aver formattato questa guida in DocBook, averla
pulita e aggiunto un po' di stile.
Grazie a Martin Pool, Philipp Rumpf, Stephen Rothwell, Paul Mackerras,
Ruedi Aschwanden, Alan Cox, Manfred Spraul, Tim Waugh, Pete Zaitcev,
James Morris, Robert Love, Paul McKenney, John Ashby per aver revisionato,
corretto, maledetto e commentato.
Grazie alla congrega per non aver avuto alcuna influenza su questo documento.
Glossario
=========
prelazione
Prima del kernel 2.5, o quando ``CONFIG_PREEMPT`` non è impostato, i processi
in contesto utente non si avvicendano nell'esecuzione (in pratica, il
processo userà il processore fino al proprio termine, a meno che non ci siano
delle interruzioni). Con l'aggiunta di ``CONFIG_PREEMPT`` nella versione
2.5.4 questo è cambiato: quando si è in contesto utente, processi con una
priorità maggiore possono subentrare nell'esecuzione: gli spinlock furono
cambiati per disabilitare la prelazioni, anche su sistemi monoprocessore.
bh
Bottom Half: per ragioni storiche, le funzioni che contengono '_bh' nel
loro nome ora si riferiscono a qualsiasi interruzione software; per esempio,
spin_lock_bh() blocca qualsiasi interuzione software sul processore
corrente. I *Bottom Halves* sono deprecati, e probabilmente verranno
sostituiti dai tasklet. In un dato momento potrà esserci solo un
*bottom half* in esecuzione.
contesto d'interruzione
Non è il contesto utente: qui si processano le interruzioni hardware e
software. La macro in_interrupt() ritorna vero.
contesto utente
Il kernel che esegue qualcosa per conto di un particolare processo (per
esempio una chiamata di sistema) o di un thread del kernel. Potete
identificare il processo con la macro ``current``. Da non confondere
con lo spazio utente. Può essere interrotto sia da interruzioni software
che hardware.
interruzione hardware
Richiesta di interruzione hardware. in_hardirq() ritorna vero in un
gestore d'interruzioni hardware.
interruzione software / softirq
Gestore di interruzioni software: in_hardirq() ritorna falso;
in_softirq() ritorna vero. I tasklet e le softirq sono entrambi
considerati 'interruzioni software'.
In soldoni, un softirq è uno delle 32 interruzioni software che possono
essere eseguite su più processori in contemporanea. A volte si usa per
riferirsi anche ai tasklet (in pratica tutte le interruzioni software).
monoprocessore / UP
(Uni-Processor) un solo processore, ovvero non è SMP. (``CONFIG_SMP=n``).
multi-processore / SMP
(Symmetric Multi-Processor) kernel compilati per sistemi multi-processore
(``CONFIG_SMP=y``).
spazio utente
Un processo che esegue il proprio codice fuori dal kernel.
tasklet
Un'interruzione software registrabile dinamicamente che ha la garanzia
d'essere eseguita solo su un processore alla volta.
timer
Un'interruzione software registrabile dinamicamente che viene eseguita
(circa) in un determinato momento. Quando è in esecuzione è come un tasklet
(infatti, sono chiamati da ``TIMER_SOFTIRQ``).
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
소개, 경쟁 상태와 임계 구역
1-101이 문서는 이탈리아어 공통 면책 고지 `../disclaimer-ita.rst`를 포함하고 C namespace를 `it_IT`로 설정합니다. 공식 영어 원문은 `Documentation/kernel-hacking/locking.rst`의 `kernel_hacking_lock` 참조이며, 번역자는 Federico Vaga, 원 저자는 Rusty Russell입니다.
`_it_kernel_hacking_lock`은 이탈리아어 페이지 앵커입니다. 제목은 ‘신뢰하기 어려운 동기화 안내서’이며 원문은 Linux 2.6 시기의 잠금 체계를 설명하지만, 로컬 Linux v6.18.37 문서에 남아 있는 내용과 식별자를 그대로 번역합니다.
멀티스레딩과 커널 선점이 널리 쓰이므로 커널 개발자는 멀티프로세서 시스템의 동시성과 잠금 기초를 알아야 합니다.
단순한 `counter++`도 실제 기계 명령에서는 읽기, 1 더하기, 쓰기의 세 단계가 됩니다. 두 실행 인스턴스가 차례대로 완료되면 초기값 5는 7이 됩니다.
In un normale programma, potete incrementare un contatore nel seguente modo:
::
contatore++;
Questo è quello che vi aspettereste che accada sempre:
.. table:: Risultati attesi
+------------------------------------+------------------------------------+
| Istanza 1 | Istanza 2 |
+====================================+====================================+
| leggi contatore (5) | |
+------------------------------------+------------------------------------+
| aggiungi 1 (6) | |
+------------------------------------+------------------------------------+
| scrivi contatore (6) | |
+------------------------------------+------------------------------------+
| | leggi contatore (6) |
+------------------------------------+------------------------------------+
| | aggiungi 1 (7) |
+------------------------------------+------------------------------------+
| | scrivi contatore (7) |
+------------------------------------+------------------------------------+
그러나 두 인스턴스가 모두 5를 읽은 뒤 각각 6을 쓰면 증가 하나가 사라져 최종값은 6이 됩니다. 소스 한 줄이 원자적이라는 보장은 없습니다.
Questo è quello che potrebbe succedere in realtà:
.. table:: Possibile risultato
+------------------------------------+------------------------------------+
| Istanza 1 | Istanza 2 |
+====================================+====================================+
| leggi contatore (5) | |
+------------------------------------+------------------------------------+
| | leggi contatore (5) |
+------------------------------------+------------------------------------+
| aggiungi 1 (6) | |
+------------------------------------+------------------------------------+
| | aggiungi 1 (6) |
+------------------------------------+------------------------------------+
| scrivi contatore (6) | |
+------------------------------------+------------------------------------+
| | scrivi contatore (6) |
+------------------------------------+------------------------------------+
여러 실행 흐름 사이의 시간 배치에 따라 결과가 달라지는 현상을 경쟁 상태라고 합니다. 이 문제가 생기는 코드 영역은 임계 구역입니다.
SMP 시스템에서는 여러 CPU가 동시에 임계 구역을 실행할 수 있어 핵심 설계 문제가 됩니다. 단일 CPU라도 선점이 임계 구역 중간에 다른 실행 흐름을 끼워 넣으면 같은 경쟁이 생깁니다.
해결책은 동시 접근 가능성을 식별하고 잠금을 사용해 한 번에 하나의 실행 인스턴스만 임계 구역에 들어가도록 만드는 것입니다.
두 인스턴스가 순차 실행되면 두 증가가 모두 반영됩니다.
두 인스턴스가 같은 이전 값을 읽으면 증가 하나가 사라집니다.
읽기-수정-쓰기가 겹치면 실행 순서가 결과를 바꿉니다.
.. include:: ../disclaimer-ita.rst
.. c:namespace:: it_IT
:Original: :ref:`Documentation/kernel-hacking/locking.rst <kernel_hacking_lock>`
:Translator: Federico Vaga <federico.vaga@vaga.pv.it>
.. _it_kernel_hacking_lock:
==========================================
L'inaffidabile guida alla sincronizzazione
==========================================
:Author: Rusty Russell
Introduzione
============
Benvenuto, alla notevole ed inaffidabile guida ai problemi di sincronizzazione
(locking) nel kernel. Questo documento descrive il sistema di sincronizzazione
nel kernel Linux 2.6.
Dato il largo utilizzo del multi-threading e della prelazione nel kernel
Linux, chiunque voglia dilettarsi col kernel deve conoscere i concetti
fondamentali della concorrenza e della sincronizzazione nei sistemi
multi-processore.
Il problema con la concorrenza
==============================
(Saltatelo se sapete già cos'è una corsa critica).
In un normale programma, potete incrementare un contatore nel seguente modo:
::
contatore++;
Questo è quello che vi aspettereste che accada sempre:
.. table:: Risultati attesi
+------------------------------------+------------------------------------+
| Istanza 1 | Istanza 2 |
+====================================+====================================+
| leggi contatore (5) | |
+------------------------------------+------------------------------------+
| aggiungi 1 (6) | |
+------------------------------------+------------------------------------+
| scrivi contatore (6) | |
+------------------------------------+------------------------------------+
| | leggi contatore (6) |
+------------------------------------+------------------------------------+
| | aggiungi 1 (7) |
+------------------------------------+------------------------------------+
| | scrivi contatore (7) |
+------------------------------------+------------------------------------+
Questo è quello che potrebbe succedere in realtà:
.. table:: Possibile risultato
+------------------------------------+------------------------------------+
| Istanza 1 | Istanza 2 |
+====================================+====================================+
| leggi contatore (5) | |
+------------------------------------+------------------------------------+
| | leggi contatore (5) |
+------------------------------------+------------------------------------+
| aggiungi 1 (6) | |
+------------------------------------+------------------------------------+
| | aggiungi 1 (6) |
+------------------------------------+------------------------------------+
| scrivi contatore (6) | |
+------------------------------------+------------------------------------+
| | scrivi contatore (6) |
+------------------------------------+------------------------------------+
Corse critiche e sezioni critiche
---------------------------------
Questa sovrapposizione, ovvero quando un risultato dipende dal tempo che
intercorre fra processi diversi, è chiamata corsa critica. La porzione
di codice che contiene questo problema è chiamata sezione critica.
In particolar modo da quando Linux ha incominciato a girare su
macchine multi-processore, le sezioni critiche sono diventate uno dei
maggiori problemi di progettazione ed implementazione del kernel.
La prelazione può sortire gli stessi effetti, anche se c'è una sola CPU:
interrompendo un processo nella sua sezione critica otterremo comunque
la stessa corsa critica. In questo caso, il thread che si avvicenda
nell'esecuzione potrebbe eseguire anch'esso la sezione critica.
La soluzione è quella di riconoscere quando avvengono questi accessi
simultanei, ed utilizzare i *lock* per accertarsi che solo un'istanza
per volta possa entrare nella sezione critica. Il kernel offre delle buone
funzioni a questo scopo. E poi ci sono quelle meno buone, ma farò finta
che non esistano.
spinlock, mutex와 단일 프로세서
102-152잠금 설계에서 가장 중요한 조언은 단순하게 유지하고 새 잠금을 도입하는 데 신중하라는 것입니다.
기본 잠금인 spinlock은 `include/asm/spinlock.h`에 정의됩니다. 한 실행 흐름만 보유할 수 있고 실패한 쪽은 잠금이 풀릴 때까지 CPU에서 능동 대기합니다.
spinlock은 작고 빠르며 잠들 수 없는 컨텍스트에서도 사용할 수 있습니다. 그러나 오래 보유하면 다른 CPU가 계속 회전해 자원을 낭비합니다.
mutex는 `include/linux/mutex.h`에 정의되며 얻지 못한 프로세스를 잠들게 하고, 해제되면 다시 깨웁니다. 기다리는 동안 CPU가 다른 작업을 할 수 있지만 인터럽트처럼 잠들 수 없는 컨텍스트에서는 사용할 수 없습니다.
spinlock과 mutex 모두 재귀 잠금이 아닙니다. 같은 실행 흐름이 같은 잠금을 다시 얻으려 하면 교착됩니다.
`CONFIG_SMP=n`과 `CONFIG_PREEMPT=n`인 커널에서는 spinlock이 사라집니다. 동시에 실행할 다른 흐름이 없으므로 상호 배제가 필요하지 않기 때문입니다.
SMP는 꺼져 있고 선점만 켜진 커널에서는 spinlock이 선점을 비활성화해 경쟁을 막습니다. 따라서 대다수 코드는 선점을 별도 특별 경우로 만들지 않고 SMP 동시성처럼 생각할 수 있습니다.
실제 멀티프로세서 장비가 없어도 `CONFIG_SMP`와 `CONFIG_PREEMPT`를 켜 잠금 문제를 검사해야 합니다. mutex는 사용자 컨텍스트의 프로세스 사이 동기화에 필요하므로 UP에서도 남습니다.
대기 방식과 호출 가능 컨텍스트가 핵심 차이입니다.
동시성 원인이 무엇인지에 따라 구현 효과가 달라집니다.
잠들 수 있는 컨텍스트인지 먼저 판단합니다.
Sincronizzazione nel kernel Linux
=================================
Se dovessi darvi un suggerimento sulla sincronizzazione: **mantenetela
semplice**.
Siate riluttanti nell'introduzione di nuovi *lock*.
I due principali tipi di *lock* nel kernel: spinlock e mutex
------------------------------------------------------------
Ci sono due tipi principali di *lock* nel kernel. Il tipo fondamentale è lo
spinlock (``include/asm/spinlock.h``), un semplice *lock* che può essere
trattenuto solo da un processo: se non si può trattenere lo spinlock, allora
rimane in attesa attiva (in inglese *spinning*) finché non ci riesce.
Gli spinlock sono molto piccoli e rapidi, possono essere utilizzati ovunque.
Il secondo tipo è il mutex (``include/linux/mutex.h``): è come uno spinlock,
ma potreste bloccarvi trattenendolo. Se non potete trattenere un mutex
il vostro processo si auto-sospenderà; verrà riattivato quando il mutex
verrà rilasciato. Questo significa che il processore potrà occuparsi d'altro
mentre il vostro processo è in attesa. Esistono molti casi in cui non potete
permettervi di sospendere un processo (vedere
`Quali funzioni possono essere chiamate in modo sicuro dalle interruzioni?`_)
e quindi dovrete utilizzare gli spinlock.
Nessuno di questi *lock* è ricorsivo: vedere
`Stallo: semplice ed avanzato`_
I *lock* e i kernel per sistemi monoprocessore
----------------------------------------------
Per i kernel compilati senza ``CONFIG_SMP`` e senza ``CONFIG_PREEMPT``
gli spinlock non esistono. Questa è un'ottima scelta di progettazione:
quando nessun altro processo può essere eseguito in simultanea, allora
non c'è la necessità di avere un *lock*.
Se il kernel è compilato senza ``CONFIG_SMP`` ma con ``CONFIG_PREEMPT``,
allora gli spinlock disabilitano la prelazione; questo è sufficiente a
prevenire le corse critiche. Nella maggior parte dei casi, possiamo considerare
la prelazione equivalente ad un sistema multi-processore senza preoccuparci
di trattarla indipendentemente.
Dovreste verificare sempre la sincronizzazione con le opzioni ``CONFIG_SMP`` e
``CONFIG_PREEMPT`` abilitate, anche quando non avete un sistema
multi-processore, questo vi permetterà di identificare alcuni problemi
di sincronizzazione.
Come vedremo di seguito, i mutex continuano ad esistere perché sono necessari
per la sincronizzazione fra processi in contesto utente.
사용자 컨텍스트, softirq, tasklet, timer
153-255자료구조가 사용자 컨텍스트에서만 쓰이면 단순 mutex로 보호할 수 있습니다. 초기화 후 `mutex_lock_interruptible()`로 얻고 `mutex_unlock()`으로 풉니다.
`mutex_lock()`은 신호가 와도 돌아오지 않으므로 피하는 편이 좋다고 원문은 권합니다. `net/netfilter/nf_sockopt.c`의 `nf_sockopt_mutex`는 로드·언로드 등록과 setsockopt/getsockopt 조회를 보호하는 사례입니다.
사용자 컨텍스트와 softirq가 자료를 공유하면 현재 CPU에서 softirq가 사용자를 선점할 수 있고 다른 CPU도 임계 구역을 실행할 수 있습니다. `spin_lock_bh()`는 현재 CPU의 softirq를 막고 spinlock을 얻으며 `spin_unlock_bh()`는 반대로 복원합니다.
`_bh`는 과거 bottom half 명칭의 흔적입니다. 같은 상황에서 `spin_lock_irq()` 또는 `spin_lock_irqsave()`를 써 하드 IRQ까지 막을 수도 있습니다.
UP 빌드에서는 spinlock 부분이 사라지고 `local_bh_disable()` 효과만 남아 softirq 실행을 막습니다.
tasklet과 timer는 softirq에서 실행되므로 사용자 컨텍스트와의 동기화 규칙이 같습니다. 잠금 관점에서 tasklet과 timer는 동일하게 다룹니다.
동일 tasklet은 동시에 두 CPU에서 실행되지 않으므로 자기 자신에 대한 재진입을 걱정할 필요가 없습니다.
서로 다른 tasklet 또는 timer가 자료를 공유하면 `spin_lock()`과 `spin_unlock()`이 필요합니다. 이미 tasklet 컨텍스트라 현재 CPU의 다른 tasklet이 실행되지 않으므로 `_bh` 변형은 불필요합니다.
같은 softirq 종류는 여러 CPU에서 동시에 실행될 수 있습니다. 성능이 중요하면 per-CPU 데이터를 고려할 수 있지만 공유 데이터에는 spinlock이 필요합니다.
서로 다른 softirq, timer, tasklet 사이의 공유 데이터도 어느 것이든 다른 CPU에서 실행될 수 있으므로 `spin_lock()`으로 보호합니다.
공유 상대가 어디서 실행되는지에 따라 최소 잠금이 달라집니다.
로컬 선점을 막는 것과 다른 CPU와 상호 배제하는 것을 함께 수행합니다.
동일 인스턴스의 직렬화 보장과 다른 인스턴스의 병렬 가능성을 구분합니다.
Sincronizzazione in contesto utente
-----------------------------------
Se avete una struttura dati che verrà utilizzata solo dal contesto utente,
allora, per proteggerla, potete utilizzare un semplice mutex
(``include/linux/mutex.h``). Questo è il caso più semplice: inizializzate il
mutex; invocate mutex_lock_interruptible() per trattenerlo e
mutex_unlock() per rilasciarlo. C'è anche mutex_lock()
ma questa dovrebbe essere evitata perché non ritorna in caso di segnali.
Per esempio: ``net/netfilter/nf_sockopt.c`` permette la registrazione
di nuove chiamate per setsockopt() e getsockopt()
usando la funzione nf_register_sockopt(). La registrazione e
la rimozione vengono eseguite solamente quando il modulo viene caricato
o scaricato (e durante l'avvio del sistema, qui non abbiamo concorrenza),
e la lista delle funzioni registrate viene consultata solamente quando
setsockopt() o getsockopt() sono sconosciute al sistema.
In questo caso ``nf_sockopt_mutex`` è perfetto allo scopo, in particolar modo
visto che setsockopt e getsockopt potrebbero dormire.
Sincronizzazione fra il contesto utente e i softirq
---------------------------------------------------
Se un softirq condivide dati col contesto utente, avete due problemi.
Primo, il contesto utente corrente potrebbe essere interroto da un softirq,
e secondo, la sezione critica potrebbe essere eseguita da un altro
processore. Questo è quando spin_lock_bh()
(``include/linux/spinlock.h``) viene utilizzato. Questo disabilita i softirq
sul processore e trattiene il *lock*. Invece, spin_unlock_bh() fa
l'opposto. (Il suffisso '_bh' è un residuo storico che fa riferimento al
"Bottom Halves", il vecchio nome delle interruzioni software. In un mondo
perfetto questa funzione si chiamerebbe 'spin_lock_softirq()').
Da notare che in questo caso potete utilizzare anche spin_lock_irq()
o spin_lock_irqsave(), queste fermano anche le interruzioni hardware:
vedere `Contesto di interruzione hardware`_.
Questo funziona alla perfezione anche sui sistemi monoprocessore: gli spinlock
svaniscono e questa macro diventa semplicemente local_bh_disable()
(``include/linux/interrupt.h``), la quale impedisce ai softirq d'essere
eseguiti.
Sincronizzazione fra contesto utente e i tasklet
------------------------------------------------
Questo caso è uguale al precedente, un tasklet viene eseguito da un softirq.
Sincronizzazione fra contesto utente e i timer
----------------------------------------------
Anche questo caso è uguale al precedente, un timer viene eseguito da un
softirq.
Dal punto di vista della sincronizzazione, tasklet e timer sono identici.
Sincronizzazione fra tasklet e timer
------------------------------------
Qualche volta un tasklet od un timer potrebbero condividere i dati con
un altro tasklet o timer
Lo stesso tasklet/timer
~~~~~~~~~~~~~~~~~~~~~~~
Dato che un tasklet non viene mai eseguito contemporaneamente su due
processori, non dovete preoccuparvi che sia rientrante (ovvero eseguito
più volte in contemporanea), perfino su sistemi multi-processore.
Differenti tasklet/timer
~~~~~~~~~~~~~~~~~~~~~~~~
Se un altro tasklet/timer vuole condividere dati col vostro tasklet o timer,
allora avrete bisogno entrambe di spin_lock() e
spin_unlock(). Qui spin_lock_bh() è inutile, siete già
in un tasklet ed avete la garanzia che nessun altro verrà eseguito sullo
stesso processore.
Sincronizzazione fra softirq
----------------------------
Spesso un softirq potrebbe condividere dati con se stesso o un tasklet/timer.
Lo stesso softirq
~~~~~~~~~~~~~~~~~
Lo stesso softirq può essere eseguito su un diverso processore: allo scopo
di migliorare le prestazioni potete utilizzare dati riservati ad ogni
processore (vedere `Dati per processore`_). Se siete arrivati
fino a questo punto nell'uso dei softirq, probabilmente tenete alla scalabilità
delle prestazioni abbastanza da giustificarne la complessità aggiuntiva.
Dovete utilizzare spin_lock() e spin_unlock() per
proteggere i dati condivisi.
Diversi Softirqs
~~~~~~~~~~~~~~~~
Dovete utilizzare spin_lock() e spin_unlock() per
proteggere i dati condivisi, che siano timer, tasklet, diversi softirq o
lo stesso o altri softirq: uno qualsiasi di essi potrebbe essere in esecuzione
su un diverso processore.
.. _`it_hardirq-context`:
하드 IRQ, 최소 잠금표와 trylock
256-389하드웨어 인터럽트는 보통 처리할 작업을 큐에 넣고 tasklet 또는 softirq가 나중에 가져가도록 통신합니다.
하드 IRQ와 softirq가 자료를 공유하면 softirq가 하드 IRQ에 선점될 수 있고 다른 CPU의 하드 IRQ도 임계 구역을 실행할 수 있습니다. softirq 쪽은 `spin_lock_irq()`로 현재 CPU IRQ를 끄고 잠금을 얻습니다.
하드 IRQ 핸들러에서는 softirq가 실행될 수 없으므로 보통 더 빠른 `spin_lock()`만 써도 됩니다. 다만 같은 잠금을 쓰는 다른 하드 IRQ 핸들러가 현재 핸들러를 선점할 수 있으면 `spin_lock_irq()`가 필요합니다.
UP에서는 spinlock 부분이 사라져 로컬 IRQ 비활성화만 남습니다. 원문은 이 효과가 softirq/tasklet/BH 실행도 막는다고 설명합니다.
`spin_lock_irqsave()`는 이전 IRQ 상태를 변수에 저장하고 `spin_unlock_irqrestore()`로 복원합니다. 이미 IRQ가 꺼진 하드 IRQ와 IRQ를 꺼야 하는 softirq 양쪽에서 같은 코드를 안전하게 쓸 수 있습니다.
softirq는 하드 IRQ 복귀 시 실행되므로 IRQ를 막으면 softirq도 막힙니다. 따라서 `spin_lock_irqsave()`는 가장 일반적이고 강력한 spinlock 변형입니다.
두 하드 IRQ 핸들러가 데이터를 공유하는 드문 경우에는 아키텍처가 핸들러 실행 중 모든 IRQ를 자동으로 막는다고 가정할 수 없으므로 `spin_lock_irqsave()`를 사용합니다.
요약 규칙은 사용자 컨텍스트끼리만 동기화하면 mutex, 인터럽트가 자료를 만질 수 있으면 `spin_lock_irqsave()`입니다. spinlock 임계 구역은 함수 호출을 포함해 5줄을 넘기지 않는 것을 원문이 경험칙으로 제시합니다.
최소 요구 표에서 `SLIS`는 `spin_lock_irqsave`, `SLI`는 `spin_lock_irq`, `SL`은 `spin_lock`, `SLBH`는 `spin_lock_bh`, `MLI`는 `mutex_lock_interruptible`입니다.
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
. IRQ Handler A IRQ Handler B Softirq A Softirq B Tasklet A Tasklet B Timer A Timer B User Context A User Context B
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
IRQ Handler A None
IRQ Handler B SLIS None
Softirq A SLI SLI SL
Softirq B SLI SLI SL SL
Tasklet A SLI SLI SL SL None
Tasklet B SLI SLI SL SL SL None
Timer A SLI SLI SL SL SL SL None
Timer B SLI SLI SL SL SL SL SL None
User Context A SLI SLI SLBH SLBH SLBH SLBH SLBH SLBH None
User Context B SLI SLI SLBH SLBH SLBH SLBH SLBH SLBH MLI None
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
Table: Tabella dei requisiti per la sincronizzazione
+--------+----------------------------+
| SLIS | spin_lock_irqsave |
+--------+----------------------------+
| SLI | spin_lock_irq |
+--------+----------------------------+
| SL | spin_lock |
+--------+----------------------------+
| SLBH | spin_lock_bh |
+--------+----------------------------+
| MLI | mutex_lock_interruptible |
+--------+----------------------------+
Table: Legenda per la tabella dei requisiti per la sincronizzazione
`spin_trylock()`은 한 번만 시도해 성공하면 0이 아닌 값, 실패하면 0을 반환합니다. 어떤 컨텍스트에서도 쓸 수 있지만 `spin_lock()`처럼 자신을 선점할 컨텍스트를 먼저 비활성화해야 합니다.
`mutex_trylock()`도 잠들지 않고 즉시 성공 여부를 반환하지만 구현이 IRQ 안전하지 않으므로 하드웨어 또는 소프트웨어 인터럽트 컨텍스트에서 사용하면 안 됩니다.
원문의 최소 요구 매트릭스에서 사용하는 약어입니다.
전체 원문 매트릭스의 핵심 조합을 읽기 쉽게 재구성합니다.
즉시 실패할 수 있는 경로를 호출자가 처리해야 합니다.
진입 전 상태를 저장하면 하드 IRQ와 softirq 공용 코드에서 쓸 수 있습니다.
Contesto di interruzione hardware
=================================
Solitamente le interruzioni hardware comunicano con un tasklet o un softirq.
Spesso questo si traduce nel mettere in coda qualcosa da fare che verrà
preso in carico da un softirq.
Sincronizzazione fra interruzioni hardware e softirq/tasklet
------------------------------------------------------------
Se un gestore di interruzioni hardware condivide dati con un softirq, allora
avrete due preoccupazioni. Primo, il softirq può essere interrotto da
un'interruzione hardware, e secondo, la sezione critica potrebbe essere
eseguita da un'interruzione hardware su un processore diverso. Questo è il caso
dove spin_lock_irq() viene utilizzato. Disabilita le interruzioni
sul processore che l'esegue, poi trattiene il lock. spin_unlock_irq()
fa l'opposto.
Il gestore d'interruzione hardware non ha bisogno di usare spin_lock_irq()
perché i softirq non possono essere eseguiti quando il gestore d'interruzione
hardware è in esecuzione: per questo si può usare spin_lock(), che è un po'
più veloce. L'unica eccezione è quando un altro gestore d'interruzioni
hardware utilizza lo stesso *lock*: spin_lock_irq() impedirà a questo
secondo gestore di interrompere quello in esecuzione.
Questo funziona alla perfezione anche sui sistemi monoprocessore: gli spinlock
svaniscono e questa macro diventa semplicemente local_irq_disable()
(``include/asm/smp.h``), la quale impedisce a softirq/tasklet/BH d'essere
eseguiti.
spin_lock_irqsave() (``include/linux/spinlock.h``) è una variante che
salva lo stato delle interruzioni in una variabile, questa verrà poi passata
a spin_unlock_irqrestore(). Questo significa che lo stesso codice
potrà essere utilizzato in un'interruzione hardware (dove le interruzioni sono
già disabilitate) e in un softirq (dove la disabilitazione delle interruzioni
è richiesta).
Da notare che i softirq (e quindi tasklet e timer) sono eseguiti al ritorno
da un'interruzione hardware, quindi spin_lock_irq() interrompe
anche questi. Tenuto conto di questo si può dire che
spin_lock_irqsave() è la funzione di sincronizzazione più generica
e potente.
Sincronizzazione fra due gestori d'interruzioni hardware
--------------------------------------------------------
Condividere dati fra due gestori di interruzione hardware è molto raro, ma se
succede, dovreste usare spin_lock_irqsave(): è una specificità
dell'architettura il fatto che tutte le interruzioni vengano interrotte
quando si eseguono di gestori di interruzioni.
Bigino della sincronizzazione
=============================
Pete Zaitcev ci offre il seguente riassunto:
- Se siete in un contesto utente (una qualsiasi chiamata di sistema)
e volete sincronizzarvi con altri processi, usate i mutex. Potete trattenere
il mutex e dormire (``copy_from_user(`` o ``kmalloc(x,GFP_KERNEL)``).
- Altrimenti (== i dati possono essere manipolati da un'interruzione) usate
spin_lock_irqsave() e spin_unlock_irqrestore().
- Evitate di trattenere uno spinlock per più di 5 righe di codice incluse
le chiamate a funzione (ad eccezione di quell per l'accesso come
readb()).
Tabella dei requisiti minimi
----------------------------
La tabella seguente illustra i requisiti **minimi** per la sincronizzazione fra
diversi contesti. In alcuni casi, lo stesso contesto può essere eseguito solo
da un processore per volta, quindi non ci sono requisiti per la
sincronizzazione (per esempio, un thread può essere eseguito solo su un
processore alla volta, ma se deve condividere dati con un altro thread, allora
la sincronizzazione è necessaria).
Ricordatevi il suggerimento qui sopra: potete sempre usare
spin_lock_irqsave(), che è un sovrainsieme di tutte le altre funzioni
per spinlock.
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
. IRQ Handler A IRQ Handler B Softirq A Softirq B Tasklet A Tasklet B Timer A Timer B User Context A User Context B
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
IRQ Handler A None
IRQ Handler B SLIS None
Softirq A SLI SLI SL
Softirq B SLI SLI SL SL
Tasklet A SLI SLI SL SL None
Tasklet B SLI SLI SL SL SL None
Timer A SLI SLI SL SL SL SL None
Timer B SLI SLI SL SL SL SL SL None
User Context A SLI SLI SLBH SLBH SLBH SLBH SLBH SLBH None
User Context B SLI SLI SLBH SLBH SLBH SLBH SLBH SLBH MLI None
============== ============= ============= ========= ========= ========= ========= ======= ======= ============== ==============
Table: Tabella dei requisiti per la sincronizzazione
+--------+----------------------------+
| SLIS | spin_lock_irqsave |
+--------+----------------------------+
| SLI | spin_lock_irq |
+--------+----------------------------+
| SL | spin_lock |
+--------+----------------------------+
| SLBH | spin_lock_bh |
+--------+----------------------------+
| MLI | mutex_lock_interruptible |
+--------+----------------------------+
Table: Legenda per la tabella dei requisiti per la sincronizzazione
Le funzioni *trylock*
=====================
Ci sono funzioni che provano a trattenere un *lock* solo una volta e
ritornano immediatamente comunicato il successo od il fallimento
dell'operazione. Posso essere usate quando non serve accedere ai dati
protetti dal *lock* quando qualche altro thread lo sta già facendo
trattenendo il *lock*. Potrete acquisire il *lock* più tardi se vi
serve accedere ai dati protetti da questo *lock*.
La funzione spin_trylock() non ritenta di acquisire il *lock*,
se ci riesce al primo colpo ritorna un valore diverso da zero, altrimenti
se fallisce ritorna 0. Questa funzione può essere utilizzata in un qualunque
contesto, ma come spin_lock(): dovete disabilitare i contesti che
potrebbero interrompervi e quindi trattenere lo spinlock.
La funzione mutex_trylock() invece di sospendere il vostro processo
ritorna un valore diverso da zero se è possibile trattenere il lock al primo
colpo, altrimenti se fallisce ritorna 0. Nonostante non dorma, questa funzione
non può essere usata in modo sicuro in contesti di interruzione hardware o
software.
사용자 컨텍스트 캐시 예제
390-510공통 예제는 이름을 숫자 ID에 매핑하고 객체별 사용 빈도를 기록하는 캐시입니다. 가득 차면 가장 인기 없는 객체를 제거합니다.
첫 버전은 모든 작업이 시스템 호출 같은 사용자 컨텍스트에서 실행되어 잠들 수 있다고 가정하므로 하나의 mutex로 캐시와 객체를 보호합니다.
`struct object`에는 연결 리스트 노드, id, 이름, popularity가 있습니다. `DEFINE_MUTEX(cache_lock)`, `LIST_HEAD(cache)`, `cache_num`, 최대 크기 10을 선언합니다.
`__cache_find()`, `__cache_delete()`, `__cache_add()`는 호출자가 `cache_lock`을 보유해야 하는 내부 함수입니다. 검색은 인기도를 올리고, 삭제는 목록 제거·해제·개수 감소, 추가는 한도 초과 시 최저 인기도 객체 제거를 수행합니다.
공개 `cache_add()`, `cache_delete()`, `cache_find()`는 mutex를 얻어 내부 함수를 호출합니다. `cache_add()`는 잠금 전에 `GFP_KERNEL`로 객체를 할당하고 필드를 초기화합니다.
객체가 목록에 들어가기 전에는 다른 흐름이 접근할 수 없으므로 잠금 밖에서 새 객체 필드를 준비하는 최적화는 안전합니다. 잠금 보유 시간을 줄이는 좋은 예입니다.
원문 C 코드는 구조체, 내부 도우미, 세 공개 함수를 한 덩어리로 제공하며 아래 원형을 보존합니다.
e tutti gli oggetti che contiene. Ecco il codice::
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/mutex.h>
#include <asm/errno.h>
struct object
{
struct list_head list;
int id;
char name[32];
int popularity;
};
/* Protects the cache, cache_num, and the objects within it */
static DEFINE_MUTEX(cache_lock);
static LIST_HEAD(cache);
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
/* Must be holding cache_lock */
static struct object *__cache_find(int id)
{
struct object *i;
list_for_each_entry(i, &cache, list)
if (i->id == id) {
i->popularity++;
return i;
}
return NULL;
}
/* Must be holding cache_lock */
static void __cache_delete(struct object *obj)
{
BUG_ON(!obj);
list_del(&obj->list);
kfree(obj);
cache_num--;
}
/* Must be holding cache_lock */
static void __cache_add(struct object *obj)
{
list_add(&obj->list, &cache);
if (++cache_num > MAX_CACHE_SIZE) {
struct object *i, *outcast = NULL;
list_for_each_entry(i, &cache, list) {
if (!outcast || i->popularity < outcast->popularity)
outcast = i;
}
__cache_delete(outcast);
}
}
int cache_add(int id, const char *name)
{
struct object *obj;
if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
return -ENOMEM;
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
mutex_lock(&cache_lock);
__cache_add(obj);
mutex_unlock(&cache_lock);
return 0;
}
void cache_delete(int id)
{
mutex_lock(&cache_lock);
__cache_delete(__cache_find(id));
mutex_unlock(&cache_lock);
}
int cache_find(int id, char *name)
{
struct object *obj;
int ret = -ENOENT;
mutex_lock(&cache_lock);
obj = __cache_find(id);
if (obj) {
ret = 0;
strcpy(name, obj->name);
}
mutex_unlock(&cache_lock);
return ret;
}
첫 버전은 하나의 mutex가 구조와 내용을 모두 보호합니다.
공유되기 전 준비는 잠금 밖에서 하고 공개 시점만 직렬화합니다.
Esempi più comuni
=================
Guardiamo un semplice esempio: una memoria che associa nomi a numeri.
La memoria tiene traccia di quanto spesso viene utilizzato ogni oggetto;
quando è piena, l'oggetto meno usato viene eliminato.
Tutto in contesto utente
------------------------
Nel primo esempio, supponiamo che tutte le operazioni avvengano in contesto
utente (in soldoni, da una chiamata di sistema), quindi possiamo dormire.
Questo significa che possiamo usare i mutex per proteggere la nostra memoria
e tutti gli oggetti che contiene. Ecco il codice::
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/string.h>
#include <linux/mutex.h>
#include <asm/errno.h>
struct object
{
struct list_head list;
int id;
char name[32];
int popularity;
};
/* Protects the cache, cache_num, and the objects within it */
static DEFINE_MUTEX(cache_lock);
static LIST_HEAD(cache);
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
/* Must be holding cache_lock */
static struct object *__cache_find(int id)
{
struct object *i;
list_for_each_entry(i, &cache, list)
if (i->id == id) {
i->popularity++;
return i;
}
return NULL;
}
/* Must be holding cache_lock */
static void __cache_delete(struct object *obj)
{
BUG_ON(!obj);
list_del(&obj->list);
kfree(obj);
cache_num--;
}
/* Must be holding cache_lock */
static void __cache_add(struct object *obj)
{
list_add(&obj->list, &cache);
if (++cache_num > MAX_CACHE_SIZE) {
struct object *i, *outcast = NULL;
list_for_each_entry(i, &cache, list) {
if (!outcast || i->popularity < outcast->popularity)
outcast = i;
}
__cache_delete(outcast);
}
}
int cache_add(int id, const char *name)
{
struct object *obj;
if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
return -ENOMEM;
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
mutex_lock(&cache_lock);
__cache_add(obj);
mutex_unlock(&cache_lock);
return 0;
}
void cache_delete(int id)
{
mutex_lock(&cache_lock);
__cache_delete(__cache_find(id));
mutex_unlock(&cache_lock);
}
int cache_find(int id, char *name)
{
struct object *obj;
int ret = -ENOENT;
mutex_lock(&cache_lock);
obj = __cache_find(id);
if (obj) {
ret = 0;
strcpy(name, obj->name);
}
mutex_unlock(&cache_lock);
return ret;
}
Da notare che ci assicuriamo sempre di trattenere cache_lock quando
aggiungiamo, rimuoviamo od ispezioniamo la memoria: sia la struttura
della memoria che il suo contenuto sono protetti dal *lock*. Questo
caso è semplice dato che copiamo i dati dall'utente e non permettiamo
mai loro di accedere direttamente agli oggetti.
C'è una piccola ottimizzazione qui: nella funzione cache_add()
impostiamo i campi dell'oggetto prima di acquisire il *lock*. Questo è
sicuro perché nessun altro potrà accedervi finché non lo inseriremo
nella memoria.
인터럽트 접근과 참조 카운팅
511-718다음 버전은 `cache_find()`나 삭제 작업이 하드웨어·소프트웨어 인터럽트에서도 실행될 수 있다고 가정합니다. mutex를 `DEFINE_SPINLOCK`으로 바꾸고 공개 함수에서 `spin_lock_irqsave()`와 `spin_unlock_irqrestore()`를 사용합니다.
이 쌍은 사용자 컨텍스트에서 IRQ를 끄고, 이미 IRQ 컨텍스트라면 기존 상태를 유지하므로 같은 함수가 어느 컨텍스트에서 호출되어도 잠금 자체는 안전합니다.
다만 `cache_add()`의 `kmalloc(..., GFP_KERNEL)`은 여전히 사용자 컨텍스트에서만 허용됩니다. 인터럽트에서도 추가해야 한다면 할당 플래그를 함수 인자로 받아 `GFP_ATOMIC` 같은 선택을 가능하게 해야 합니다.
::
--- cache.c.usercontext 2003-12-09 13:58:54.000000000 +1100
+++ cache.c.interrupt 2003-12-09 14:07:49.000000000 +1100
@@ -12,7 +12,7 @@
int popularity;
};
-static DEFINE_MUTEX(cache_lock);
+static DEFINE_SPINLOCK(cache_lock);
static LIST_HEAD(cache);
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
@@ -55,6 +55,7 @@
int cache_add(int id, const char *name)
{
struct object *obj;
+ unsigned long flags;
if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
return -ENOMEM;
@@ -63,30 +64,33 @@
obj->id = id;
obj->popularity = 0;
- mutex_lock(&cache_lock);
+ spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
return 0;
}
void cache_delete(int id)
{
- mutex_lock(&cache_lock);
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
__cache_delete(__cache_find(id));
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
}
int cache_find(int id, char *name)
{
struct object *obj;
int ret = -ENOENT;
+ unsigned long flags;
- mutex_lock(&cache_lock);
+ spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
if (obj) {
ret = 0;
strcpy(name, obj->name);
}
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
return ret;
}
객체 포인터를 파일 밖으로 노출하면 잠금과 수명이라는 두 문제가 생깁니다. 외부 코드가 객체를 수정하려면 잠금 정책도 공개해야 하고, 잠금을 풀고도 포인터가 유효하려면 삭제와 재사용을 막아야 합니다.
전역 잠금을 포인터 사용 기간 내내 잡을 수는 없습니다. 다른 작업을 모두 막기 때문입니다. 해법은 포인터를 보유한 주체마다 참조 카운터를 증가시키고 사용이 끝나면 감소시키는 것입니다.
카운터가 0이 되면 더 이상 사용자가 없어 객체를 해제할 수 있습니다. 캐시 자체도 객체를 보유하는 하나의 참조로 계산해 삽입 시 1로 시작합니다.
`object_get()`과 `object_put()`은 전형적인 get/put API로 카운터 조작을 감쌉니다. `cache_find()`는 객체를 찾은 뒤 잠금 안에서 참조를 추가해 반환하므로 호출자는 잠금을 풀고 잠들면서 `copy_to_user()` 같은 작업을 할 수 있습니다.
삭제는 목록에서 객체를 제거하고 캐시가 보유한 참조를 내려놓습니다. 다른 외부 참조가 남아 있으면 즉시 해제되지 않습니다.
Ecco il codice::
--- cache.c.interrupt 2003-12-09 14:25:43.000000000 +1100
+++ cache.c.refcnt 2003-12-09 14:33:05.000000000 +1100
@@ -7,6 +7,7 @@
struct object
{
struct list_head list;
+ unsigned int refcnt;
int id;
char name[32];
int popularity;
@@ -17,6 +18,35 @@
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
+static void __object_put(struct object *obj)
+{
+ if (--obj->refcnt == 0)
+ kfree(obj);
+}
+
+static void __object_get(struct object *obj)
+{
+ obj->refcnt++;
+}
+
+void object_put(struct object *obj)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
+ __object_put(obj);
+ spin_unlock_irqrestore(&cache_lock, flags);
+}
+
+void object_get(struct object *obj)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
+ __object_get(obj);
+ spin_unlock_irqrestore(&cache_lock, flags);
+}
+
/* Must be holding cache_lock */
static struct object *__cache_find(int id)
{
@@ -35,6 +65,7 @@
{
BUG_ON(!obj);
list_del(&obj->list);
+ __object_put(obj);
cache_num--;
}
@@ -63,6 +94,7 @@
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
+ obj->refcnt = 1; /* The cache holds a reference */
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
@@ -79,18 +111,15 @@
spin_unlock_irqrestore(&cache_lock, flags);
}
-int cache_find(int id, char *name)
+struct object *cache_find(int id)
{
struct object *obj;
- int ret = -ENOENT;
unsigned long flags;
spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
- if (obj) {
- ret = 0;
- strcpy(name, obj->name);
- }
+ if (obj)
+ __object_get(obj);
spin_unlock_irqrestore(&cache_lock, flags);
- return ret;
+ return obj;
}
객체 포인터를 파일 밖으로 넘기는 순간에는 잠금이 보호하던 범위와 포인터의 유효 기간이 분리됩니다. `cache_lock`을 잡은 동안 찾은 주소라도 잠금을 놓은 직후 다른 실행 흐름이 `cache_delete()`를 호출하면 해제될 수 있고, 같은 주소가 새 객체에 재사용되면 호출자는 전혀 다른 객체를 가리키게 됩니다.
이 수명 문제는 참조 계수로 해결합니다. 객체를 가리키는 각 소유자는 `get`으로 계수를 올리고 사용을 마칠 때 `put`으로 내립니다. 계수가 0이 되는 순간에만 객체를 해제하므로, 호출자는 객체를 보유한 채 잠들거나 `copy_to_user()`처럼 수면 가능한 작업을 수행할 수 있습니다.
캐시 자체도 하나의 참조를 소유합니다. 따라서 새 객체를 캐시에 넣을 때 `refcnt`를 1로 시작하고, `__cache_delete()`가 목록에서 제거할 때 그 캐시 참조를 내려놓습니다. `cache_find()`는 잠금 안에서 객체를 찾은 뒤 호출자 몫의 참조를 먼저 올리고 포인터를 반환해야 합니다.
`__object_get()`과 `__object_put()`은 호출자가 이미 `cache_lock`을 보유한다는 내부 규약을 따르고, 공개 `object_get()`과 `object_put()`은 스스로 IRQ 상태를 저장해 잠금을 잡습니다. 이 구분은 중복 잠금을 피하면서도 파일 밖의 호출자에게 안전한 수명 API를 제공하려는 것입니다.
상호 배제와 수명 관리는 서로 다른 문제입니다.
목록 포함 여부와 객체 생존 여부를 분리합니다.
찾기와 참조 획득을 같은 잠금 구간에 넣어 삭제 사이 틈을 없앱니다.
Accesso dal contesto utente
---------------------------
Ora consideriamo il caso in cui cache_find() può essere invocata
dal contesto d'interruzione: sia hardware che software. Un esempio potrebbe
essere un timer che elimina oggetti dalla memoria.
Qui di seguito troverete la modifica nel formato *patch*: le righe ``-``
sono quelle rimosse, mentre quelle ``+`` sono quelle aggiunte.
::
--- cache.c.usercontext 2003-12-09 13:58:54.000000000 +1100
+++ cache.c.interrupt 2003-12-09 14:07:49.000000000 +1100
@@ -12,7 +12,7 @@
int popularity;
};
-static DEFINE_MUTEX(cache_lock);
+static DEFINE_SPINLOCK(cache_lock);
static LIST_HEAD(cache);
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
@@ -55,6 +55,7 @@
int cache_add(int id, const char *name)
{
struct object *obj;
+ unsigned long flags;
if ((obj = kmalloc(sizeof(*obj), GFP_KERNEL)) == NULL)
return -ENOMEM;
@@ -63,30 +64,33 @@
obj->id = id;
obj->popularity = 0;
- mutex_lock(&cache_lock);
+ spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
return 0;
}
void cache_delete(int id)
{
- mutex_lock(&cache_lock);
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
__cache_delete(__cache_find(id));
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
}
int cache_find(int id, char *name)
{
struct object *obj;
int ret = -ENOENT;
+ unsigned long flags;
- mutex_lock(&cache_lock);
+ spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
if (obj) {
ret = 0;
strcpy(name, obj->name);
}
- mutex_unlock(&cache_lock);
+ spin_unlock_irqrestore(&cache_lock, flags);
return ret;
}
Da notare che spin_lock_irqsave() disabiliterà le interruzioni
se erano attive, altrimenti non farà niente (quando siamo già in un contesto
d'interruzione); dunque queste funzioni possono essere chiamante in
sicurezza da qualsiasi contesto.
Sfortunatamente, cache_add() invoca kmalloc() con
l'opzione ``GFP_KERNEL`` che è permessa solo in contesto utente. Ho supposto
che cache_add() venga chiamata dal contesto utente, altrimenti
questa opzione deve diventare un parametro di cache_add().
Esporre gli oggetti al di fuori del file
----------------------------------------
Se i vostri oggetti contengono più informazioni, potrebbe non essere
sufficiente copiare i dati avanti e indietro: per esempio, altre parti del
codice potrebbero avere un puntatore a questi oggetti piuttosto che cercarli
ogni volta. Questo introduce due problemi.
Il primo problema è che utilizziamo ``cache_lock`` per proteggere gli oggetti:
dobbiamo renderlo dinamico così che il resto del codice possa usarlo. Questo
rende la sincronizzazione più complicata dato che non avviene più in un unico
posto.
Il secondo problema è il problema del ciclo di vita: se un'altra struttura
mantiene un puntatore ad un oggetto, presumibilmente si aspetta che questo
puntatore rimanga valido. Sfortunatamente, questo è garantito solo mentre
si trattiene il *lock*, altrimenti qualcuno potrebbe chiamare
cache_delete() o peggio, aggiungere un oggetto che riutilizza lo
stesso indirizzo.
Dato che c'è un solo *lock*, non potete trattenerlo a vita: altrimenti
nessun altro potrà eseguire il proprio lavoro.
La soluzione a questo problema è l'uso di un contatore di riferimenti:
chiunque punti ad un oggetto deve incrementare il contatore, e decrementarlo
quando il puntatore non viene più usato. Quando il contatore raggiunge lo zero
significa che non è più usato e l'oggetto può essere rimosso.
Ecco il codice::
--- cache.c.interrupt 2003-12-09 14:25:43.000000000 +1100
+++ cache.c.refcnt 2003-12-09 14:33:05.000000000 +1100
@@ -7,6 +7,7 @@
struct object
{
struct list_head list;
+ unsigned int refcnt;
int id;
char name[32];
int popularity;
@@ -17,6 +18,35 @@
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
+static void __object_put(struct object *obj)
+{
+ if (--obj->refcnt == 0)
+ kfree(obj);
+}
+
+static void __object_get(struct object *obj)
+{
+ obj->refcnt++;
+}
+
+void object_put(struct object *obj)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
+ __object_put(obj);
+ spin_unlock_irqrestore(&cache_lock, flags);
+}
+
+void object_get(struct object *obj)
+{
+ unsigned long flags;
+
+ spin_lock_irqsave(&cache_lock, flags);
+ __object_get(obj);
+ spin_unlock_irqrestore(&cache_lock, flags);
+}
+
/* Must be holding cache_lock */
static struct object *__cache_find(int id)
{
@@ -35,6 +65,7 @@
{
BUG_ON(!obj);
list_del(&obj->list);
+ __object_put(obj);
cache_num--;
}
@@ -63,6 +94,7 @@
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
+ obj->refcnt = 1; /* The cache holds a reference */
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
@@ -79,18 +111,15 @@
spin_unlock_irqrestore(&cache_lock, flags);
}
-int cache_find(int id, char *name)
+struct object *cache_find(int id)
{
struct object *obj;
- int ret = -ENOENT;
unsigned long flags;
spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
- if (obj) {
- ret = 0;
- strcpy(name, obj->name);
- }
+ if (obj)
+ __object_get(obj);
spin_unlock_irqrestore(&cache_lock, flags);
- return ret;
+ return obj;
}
Abbiamo incapsulato il contatore di riferimenti nelle tipiche funzioni
di 'get' e 'put'. Ora possiamo ritornare l'oggetto da cache_find()
col vantaggio che l'utente può dormire trattenendo l'oggetto (per esempio,
copy_to_user() per copiare il nome verso lo spazio utente).
Un altro punto da notare è che ho detto che il contatore dovrebbe incrementarsi
per ogni puntatore ad un oggetto: quindi il contatore di riferimenti è 1
quando l'oggetto viene inserito nella memoria. In altre versione il framework
non trattiene un riferimento per se, ma diventa più complicato.
원자 참조 카운터와 객체별 잠금
719-890참조 카운터는 `atomic_t`로 바꿀 수 있습니다. `include/asm/atomic.h`의 연산은 모든 CPU에서 원자적이므로 카운터 자체를 위한 spinlock이 필요 없습니다.
`atomic_inc()`는 참조를 늘리고 `atomic_dec_and_test()`는 감소 후 0인지 검사해 최종 해제를 결정합니다. 초기값은 `atomic_set(&obj->refcnt, 1)`로 설정합니다.
원자 연산은 단순 카운터에는 더 간단하지만 비단순 상태 전이에는 spinlock이 더 명확할 수 있습니다.
::
--- cache.c.refcnt 2003-12-09 15:00:35.000000000 +1100
+++ cache.c.refcnt-atomic 2003-12-11 15:49:42.000000000 +1100
@@ -7,7 +7,7 @@
struct object
{
struct list_head list;
- unsigned int refcnt;
+ atomic_t refcnt;
int id;
char name[32];
int popularity;
@@ -18,33 +18,15 @@
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
-static void __object_put(struct object *obj)
-{
- if (--obj->refcnt == 0)
- kfree(obj);
-}
-
-static void __object_get(struct object *obj)
-{
- obj->refcnt++;
-}
-
void object_put(struct object *obj)
{
- unsigned long flags;
-
- spin_lock_irqsave(&cache_lock, flags);
- __object_put(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ if (atomic_dec_and_test(&obj->refcnt))
+ kfree(obj);
}
void object_get(struct object *obj)
{
- unsigned long flags;
-
- spin_lock_irqsave(&cache_lock, flags);
- __object_get(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ atomic_inc(&obj->refcnt);
}
/* Must be holding cache_lock */
@@ -65,7 +47,7 @@
{
BUG_ON(!obj);
list_del(&obj->list);
- __object_put(obj);
+ object_put(obj);
cache_num--;
}
@@ -94,7 +76,7 @@
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
- obj->refcnt = 1; /* The cache holds a reference */
+ atomic_set(&obj->refcnt, 1); /* The cache holds a reference */
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
@@ -119,7 +101,7 @@
spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
if (obj)
- __object_get(obj);
+ object_get(obj);
spin_unlock_irqrestore(&cache_lock, flags);
return obj;
}
참조 계수만 보호하려고 전역 spinlock을 잡는 비용은 `atomic_t`로 줄일 수 있습니다. `atomic_inc()`는 참조 획득을 원자적으로 수행하고, `atomic_dec_and_test()`는 감소 결과가 0인 마지막 참조인지 한 연산으로 판정하여 참일 때만 `kfree()`를 실행합니다.
변경 뒤에는 `object_get()`과 `object_put()`이 더 이상 `cache_lock`을 요구하지 않습니다. 다만 원자 연산은 참조 계수 하나의 갱신만 보장할 뿐 객체의 다른 필드나 목록 구조까지 보호하지 않으므로, 목록 삽입·삭제와 객체 내용 변경에는 각각의 잠금 규약이 계속 필요합니다.
초기화도 일반 대입 대신 `atomic_set(&obj->refcnt, 1)`을 사용합니다. 캐시가 가진 최초 참조와 조회자가 얻는 추가 참조가 같은 원자 계수에 합쳐지며, 목록에서 제거되더라도 외부 참조가 남아 있으면 메모리는 즉시 해제되지 않습니다.
객체 생성 뒤 이름 변경을 허용하려면 세 선택이 있습니다. 전역 `cache_lock`을 외부에 공개하고 호출자가 잡게 하거나, 잠금을 내부에서 잡는 `cache_obj_rename()`을 제공하거나, 캐시 구조와 객체 필드를 서로 다른 잠금으로 보호합니다.
실제 설계에서는 인프라와 모든 객체를 하나의 잠금으로 보호하거나, 리스트 연결과 인프라는 전역 잠금으로 보호하고 객체 나머지는 객체별 잠금으로 보호하거나, 여러 인프라 잠금과 객체별 잠금을 조합합니다.
객체별 잠금 버전은 `cache_lock`이 list와 popularity를 보호하고, `id`는 생성 뒤 불변이며, `obj->lock`이 name을 보호하도록 주석으로 명시합니다.
popularity를 전역 잠금 아래 두면 가장 인기 없는 객체를 찾을 때 모든 객체 잠금을 차례로 얻지 않아도 됩니다. `id`는 불변이라 조회 시 객체 잠금이 필요 없습니다.
어떤 잠금이 어떤 데이터를 보호하는지 주석으로 기록하는 것은 코드의 동작 계약입니다. 원문은 Alan Cox의 ‘Lock data, not code’라는 원칙을 인용합니다.
Qui di seguito un'implementazione con "un lock per oggetto":
::
--- cache.c.refcnt-atomic 2003-12-11 15:50:54.000000000 +1100
+++ cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
@@ -6,11 +6,17 @@
struct object
{
+ /* These two protected by cache_lock. */
struct list_head list;
+ int popularity;
+
atomic_t refcnt;
+
+ /* Doesn't change once created. */
int id;
+
+ spinlock_t lock; /* Protects the name */
char name[32];
- int popularity;
};
static DEFINE_SPINLOCK(cache_lock);
@@ -77,6 +84,7 @@
obj->id = id;
obj->popularity = 0;
atomic_set(&obj->refcnt, 1); /* The cache holds a reference */
+ spin_lock_init(&obj->lock);
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
객체의 `name`을 변경 가능하게 만들 때는 세 가지 선택지가 있습니다. 전역 `cache_lock`을 외부에 공개하거나, 잠금을 내부에서 잡는 `cache_obj_rename()` 같은 함수를 제공하거나, 캐시 인프라와 객체 내용에 서로 다른 잠금을 둘 수 있습니다. 외부 공개는 규약을 넓혀 교착 분석을 어렵게 하므로 보통 캡슐화된 API가 더 낫습니다.
객체별 잠금 설계에서는 `cache_lock`이 목록 링크와 `popularity`처럼 캐시 정책에 속한 상태를 보호합니다. 각 객체의 `lock`은 변경 가능한 `name`만 보호하고, 생성 뒤 바뀌지 않는 `id`는 잠금 없이 읽을 수 있습니다. 이 경계 덕분에 최소 인기 객체를 찾을 때 모든 객체 잠금을 차례로 잡지 않아도 됩니다.
잠금이 무엇을 보호하는지는 코드 주석으로 명확히 기록해야 합니다. `/* These two protected by cache_lock. */`, `/* Doesn't change once created. */`, `/* Protects the name */` 같은 표시는 단순 설명이 아니라 유지보수자가 올바른 잠금을 선택하도록 하는 동기화 계약입니다.
원문의 원칙 `Lock data, not code`는 함수 단위로 막연히 잠그지 말고 공유 데이터와 불변식을 기준으로 잠금 범위를 정하라는 뜻입니다. 같은 함수도 서로 독립적인 데이터를 다룰 수 있고, 같은 데이터는 여러 함수에서 접근할 수 있으므로 보호 대상은 코드 위치보다 데이터 소유권으로 설명해야 합니다.
객체별 잠금 예제의 데이터 소유권입니다.
잠금 수가 늘면 병렬성은 좋아질 수 있지만 규칙은 복잡해집니다.
0으로 바뀐 단 하나의 실행 흐름만 객체를 해제합니다.
Usare operazioni atomiche per il contatore di riferimenti
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In sostanza, :c:type:`atomic_t` viene usato come contatore di riferimenti.
Ci sono un certo numbero di operazioni atomiche definite
in ``include/asm/atomic.h``: queste sono garantite come atomiche su qualsiasi
processore del sistema, quindi non sono necessari i *lock*. In questo caso è
più semplice rispetto all'uso degli spinlock, benché l'uso degli spinlock
sia più elegante per casi non banali. Le funzioni atomic_inc() e
atomic_dec_and_test() vengono usate al posto dei tipici operatori di
incremento e decremento, e i *lock* non sono più necessari per proteggere il
contatore stesso.
::
--- cache.c.refcnt 2003-12-09 15:00:35.000000000 +1100
+++ cache.c.refcnt-atomic 2003-12-11 15:49:42.000000000 +1100
@@ -7,7 +7,7 @@
struct object
{
struct list_head list;
- unsigned int refcnt;
+ atomic_t refcnt;
int id;
char name[32];
int popularity;
@@ -18,33 +18,15 @@
static unsigned int cache_num = 0;
#define MAX_CACHE_SIZE 10
-static void __object_put(struct object *obj)
-{
- if (--obj->refcnt == 0)
- kfree(obj);
-}
-
-static void __object_get(struct object *obj)
-{
- obj->refcnt++;
-}
-
void object_put(struct object *obj)
{
- unsigned long flags;
-
- spin_lock_irqsave(&cache_lock, flags);
- __object_put(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ if (atomic_dec_and_test(&obj->refcnt))
+ kfree(obj);
}
void object_get(struct object *obj)
{
- unsigned long flags;
-
- spin_lock_irqsave(&cache_lock, flags);
- __object_get(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ atomic_inc(&obj->refcnt);
}
/* Must be holding cache_lock */
@@ -65,7 +47,7 @@
{
BUG_ON(!obj);
list_del(&obj->list);
- __object_put(obj);
+ object_put(obj);
cache_num--;
}
@@ -94,7 +76,7 @@
strscpy(obj->name, name, sizeof(obj->name));
obj->id = id;
obj->popularity = 0;
- obj->refcnt = 1; /* The cache holds a reference */
+ atomic_set(&obj->refcnt, 1); /* The cache holds a reference */
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
@@ -119,7 +101,7 @@
spin_lock_irqsave(&cache_lock, flags);
obj = __cache_find(id);
if (obj)
- __object_get(obj);
+ object_get(obj);
spin_unlock_irqrestore(&cache_lock, flags);
return obj;
}
Proteggere l'oggetto stesso
---------------------------
In questo esempio, assumiamo che gli oggetti (ad eccezione del contatore
di riferimenti) non cambino mai dopo la loro creazione. Se vogliamo permettere
al nome di cambiare abbiamo tre possibilità:
- Si può togliere static da ``cache_lock`` e dire agli utenti che devono
trattenere il *lock* prima di modificare il nome di un oggetto.
- Si può fornire una funzione cache_obj_rename() che prende il
*lock* e cambia il nome per conto del chiamante; si dirà poi agli utenti
di usare questa funzione.
- Si può decidere che ``cache_lock`` protegge solo la memoria stessa, ed
un altro *lock* è necessario per la protezione del nome.
Teoricamente, possiamo avere un *lock* per ogni campo e per ogni oggetto.
In pratica, le varianti più comuni sono:
- un *lock* che protegge l'infrastruttura (la lista ``cache`` di questo
esempio) e gli oggetti. Questo è quello che abbiamo fatto finora.
- un *lock* che protegge l'infrastruttura (inclusi i puntatori alla lista
negli oggetti), e un *lock* nell'oggetto per proteggere il resto
dell'oggetto stesso.
- *lock* multipli per proteggere l'infrastruttura (per esempio un *lock*
per ogni lista), possibilmente con un *lock* per oggetto.
Qui di seguito un'implementazione con "un lock per oggetto":
::
--- cache.c.refcnt-atomic 2003-12-11 15:50:54.000000000 +1100
+++ cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
@@ -6,11 +6,17 @@
struct object
{
+ /* These two protected by cache_lock. */
struct list_head list;
+ int popularity;
+
atomic_t refcnt;
+
+ /* Doesn't change once created. */
int id;
+
+ spinlock_t lock; /* Protects the name */
char name[32];
- int popularity;
};
static DEFINE_SPINLOCK(cache_lock);
@@ -77,6 +84,7 @@
obj->id = id;
obj->popularity = 0;
atomic_set(&obj->refcnt, 1); /* The cache holds a reference */
+ spin_lock_init(&obj->lock);
spin_lock_irqsave(&cache_lock, flags);
__cache_add(obj);
Da notare che ho deciso che il contatore di popolarità dovesse essere
protetto da ``cache_lock`` piuttosto che dal *lock* dell'oggetto; questo
perché è logicamente parte dell'infrastruttura (come
:c:type:`struct list_head <list_head>` nell'oggetto). In questo modo,
in __cache_add(), non ho bisogno di trattenere il *lock* di ogni
oggetto mentre si cerca il meno popolare.
Ho anche deciso che il campo id è immutabile, quindi non ho bisogno di
trattenere il lock dell'oggetto quando si usa __cache_find()
per leggere questo campo; il *lock* dell'oggetto è usato solo dal chiamante
che vuole leggere o scrivere il campo name.
Inoltre, da notare che ho aggiunto un commento che descrive i dati che sono
protetti dal *lock*. Questo è estremamente importante in quanto descrive il
comportamento del codice, che altrimenti sarebbe di difficile comprensione
leggendo solamente il codice. E come dice Alan Cox: “Lock data, not code”.
교착과 timer 경쟁
891-1036같은 spinlock을 두 번 얻으면 영원히 스스로 해제를 기다립니다. spinlock, rwlock, mutex는 재귀 잠금이 아닙니다.
사용자 컨텍스트와 softirq가 공유하는 데이터를 단순 `spin_lock()`으로 보호하면, 사용자가 잠금을 보유한 채 softirq에 선점되고 softirq가 같은 잠금을 회전 대기하는 단일 CPU 교착이 생길 수 있습니다.
SMP에서는 watchdog이나 `DEBUG_SPINLOCK`이 이런 문제를 찾는 데 도움을 줍니다. UP에서 spinlock이 사라지는 구성이라도 잘못된 보호 때문에 데이터 손상은 남을 수 있습니다.
둘 이상의 잠금을 반대 순서로 얻으면 ABBA 또는 ‘죽음의 포옹’ 교착이 생깁니다. CPU 1이 A를 잡고 B를 기다리는 동시에 CPU 2가 B를 잡고 A를 기다리면 둘 다 영원히 진행하지 못합니다.
두 CPU가 서로 상대가 보유한 잠금을 기다립니다.
교과서의 동일 잠금 순서 규칙은 필요하지만 수천 개 잠금이 있는 큰 시스템에서 전역 순서를 이해하기 어렵습니다. 더 좋은 잠금은 한 파일 안에 캡슐화되어 헤더로 노출되지 않고 외부 호출자가 존재조차 몰라도 되는 잠금입니다.
잠금을 보유한 채 callback이나 hook을 호출하면 그 코드가 무엇을 얻을지 알 수 없어 교착 위험이 큽니다.
교착을 지나치게 피하려다 읽기 잠금을 풀고 쓰기 잠금을 다시 얻는 사이 상태가 바뀌는 경쟁을 만들 수도 있습니다. 잠금 전환 사이에는 조건을 다시 검사해야 합니다.
timer가 객체를 삭제하는 목록을 모듈 제거 때 한꺼번에 해제하면 timer callback이 이미 시작된 상태와 경쟁할 수 있습니다. 단순히 `spin_lock_bh()` 안에서 timer를 지우고 객체를 해제하면 callback이 잠금 뒤에 실행되어 이미 해제된 객체를 다시 만질 수 있습니다.
Se volete eliminare l'intera collezione (diciamo quando rimuovete un modulo),
potreste fare come segue::
/* THIS CODE BAD BAD BAD BAD: IF IT WAS ANY WORSE IT WOULD USE
HUNGARIAN NOTATION */
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
timer_delete(&list->timer);
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
목록 전체를 제거하는 첫 코드는 `spin_lock_bh()` 아래에서 각 timer를 삭제하고 객체를 즉시 해제하지만, 다른 CPU에서 timer 콜백이 이미 시작된 경우를 막지 못합니다. 그 콜백은 목록 잠금이 풀린 뒤 진행하여 이미 해제된 객체를 다시 삭제하려 하므로 use-after-free가 됩니다.
`timer_delete()`가 1이면 timer가 제거된 것이고, 0이면 이 문맥에서는 callback이 실행 중일 수 있습니다. 0일 때 잠금을 풀고 처음부터 재시도해 callback이 객체를 제거할 기회를 주는 예제가 제시됩니다.
Questo può essere evitato controllando il valore di ritorno di
timer_delete(): se ritorna 1, il temporizzatore è stato già
rimosso. Se 0, significa (in questo caso) che il temporizzatore è in
esecuzione, quindi possiamo fare come segue::
retry:
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
if (!timer_delete(&list->timer)) {
/* Give timer a chance to delete this */
spin_unlock_bh(&list_lock);
goto retry;
}
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
수정된 코드는 `timer_delete()`가 1을 반환할 때만 timer가 성공적으로 제거되어 더 실행되지 않는다고 판단하고 객체를 해제합니다. 0이면 이 상황에서는 콜백이 실행 중일 수 있으므로 목록 잠금을 풀고 `retry`로 돌아가 콜백이 객체를 처리할 기회를 줍니다.
재시도 전에 잠금을 반드시 풀어야 합니다. 실행 중인 timer 콜백도 같은 `list_lock`을 얻어야 끝날 수 있는데 제거 경로가 잠금을 잡은 채 기다리면 양쪽이 서로의 진행을 막아 교착됩니다. 재검사 루프는 목록이 그 사이 바뀔 수 있음을 전제로 처음부터 다시 순회합니다.
콜백이 끝에서 스스로 `add_timer()`를 호출해 재무장하는 경우에는 단순 삭제만으로 종료를 보장할 수 없습니다. `timer_delete_sync()`는 실행 중 콜백과 동기화하고, 객체를 놓기 전 `timer_shutdown()` 또는 `timer_shutdown_sync()`를 호출하면 이후 재무장 시도를 조용히 무시하여 수명 종료를 확정합니다.
스스로 `add_timer()`로 재무장하는 timer에는 `timer_delete_sync()`를 사용합니다. 객체를 해제하기 전에는 `timer_shutdown()` 또는 `timer_shutdown_sync()`로 이후 재무장을 조용히 무시하게 해야 합니다.
반대 잠금 순서는 각 CPU가 한 자원씩 가진 채 상대를 기다리게 합니다.
실행 중 callback을 발견하면 잠금을 풀어 완료 기회를 준 뒤 다시 순회합니다.
삭제와 재무장 방지를 구분합니다.
Problemi comuni
===============
Stallo: semplice ed avanzato
----------------------------
Esiste un tipo di baco dove un pezzo di codice tenta di trattenere uno
spinlock due volte: questo rimarrà in attesa attiva per sempre aspettando che
il *lock* venga rilasciato (in Linux spinlocks, rwlocks e mutex non sono
ricorsivi).
Questo è facile da diagnosticare: non è uno di quei problemi che ti tengono
sveglio 5 notti a parlare da solo.
Un caso un pochino più complesso; immaginate d'avere una spazio condiviso
fra un softirq ed il contesto utente. Se usate spin_lock() per
proteggerlo, il contesto utente potrebbe essere interrotto da un softirq
mentre trattiene il lock, da qui il softirq rimarrà in attesa attiva provando
ad acquisire il *lock* già trattenuto nel contesto utente.
Questi casi sono chiamati stalli (*deadlock*), e come mostrato qui sopra,
può succedere anche con un solo processore (Ma non sui sistemi
monoprocessore perché gli spinlock spariscano quando il kernel è compilato
con ``CONFIG_SMP``\ =n. Nonostante ciò, nel secondo caso avrete comunque
una corruzione dei dati).
Questi casi sono facili da diagnosticare; sui sistemi multi-processore
il supervisione (*watchdog*) o l'opzione di compilazione ``DEBUG_SPINLOCK``
(``include/linux/spinlock.h``) permettono di scovare immediatamente quando
succedono.
Esiste un caso più complesso che è conosciuto come l'abbraccio della morte;
questo coinvolge due o più *lock*. Diciamo che avete un vettore di hash in cui
ogni elemento è uno spinlock a cui è associata una lista di elementi con lo
stesso hash. In un gestore di interruzioni software, dovete modificare un
oggetto e spostarlo su un altro hash; quindi dovrete trattenete lo spinlock
del vecchio hash e di quello nuovo, quindi rimuovere l'oggetto dal vecchio ed
inserirlo nel nuovo.
Qui abbiamo due problemi. Primo, se il vostro codice prova a spostare un
oggetto all'interno della stessa lista, otterrete uno stallo visto che
tenterà di trattenere lo stesso *lock* due volte. Secondo, se la stessa
interruzione software su un altro processore sta tentando di spostare
un altro oggetto nella direzione opposta, potrebbe accadere quanto segue:
+---------------------------------+---------------------------------+
| CPU 1 | CPU 2 |
+=================================+=================================+
| Trattiene *lock* A -> OK | Trattiene *lock* B -> OK |
+---------------------------------+---------------------------------+
| Trattiene *lock* B -> attesa | Trattiene *lock* A -> attesa |
+---------------------------------+---------------------------------+
Table: Conseguenze
Entrambe i processori rimarranno in attesa attiva sul *lock* per sempre,
aspettando che l'altro lo rilasci. Sembra e puzza come un blocco totale.
Prevenire gli stalli
--------------------
I libri di testo vi diranno che se trattenete i *lock* sempre nello stesso
ordine non avrete mai un simile stallo. La pratica vi dirà che questo
approccio non funziona all'ingrandirsi del sistema: quando creo un nuovo
*lock* non ne capisco abbastanza del kernel per dire in quale dei 5000 *lock*
si incastrerà.
I *lock* migliori sono quelli incapsulati: non vengono esposti nei file di
intestazione, e non vengono mai trattenuti fuori dallo stesso file. Potete
rileggere questo codice e vedere che non ci sarà mai uno stallo perché
non tenterà mai di trattenere un altro *lock* quando lo ha già.
Le persone che usano il vostro codice non devono nemmeno sapere che voi
state usando dei *lock*.
Un classico problema deriva dall'uso di *callback* e di *hook*: se li
chiamate mentre trattenete un *lock*, rischiate uno stallo o un abbraccio
della morte (chi lo sa cosa farà una *callback*?).
Ossessiva prevenzione degli stalli
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Gli stalli sono un problema, ma non così terribile come la corruzione dei dati.
Un pezzo di codice trattiene un *lock* di lettura, cerca in una lista,
fallisce nel trovare quello che vuole, quindi rilascia il *lock* di lettura,
trattiene un *lock* di scrittura ed inserisce un oggetto; questo genere di
codice presenta una corsa critica.
corsa fra temporizzatori: un passatempo del kernel
--------------------------------------------------
I temporizzatori potrebbero avere dei problemi con le corse critiche.
Considerate una collezione di oggetti (liste, hash, eccetera) dove ogni oggetto
ha un temporizzatore che sta per distruggerlo.
Se volete eliminare l'intera collezione (diciamo quando rimuovete un modulo),
potreste fare come segue::
/* THIS CODE BAD BAD BAD BAD: IF IT WAS ANY WORSE IT WOULD USE
HUNGARIAN NOTATION */
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
timer_delete(&list->timer);
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Primo o poi, questo esploderà su un sistema multiprocessore perché un
temporizzatore potrebbe essere già partiro prima di spin_lock_bh(),
e prenderà il *lock* solo dopo spin_unlock_bh(), e cercherà
di eliminare il suo oggetto (che però è già stato eliminato).
Questo può essere evitato controllando il valore di ritorno di
timer_delete(): se ritorna 1, il temporizzatore è stato già
rimosso. Se 0, significa (in questo caso) che il temporizzatore è in
esecuzione, quindi possiamo fare come segue::
retry:
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
if (!timer_delete(&list->timer)) {
/* Give timer a chance to delete this */
spin_unlock_bh(&list_lock);
goto retry;
}
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Un altro problema è l'eliminazione dei temporizzatori che si riavviano
da soli (chiamando add_timer() alla fine della loro esecuzione).
Dato che questo è un problema abbastanza comune con una propensione
alle corse critiche, dovreste usare timer_delete_sync()
(``include/linux/timer.h``) per gestire questo caso.
Prima di rilasciare un temporizzatore dovreste chiamare la funzione
timer_shutdown() o timer_shutdown_sync() di modo che non venga più riarmato.
Ogni successivo tentativo di riarmare il temporizzatore verrà silenziosamente
ignorato.
잠금 성능과 읽기/쓰기 변형
1037-1092잠금 성능은 경쟁 정도, 비경쟁 획득·해제 비용, 잠금 수를 줄이거나 더 적합한 기법을 쓰는지의 세 요소로 평가합니다.
경쟁은 잠금 보유 시간에 좌우되므로 필요한 최소 시간만 보유해야 합니다. 캐시 예제처럼 새 객체를 잠금 밖에서 만들고 목록 삽입 직전에만 잠금을 잡습니다.
획득 비용은 파이프라인 정지와 잠금 캐시라인이 현재 CPU에 있는지에 영향을 받습니다. 원문은 700MHz Pentium III의 역사적 수치로 명령 0.7ns, 원자 증가 58ns, 로컬 캐시 잠금 160ns, 다른 CPU 캐시 전송 추가 170/360ns를 제시합니다.
잠금을 더 세분화하면 보유 시간과 경쟁은 줄 수 있지만 획득 횟수와 복잡성이 늘어 오히려 단일 잠금보다 느려질 수 있습니다. 다시 단순성이 중요합니다.
spinlock과 mutex에는 읽기/쓰기 변형 `rwlock_t`와 `struct rw_semaphore`가 있습니다. 여러 reader는 동시에 읽기 잠금을 가질 수 있지만 writer는 하나만 쓰기 잠금을 가집니다.
reader와 writer 코드가 명확히 나뉘고 reader 잠금이 오래 유지되는 경우 도움이 될 수 있습니다. 변형 자체가 일반 잠금보다 약간 느려 실제로 `rwlock_t`가 이득이 아닌 경우가 많습니다.
하나의 지표만 최적화하면 전체 성능이 나빠질 수 있습니다.
reader 병렬성과 writer 배타성을 제공합니다.
작은 임계 구역과 많은 획득 비용 사이에서 실제 측정이 필요합니다.
Velocità della sincronizzazione
===============================
Ci sono tre cose importanti da tenere in considerazione quando si valuta
la velocità d'esecuzione di un pezzo di codice che necessita di
sincronizzazione. La prima è la concorrenza: quante cose rimangono in attesa
mentre qualcuno trattiene un *lock*. La seconda è il tempo necessario per
acquisire (senza contese) e rilasciare un *lock*. La terza è di usare meno
*lock* o di più furbi. Immagino che i *lock* vengano usati regolarmente,
altrimenti, non sareste interessati all'efficienza.
La concorrenza dipende da quanto a lungo un *lock* è trattenuto: dovreste
trattenere un *lock* solo il tempo minimo necessario ma non un istante in più.
Nella memoria dell'esempio precedente, creiamo gli oggetti senza trattenere
il *lock*, poi acquisiamo il *lock* quando siamo pronti per inserirlo nella
lista.
Il tempo di acquisizione di un *lock* dipende da quanto danno fa
l'operazione sulla *pipeline* (ovvero stalli della *pipeline*) e quant'è
probabile che il processore corrente sia stato anche l'ultimo ad acquisire
il *lock* (in pratica, il *lock* è nella memoria cache del processore
corrente?): su sistemi multi-processore questa probabilità precipita
rapidamente. Consideriamo un processore Intel Pentium III a 700Mhz: questo
esegue un'istruzione in 0.7ns, un incremento atomico richiede 58ns, acquisire
un *lock* che è nella memoria cache del processore richiede 160ns, e un
trasferimento dalla memoria cache di un altro processore richiede altri
170/360ns (Leggetevi l'articolo di Paul McKenney's `Linux Journal RCU
article <http://www.linuxjournal.com/article.php?sid=6993>`__).
Questi due obiettivi sono in conflitto: trattenere un *lock* per il minor
tempo possibile potrebbe richiedere la divisione in più *lock* per diverse
parti (come nel nostro ultimo esempio con un *lock* per ogni oggetto),
ma questo aumenta il numero di acquisizioni di *lock*, ed il risultato
spesso è che tutto è più lento che con un singolo *lock*. Questo è un altro
argomento in favore della semplicità quando si parla di sincronizzazione.
Il terzo punto è discusso di seguito: ci sono alcune tecniche per ridurre
il numero di sincronizzazioni che devono essere fatte.
Read/Write Lock Variants
------------------------
Sia gli spinlock che i mutex hanno una variante per la lettura/scrittura
(read/write): ``rwlock_t`` e :c:type:`struct rw_semaphore <rw_semaphore>`.
Queste dividono gli utenti in due categorie: i lettori e gli scrittori.
Se state solo leggendo i dati, potete acquisire il *lock* di lettura, ma
per scrivere avrete bisogno del *lock* di scrittura. Molti possono trattenere
il *lock* di lettura, ma solo uno scrittore alla volta può trattenere
quello di scrittura.
Se il vostro codice si divide chiaramente in codice per lettori e codice
per scrittori (come nel nostro esempio), e il *lock* dei lettori viene
trattenuto per molto tempo, allora l'uso di questo tipo di *lock* può aiutare.
Questi sono leggermente più lenti rispetto alla loro versione normale, quindi
nella pratica l'uso di ``rwlock_t`` non ne vale la pena.
잠금을 피하는 Read-Copy-Update
1093-1278Read-Copy-Update(RCU)는 reader가 일반 잠금을 얻지 않도록 하는 읽기 중심 동기화 기법입니다. reader가 writer보다 많은 캐시 예제에서 유리합니다.
writer가 reader 아래에서 목록을 바꾸더라도 reader가 새 노드를 전혀 보지 않거나 완전히 초기화된 노드만 보게 게시 순서를 보장하면 안전하게 읽을 수 있습니다.
새 노드를 연결할 때 먼저 `new->next`를 설정하고 `wmb()` 쓰기 메모리 장벽 뒤에 목록 포인터를 새 노드로 바꿉니다. 컴파일러와 CPU의 재정렬 때문에 이 순서가 필요합니다.
precauzioni. Per esempio, aggiungendo ``new`` ad una lista concatenata
chiamata ``list``::
new->next = list->next;
wmb();
list->next = new;
RCU 삽입에서 `new->next`를 먼저 완성하고 `wmb()` 뒤에 `list->next = new`로 게시하는 순서는 필수입니다. 컴파일러나 CPU가 두 저장을 뒤집으면 reader가 목록에서는 새 노드를 보면서 그 노드의 다음 포인터는 아직 초기화되지 않은 상태를 관찰할 수 있습니다.
쓰기 장벽은 reader에게 두 가지 일관된 관찰만 허용하려는 장치입니다. reader는 새 노드를 아직 보지 못해 기존 목록을 따르거나, 새 노드를 보았다면 이미 올바르게 연결된 `next`까지 볼 수 있어야 합니다. `list_add_rcu()`가 이 게시 규약을 목록 API로 캡슐화합니다.
`list_add_rcu()`는 `struct list_head` 목록에 이 게시 절차를 구현합니다. 삭제는 이전 노드가 새 노드를 건너뛰게 만들며 `list_del_rcu()`가 제거된 객체를 즉시 손상시키지 않고 수행합니다.
Rimuovere un elemento dalla lista è anche più facile: sostituiamo il puntatore
al vecchio elemento con quello del suo successore, e i lettori vedranno
l'elemento o lo salteranno.
::
list->next = old->next;
La funzione list_del_rcu() (``include/linux/list.h``) fa esattamente
questo (la versione normale corrompe il vecchio oggetto, e non vogliamo che
accada).
삭제는 새 reader가 더 이상 old 노드에 도달하지 못하도록 선행 노드의 포인터를 후속 노드로 바꿉니다. 그러나 삭제 직전에 old를 읽은 reader는 여전히 그 메모리를 사용할 수 있으므로 `list_del_rcu()` 직후 객체를 해제해서는 안 됩니다.
일반 `list_del()`은 제거한 노드의 링크를 디버깅 값 등으로 덮어쓸 수 있어 기존 reader를 깨뜨릴 수 있지만, `list_del_rcu()`는 RCU reader가 남은 순회를 끝낼 수 있는 형태를 유지합니다. reader 쪽도 의존성 순서를 지키는 `list_for_each_entry_rcu()`를 사용해야 합니다.
reader도 포인터와 다음 노드 내용을 잘못된 순서로 읽지 않도록 `list_for_each_entry_rcu()`를 사용합니다. writer는 서로 직렬화되어 있으므로 일반 `list_for_each_entry()`를 사용할 수 있습니다.
제거된 객체는 기존 reader가 여전히 보고 있을 수 있어 즉시 해제하면 안 됩니다. `call_rcu()`로 현재 reader가 끝난 뒤 실행할 callback을 등록하거나 `synchronize_rcu()`로 grace period가 끝날 때까지 기다립니다.
reader는 `rcu_read_lock()`과 `rcu_read_unlock()` 사이에서 목록을 접근합니다. 원문 설명에서 이 구간은 선점을 막아 reader가 목록을 읽는 도중 잠들지 않게 합니다.
모든 CPU가 적어도 한 번 quiescent state를 지나면 제거 시점에 존재하던 reader가 끝났다고 판단할 수 있고 callback을 실행합니다. 실제 RCU 구현은 더 최적화되어 있지만 이것이 기본 개념입니다.
RCU 패치는 `struct rcu_head`를 객체에 추가하고 검색을 `list_for_each_entry_rcu`, 삭제를 `list_del_rcu`와 `call_rcu`, 추가를 `list_add_rcu`, 조회 구간을 `rcu_read_lock/unlock`으로 바꿉니다.
::
--- cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
+++ cache.c.rcupdate 2003-12-11 17:55:14.000000000 +1100
@@ -1,15 +1,18 @@
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/string.h>
+#include <linux/rcupdate.h>
#include <linux/mutex.h>
#include <asm/errno.h>
struct object
{
- /* These two protected by cache_lock. */
+ /* This is protected by RCU */
struct list_head list;
int popularity;
+ struct rcu_head rcu;
+
atomic_t refcnt;
/* Doesn't change once created. */
@@ -40,7 +43,7 @@
{
struct object *i;
- list_for_each_entry(i, &cache, list) {
+ list_for_each_entry_rcu(i, &cache, list) {
if (i->id == id) {
i->popularity++;
return i;
@@ -49,19 +52,25 @@
return NULL;
}
+/* Final discard done once we know no readers are looking. */
+static void cache_delete_rcu(void *arg)
+{
+ object_put(arg);
+}
+
/* Must be holding cache_lock */
static void __cache_delete(struct object *obj)
{
BUG_ON(!obj);
- list_del(&obj->list);
- object_put(obj);
+ list_del_rcu(&obj->list);
cache_num--;
+ call_rcu(&obj->rcu, cache_delete_rcu);
}
/* Must be holding cache_lock */
static void __cache_add(struct object *obj)
{
- list_add(&obj->list, &cache);
+ list_add_rcu(&obj->list, &cache);
if (++cache_num > MAX_CACHE_SIZE) {
struct object *i, *outcast = NULL;
list_for_each_entry(i, &cache, list) {
@@ -104,12 +114,11 @@
struct object *cache_find(int id)
{
struct object *obj;
- unsigned long flags;
- spin_lock_irqsave(&cache_lock, flags);
+ rcu_read_lock();
obj = __cache_find(id);
if (obj)
object_get(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ rcu_read_unlock();
return obj;
}
`call_rcu()`는 현재 reader들이 끝난 뒤 실행할 회수 콜백을 등록하고 즉시 반환합니다. 반대로 `synchronize_rcu()`는 grace period가 끝날 때까지 호출자를 기다리게 합니다. 비동기 회수와 동기 대기 중 어느 쪽을 쓸지는 호출 경로가 잠들 수 있는지와 지연 요구에 따라 정합니다.
reader는 `rcu_read_lock()`과 `rcu_read_unlock()` 사이에서 목록을 순회합니다. 이 문서의 개념적 설명에서는 reader가 그 구간에서 잠들지 못하도록 하고, 모든 CPU가 적어도 한 번 quiescent state를 지나면 제거 당시 존재하던 reader가 모두 끝났다고 판단합니다. 실제 커널 구현은 이 기본 아이디어를 더 정교하게 최적화합니다.
예제의 writer는 계속 `cache_lock`으로 서로 직렬화되지만 reader인 `cache_find()`는 그 잠금 대신 RCU read-side 구간을 사용합니다. 찾은 객체에는 구간을 벗어나기 전에 `object_get()`으로 참조를 추가하므로, RCU 보호가 끝난 뒤에도 호출자가 객체를 안전하게 보유할 수 있습니다.
`popularity`는 reader들이 잠금 없이 증가하므로 갱신 손실이 생길 수 있습니다. 예제는 이 값이 퇴출 대상을 고르는 근사 통계라 정확한 합계가 필요 없다고 판단해 경합을 허용합니다. 정확성이 요구되는 상태라면 `atomic_t`나 별도 동기화를 적용해야 합니다.
호출자가 RCU read-side 구간 전체에서 선점을 비활성화한 채 객체를 사용한다면 별도 참조 계수 증가와 감소를 생략할 여지도 있습니다. 이 최적화는 객체 캐시라인에 쓰기를 만들지 않아 SMP 확장성이 좋아지지만, 사용 기간이 RCU 보호 구간을 절대 벗어나지 않는다는 강한 API 계약이 필요합니다.
reader가 `popularity`를 잠금 없이 증가시키는 경쟁은 근사값이면 충분하다는 응용 요구 때문에 허용됩니다. 정확성이 필요하면 `atomic_t` 같은 별도 보호가 필요합니다.
결과적으로 `cache_find()`는 writer와 전통적인 잠금 경쟁 없이 SMP에서도 UP에 가까운 읽기 성능을 얻습니다.
호출자가 조회부터 사용 종료까지 선점을 계속 비활성화한다면 객체 삭제가 완료될 수 없으므로 별도 참조 카운터 증가·감소를 피할 수 있습니다. 변경 쓰기가 없어져 캐시라인 이동도 줄어듭니다.
일반 목록 API 대신 게시·순회·삭제 수명을 보장하는 변형을 사용합니다.
초기화가 전역 연결보다 먼저 관찰되도록 메모리 순서를 보장합니다.
논리적 제거와 물리적 메모리 해제 사이에 grace period가 있습니다.
reader는 매우 가볍지만 writer가 게시 순서와 지연 해제를 책임집니다.
Evitare i *lock*: Read Copy Update
--------------------------------------------
Esiste un metodo di sincronizzazione per letture e scritture detto
Read Copy Update. Con l'uso della tecnica RCU, i lettori possono scordarsi
completamente di trattenere i *lock*; dato che nel nostro esempio ci
aspettiamo d'avere più lettore che scrittori (altrimenti questa memoria
sarebbe uno spreco) possiamo dire che questo meccanismo permette
un'ottimizzazione.
Come facciamo a sbarazzarci dei *lock* di lettura? Sbarazzarsi dei *lock* di
lettura significa che uno scrittore potrebbe cambiare la lista sotto al naso
dei lettori. Questo è abbastanza semplice: possiamo leggere una lista
concatenata se lo scrittore aggiunge elementi alla fine e con certe
precauzioni. Per esempio, aggiungendo ``new`` ad una lista concatenata
chiamata ``list``::
new->next = list->next;
wmb();
list->next = new;
La funzione wmb() è una barriera di sincronizzazione delle
scritture. Questa garantisce che la prima operazione (impostare l'elemento
``next`` del nuovo elemento) venga completata e vista da tutti i processori
prima che venga eseguita la seconda operazione (che sarebbe quella di mettere
il nuovo elemento nella lista). Questo è importante perché i moderni
compilatori ed i moderni processori possono, entrambe, riordinare le istruzioni
se non vengono istruiti altrimenti: vogliamo che i lettori non vedano
completamente il nuovo elemento; oppure che lo vedano correttamente e quindi
il puntatore ``next`` deve puntare al resto della lista.
Fortunatamente, c'è una funzione che fa questa operazione sulle liste
:c:type:`struct list_head <list_head>`: list_add_rcu()
(``include/linux/list.h``).
Rimuovere un elemento dalla lista è anche più facile: sostituiamo il puntatore
al vecchio elemento con quello del suo successore, e i lettori vedranno
l'elemento o lo salteranno.
::
list->next = old->next;
La funzione list_del_rcu() (``include/linux/list.h``) fa esattamente
questo (la versione normale corrompe il vecchio oggetto, e non vogliamo che
accada).
Anche i lettori devono stare attenti: alcuni processori potrebbero leggere
attraverso il puntatore ``next`` il contenuto dell'elemento successivo
troppo presto, ma non accorgersi che il contenuto caricato è sbagliato quando
il puntatore ``next`` viene modificato alla loro spalle. Ancora una volta
c'è una funzione che viene in vostro aiuto list_for_each_entry_rcu()
(``include/linux/list.h``). Ovviamente, gli scrittori possono usare
list_for_each_entry() dato che non ci possono essere due scrittori
in contemporanea.
Il nostro ultimo dilemma è il seguente: quando possiamo realmente distruggere
l'elemento rimosso? Ricordate, un lettore potrebbe aver avuto accesso a questo
elemento proprio ora: se eliminiamo questo elemento ed il puntatore ``next``
cambia, il lettore salterà direttamente nella spazzatura e scoppierà. Dobbiamo
aspettare finché tutti i lettori che stanno attraversando la lista abbiano
finito. Utilizziamo call_rcu() per registrare una funzione di
richiamo che distrugga l'oggetto quando tutti i lettori correnti hanno
terminato. In alternative, potrebbe essere usata la funzione
synchronize_rcu() che blocca l'esecuzione finché tutti i lettori
non terminano di ispezionare la lista.
Ma come fa l'RCU a sapere quando i lettori sono finiti? Il meccanismo è
il seguente: innanzi tutto i lettori accedono alla lista solo fra la coppia
rcu_read_lock()/rcu_read_unlock() che disabilita la
prelazione così che i lettori non vengano sospesi mentre stanno leggendo
la lista.
Poi, l'RCU aspetta finché tutti i processori non abbiano dormito almeno
una volta; a questo punto, dato che i lettori non possono dormire, possiamo
dedurre che un qualsiasi lettore che abbia consultato la lista durante la
rimozione abbia già terminato, quindi la *callback* viene eseguita. Il vero
codice RCU è un po' più ottimizzato di così, ma questa è l'idea di fondo.
::
--- cache.c.perobjectlock 2003-12-11 17:15:03.000000000 +1100
+++ cache.c.rcupdate 2003-12-11 17:55:14.000000000 +1100
@@ -1,15 +1,18 @@
#include <linux/list.h>
#include <linux/slab.h>
#include <linux/string.h>
+#include <linux/rcupdate.h>
#include <linux/mutex.h>
#include <asm/errno.h>
struct object
{
- /* These two protected by cache_lock. */
+ /* This is protected by RCU */
struct list_head list;
int popularity;
+ struct rcu_head rcu;
+
atomic_t refcnt;
/* Doesn't change once created. */
@@ -40,7 +43,7 @@
{
struct object *i;
- list_for_each_entry(i, &cache, list) {
+ list_for_each_entry_rcu(i, &cache, list) {
if (i->id == id) {
i->popularity++;
return i;
@@ -49,19 +52,25 @@
return NULL;
}
+/* Final discard done once we know no readers are looking. */
+static void cache_delete_rcu(void *arg)
+{
+ object_put(arg);
+}
+
/* Must be holding cache_lock */
static void __cache_delete(struct object *obj)
{
BUG_ON(!obj);
- list_del(&obj->list);
- object_put(obj);
+ list_del_rcu(&obj->list);
cache_num--;
+ call_rcu(&obj->rcu, cache_delete_rcu);
}
/* Must be holding cache_lock */
static void __cache_add(struct object *obj)
{
- list_add(&obj->list, &cache);
+ list_add_rcu(&obj->list, &cache);
if (++cache_num > MAX_CACHE_SIZE) {
struct object *i, *outcast = NULL;
list_for_each_entry(i, &cache, list) {
@@ -104,12 +114,11 @@
struct object *cache_find(int id)
{
struct object *obj;
- unsigned long flags;
- spin_lock_irqsave(&cache_lock, flags);
+ rcu_read_lock();
obj = __cache_find(id);
if (obj)
object_get(obj);
- spin_unlock_irqrestore(&cache_lock, flags);
+ rcu_read_unlock();
return obj;
}
Da notare che i lettori modificano il campo popularity nella funzione
__cache_find(), e ora non trattiene alcun *lock*. Una soluzione
potrebbe essere quella di rendere la variabile ``atomic_t``, ma per l'uso
che ne abbiamo fatto qui, non ci interessano queste corse critiche perché un
risultato approssimativo è comunque accettabile, quindi non l'ho cambiato.
Il risultato è che la funzione cache_find() non ha bisogno di alcuna
sincronizzazione con le altre funzioni, quindi è veloce su un sistema
multi-processore tanto quanto lo sarebbe su un sistema mono-processore.
Esiste un'ulteriore ottimizzazione possibile: vi ricordate il codice originale
della nostra memoria dove non c'erano contatori di riferimenti e il chiamante
semplicemente tratteneva il *lock* prima di accedere ad un oggetto? Questo è
ancora possibile: se trattenete un *lock* nessuno potrà cancellare l'oggetto,
quindi non avete bisogno di incrementare e decrementare il contatore di
riferimenti.
Ora, dato che il '*lock* di lettura' di un RCU non fa altro che disabilitare
la prelazione, un chiamante che ha sempre la prelazione disabilitata fra le
chiamate cache_find() e object_put() non necessita
di incrementare e decrementare il contatore di riferimenti. Potremmo
esporre la funzione __cache_find() dichiarandola non-static,
e quel chiamante potrebbe usare direttamente questa funzione.
Il beneficio qui sta nel fatto che il contatore di riferimenti no
viene scritto: l'oggetto non viene alterato in alcun modo e quindi diventa
molto più veloce su sistemi molti-processore grazie alla loro memoria cache.
per-CPU와 IRQ 중심 데이터
1279-1337동기화를 피하는 또 다른 방법은 CPU마다 정보를 복제하는 것입니다. 단일 카운터와 spinlock이 충분히 빠르면 그 단순한 설계를 우선합니다.
실측으로 병목임이 확인되면 CPU별 카운터를 두어 상호 배제를 없앨 수 있습니다. `include/linux/percpu.h`의 `DEFINE_PER_CPU()`, `get_cpu_var()`, `put_cpu_var()`를 사용합니다.
단순 CPU별 카운터에는 `include/asm/local.h`의 `local_t`, `cpu_local_inc()` 계열이 유용하고 일부 아키텍처에서 더 효율적입니다.
CPU별 값을 하나의 정확한 총합으로 읽으려면 다시 동기화가 필요해 간단하고 신뢰할 만한 방법이 없습니다. 근사 통계처럼 정확한 순간값이 필요 없는 경우에 적합합니다.
데이터가 항상 동일 IRQ 핸들러에서만 쓰이면 커널이 동일 핸들러의 CPU 간 동시 실행을 막으므로 별도 잠금이 필요 없습니다.
사용자 컨텍스트나 softirq가 아주 드물게 같은 자료를 접근한다면 그 경로에서 mutex를 잡고 `disable_irq(irq)`로 핸들러를 막은 뒤 작업하고 `enable_irq()`와 mutex 해제를 수행할 수 있습니다.
Manfred Spraul fa notare che potreste comunque comportarvi così anche
se i dati vengono occasionalmente utilizzati da un contesto utente o
da un'interruzione software. Il gestore d'interruzione non utilizza alcun
*lock*, e tutti gli altri accessi verranno fatti così::
mutex_lock(&lock);
disable_irq(irq);
...
enable_irq(irq);
mutex_unlock(&lock);
`disable_irq()`는 다른 CPU에서 이미 실행 중인 핸들러가 끝날 때까지 기다립니다. 이 방식은 `spin_lock_irq()`보다 느리므로 비IRQ 접근이 극히 드문 경우에만 의미가 있습니다.
성능 문제가 입증되기 전에는 단일 잠금 설계가 우선입니다.
핸들러의 빠른 경로에는 잠금을 추가하지 않고 드문 경로가 IRQ를 동기 정지합니다.
Dati per processore
-------------------
Un'altra tecnica comunemente usata per evitare la sincronizzazione è quella
di duplicare le informazioni per ogni processore. Per esempio, se volete
avere un contatore di qualcosa, potreste utilizzare uno spinlock ed un
singolo contatore. Facile e pulito.
Se questo dovesse essere troppo lento (solitamente non lo è, ma se avete
dimostrato che lo è devvero), potreste usare un contatore per ogni processore
e quindi non sarebbe più necessaria la mutua esclusione. Vedere
DEFINE_PER_CPU(), get_cpu_var() e put_cpu_var()
(``include/linux/percpu.h``).
Il tipo di dato ``local_t``, la funzione cpu_local_inc() e tutte
le altre funzioni associate, sono di particolare utilità per semplici contatori
per-processore; su alcune architetture sono anche più efficienti
(``include/asm/local.h``).
Da notare che non esiste un modo facile ed affidabile per ottenere il valore
di un simile contatore senza introdurre altri *lock*. In alcuni casi questo
non è un problema.
Dati che sono usati prevalentemente dai gestori d'interruzioni
--------------------------------------------------------------
Se i dati vengono utilizzati sempre dallo stesso gestore d'interruzioni,
allora i *lock* non vi servono per niente: il kernel già vi garantisce che
il gestore d'interruzione non verrà eseguito in contemporanea su diversi
processori.
Manfred Spraul fa notare che potreste comunque comportarvi così anche
se i dati vengono occasionalmente utilizzati da un contesto utente o
da un'interruzione software. Il gestore d'interruzione non utilizza alcun
*lock*, e tutti gli altri accessi verranno fatti così::
mutex_lock(&lock);
disable_irq(irq);
...
enable_irq(irq);
mutex_unlock(&lock);
La funzione disable_irq() impedisce al gestore d'interruzioni
d'essere eseguito (e aspetta che finisca nel caso fosse in esecuzione su
un altro processore). Lo spinlock, invece, previene accessi simultanei.
Naturalmente, questo è più lento della semplice chiamata
spin_lock_irq(), quindi ha senso solo se questo genere di accesso
è estremamente raro.
Quali funzioni possono essere chiamate in modo sicuro dalle interruzioni?
=========================================================================
Molte funzioni del kernel dormono (in sostanza, chiamano schedule())
direttamente od indirettamente: non potete chiamarle se trattenere uno
spinlock o avete la prelazione disabilitata, mai. Questo significa che
dovete necessariamente essere nel contesto utente: chiamarle da un
contesto d'interruzione è illegale.
IRQ 안전 함수와 mutex/futex API
1338-1408많은 커널 함수는 직접 또는 간접으로 `schedule()`을 호출해 잠듭니다. spinlock을 보유하거나 선점이 비활성화된 상태에서는 절대 호출할 수 없으며 IRQ 컨텍스트에서도 불법입니다.
다른 호출자가 잠들 수 있는 함수라면 자신도 잠들 수 있어야 합니다. 등록·등록 해제 함수는 보통 사용자 컨텍스트를 기대해 잠들 수 있습니다.
대표 수면 함수에는 `copy_from_user()`, `copy_to_user()`, `get_user()`, `put_user()`, `kmalloc(GFP_KERNEL)`, `mutex_lock_interruptible()`, `mutex_lock()`이 있습니다.
`mutex_trylock()`은 잠들지 않지만 IRQ 안전 구현이 아니므로 인터럽트에서 사용하면 안 됩니다. `mutex_unlock()`도 잠들지 않지만 mutex는 획득한 같은 프로세스가 풀어야 하므로 IRQ에서 대신 해제할 수 없습니다.
어느 컨텍스트에서도 호출할 수 있는 대표 비수면 함수는 `printk()`, `kfree()`, `add_timer()`, `timer_delete()`입니다.
mutex API 참조는 `include/linux/mutex.h`의 내부 문서와 `kernel/locking/mutex.c`의 내보낸 문서를 `kernel-doc` 지시문으로 포함합니다.
futex API 참조는 `kernel/futex/core.c`, `kernel/futex/futex.h`, `kernel/futex/pi.c`, `kernel/futex/requeue.c`, `kernel/futex/waitwake.c`의 내부 문서를 포함합니다.
이 함수들은 사용자 컨텍스트이며 잠금·선점 제약을 만족할 때만 호출합니다.
비수면이라는 사실과 별개로 각 API의 다른 컨텍스트 규칙도 확인해야 합니다.
페이지 하단에서 자동 추출하는 구현 문서입니다.
함수 이름만이 아니라 전체 호출 그래프가 schedule에 도달하는지 확인합니다.
Alcune funzioni che dormono
---------------------------
Le più comuni sono elencate qui di seguito, ma solitamente dovete leggere
il codice per scoprire se altre chiamate sono sicure. Se chiunque altro
le chiami dorme, allora dovreste poter dormire anche voi. In particolar
modo, le funzioni di registrazione e deregistrazione solitamente si
aspettano d'essere chiamante da un contesto utente e quindi che possono
dormire.
- Accessi allo spazio utente:
- copy_from_user()
- copy_to_user()
- get_user()
- put_user()
- kmalloc(GFP_KERNEL) <kmalloc>`
- mutex_lock_interruptible() and
mutex_lock()
C'è anche mutex_trylock() che però non dorme.
Comunque, non deve essere usata in un contesto d'interruzione dato
che la sua implementazione non è sicura in quel contesto.
Anche mutex_unlock() non dorme mai. Non può comunque essere
usata in un contesto d'interruzione perché un mutex deve essere rilasciato
dallo stesso processo che l'ha acquisito.
Alcune funzioni che non dormono
-------------------------------
Alcune funzioni possono essere chiamate tranquillamente da qualsiasi
contesto, o trattenendo un qualsiasi *lock*.
- printk()
- kfree()
- add_timer() e timer_delete()
Riferimento per l'API dei Mutex
===============================
.. kernel-doc:: include/linux/mutex.h
:internal:
.. kernel-doc:: kernel/locking/mutex.c
:export:
Riferimento per l'API dei Futex
===============================
.. kernel-doc:: kernel/futex/core.c
:internal:
.. kernel-doc:: kernel/futex/futex.h
:internal:
.. kernel-doc:: kernel/futex/pi.c
:internal:
.. kernel-doc:: kernel/futex/requeue.c
:internal:
.. kernel-doc:: kernel/futex/waitwake.c
:internal:
추가 자료와 감사의 글
1409-1436추가 자료로 `Documentation/locking/spinlocks.rst`의 Linus Torvalds spinlock 안내서를 제시합니다.
Curt Schimmel의 ‘Unix Systems for Modern Architectures: Symmetric Multiprocessing and Caching for Kernel Programmers’도 커널 수준 SMP 동기화 입문서로 추천하며 ISBN 0201633388을 기록합니다.
Telsa Gwynne은 문서를 DocBook으로 포맷하고 정리했으며 스타일을 더했습니다.
Martin Pool, Philipp Rumpf, Stephen Rothwell, Paul Mackerras, Ruedi Aschwanden, Alan Cox, Manfred Spraul, Tim Waugh, Pete Zaitcev, James Morris, Robert Love, Paul McKenney, John Ashby가 검토·수정·논평에 기여했습니다.
잠금 기초를 더 깊게 공부할 수 있는 두 자료입니다.
이 안내서의 원칙을 전용 spinlock 문서와 SMP 서적으로 확장합니다.
Approfondimenti
===============
- ``Documentation/locking/spinlocks.rst``: la guida di Linus Torvalds agli
spinlock del kernel.
- Unix Systems for Modern Architectures: Symmetric Multiprocessing and
Caching for Kernel Programmers.
L'introduzione alla sincronizzazione a livello di kernel di Curt Schimmel
è davvero ottima (non è scritta per Linux, ma approssimativamente si adatta
a tutte le situazioni). Il libro è costoso, ma vale ogni singolo spicciolo
per capire la sincronizzazione nei sistemi multi-processore.
[ISBN: 0201633388]
Ringraziamenti
==============
Grazie a Telsa Gwynne per aver formattato questa guida in DocBook, averla
pulita e aggiunto un po' di stile.
Grazie a Martin Pool, Philipp Rumpf, Stephen Rothwell, Paul Mackerras,
Ruedi Aschwanden, Alan Cox, Manfred Spraul, Tim Waugh, Pete Zaitcev,
James Morris, Robert Love, Paul McKenney, John Ashby per aver revisionato,
corretto, maledetto e commentato.
Grazie alla congrega per non aver avuto alcuna influenza su questo documento.
용어집
1437-1498선점(preemption)은 더 높은 우선순위 프로세스가 사용자 컨텍스트의 커널 실행을 대신하는 기능입니다. 원문은 Linux 2.5.4의 `CONFIG_PREEMPT` 도입과 함께 UP에서도 spinlock이 선점을 끄도록 바뀌었다고 설명합니다.
BH(bottom half)는 역사적 소프트웨어 인터럽트 이름입니다. 함수명의 `_bh`는 현재 CPU에서 모든 소프트웨어 인터럽트를 막는다는 뜻이며, 과거 bottom half는 폐기 방향이고 한 번에 하나만 실행되었습니다.
인터럽트 컨텍스트는 사용자 컨텍스트가 아닌 하드웨어·소프트웨어 인터럽트 처리 상태이며 `in_interrupt()`가 참입니다.
사용자 컨텍스트는 특정 프로세스 또는 커널 스레드를 대신해 커널이 실행하는 상태입니다. `current`로 프로세스를 식별하며 사용자 공간 자체와는 다르고 하드·소프트 IRQ 모두에 선점될 수 있습니다.
하드웨어 인터럽트 핸들러에서는 `in_hardirq()`가 참입니다. 소프트웨어 인터럽트에서는 `in_hardirq()`가 거짓이고 `in_softirq()`가 참이며 tasklet도 소프트웨어 인터럽트로 봅니다.
softirq는 여러 CPU에서 동시에 실행될 수 있는 정적 소프트웨어 인터럽트 종류를 가리키며 문맥에 따라 tasklet을 포함한 소프트웨어 인터럽트 전체를 뜻하기도 합니다.
UP는 `CONFIG_SMP=n`인 단일 프로세서 구성이고 SMP는 `CONFIG_SMP=y`인 대칭 멀티프로세서 커널입니다.
사용자 공간은 프로세스가 커널 밖에서 자신의 코드를 실행하는 상태입니다.
tasklet은 동적 등록 가능한 소프트웨어 인터럽트로 동일 tasklet이 한 번에 한 CPU에서만 실행됩니다. timer도 동적으로 등록되며 지정 시점 근처에 실행되고 실행 중에는 tasklet과 같은 성질을 가지며 `TIMER_SOFTIRQ`에서 호출됩니다.
원문 용어집의 실행 위치와 판별 방법을 정리합니다.
프로세서 수와 선점 설정을 구분합니다.
softirq 계열의 등록과 동시 실행 보장입니다.
Glossario
=========
prelazione
Prima del kernel 2.5, o quando ``CONFIG_PREEMPT`` non è impostato, i processi
in contesto utente non si avvicendano nell'esecuzione (in pratica, il
processo userà il processore fino al proprio termine, a meno che non ci siano
delle interruzioni). Con l'aggiunta di ``CONFIG_PREEMPT`` nella versione
2.5.4 questo è cambiato: quando si è in contesto utente, processi con una
priorità maggiore possono subentrare nell'esecuzione: gli spinlock furono
cambiati per disabilitare la prelazioni, anche su sistemi monoprocessore.
bh
Bottom Half: per ragioni storiche, le funzioni che contengono '_bh' nel
loro nome ora si riferiscono a qualsiasi interruzione software; per esempio,
spin_lock_bh() blocca qualsiasi interuzione software sul processore
corrente. I *Bottom Halves* sono deprecati, e probabilmente verranno
sostituiti dai tasklet. In un dato momento potrà esserci solo un
*bottom half* in esecuzione.
contesto d'interruzione
Non è il contesto utente: qui si processano le interruzioni hardware e
software. La macro in_interrupt() ritorna vero.
contesto utente
Il kernel che esegue qualcosa per conto di un particolare processo (per
esempio una chiamata di sistema) o di un thread del kernel. Potete
identificare il processo con la macro ``current``. Da non confondere
con lo spazio utente. Può essere interrotto sia da interruzioni software
che hardware.
interruzione hardware
Richiesta di interruzione hardware. in_hardirq() ritorna vero in un
gestore d'interruzioni hardware.
interruzione software / softirq
Gestore di interruzioni software: in_hardirq() ritorna falso;
in_softirq() ritorna vero. I tasklet e le softirq sono entrambi
considerati 'interruzioni software'.
In soldoni, un softirq è uno delle 32 interruzioni software che possono
essere eseguite su più processori in contemporanea. A volte si usa per
riferirsi anche ai tasklet (in pratica tutte le interruzioni software).
monoprocessore / UP
(Uni-Processor) un solo processore, ovvero non è SMP. (``CONFIG_SMP=n``).
multi-processore / SMP
(Symmetric Multi-Processor) kernel compilati per sistemi multi-processore
(``CONFIG_SMP=y``).
spazio utente
Un processo che esegue il proprio codice fuori dal kernel.
tasklet
Un'interruzione software registrabile dinamicamente che ha la garanzia
d'essere eseguita solo su un processore alla volta.
timer
Un'interruzione software registrabile dinamicamente che viene eseguita
(circa) in un determinato momento. Quando è in esecuzione è come un tasklet
(infatti, sono chiamati da ``TIMER_SOFTIRQ``).
요약·해설
locking.rst:1-1498이 안내서는 실행 컨텍스트 조합별 최소 잠금부터 객체 수명, ABBA 교착, timer 종료 경쟁, RCU grace period와 per-CPU 데이터까지 잠금 설계의 전체 흐름을 예제 중심으로 설명합니다.
원문은 Linux 2.6 시기의 역사적 설명과 API도 포함합니다. 이 페이지는 로컬 Linux v6.18.37 원문 1,498줄을 보존해 번역하며, 실제 새 코드에는 현재 잠금·timer·RCU 하위 문서와 구현을 함께 확인해야 합니다.