Uno spin lock è un modo per proteggere una risorsa condivisa dall'essere modificata da due o più processi contemporaneamente. Il primo processo che prova a modificare la risorsa "acquisisce" il lock e continua il suo cammino, facendo quello che doveva fare con la risorsa. Eventuali altri processi che successivamente tentano di acquisire il blocco vengono arrestati; si dice che "ruotino sul posto" aspettando che il blocco venga rilasciato dal primo processo, da cui il nome spin lock.
Il kernel Linux utilizza gli spin lock per molte cose, ad esempio quando si inviano dati a una particolare periferica. La maggior parte delle periferiche hardware non è progettata per gestire più aggiornamenti di stato simultanei. Se devono avvenire due modifiche diverse, una deve seguire rigorosamente l'altra, non possono sovrapporsi. Uno spin lock fornisce la protezione necessaria, assicurando che le modifiche avvengano una alla volta.
I blocchi di rotazione sono un problema perché la rotazione impedisce al core della CPU di quel thread di eseguire qualsiasi altro lavoro. Sebbene il kernel di Linux fornisca servizi multitasking ai programmi in spazio utente in esecuzione sotto di esso, quella funzionalità multitasking generica non si estende al codice del kernel.
Questa situazione sta cambiando, e lo è stata per la maggior parte dell'esistenza di Linux. Fino a Linux 2.0, il kernel era quasi esclusivamente un programma single-tasking:ogni volta che la CPU eseguiva il codice del kernel, veniva utilizzato un solo core della CPU, perché c'era un singolo spin lock che proteggeva tutte le risorse condivise, chiamato Big Kernel Lock (BKL ). A partire da Linux 2.2, il BKL viene lentamente suddiviso in molti blocchi indipendenti, ognuno dei quali protegge una classe di risorse più focalizzata. Oggi, con il kernel 2.6, il BKL esiste ancora, ma è utilizzato solo da codice molto vecchio che non può essere facilmente spostato in un blocco più granulare. Ora è del tutto possibile per una macchina multicore avere ogni CPU che esegue codice kernel utile.
C'è un limite all'utilità di rompere il BKL perché il kernel Linux manca di multitasking generale. Se un core della CPU viene bloccato durante la rotazione su uno spin lock del kernel, non può essere rielaborato, per andare a fare qualcos'altro fino a quando il blocco non viene rilasciato. Si siede e gira finché il blocco non viene rilasciato.
Gli spin lock possono effettivamente trasformare un mostruoso box da 16 core in un box single-core, se il carico di lavoro è tale che ogni core è sempre in attesa di un singolo spin lock. Questo è il limite principale alla scalabilità del kernel Linux:raddoppiare i core della CPU da 2 a 4 probabilmente raddoppierà quasi la velocità di un box Linux, ma raddoppiarla da 16 a 32 probabilmente non lo farà, con la maggior parte dei carichi di lavoro.
Uno spin lock si verifica quando un processo esegue continuamente il polling per rimuovere un blocco. È considerato cattivo perché il processo sta consumando cicli (di solito) inutilmente. Non è specifico per Linux, ma un modello di programmazione generale. E sebbene sia generalmente considerata una cattiva pratica, in realtà è la soluzione corretta; ci sono casi in cui il costo dell'utilizzo dello scheduler è superiore (in termini di cicli della CPU) rispetto al costo dei pochi cicli previsti per la durata dello spinlock.
Esempio di spinlock:
#!/bin/sh
#wait for some program to clear a lock before doing stuff
while [ -f /var/run/example.lock ]; do
sleep 1
done
#do stuff
C'è spesso un modo per evitare uno spin lock. Per questo particolare esempio, esiste uno strumento Linux chiamato inotifywait (di solito non è installato per impostazione predefinita). Se fosse scritto in C, useresti semplicemente l'API inotify fornita da Linux.
Lo stesso esempio, usando inotifywait, mostra come ottenere la stessa cosa senza uno spin lock:
#/bin/sh
inotifywait -e delete_self /var/run/example.lock
#do stuff
Quando un thread tenta di acquisire un blocco, possono accadere tre cose se fallisce:può provare a bloccarsi, può provare e continuare, può provare e poi andare a dormire dicendo al sistema operativo di riattivarlo quando si verifica un evento.
Ora un tentativo e continua richiede molto meno tempo di un tentativo e blocco. Diciamo per il momento che un "prova e continua" impiegherà un'unità di tempo e un "prova e blocca" ne impiegherà cento.
Supponiamo ora per il momento che in media un thread impiegherà 4 unità di tempo a mantenere il blocco. È uno spreco aspettare 100 unità. Quindi, invece, scrivi un ciclo di "prova e continua". Al quarto tentativo, di solito acquisirai il blocco. Questo è uno spin lock. Si chiama così perché il thread continua a girare sul posto finché non ottiene il lock.
Un'ulteriore misura di sicurezza consiste nel limitare il numero di volte in cui il ciclo viene eseguito. Quindi, ad esempio, esegui un ciclo for, diciamo, sei volte, se fallisce allora "provi a bloccare".
Se sai che un thread manterrà sempre il blocco per, diciamo, 200 unità, allora stai sprecando il tempo del computer per ogni tentativo e continua.
Quindi, alla fine, uno spin lock può essere molto efficiente o dispendioso. È uno spreco quando il tempo "tipico" per mantenere un blocco è superiore al tempo necessario per "provare a bloccare". È efficiente quando il tempo tipico per mantenere un blocco è molto inferiore al tempo per "provare a bloccare".
Ps:Il libro da leggere sui thread è "A Thread Primer", se riesci ancora a trovarlo.