Che cos'è uno spinlock in Linux?


32

Mi piacerebbe conoscere gli spinlock di Linux in dettaglio; qualcuno potrebbe spiegarmeli?

Risposte:


34

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.


@Warren: un paio di dubbi. Vorrei sapere di più su questo Big Kernel Lock e le sue implicazioni. Capisco anche l'ultimo paragrafo "il raddoppio dei core della CPU da 2 a 4 probabilmente raddoppierà quasi la velocità di un box Linux, ma il doppio da 16 a 32 probabilmente non lo farà"
Sen

2
Ri: implicazioni di BKL: Pensavo di averlo chiarito sopra. Con un solo blocco nel kernel, ogni volta che due core tentano di fare qualcosa di protetto da BKL, un core viene bloccato mentre il primo finisce di usare la sua risorsa protetta. Più è preciso il blocco, minore è la possibilità che ciò accada, quindi maggiore è l'utilizzo del processore.
Warren Young,

2
Ri: raddoppio: voglio dire che c'è una legge di rendimenti decrescenti quando si aggiungono i core del processore a un computer. All'aumentare del numero di core, aumenta anche la possibilità che due o più di essi debbano accedere a una risorsa protetta da un determinato blocco. Aumentare la granularità dei blocchi riduce le possibilità di tali collisioni, ma c'è anche un sovraccarico nell'aggiungerne troppi. Lo si può facilmente vedere nei supercomputer, che oggigiorno hanno migliaia di processori: la maggior parte dei carichi di lavoro sono inefficienti su di essi perché non possono evitare inattività molti processori a causa della contesa di risorse condivise.
Warren Young,

1
Mentre questa è una spiegazione interessante (+1 per quello), non penso che sia efficace nel comunicare la differenza tra spinlock e altri tipi di blocchi.
Gilles 'SO- smetti di essere malvagio' il

2
Se si vuole conoscere la differenza tra un blocco spin e, diciamo, un semaforo, questa è una domanda diversa. Un'altra domanda positiva ma tangenziale è: che cos'è il design del kernel Linux che rende spin lock buone scelte invece di qualcosa di più comune nel codice userland, come un mutex. Questa risposta è molto diversa.
Warren Young,

11

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

Qual è il ruolo dello scheduler lì? O ha qualche ruolo?
Sen

1
Nel metodo spin lock, lo scheduler riprende il processo ogni ~ 1 secondo per fare il suo compito (che è semplicemente controllare l'esistenza di un file). Nell'esempio di inotifywait, lo scheduler riprende il processo solo quando termina il processo figlio (inotifywait). Anche Inotifywait sta dormendo; lo scheduler lo riprende solo quando si verifica l'evento inotify.
Shawn J. Goff,

Quindi, come viene gestito questo scenario in un sistema con processore single core?
Sen

@Sen: questo è spiegato piuttosto bene in Driver di dispositivo Linux .
Gilles 'SO- smetti di essere malvagio' il

1
Questo script bash è un cattivo esempio di spinlock. Stai mettendo in pausa il processo facendolo dormire. Uno spinlock non dorme mai. Su una macchina single core sospende semplicemente lo scheduler e continua (senza attendere occupato)
Martin

7

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.


il filo continua a girare in posizione finché non si blocca . Potresti per favore dirmi che cosa sta girando? È come se si trattasse di wait_queue e venisse eseguito dallo Scheduler? Forse sto cercando di passare al livello basso, ma non riesco ancora a sostenere i miei dubbi.
Sen

In pratica, gira tra le 3-4 istruzioni del loop.
Paul Stelian,

5

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.


Quale sarebbe un buon modo per implementare altre primitive di sincronizzazione? Prendi uno spinlock, controlla se qualcun altro ha implementato la primitiva effettiva, quindi usa lo scheduler o concedi l'accesso? Potremmo considerare il blocco synchronized () in Java una forma di spinlock, e nelle primitive correttamente implementate basta usare uno spinlock?
Paul Stelian,

@PaulStelian È infatti comune implementare blocchi "lenti" in questo modo. Non conosco abbastanza Java per rispondere a quella parte, ma dubito che synchronizedsarebbe stato implementato da uno spinlock: un synchronizedblocco 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.
Gilles 'SO- smetti di essere malvagio' il

3

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.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.