요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.
1. 요약·해설
원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.
2. 영어 원문 전체
번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.
원문 전체 펼치기
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
.. _it_kernel_hacking_locktypes:
========================================
Tipologie di blocco e le loro istruzioni
========================================
Introduzione
============
Il kernel fornisce un certo numero di primitive di blocco che possiamo dividere
in tre categorie:
- blocchi ad attesa con sospensione
- blocchi locali per CPU
- blocchi ad attesa attiva
Questo documento descrive questi tre tipi e fornisce istruzioni su come
annidarli, ed usarli su kernel PREEMPT_RT.
Categorie di blocchi
====================
Blocchi ad attesa con sospensione
---------------------------------
I blocchi ad attesa con sospensione possono essere acquisiti solo in un contesti
dov'è possibile la prelazione.
Diverse implementazioni permettono di usare try_lock() anche in altri contesti,
nonostante ciò è bene considerare anche la sicurezza dei corrispondenti
unlock(). Inoltre, vanno prese in considerazione anche le varianti di *debug*
di queste primitive. Insomma, non usate i blocchi ad attesa con sospensioni in
altri contesti a meno che proprio non vi siano alternative.
In questa categoria troviamo:
- mutex
- rt_mutex
- semaphore
- rw_semaphore
- ww_mutex
- percpu_rw_semaphore
Nei kernel con PREEMPT_RT, i seguenti blocchi sono convertiti in blocchi ad
attesa con sospensione:
- local_lock
- spinlock_t
- rwlock_t
Blocchi locali per CPU
----------------------
- local_lock
Su kernel non-PREEMPT_RT, le funzioni local_lock gestiscono le primitive di
disabilitazione di prelazione ed interruzioni. Al contrario di altri meccanismi,
la disabilitazione della prelazione o delle interruzioni sono puri meccanismi
per il controllo della concorrenza su una CPU e quindi non sono adatti per la
gestione della concorrenza inter-CPU.
Blocchi ad attesa attiva
------------------------
- raw_spinlcok_t
- bit spinlocks
Nei kernel non-PREEMPT_RT, i seguenti blocchi sono ad attesa attiva:
- spinlock_t
- rwlock_t
Implicitamente, i blocchi ad attesa attiva disabilitano la prelazione e le
funzioni lock/unlock hanno anche dei suffissi per gestire il livello di
protezione:
=================== =========================================================================
_bh() disabilita / abilita *bottom halves* (interruzioni software)
_irq() disabilita / abilita le interruzioni
_irqsave/restore() salva e disabilita le interruzioni / ripristina ed attiva le interruzioni
=================== =========================================================================
Semantica del proprietario
==========================
Eccetto i semafori, i sopracitati tipi di blocchi hanno tutti una semantica
molto stringente riguardo al proprietario di un blocco:
Il contesto (attività) che ha acquisito il blocco deve rilasciarlo
I semafori rw_semaphores hanno un'interfaccia speciale che permette anche ai non
proprietari del blocco di rilasciarlo per i lettori.
rtmutex
=======
I blocchi a mutua esclusione RT (*rtmutex*) sono un sistema a mutua esclusione
con supporto all'ereditarietà della priorità (PI).
Questo meccanismo ha delle limitazioni sui kernel non-PREEMPT_RT dovuti alla
prelazione e alle sezioni con interruzioni disabilitate.
Chiaramente, questo meccanismo non può avvalersi della prelazione su una sezione
dove la prelazione o le interruzioni sono disabilitate; anche sui kernel
PREEMPT_RT. Tuttavia, i kernel PREEMPT_RT eseguono la maggior parte delle
sezioni in contesti dov'è possibile la prelazione, specialmente in contesti
d'interruzione (anche software). Questa conversione permette a spinlock_t e
rwlock_t di essere implementati usando rtmutex.
semaphore
=========
La primitiva semaphore implementa un semaforo con contatore.
I semafori vengono spesso utilizzati per la serializzazione e l'attesa, ma per
nuovi casi d'uso si dovrebbero usare meccanismi diversi, come mutex e
completion.
semaphore e PREEMPT_RT
----------------------
I kernel PREEMPT_RT non cambiano l'implementazione di semaphore perché non hanno
un concetto di proprietario, dunque impediscono a PREEMPT_RT d'avere
l'ereditarietà della priorità sui semafori. Un proprietario sconosciuto non può
ottenere una priorità superiore. Di consequenza, bloccarsi sui semafori porta
all'inversione di priorità.
rw_semaphore
============
Il blocco rw_semaphore è un meccanismo che permette più lettori ma un solo scrittore.
Sui kernel non-PREEMPT_RT l'implementazione è imparziale, quindi previene
l'inedia dei processi scrittori.
Questi blocchi hanno una semantica molto stringente riguardo il proprietario, ma
offre anche interfacce speciali che permettono ai processi non proprietari di
rilasciare un processo lettore. Queste interfacce funzionano indipendentemente
dalla configurazione del kernel.
rw_semaphore e PREEMPT_RT
-------------------------
I kernel PREEMPT_RT sostituiscono i rw_semaphore con un'implementazione basata
su rt_mutex, e questo ne modifica l'imparzialità:
Dato che uno scrittore rw_semaphore non può assicurare la propria priorità ai
suoi lettori, un lettore con priorità più bassa che ha subito la prelazione
continuerà a trattenere il blocco, quindi porta all'inedia anche gli scrittori
con priorità più alta. Per contro, dato che i lettori possono garantire la
propria priorità agli scrittori, uno scrittore a bassa priorità che subisce la
prelazione vedrà la propria priorità alzata finché non rilascerà il blocco, e
questo preverrà l'inedia dei processi lettori a causa di uno scrittore.
local_lock
==========
I local_lock forniscono nomi agli ambiti di visibilità delle sezioni critiche
protette tramite la disattivazione della prelazione o delle interruzioni.
Sui kernel non-PREEMPT_RT le operazioni local_lock si traducono
nell'abilitazione o disabilitazione della prelazione o le interruzioni.
=============================== ======================
local_lock(&llock) preempt_disable()
local_unlock(&llock) preempt_enable()
local_lock_irq(&llock) local_irq_disable()
local_unlock_irq(&llock) local_irq_enable()
local_lock_irqsave(&llock) local_irq_save()
local_unlock_irqrestore(&llock) local_irq_restore()
=============================== ======================
Gli ambiti di visibilità con nome hanno due vantaggi rispetto alle primitive di
base:
- Il nome del blocco permette di fare un'analisi statica, ed è anche chiaro su
cosa si applichi la protezione cosa che invece non si può fare con le
classiche primitive in quanto sono opache e senza alcun ambito di
visibilità.
- Se viene abilitato lockdep, allora local_lock ottiene un lockmap che
permette di verificare la bontà della protezione. Per esempio, questo può
identificare i casi dove una funzione usa preempt_disable() come meccanismo
di protezione in un contesto d'interruzione (anche software). A parte
questo, lockdep_assert_held(&llock) funziona come tutte le altre primitive
di sincronizzazione.
local_lock e PREEMPT_RT
-------------------------
I kernel PREEMPT_RT sostituiscono local_lock con uno spinlock_t per CPU, quindi
ne cambia la semantica:
- Tutte le modifiche a spinlock_t si applicano anche a local_lock
L'uso di local_lock
-------------------
I local_lock dovrebbero essere usati su kernel non-PREEMPT_RT quando la
disabilitazione della prelazione o delle interruzioni è il modo più adeguato per
gestire l'accesso concorrente a strutture dati per CPU.
Questo meccanismo non è adatto alla protezione da prelazione o interruzione su
kernel PREEMPT_RT dato che verrà convertito in spinlock_t.
raw_spinlock_t e spinlock_t
===========================
raw_spinlock_t
--------------
I blocco raw_spinlock_t è un blocco ad attesa attiva su tutti i tipi di kernel,
incluso quello PREEMPT_RT. Usate raw_spinlock_t solo in sezioni critiche nel
cuore del codice, nella gestione delle interruzioni di basso livello, e in posti
dove è necessario disabilitare la prelazione o le interruzioni. Per esempio, per
accedere in modo sicuro lo stato dell'hardware. A volte, i raw_spinlock_t
possono essere usati quando la sezione critica è minuscola, per evitare gli
eccessi di un rtmutex.
spinlock_t
----------
Il significato di spinlock_t cambia in base allo stato di PREEMPT_RT.
Sui kernel non-PREEMPT_RT, spinlock_t si traduce in un raw_spinlock_t ed ha
esattamente lo stesso significato.
spinlock_t e PREEMPT_RT
-----------------------
Sui kernel PREEMPT_RT, spinlock_t ha un'implementazione dedicata che si basa
sull'uso di rt_mutex. Questo ne modifica il significato:
- La prelazione non viene disabilitata.
- I suffissi relativi alla interruzioni (_irq, _irqsave / _irqrestore) per le
operazioni spin_lock / spin_unlock non hanno alcun effetto sullo stato delle
interruzioni della CPU.
- I suffissi relativi alle interruzioni software (_bh()) disabilitano i
relativi gestori d'interruzione.
I kernel non-PREEMPT_RT disabilitano la prelazione per ottenere lo stesso effetto.
I kernel PREEMPT_RT usano un blocco per CPU per la serializzazione, il che
permette di tenere attiva la prelazione. Il blocco disabilita i gestori
d'interruzione software e previene la rientranza vista la prelazione attiva.
A parte quanto appena discusso, i kernel PREEMPT_RT preservano il significato
di tutti gli altri aspetti di spinlock_t:
- Le attività che trattengono un blocco spinlock_t non migrano su altri
processori. Disabilitando la prelazione, i kernel non-PREEMPT_RT evitano la
migrazione. Invece, i kernel PREEMPT_RT disabilitano la migrazione per
assicurarsi che i puntatori a variabili per CPU rimangano validi anche
quando un'attività subisce la prelazione.
- Lo stato di un'attività si mantiene durante le acquisizioni del blocco al
fine di garantire che le regole basate sullo stato delle attività si possano
applicare a tutte le configurazioni del kernel. I kernel non-PREEMPT_RT
lasciano lo stato immutato. Tuttavia, la funzionalità PREEMPT_RT deve
cambiare lo stato se l'attività si blocca durante l'acquisizione. Dunque,
salva lo stato attuale prima di bloccarsi ed il rispettivo risveglio lo
ripristinerà come nell'esempio seguente::
task->state = TASK_INTERRUPTIBLE
lock()
block()
task->saved_state = task->state
task->state = TASK_UNINTERRUPTIBLE
schedule()
lock wakeup
task->state = task->saved_state
Altri tipi di risvegli avrebbero impostato direttamente lo stato a RUNNING,
ma in questo caso non avrebbe funzionato perché l'attività deve rimanere
bloccata fintanto che il blocco viene trattenuto. Quindi, lo stato salvato
viene messo a RUNNING quando il risveglio di un non-blocco cerca di
risvegliare un'attività bloccata in attesa del rilascio di uno spinlock. Poi,
quando viene completata l'acquisizione del blocco, il suo risveglio
ripristinerà lo stato salvato, in questo caso a RUNNING::
task->state = TASK_INTERRUPTIBLE
lock()
block()
task->saved_state = task->state
task->state = TASK_UNINTERRUPTIBLE
schedule()
non lock wakeup
task->saved_state = TASK_RUNNING
lock wakeup
task->state = task->saved_state
Questo garantisce che il vero risveglio non venga perso.
rwlock_t
========
Il blocco rwlock_t è un meccanismo che permette più lettori ma un solo scrittore.
Sui kernel non-PREEMPT_RT questo è un blocco ad attesa e per i suoi suffissi si
applicano le stesse regole per spinlock_t. La sua implementazione è imparziale,
quindi previene l'inedia dei processi scrittori.
rwlock_t e PREEMPT_RT
---------------------
Sui kernel PREEMPT_RT rwlock_t ha un'implementazione dedicata che si basa
sull'uso di rt_mutex. Questo ne modifica il significato:
- Tutte le modifiche fatte a spinlock_t si applicano anche a rwlock_t.
- Dato che uno scrittore rw_semaphore non può assicurare la propria priorità ai
suoi lettori, un lettore con priorità più bassa che ha subito la prelazione
continuerà a trattenere il blocco, quindi porta all'inedia anche gli
scrittori con priorità più alta. Per contro, dato che i lettori possono
garantire la propria priorità agli scrittori, uno scrittore a bassa priorità
che subisce la prelazione vedrà la propria priorità alzata finché non
rilascerà il blocco, e questo preverrà l'inedia dei processi lettori a causa
di uno scrittore.
Precisazioni su PREEMPT_RT
==========================
local_lock su RT
----------------
Sui kernel PREEMPT_RT Ci sono alcune implicazioni dovute alla conversione di
local_lock in un spinlock_t. Per esempio, su un kernel non-PREEMPT_RT il
seguente codice funzionerà come ci si aspetta::
local_lock_irq(&local_lock);
raw_spin_lock(&lock);
ed è equivalente a::
raw_spin_lock_irq(&lock);
Ma su un kernel PREEMPT_RT questo codice non funzionerà perché local_lock_irq()
si traduce in uno spinlock_t per CPU che non disabilita né le interruzioni né la
prelazione. Il seguente codice funzionerà su entrambe i kernel con o senza
PREEMPT_RT::
local_lock_irq(&local_lock);
spin_lock(&lock);
Un altro dettaglio da tenere a mente con local_lock è che ognuno di loro ha un
ambito di protezione ben preciso. Dunque, la seguente sostituzione è errate::
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_1, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_2, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
Questo funziona correttamente su un kernel non-PREEMPT_RT, ma su un kernel
PREEMPT_RT local_lock_1 e local_lock_2 sono distinti e non possono serializzare
i chiamanti di func3(). L'*assert* di lockdep verrà attivato su un kernel
PREEMPT_RT perché local_lock_irqsave() non disabilita le interruzione a casa
della specifica semantica di spinlock_t in PREEMPT_RT. La corretta sostituzione
è::
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
spinlock_t e rwlock_t
---------------------
Ci sono alcune conseguenze di cui tener conto dal cambiamento di semantica di
spinlock_t e rwlock_t sui kernel PREEMPT_RT. Per esempio, sui kernel non
PREEMPT_RT il seguente codice funziona come ci si aspetta::
local_irq_disable();
spin_lock(&lock);
ed è equivalente a::
spin_lock_irq(&lock);
Lo stesso vale per rwlock_t e le varianti con _irqsave().
Sui kernel PREEMPT_RT questo codice non funzionerà perché gli rtmutex richiedono
un contesto con la possibilità di prelazione. Al suo posto, usate
spin_lock_irq() o spin_lock_irqsave() e le loro controparti per il rilascio. I
kernel PREEMPT_RT offrono un meccanismo local_lock per i casi in cui la
disabilitazione delle interruzioni ed acquisizione di un blocco devono rimanere
separati. Acquisire un local_lock àncora un processo ad una CPU permettendo cose
come un'acquisizione di un blocco con interruzioni disabilitate per singola CPU.
Il tipico scenario è quando si vuole proteggere una variabile di processore nel
contesto di un thread::
struct foo *p = get_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
Questo codice è corretto su un kernel non-PREEMPT_RT, ma non lo è su un
PREEMPT_RT. La modifica della semantica di spinlock_t su PREEMPT_RT non permette
di acquisire p->lock perché, implicitamente, get_cpu_ptr() disabilita la
prelazione. La seguente sostituzione funzionerà su entrambe i kernel::
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
La funzione migrate_disable() assicura che il processo venga tenuto sulla CPU
corrente, e di conseguenza garantisce che gli accessi per-CPU alle variabili var1 e
var2 rimangano sulla stessa CPU fintanto che il processo rimane prelabile.
La sostituzione con migrate_disable() non funzionerà nel seguente scenario::
func()
{
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
p->val = func2();
Questo non funziona perché migrate_disable() non protegge dal ritorno da un
processo che aveva avuto il diritto di prelazione. Una sostituzione più adatta
per questo caso è::
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
Su un kernel non-PREEMPT_RT, questo codice protegge dal rientro disabilitando la
prelazione. Su un kernel PREEMPT_RT si ottiene lo stesso risultato acquisendo lo
spinlock di CPU.
raw_spinlock_t su RT
--------------------
Acquisire un raw_spinlock_t disabilita la prelazione e possibilmente anche le
interruzioni, quindi la sezione critica deve evitare di acquisire uno spinlock_t
o rwlock_t. Per esempio, la sezione critica non deve fare allocazioni di
memoria. Su un kernel non-PREEMPT_RT il seguente codice funziona perfettamente::
raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
Ma lo stesso codice non funziona su un kernel PREEMPT_RT perché l'allocatore di
memoria può essere oggetto di prelazione e quindi non può essere chiamato in un
contesto atomico. Tuttavia, si può chiamare l'allocatore di memoria quando si
trattiene un blocco *non-raw* perché non disabilitano la prelazione sui kernel
PREEMPT_RT::
spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
bit spinlocks
-------------
I kernel PREEMPT_RT non possono sostituire i bit spinlock perché un singolo bit
è troppo piccolo per farci stare un rtmutex. Dunque, la semantica dei bit
spinlock è mantenuta anche sui kernel PREEMPT_RT. Quindi, le precisazioni fatte
per raw_spinlock_t valgono anche qui.
In PREEMPT_RT, alcuni bit spinlock sono sostituiti con normali spinlock_t usando
condizioni di preprocessore in base a dove vengono usati. Per contro, questo non
serve quando si sostituiscono gli spinlock_t. Invece, le condizioni poste sui
file d'intestazione e sul cuore dell'implementazione della sincronizzazione
permettono al compilatore di effettuare la sostituzione in modo trasparente.
Regole d'annidamento dei tipi di blocchi
========================================
Le regole principali sono:
- I tipi di blocco appartenenti alla stessa categoria possono essere annidati
liberamente a patto che si rispetti l'ordine di blocco al fine di evitare
stalli.
- I blocchi con sospensione non possono essere annidati in blocchi del tipo
CPU locale o ad attesa attiva
- I blocchi ad attesa attiva e su CPU locale possono essere annidati nei
blocchi ad attesa con sospensione.
- I blocchi ad attesa attiva possono essere annidati in qualsiasi altro tipo.
Queste limitazioni si applicano ad entrambe i kernel con o senza PREEMPT_RT.
Il fatto che un kernel PREEMPT_RT cambi i blocchi spinlock_t e rwlock_t dal tipo
ad attesa attiva a quello con sospensione, e che sostituisca local_lock con uno
spinlock_t per CPU, significa che non possono essere acquisiti quando si è in un
blocco raw_spinlock. Ne consegue il seguente ordine d'annidamento:
1) blocchi ad attesa con sospensione
2) spinlock_t, rwlock_t, local_lock
3) raw_spinlock_t e bit spinlocks
Se queste regole verranno violate, allora lockdep se ne accorgerà e questo sia
con o senza PREEMPT_RT.
3. 한국어 전문 번역
영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.
잠금 범주와 소유자 의미
1-97Kernel locking primitive는 sleeping lock, CPU local lock, spinning lock의 세 category로 나눌 수 있다. 이 문서는 각 lock을 개념적으로 설명하고 PREEMPT_RT를 포함한 nesting rule을 제시한다.
| Category | Lock type | 기본 규칙 |
|---|---|---|
| Sleeping | mutex, rt_mutex, semaphore, rw_semaphore, ww_mutex, percpu_rw_semaphore | Preemptible task context에서만 획득 |
| CPU local | local_lock | Non-RT에서는 preemption/IRQ disable wrapper이며 inter-CPU serialization을 제공하지 않음 |
| Spinning | raw_spinlock_t, bit spinlock | Preemption을 암묵적으로 disable |
| Non-PREEMPT_RT에서 spinning | spinlock_t, rwlock_t | raw spinning semantics |
| PREEMPT_RT에서 sleeping으로 변환 | local_lock, spinlock_t, rwlock_t | rt_mutex 기반 또는 per-CPU sleeping lock semantics |
Sleeping lock 구현이 다른 context에서 try_lock()을 허용하더라도 try_lock뿐 아니라 unlock 안전성과 debugging version까지 검토해야 한다. 다른 선택지가 없다면 모를까 sleeping lock을 비-task context에서 획득하지 않는 것이 원칙이다.
CPU에서 preemption 또는 interrupt를 disable하는 것은 순수한 CPU-local concurrency control이다. 다른 CPU와의 동시 접근을 직렬화하지 못한다.
| Spinning lock suffix | 추가 보호 |
|---|---|
| _bh() | Bottom half, 즉 soft interrupt disable/enable |
| _irq() | Interrupt disable/enable |
| _irqsave/restore() | 기존 interrupt-disabled state 저장 후 disable / 저장 state 복원 |
Semaphore를 제외한 앞의 lock type은 strict owner semantics를 갖는다. Lock을 획득한 context 또는 task가 직접 release해야 한다. rw_semaphore에는 reader를 위해 non-owner release를 허용하는 특별 interface가 있다.
커널 잠금은 잠들 수 있는 잠금, CPU 로컬 잠금, 회전 잠금으로 나뉩니다. 어떤 API가 현재 커널 설정에서 어느 범주에 속하는지를 먼저 판단해야 해당 문맥에서 선점·인터럽트·이주가 허용되는지 결정할 수 있습니다.
`mutex`, `rt_mutex`, semaphore 계열은 task 문맥에서 대기할 수 있고, `local_lock`은 비 RT 커널에서 CPU 로컬 선점 또는 IRQ 제어를 이름 있는 범위로 표현합니다. `raw_spinlock_t`와 bit spinlock은 모든 설정에서 실제 회전 잠금이며, `spinlock_t`와 `rwlock_t`는 PREEMPT_RT에서 잠들 수 있는 구현으로 바뀝니다.
semaphore의 일부 예외를 빼면 잠금을 획득한 실행 문맥이나 task가 직접 해제해야 하는 엄격한 소유자 의미를 따릅니다. 다른 문맥이 대신 해제하도록 설계하면 잠금 디버깅과 우선순위 상속의 전제가 무너집니다.
.. SPDX-License-Identifier: GPL-2.0
.. include:: ../disclaimer-ita.rst
.. _it_kernel_hacking_locktypes:
========================================
Tipologie di blocco e le loro istruzioni
========================================
Introduzione
============
Il kernel fornisce un certo numero di primitive di blocco che possiamo dividere
in tre categorie:
- blocchi ad attesa con sospensione
- blocchi locali per CPU
- blocchi ad attesa attiva
Questo documento descrive questi tre tipi e fornisce istruzioni su come
annidarli, ed usarli su kernel PREEMPT_RT.
Categorie di blocchi
====================
Blocchi ad attesa con sospensione
---------------------------------
I blocchi ad attesa con sospensione possono essere acquisiti solo in un contesti
dov'è possibile la prelazione.
Diverse implementazioni permettono di usare try_lock() anche in altri contesti,
nonostante ciò è bene considerare anche la sicurezza dei corrispondenti
unlock(). Inoltre, vanno prese in considerazione anche le varianti di *debug*
di queste primitive. Insomma, non usate i blocchi ad attesa con sospensioni in
altri contesti a meno che proprio non vi siano alternative.
In questa categoria troviamo:
- mutex
- rt_mutex
- semaphore
- rw_semaphore
- ww_mutex
- percpu_rw_semaphore
Nei kernel con PREEMPT_RT, i seguenti blocchi sono convertiti in blocchi ad
attesa con sospensione:
- local_lock
- spinlock_t
- rwlock_t
Blocchi locali per CPU
----------------------
- local_lock
Su kernel non-PREEMPT_RT, le funzioni local_lock gestiscono le primitive di
disabilitazione di prelazione ed interruzioni. Al contrario di altri meccanismi,
la disabilitazione della prelazione o delle interruzioni sono puri meccanismi
per il controllo della concorrenza su una CPU e quindi non sono adatti per la
gestione della concorrenza inter-CPU.
Blocchi ad attesa attiva
------------------------
- raw_spinlcok_t
- bit spinlocks
Nei kernel non-PREEMPT_RT, i seguenti blocchi sono ad attesa attiva:
- spinlock_t
- rwlock_t
Implicitamente, i blocchi ad attesa attiva disabilitano la prelazione e le
funzioni lock/unlock hanno anche dei suffissi per gestire il livello di
protezione:
=================== =========================================================================
_bh() disabilita / abilita *bottom halves* (interruzioni software)
_irq() disabilita / abilita le interruzioni
_irqsave/restore() salva e disabilita le interruzioni / ripristina ed attiva le interruzioni
=================== =========================================================================
Semantica del proprietario
==========================
Eccetto i semafori, i sopracitati tipi di blocchi hanno tutti una semantica
molto stringente riguardo al proprietario di un blocco:
Il contesto (attività) che ha acquisito il blocco deve rilasciarlo
I semafori rw_semaphores hanno un'interfaccia speciale che permette anche ai non
proprietari del blocco di rilasciarlo per i lettori.
rtmutex와 semaphore
98-132RT-mutex는 priority inheritance(PI)를 지원하는 mutex다. Non-PREEMPT_RT kernel에서는 preemption-disabled와 interrupt-disabled section 때문에 PI에 한계가 있다.
PREEMPT_RT에서도 PI가 preemption-disabled 또는 interrupt-disabled code region을 preempt할 수는 없다. 대신 PREEMPT_RT는 interrupt handler와 soft interrupt를 포함한 이런 region 대부분을 preemptible task context에서 실행하도록 변환한다. 이 변환 덕분에 spinlock_t와 rwlock_t를 RT-mutex로 구현할 수 있다.
Semaphore는 counting semaphore 구현이다. Serialization과 waiting 두 목적으로 자주 쓰였지만 새 use case는 mutex와 completion처럼 serialization mechanism과 wait mechanism을 분리해야 한다.
PREEMPT_RT는 semaphore 구현을 바꾸지 않는다. Counting semaphore에는 owner 개념이 없어 priority inheritance를 제공할 수 없기 때문이다. 알 수 없는 owner의 priority는 boost할 수 없다. 따라서 semaphore에서 block하면 priority inversion이 생길 수 있다.
`rt_mutex`는 소유자가 있는 mutex이므로 높은 우선순위 대기자의 우선순위를 소유자에게 전달할 수 있습니다. PREEMPT_RT는 IRQ와 softirq 작업 대부분을 선점 가능한 task 문맥으로 옮겨 이 우선순위 상속이 작동할 수 있는 영역을 넓힙니다.
counting semaphore는 소유자가 정해지지 않아 우선순위 상속 대상을 알 수 없습니다. 따라서 새 코드는 직렬화에는 mutex를, 사건 대기에는 completion을 사용해 두 역할을 분리해야 하며, PREEMPT_RT에서도 semaphore 구현과 역전 위험은 그대로 남습니다.
rtmutex
=======
I blocchi a mutua esclusione RT (*rtmutex*) sono un sistema a mutua esclusione
con supporto all'ereditarietà della priorità (PI).
Questo meccanismo ha delle limitazioni sui kernel non-PREEMPT_RT dovuti alla
prelazione e alle sezioni con interruzioni disabilitate.
Chiaramente, questo meccanismo non può avvalersi della prelazione su una sezione
dove la prelazione o le interruzioni sono disabilitate; anche sui kernel
PREEMPT_RT. Tuttavia, i kernel PREEMPT_RT eseguono la maggior parte delle
sezioni in contesti dov'è possibile la prelazione, specialmente in contesti
d'interruzione (anche software). Questa conversione permette a spinlock_t e
rwlock_t di essere implementati usando rtmutex.
semaphore
=========
La primitiva semaphore implementa un semaforo con contatore.
I semafori vengono spesso utilizzati per la serializzazione e l'attesa, ma per
nuovi casi d'uso si dovrebbero usare meccanismi diversi, come mutex e
completion.
semaphore e PREEMPT_RT
----------------------
I kernel PREEMPT_RT non cambiano l'implementazione di semaphore perché non hanno
un concetto di proprietario, dunque impediscono a PREEMPT_RT d'avere
l'ereditarietà della priorità sui semafori. Un proprietario sconosciuto non può
ottenere una priorità superiore. Di consequenza, bloccarsi sui semafori porta
all'inversione di priorità.
rw_semaphore와 PREEMPT_RT
133-160rw_semaphore는 여러 reader 또는 한 writer를 허용하는 lock mechanism이다. Non-PREEMPT_RT 구현은 fair하므로 writer starvation을 막는다. 기본적으로 strict owner semantics를 따르지만 reader용 non-owner release special-purpose interface가 있으며 kernel configuration과 무관하게 동작한다.
PREEMPT_RT는 rw_semaphore를 별도의 rt_mutex 기반 구현으로 mapping하여 fairness를 바꾼다. rw_semaphore writer는 여러 reader에게 자기 priority를 넘겨줄 수 없다. 따라서 preempt된 low-priority reader가 lock을 계속 보유하면 high-priority writer도 starvation될 수 있다.
반대 방향에서는 reader들이 writer에게 priority를 전달할 수 있다. Preempt된 low-priority writer는 lock을 release할 때까지 priority가 boost되므로 그 writer 때문에 reader가 starvation되는 일을 막는다.
`rw_semaphore`는 여러 reader 또는 한 writer를 허용합니다. 비 RT 구현은 writer 기아를 막는 공정성을 제공하지만 PREEMPT_RT의 rt_mutex 기반 구현에서는 한 writer가 여러 reader에게 우선순위를 동시에 전달할 수 없어 낮은 우선순위 reader가 높은 우선순위 writer를 지연시킬 수 있습니다.
반대 방향에서는 reader들이 단일 writer에게 우선순위를 전달할 수 있으므로 선점된 낮은 우선순위 writer가 해제 시점까지 boost됩니다. 읽기와 쓰기 방향의 지연 보장이 대칭이 아니라는 점을 실시간 설계에서 고려해야 합니다.
rw_semaphore
============
Il blocco rw_semaphore è un meccanismo che permette più lettori ma un solo scrittore.
Sui kernel non-PREEMPT_RT l'implementazione è imparziale, quindi previene
l'inedia dei processi scrittori.
Questi blocchi hanno una semantica molto stringente riguardo il proprietario, ma
offre anche interfacce speciali che permettono ai processi non proprietari di
rilasciare un processo lettore. Queste interfacce funzionano indipendentemente
dalla configurazione del kernel.
rw_semaphore e PREEMPT_RT
-------------------------
I kernel PREEMPT_RT sostituiscono i rw_semaphore con un'implementazione basata
su rt_mutex, e questo ne modifica l'imparzialità:
Dato che uno scrittore rw_semaphore non può assicurare la propria priorità ai
suoi lettori, un lettore con priorità più bassa che ha subito la prelazione
continuerà a trattenere il blocco, quindi porta all'inedia anche gli scrittori
con priorità più alta. Per contro, dato che i lettori possono garantire la
propria priorità agli scrittori, uno scrittore a bassa priorità che subisce la
prelazione vedrà la propria priorità alzata finché non rilascerà il blocco, e
questo preverrà l'inedia dei processi lettori a causa di uno scrittore.
local_lock의 CPU 로컬 범위
161-212local_lock은 preemption 또는 interrupt disable로 보호하는 critical section에 이름 있는 scope를 제공한다. Non-PREEMPT_RT에서 operation은 다음 primitive로 mapping된다.
| local_lock API | Non-PREEMPT_RT mapping |
|---|---|
| local_lock(&llock) | preempt_disable() |
| local_unlock(&llock) | preempt_enable() |
| local_lock_irq(&llock) | local_irq_disable() |
| local_unlock_irq(&llock) | local_irq_enable() |
| local_lock_irqsave(&llock) | local_irq_save() |
| local_unlock_irqrestore(&llock) | local_irq_restore() |
이름 있는 scope에는 두 장점이 있다. Lock name은 static analysis에 사용할 수 있고 보호 범위를 분명히 문서화한다. 일반 preemption·IRQ primitive는 이름과 scope가 없어 어떤 data를 보호하는지 알기 어렵다.
Lockdep를 enable하면 local_lock에 lockmap이 생겨 보호가 올바른지 검증할 수 있다. 예를 들어 preempt_disable()로 보호하는 function이 interrupt 또는 soft-interrupt context에서 불리는 오류를 찾는다. lockdep_assert_held(&llock)도 다른 locking primitive와 같은 방식으로 사용할 수 있다.
PREEMPT_RT는 local_lock을 per-CPU spinlock_t로 mapping하므로 spinlock_t의 모든 의미 변화가 local_lock에도 적용된다. Local_lock은 non-RT에서 per-CPU data structure를 보호하기 위해 preemption 또는 interrupt disable이 적절한 경우에 사용한다. RT에서는 spinlock_t semantics 때문에 preemption이나 interrupt 자체를 막는 용도로 사용할 수 없다.
`local_lock`은 익명의 `preempt_disable()` 또는 IRQ disable 구간에 이름을 부여하여 보호 대상을 문서화하고 lockdep 검증을 가능하게 합니다. 비 RT에서는 대응 선점·IRQ primitive로 치환되고, PREEMPT_RT에서는 CPU별 `spinlock_t`로 구현됩니다.
따라서 `local_lock`을 단순히 인터럽트를 끄는 도구로 해석하면 안 됩니다. 같은 보호 데이터를 만지는 모든 호출자가 동일한 lock 객체를 사용해야 하며, callee는 `lockdep_assert_held()`로 그 명시적 범위를 검사해야 RT와 비 RT에서 같은 규약을 유지할 수 있습니다.
local_lock
==========
I local_lock forniscono nomi agli ambiti di visibilità delle sezioni critiche
protette tramite la disattivazione della prelazione o delle interruzioni.
Sui kernel non-PREEMPT_RT le operazioni local_lock si traducono
nell'abilitazione o disabilitazione della prelazione o le interruzioni.
=============================== ======================
local_lock(&llock) preempt_disable()
local_unlock(&llock) preempt_enable()
local_lock_irq(&llock) local_irq_disable()
local_unlock_irq(&llock) local_irq_enable()
local_lock_irqsave(&llock) local_irq_save()
local_unlock_irqrestore(&llock) local_irq_restore()
=============================== ======================
Gli ambiti di visibilità con nome hanno due vantaggi rispetto alle primitive di
base:
- Il nome del blocco permette di fare un'analisi statica, ed è anche chiaro su
cosa si applichi la protezione cosa che invece non si può fare con le
classiche primitive in quanto sono opache e senza alcun ambito di
visibilità.
- Se viene abilitato lockdep, allora local_lock ottiene un lockmap che
permette di verificare la bontà della protezione. Per esempio, questo può
identificare i casi dove una funzione usa preempt_disable() come meccanismo
di protezione in un contesto d'interruzione (anche software). A parte
questo, lockdep_assert_held(&llock) funziona come tutte le altre primitive
di sincronizzazione.
local_lock e PREEMPT_RT
-------------------------
I kernel PREEMPT_RT sostituiscono local_lock con uno spinlock_t per CPU, quindi
ne cambia la semantica:
- Tutte le modifiche a spinlock_t si applicano anche a local_lock
L'uso di local_lock
-------------------
I local_lock dovrebbero essere usati su kernel non-PREEMPT_RT quando la
disabilitazione della prelazione o delle interruzioni è il modo più adeguato per
gestire l'accesso concorrente a strutture dati per CPU.
Questo meccanismo non è adatto alla protezione da prelazione o interruzione su
kernel PREEMPT_RT dato che verrà convertito in spinlock_t.
raw_spinlock_t, spinlock_t와 task 상태 보존
213-303raw_spinlock_t는 PREEMPT_RT를 포함한 모든 kernel에서 strict spinning lock이다. Real critical core code, low-level interrupt handling, hardware state에 안전하게 접근하기 위해 preemption 또는 interrupt disable이 반드시 필요한 곳에서만 사용한다. Critical section이 극히 작아 RT-mutex overhead를 피하려는 경우에도 제한적으로 쓸 수 있다.
spinlock_t 의미는 PREEMPT_RT 여부에 따라 달라진다. Non-PREEMPT_RT에서는 raw_spinlock_t에 mapping되어 완전히 같은 semantics를 갖는다.
PREEMPT_RT에서는 spinlock_t가 rt_mutex 기반 별도 구현으로 mapping된다. Preemption을 disable하지 않으며 spin_lock/spin_unlock의 _irq, _irqsave/_irqrestore suffix도 CPU interrupt-disabled state를 바꾸지 않는다.
_bh suffix는 여전히 softirq handler를 disable한다. Non-RT는 preemption을 disable해 이 효과를 얻지만 PREEMPT_RT는 preemption을 유지하면서 per-CPU lock으로 serialization한다. 이 lock은 softirq handler를 disable하고 task preemption에 의한 reentrancy도 막는다.
그 밖의 spinlock_t semantics는 유지된다. Lock을 가진 task는 migrate하지 않는다. Non-RT는 preemption disable로 migration을 막고 PREEMPT_RT는 migration만 disable하여 task가 preempt되더라도 per-CPU variable pointer가 유효하도록 보장한다.
Spinlock 획득 전후의 task state도 모든 kernel configuration에서 보존해야 한다. Non-PREEMPT_RT는 task state를 건드리지 않는다. 하지만 PREEMPT_RT에서는 rt_mutex 기반 spinlock 획득 중 task가 block할 수 있어 state를 바꿔야 한다. Block 전에 current state를 saved_state에 저장하고 lock wakeup이 이를 복원한다.
TASK_INTERRUPTIBLE 상태에서 lock을 기다리면 scheduler block을 위해 TASK_UNINTERRUPTIBLE로 잠시 바꾼다. Lock이 가능해지면 wakeup path가 original saved_state를 되돌린다.
일반 wakeup은 보통 task state를 무조건 RUNNING으로 바꾼다. 그러나 spinlock을 기다리며 block된 task에 그렇게 하면 lock이 가능하기 전에 실행될 수 있어 안 된다. 따라서 non-lock wakeup은 실제 state가 아니라 saved_state를 RUNNING으로 바꾼다. Lock 획득이 끝나면 lock wakeup이 saved_state를 current state에 복원한다.
일반 wakeup은 task를 즉시 runnable로 만들지 않고 saved_state에 TASK_RUNNING을 기록한다. Lock wakeup이 뒤에 이 값을 복원하므로 실제 wakeup event가 사라지지 않는다.
이 two-stage state update 덕분에 실제 wakeup event가 lock wait에 가려져 유실되지 않는다.
`raw_spinlock_t`는 모든 커널에서 선점을 막는 실제 회전 잠금이므로 하드웨어 상태와 매우 짧은 저수준 임계 구역에 한정합니다. 반면 PREEMPT_RT의 `spinlock_t`는 rt_mutex 기반이고 선점을 막지 않으며 `_irq`와 `_irqsave` 접미사도 CPU IRQ 상태를 직접 바꾸지 않습니다.
RT의 spinlock 대기는 task를 block시킬 수 있으므로 원래 task 상태를 `saved_state`에 보관합니다. 일반 wakeup은 이 저장 상태를 `TASK_RUNNING`으로 바꾸고, 잠금 wakeup이 실제 state로 복원하여 잠금 대기 중 발생한 wakeup 사건이 유실되지 않게 합니다.
raw_spinlock_t e spinlock_t
===========================
raw_spinlock_t
--------------
I blocco raw_spinlock_t è un blocco ad attesa attiva su tutti i tipi di kernel,
incluso quello PREEMPT_RT. Usate raw_spinlock_t solo in sezioni critiche nel
cuore del codice, nella gestione delle interruzioni di basso livello, e in posti
dove è necessario disabilitare la prelazione o le interruzioni. Per esempio, per
accedere in modo sicuro lo stato dell'hardware. A volte, i raw_spinlock_t
possono essere usati quando la sezione critica è minuscola, per evitare gli
eccessi di un rtmutex.
spinlock_t
----------
Il significato di spinlock_t cambia in base allo stato di PREEMPT_RT.
Sui kernel non-PREEMPT_RT, spinlock_t si traduce in un raw_spinlock_t ed ha
esattamente lo stesso significato.
spinlock_t e PREEMPT_RT
-----------------------
Sui kernel PREEMPT_RT, spinlock_t ha un'implementazione dedicata che si basa
sull'uso di rt_mutex. Questo ne modifica il significato:
- La prelazione non viene disabilitata.
- I suffissi relativi alla interruzioni (_irq, _irqsave / _irqrestore) per le
operazioni spin_lock / spin_unlock non hanno alcun effetto sullo stato delle
interruzioni della CPU.
- I suffissi relativi alle interruzioni software (_bh()) disabilitano i
relativi gestori d'interruzione.
I kernel non-PREEMPT_RT disabilitano la prelazione per ottenere lo stesso effetto.
I kernel PREEMPT_RT usano un blocco per CPU per la serializzazione, il che
permette di tenere attiva la prelazione. Il blocco disabilita i gestori
d'interruzione software e previene la rientranza vista la prelazione attiva.
A parte quanto appena discusso, i kernel PREEMPT_RT preservano il significato
di tutti gli altri aspetti di spinlock_t:
- Le attività che trattengono un blocco spinlock_t non migrano su altri
processori. Disabilitando la prelazione, i kernel non-PREEMPT_RT evitano la
migrazione. Invece, i kernel PREEMPT_RT disabilitano la migrazione per
assicurarsi che i puntatori a variabili per CPU rimangano validi anche
quando un'attività subisce la prelazione.
- Lo stato di un'attività si mantiene durante le acquisizioni del blocco al
fine di garantire che le regole basate sullo stato delle attività si possano
applicare a tutte le configurazioni del kernel. I kernel non-PREEMPT_RT
lasciano lo stato immutato. Tuttavia, la funzionalità PREEMPT_RT deve
cambiare lo stato se l'attività si blocca durante l'acquisizione. Dunque,
salva lo stato attuale prima di bloccarsi ed il rispettivo risveglio lo
ripristinerà come nell'esempio seguente::
task->state = TASK_INTERRUPTIBLE
lock()
block()
task->saved_state = task->state
task->state = TASK_UNINTERRUPTIBLE
schedule()
lock wakeup
task->state = task->saved_state
Altri tipi di risvegli avrebbero impostato direttamente lo stato a RUNNING,
ma in questo caso non avrebbe funzionato perché l'attività deve rimanere
bloccata fintanto che il blocco viene trattenuto. Quindi, lo stato salvato
viene messo a RUNNING quando il risveglio di un non-blocco cerca di
risvegliare un'attività bloccata in attesa del rilascio di uno spinlock. Poi,
quando viene completata l'acquisizione del blocco, il suo risveglio
ripristinerà lo stato salvato, in questo caso a RUNNING::
task->state = TASK_INTERRUPTIBLE
lock()
block()
task->saved_state = task->state
task->state = TASK_UNINTERRUPTIBLE
schedule()
non lock wakeup
task->saved_state = TASK_RUNNING
lock wakeup
task->state = task->saved_state
Questo garantisce che il vero risveglio non venga perso.
rwlock_t의 구현별 공정성
304-330rwlock_t는 여러 reader 또는 한 writer를 허용한다. Non-PREEMPT_RT에서는 spinning lock으로 구현되어 spinlock_t suffix rule이 적용되고 fair implementation이 writer starvation을 막는다.
PREEMPT_RT는 rwlock_t를 별도 rt_mutex 기반 구현으로 mapping한다. Spinlock_t의 모든 변화가 rwlock_t에도 적용된다.
Writer는 여러 reader에게 priority를 전달할 수 없으므로 preempt된 low-priority reader가 lock을 계속 보유해 high-priority writer를 starvation시킬 수 있다. 반대로 reader는 writer에게 priority를 전달할 수 있어 preempt된 low-priority writer는 release할 때까지 boost되고 reader starvation을 막는다.
비 RT의 `rwlock_t`는 회전 잠금이며 writer 기아를 막는 공정성을 갖습니다. PREEMPT_RT에서는 rt_mutex 기반으로 바뀌어 `spinlock_t`와 같은 선점·이주 의미를 따르며, 여러 reader와 단일 writer 사이의 우선순위 전달 한계도 `rw_semaphore`와 유사합니다.
rwlock_t
========
Il blocco rwlock_t è un meccanismo che permette più lettori ma un solo scrittore.
Sui kernel non-PREEMPT_RT questo è un blocco ad attesa e per i suoi suffissi si
applicano le stesse regole per spinlock_t. La sua implementazione è imparziale,
quindi previene l'inedia dei processi scrittori.
rwlock_t e PREEMPT_RT
---------------------
Sui kernel PREEMPT_RT rwlock_t ha un'implementazione dedicata che si basa
sull'uso di rt_mutex. Questo ne modifica il significato:
- Tutte le modifiche fatte a spinlock_t si applicano anche a rwlock_t.
- Dato che uno scrittore rw_semaphore non può assicurare la propria priorità ai
suoi lettori, un lettore con priorità più bassa che ha subito la prelazione
continuerà a trattenere il blocco, quindi porta all'inedia anche gli
scrittori con priorità più alta. Per contro, dato che i lettori possono
garantire la propria priorità agli scrittori, uno scrittore a bassa priorità
che subisce la prelazione vedrà la propria priorità alzata finché non
rilascerà il blocco, e questo preverrà l'inedia dei processi lettori a causa
di uno scrittore.
PREEMPT_RT의 local_lock 변환 주의점
331-406Non-PREEMPT_RT에서 다음 sequence는 local_lock_irq()가 interrupt를 disable하므로 기대대로 동작하며 raw_spin_lock_irq(&lock)와 같다.
local_lock_irq(&local_lock);
raw_spin_lock(&lock);
/* non-RT에서 동등 */
raw_spin_lock_irq(&lock);
PREEMPT_RT에서는 local_lock_irq()가 interrupt나 preemption을 disable하지 않는 per-CPU spinlock_t로 mapping되어 이 sequence가 깨진다. 다음처럼 일반 spin_lock을 nesting하면 RT와 non-RT 모두 올바르다.
local_lock_irq(&local_lock);
spin_lock(&lock);
또 다른 함정은 local_lock마다 고유한 protection scope가 있다는 점이다. 두 caller가 같은 func3()의 protected data에 접근하면서 서로 다른 local_lock_1과 local_lock_2를 사용하면 RT에서 두 caller를 직렬화하지 못한다. local_lock_irqsave()가 interrupt를 실제로 disable하지 않으므로 lockdep_assert_irqs_disabled()도 fail한다.
/* 잘못된 변환: 서로 다른 scope */
func1()
{
local_lock_irqsave(&local_lock_1, flags);
func3();
local_unlock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_lock_irqsave(&local_lock_2, flags);
func3();
local_unlock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
올바른 변환은 두 caller가 같은 local_lock을 사용하고 callee가 lockdep_assert_held()로 그 lock을 확인하는 것이다.
func1()
{
local_lock_irqsave(&local_lock, flags);
func3();
local_unlock_irqrestore(&local_lock, flags);
}
func2()
{
local_lock_irqsave(&local_lock, flags);
func3();
local_unlock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
비 RT에서 `local_lock_irq()` 뒤 `raw_spin_lock()`을 쓰면 IRQ가 이미 꺼져 있지만, RT의 local lock은 IRQ를 끄지 않으므로 같은 코드는 안전하지 않습니다. 일반 `spin_lock()`을 중첩하거나 실제 IRQ 제어가 필요한 저수준 잠금 API를 명시적으로 선택해야 합니다.
서로 다른 `local_lock_1`과 `local_lock_2`로 같은 보호 함수에 들어가는 변환도 RT에서 직렬화를 제공하지 않습니다. 공통 `local_lock` 하나로 범위를 통합하고 `lockdep_assert_held()`로 확인해야 두 커널 설정에서 동일한 보호 관계가 성립합니다.
Precisazioni su PREEMPT_RT
==========================
local_lock su RT
----------------
Sui kernel PREEMPT_RT Ci sono alcune implicazioni dovute alla conversione di
local_lock in un spinlock_t. Per esempio, su un kernel non-PREEMPT_RT il
seguente codice funzionerà come ci si aspetta::
local_lock_irq(&local_lock);
raw_spin_lock(&lock);
ed è equivalente a::
raw_spin_lock_irq(&lock);
Ma su un kernel PREEMPT_RT questo codice non funzionerà perché local_lock_irq()
si traduce in uno spinlock_t per CPU che non disabilita né le interruzioni né la
prelazione. Il seguente codice funzionerà su entrambe i kernel con o senza
PREEMPT_RT::
local_lock_irq(&local_lock);
spin_lock(&lock);
Un altro dettaglio da tenere a mente con local_lock è che ognuno di loro ha un
ambito di protezione ben preciso. Dunque, la seguente sostituzione è errate::
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_1, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_2, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
Questo funziona correttamente su un kernel non-PREEMPT_RT, ma su un kernel
PREEMPT_RT local_lock_1 e local_lock_2 sono distinti e non possono serializzare
i chiamanti di func3(). L'*assert* di lockdep verrà attivato su un kernel
PREEMPT_RT perché local_lock_irqsave() non disabilita le interruzione a casa
della specifica semantica di spinlock_t in PREEMPT_RT. La corretta sostituzione
è::
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
IRQ, per-CPU 포인터와 RT 잠금 중첩
407-481Non-PREEMPT_RT에서 local_irq_disable() 뒤 spin_lock()을 호출하는 sequence는 spin_lock_irq()와 같다. rwlock_t와 _irqsave suffix도 마찬가지다.
local_irq_disable();
spin_lock(&lock);
/* non-RT에서 동등 */
spin_lock_irq(&lock);
PREEMPT_RT에서는 RT-mutex가 fully preemptible context를 요구하므로 첫 sequence가 깨진다. 대신 spin_lock_irq() 또는 spin_lock_irqsave()와 대응 unlock을 사용한다. IRQ disable과 locking을 분리해야 한다면 local_lock으로 task를 CPU에 pin한 뒤 per-CPU interrupt-disabled lock 등을 획득할 수 있지만 반드시 필요한 경우에만 사용한다.
Thread context에서 per-CPU variable을 보호하는 다음 code는 non-RT에서는 맞지만 RT에서는 get_cpu_ptr()가 암묵적으로 preemption을 disable하기 때문에 p->lock을 획득할 수 없다.
struct foo *p = get_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
두 kernel에서 모두 동작하려면 migration만 disable하고 task는 preemptible하게 유지한다. migrate_disable()은 task를 current CPU에 pin하여 var1과 var2의 per-CPU access가 같은 CPU에 남게 한다.
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
그러나 migrate_disable()은 preempting task의 reentrancy를 막지 못한다. Function이 per-CPU data를 읽은 뒤 preempt될 수 있고 다른 task가 같은 data에 다시 들어올 수 있는 경우에는 local_lock을 사용해야 한다.
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
}
Non-RT에서는 local_lock이 preemption을 disable해 reentrancy를 막고 PREEMPT_RT에서는 underlying per-CPU spinlock을 획득해 같은 보호를 제공한다.
PREEMPT_RT의 일반 spinlock은 완전히 선점 가능한 문맥을 요구하므로 `local_irq_disable()`이나 `get_cpu_ptr()`처럼 선점을 먼저 막은 뒤 획득할 수 없습니다. IRQ 상태와 잠금이 결합된 API를 사용하거나 `migrate_disable()`로 CPU 이주만 막아 task는 선점 가능하게 유지해야 합니다.
`migrate_disable()`은 per-CPU 포인터의 CPU 안정성만 보장하고 같은 CPU에서 다른 task가 재진입하는 것은 막지 않습니다. 재진입까지 직렬화해야 한다면 이름 있는 `local_lock`을 사용하여 비 RT에서는 선점 차단, RT에서는 CPU별 실제 잠금으로 같은 불변식을 구현합니다.
spinlock_t e rwlock_t
---------------------
Ci sono alcune conseguenze di cui tener conto dal cambiamento di semantica di
spinlock_t e rwlock_t sui kernel PREEMPT_RT. Per esempio, sui kernel non
PREEMPT_RT il seguente codice funziona come ci si aspetta::
local_irq_disable();
spin_lock(&lock);
ed è equivalente a::
spin_lock_irq(&lock);
Lo stesso vale per rwlock_t e le varianti con _irqsave().
Sui kernel PREEMPT_RT questo codice non funzionerà perché gli rtmutex richiedono
un contesto con la possibilità di prelazione. Al suo posto, usate
spin_lock_irq() o spin_lock_irqsave() e le loro controparti per il rilascio. I
kernel PREEMPT_RT offrono un meccanismo local_lock per i casi in cui la
disabilitazione delle interruzioni ed acquisizione di un blocco devono rimanere
separati. Acquisire un local_lock àncora un processo ad una CPU permettendo cose
come un'acquisizione di un blocco con interruzioni disabilitate per singola CPU.
Il tipico scenario è quando si vuole proteggere una variabile di processore nel
contesto di un thread::
struct foo *p = get_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
Questo codice è corretto su un kernel non-PREEMPT_RT, ma non lo è su un
PREEMPT_RT. La modifica della semantica di spinlock_t su PREEMPT_RT non permette
di acquisire p->lock perché, implicitamente, get_cpu_ptr() disabilita la
prelazione. La seguente sostituzione funzionerà su entrambe i kernel::
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
La funzione migrate_disable() assicura che il processo venga tenuto sulla CPU
corrente, e di conseguenza garantisce che gli accessi per-CPU alle variabili var1 e
var2 rimangano sulla stessa CPU fintanto che il processo rimane prelabile.
La sostituzione con migrate_disable() non funzionerà nel seguente scenario::
func()
{
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
p->val = func2();
Questo non funziona perché migrate_disable() non protegge dal ritorno da un
processo che aveva avuto il diritto di prelazione. Una sostituzione più adatta
per questo caso è::
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
Su un kernel non-PREEMPT_RT, questo codice protegge dal rientro disabilitando la
prelazione. Su un kernel PREEMPT_RT si ottiene lo stesso risultato acquisendo lo
spinlock di CPU.
raw 및 bit spinlock의 원자 문맥 제약
482-517raw_spinlock_t를 획득하면 preemption과 경우에 따라 interrupt도 disable된다. 따라서 critical section은 일반 spinlock_t 또는 rwlock_t를 추가로 획득해서는 안 되고 memory allocation 같은 operation도 피해야 한다.
다음 code는 non-PREEMPT_RT에서는 동작하지만 fully preemptible memory allocator를 truly atomic context에서 호출할 수 없는 PREEMPT_RT에서는 실패한다.
raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
PREEMPT_RT에서 일반 non-raw spinlock은 preemption을 disable하지 않으므로 해당 lock을 가진 상태에서 allocator를 호출하는 것은 허용된다.
spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
Bit 하나에는 RT-mutex를 담을 수 없어 PREEMPT_RT가 bit spinlock을 대체할 수 없다. 따라서 bit spinlock semantics는 그대로이며 raw_spinlock_t의 주의점이 모두 적용된다.
일부 bit spinlock은 사용 위치의 #ifdef code로 PREEMPT_RT에서 일반 spinlock_t로 바꾼다. 반면 spinlock_t 자체의 대체는 사용 위치를 고칠 필요가 없다. Header와 core locking implementation의 conditional이 compiler가 투명하게 substitution하도록 만든다.
`raw_spinlock_t` 보유 구간은 실제 원자 문맥이므로 일반 `spinlock_t`나 `rwlock_t`를 추가로 획득하거나 메모리 할당처럼 잠들 수 있는 연산을 호출할 수 없습니다. 비 RT에서 우연히 작동한 `GFP_ATOMIC` 경로도 PREEMPT_RT의 선점 가능한 allocator에서는 허용되지 않을 수 있습니다.
bit 하나에는 rt_mutex 상태를 담을 수 없으므로 bit spinlock은 PREEMPT_RT에서도 raw 회전 의미를 유지합니다. RT 전환이 필요한 사용처는 별도 `spinlock_t`로 조건부 대체해야 하며 raw 잠금과 같은 중첩 제약을 적용합니다.
raw_spinlock_t su RT
--------------------
Acquisire un raw_spinlock_t disabilita la prelazione e possibilmente anche le
interruzioni, quindi la sezione critica deve evitare di acquisire uno spinlock_t
o rwlock_t. Per esempio, la sezione critica non deve fare allocazioni di
memoria. Su un kernel non-PREEMPT_RT il seguente codice funziona perfettamente::
raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
Ma lo stesso codice non funziona su un kernel PREEMPT_RT perché l'allocatore di
memoria può essere oggetto di prelazione e quindi non può essere chiamato in un
contesto atomico. Tuttavia, si può chiamare l'allocatore di memoria quando si
trattiene un blocco *non-raw* perché non disabilitano la prelazione sui kernel
PREEMPT_RT::
spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
bit spinlocks
-------------
I kernel PREEMPT_RT non possono sostituire i bit spinlock perché un singolo bit
è troppo piccolo per farci stare un rtmutex. Dunque, la semantica dei bit
spinlock è mantenuta anche sui kernel PREEMPT_RT. Quindi, le precisazioni fatte
per raw_spinlock_t valgono anche qui.
In PREEMPT_RT, alcuni bit spinlock sono sostituiti con normali spinlock_t usando
condizioni di preprocessore in base a dove vengono usati. Per contro, questo non
serve quando si sostituiscono gli spinlock_t. Invece, le condizioni poste sui
file d'intestazione e sul cuore dell'implementazione della sincronizzazione
permettono al compilatore di effettuare la sostituzione in modo trasparente.
잠금 범주 중첩 순서
518-547- 같은 category의 lock끼리는 일반 lock ordering rule을 지켜 deadlock을 막는 한 자유롭게 nesting할 수 있다.
- Sleeping lock은 CPU-local lock 또는 spinning lock 안에 nesting할 수 없다.
- CPU-local lock과 spinning lock은 sleeping lock 안에 nesting할 수 있다.
- Spinning lock은 모든 lock type 안에 nesting할 수 있다.
이 제약은 PREEMPT_RT와 non-RT 모두에 적용된다. PREEMPT_RT에서는 spinlock_t와 rwlock_t category가 spinning에서 sleeping으로 바뀌고 local_lock이 per-CPU spinlock_t가 되므로 raw spinlock을 가진 상태에서 이들을 획득할 수 없다.
| Nesting level | PREEMPT_RT에서 바깥쪽부터 안쪽 순서 |
|---|---|
| 1 | Sleeping locks |
| 2 | spinlock_t, rwlock_t, local_lock |
| 3 | raw_spinlock_t, bit spinlock |
PREEMPT_RT 여부와 관계없이 이 제약을 위반하면 lockdep가 경고한다.
같은 범주 안에서는 일반적인 전역 잠금 순서를 지키면 중첩할 수 있습니다. sleeping lock 안에는 CPU 로컬 잠금과 회전 잠금을 둘 수 있지만, CPU 로컬 또는 회전 잠금 안에는 sleeping lock을 둘 수 없습니다.
PREEMPT_RT에서는 `spinlock_t`, `rwlock_t`, `local_lock`이 잠들 수 있는 계층으로 이동하므로 바깥에서 안쪽 순서는 sleeping lock, RT 변환 잠금, `raw_spinlock_t`·bit spinlock이 됩니다. lockdep은 커널 설정과 관계없이 이 범주 위반을 경고합니다.
Regole d'annidamento dei tipi di blocchi
========================================
Le regole principali sono:
- I tipi di blocco appartenenti alla stessa categoria possono essere annidati
liberamente a patto che si rispetti l'ordine di blocco al fine di evitare
stalli.
- I blocchi con sospensione non possono essere annidati in blocchi del tipo
CPU locale o ad attesa attiva
- I blocchi ad attesa attiva e su CPU locale possono essere annidati nei
blocchi ad attesa con sospensione.
- I blocchi ad attesa attiva possono essere annidati in qualsiasi altro tipo.
Queste limitazioni si applicano ad entrambe i kernel con o senza PREEMPT_RT.
Il fatto che un kernel PREEMPT_RT cambi i blocchi spinlock_t e rwlock_t dal tipo
ad attesa attiva a quello con sospensione, e che sostituisca local_lock con uno
spinlock_t per CPU, significa che non possono essere acquisiti quando si è in un
blocco raw_spinlock. Ne consegue il seguente ordine d'annidamento:
1) blocchi ad attesa con sospensione
2) spinlock_t, rwlock_t, local_lock
3) raw_spinlock_t e bit spinlocks
Se queste regole verranno violate, allora lockdep se ne accorgerà e questo sia
con o senza PREEMPT_RT.
요약·해설
locktypes.rst:1-547잠금 API를 sleeping, CPU 로컬, spinning 범주로 나누고 PREEMPT_RT에서 `local_lock`, `spinlock_t`, `rwlock_t`의 구현 의미가 어떻게 달라지는지 설명합니다.
후반부는 IRQ·per-CPU 접근·task 상태 보존의 함정과 sleeping 잠금에서 raw 및 bit spinlock으로 내려가는 중첩 순서를 실제 코드로 정리합니다.