Risposte:
Uno spin lock è un modo per proteggere una risorsa condivisa dalla modifica di due o più processi contemporaneamente. Il primo processo che tenta di modificare la risorsa "acquisisce" il blocco e continua sulla sua strada, facendo ciò che era necessario con la risorsa. Tutti gli altri processi che successivamente tentano di acquisire il blocco vengono arrestati; si dice che "ruotano in posizione" in attesa che il blocco venga rilasciato dal primo processo, da cui il nome spin lock.
Il kernel Linux utilizza spin lock per molte cose, come ad esempio quando si inviano dati a una determinata periferica. La maggior parte delle periferiche hardware non è progettata per gestire più aggiornamenti di stato simultanei. Se devono verificarsi due diverse modifiche, una deve seguire rigorosamente l'altra, non possono sovrapporsi. Un blocco spin fornisce la protezione necessaria, garantendo che le modifiche avvengano una alla volta.
I blocchi di spin rappresentano un problema perché i blocchi di spin che il core della CPU del thread eseguono qualsiasi altro lavoro. Mentre il kernel Linux fornisce servizi multitasking ai programmi spaziali dell'utente in esecuzione al suo interno, tale funzione multitasking per scopi generici non si estende al codice del kernel.
Questa situazione sta cambiando ed è stata per gran parte dell'esistenza di Linux. Fino a Linux 2.0, il kernel era quasi puramente un programma single-tasking: ogni volta che la CPU eseguiva il codice kernel, veniva usato un solo core della CPU, perché c'era un singolo blocco spin 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 che proteggono ciascuno una classe di risorse più focalizzata. Oggi, con il kernel 2.6, il BKL esiste ancora, ma è usato solo da un codice molto vecchio che non può essere facilmente spostato in un blocco più granulare. È ora possibile per un box multicore avere ogni CPU che esegue un utile codice kernel.
C'è un limite all'utilità di spezzare il BKL perché il kernel Linux manca di multitasking generale. Se un core della CPU viene bloccato ruotando su un blocco di rotazione del kernel, non può essere sottoposto a nuovo test, per fare qualcos'altro fino a quando il blocco non viene rilasciato. Si siede e gira fino a quando il blocco non viene rilasciato.
Gli spin lock possono trasformare efficacemente un monster box a 16 core in un box single core, se il carico di lavoro è tale che ogni core è sempre in attesa di un singolo lock spin. 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 raddoppiandolo da 16 a 32 probabilmente no, con la maggior parte dei carichi di lavoro.
Un blocco di rotazione è quando un processo esegue continuamente il polling per rimuovere un blocco. È considerato negativo perché il processo sta consumando cicli (di solito) inutilmente. Non è specifico di Linux, ma un modello di programmazione generale. E mentre è generalmente considerata una cattiva pratica, è, di fatto, la soluzione corretta; ci sono casi in cui il costo dell'utilizzo dello scheduler è più elevato (in termini di cicli della CPU) rispetto al costo dei pochi cicli che lo spinlock dovrebbe durare.
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 un blocco di rotazione. Per questo esempio particolare, esiste uno strumento Linux chiamato inotifywait (di solito non è installato di default). Se fosse scritto in C, useresti semplicemente l' API di inotify fornita da Linux.
Lo stesso esempio, l'utilizzo di inotifywait mostra come realizzare la stessa cosa senza un blocco spin:
#/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 bloccare, può provare e continuare, può provare quindi andare in modalità di sospensione dicendo al sistema operativo di riattivarlo quando si verifica un evento.
Ora un tentativo e continua utilizza molto meno tempo di un tentativo e blocco. Diciamo per il momento che un "prova e continua" richiederà un'unità di tempo e un "prova e blocco" richiederà un centinaio.
Ora supponiamo per il momento che in media un thread impiegherà 4 unità di tempo tenendo il blocco. È inutile aspettare 100 unità. Quindi invece scrivi un ciclo di "prova e continua". Al quarto tentativo di solito acquisirai il lucchetto. Questo è un blocco spin. Si chiama così perché il thread continua a girare in posizione fino a quando non ottiene il blocco.
Un'ulteriore misura di sicurezza consiste nel limitare il numero di volte in cui il ciclo viene eseguito. Quindi nell'esempio esegui un ciclo for per esempio sei volte, se fallisce, allora "provi e blocchi".
Se sai che un thread manterrà sempre il blocco per circa 200 unità, allora stai sprecando il tempo del computer per ogni tentativo e continua.
Quindi, alla fine, un blocco di rotazione può essere molto efficiente o dispendioso. È inutile quando il tempo "tipico" per tenere un lucchetto è maggiore del tempo necessario per "provare a bloccare". È efficiente quando il tempo tipico per tenere un lucchetto è molto più piccolo del tempo per "provare a bloccare".
Ps: Il libro da leggere sui thread è "A Thread Primer", se riesci ancora a trovarlo.
Un blocco è un modo per sincronizzare due o più attività (processi, thread). In particolare, quando entrambe le attività necessitano di un accesso intermittente a una risorsa che può essere utilizzata da una sola attività alla volta, è un modo per le attività di fare in modo di non utilizzare la risorsa contemporaneamente. Per accedere alla risorsa, un'attività deve eseguire i seguenti passaggi:
take the lock
use the resource
release the lock
Non è possibile eseguire un blocco se è già stato eseguito da un'altra attività. (Pensa al lucchetto come a un oggetto token fisico. O l'oggetto è in un cassetto o qualcuno lo ha in mano. Solo la persona che detiene l'oggetto può accedere alla risorsa.) Quindi "prendere il lucchetto" significa davvero "aspetta fino nessun altro ha il lucchetto, quindi prendilo ”.
Da un punto di vista di alto livello, ci sono due modi principali per implementare i blocchi: spinlock e condizioni. Con gli spinlock , prendere il lucchetto significa semplicemente "girare" (cioè non fare nulla in un ciclo) fino a quando nessun altro ha il lucchetto. Con le condizioni, se un'attività tenta di ottenere il blocco ma è bloccata perché un'altra attività lo tiene, il nuovo arrivato entra in una coda di attesa; l'operazione di rilascio segnala a qualsiasi attività in attesa che il blocco è ora disponibile.
(Queste spiegazioni non sono sufficienti per permetterti di implementare una serratura, perché non ho detto nulla sull'atomicità. Ma l'atomicità non è importante qui.)
Gli spinlock sono ovviamente dispendiosi: l'attività di attesa continua a verificare se il blocco è stato eseguito. Allora perché e quando viene utilizzato? Gli spinlock sono spesso molto economici da ottenere nel caso in cui il blocco non sia tenuto. Questo lo rende attraente quando la possibilità di tenere la serratura è piccola. Inoltre, gli spinlock sono fattibili solo se non si prevede che l'ottenimento del blocco richiederà molto tempo. Quindi gli spinlock tendono ad essere utilizzati in situazioni in cui rimarranno sospesi per un tempo molto breve, in modo che la maggior parte dei tentativi abbiano successo al primo tentativo e quelli che necessitano di un'attesa non attendono a lungo.
C'è una buona spiegazione degli spinlock e di altri meccanismi di concorrenza del kernel Linux in Linux Device Driver , capitolo 5.
synchronized
sarebbe stato implementato da uno spinlock: un synchronized
blocco potrebbe essere eseguito per un tempo molto lungo. synchronized
è un costrutto di linguaggio per rendere i blocchi facili da usare in alcuni casi, non una primitiva per costruire primitive di sincronizzazione più grandi.
Uno spinlock è un blocco che funziona disabilitando lo scheduler e eventualmente interrompe (variante irqsave) su quel particolare core su cui è acquisito il blocco. È diverso da un mutex in quanto disabilita la pianificazione in modo che solo il thread possa essere eseguito mentre si tiene premuto spinlock. Un mutex consente di programmare altri thread con priorità più elevata mentre viene tenuto, ma non consente loro di eseguire contemporaneamente la sezione protetta. Poiché gli spinlock disabilitano il multitasking non è possibile eseguire uno spinlock e quindi chiamare un altro codice che tenterà di acquisire un mutex. Il tuo codice all'interno della sezione spinlock non deve mai dormire (il codice in genere dorme quando incontra un mutex bloccato o un semaforo vuoto).
Un'altra differenza con un mutex è che i thread in genere fanno la coda per un mutex, quindi un mutex al di sotto ha una coda. Considerando che spinlock garantisce solo che nessun altro thread verrà eseguito anche se è necessario. Pertanto, non è mai necessario tenere uno spinlock quando si chiamano funzioni al di fuori del file che non si è sicuri che non verranno disattivate.
Quando vuoi condividere il tuo spinlock con un interrupt devi usare la variante irqsave. Questo non solo disabiliterà lo scheduler ma disabiliterà anche gli interrupt. Ha senso vero? Spinlock funziona assicurandosi che non funzionerà nient'altro. Se non desideri che venga eseguito un interrupt, lo disabiliti e procedi in modo sicuro nella sezione critica.
Su macchine multicore uno spinlock girerà effettivamente in attesa di un altro core che trattiene il blocco per rilasciarlo. Questa rotazione avviene solo su macchine multicore perché su quelle single core non può avvenire (o tieni premuto spinlock e procedi o non corri mai fino a quando il blocco non viene rilasciato).
Spinlock non è uno spreco dove ha senso. Per sezioni critiche molto piccole sarebbe inutile allocare una coda di attività mutex rispetto alla semplice sospensione dello scheduler per alcuni microsecondi necessari per completare l'importante lavoro. Se hai bisogno di dormire o tenere il blocco su un'operazione io (che potrebbe dormire), usa un mutex. Certamente non bloccare mai uno spinlock e quindi provare a rilasciarlo all'interno di un interrupt. Mentre questo funzionerà, sarà come la merda arduino di while (flagnotset); in tal caso usa un semaforo.
Prendi uno spinlock quando hai bisogno di una semplice esclusione reciproca per blocchi di transazioni di memoria. Prendi un mutex quando vuoi che più thread si fermino subito prima di un blocco mutex e quindi il thread con la priorità più alta da scegliere per continuare quando mutex diventa libero e quando blocchi e rilasci nello stesso thread. Prendi un semaforo quando intendi pubblicarlo in un thread o in un interrupt e prenderlo in un altro thread. Sono tre modi leggermente diversi per garantire l'esclusione reciproca e vengono utilizzati per scopi leggermente diversi.