Come viene implementato conditional_wait() a livello di kernel e hardware/assembly?

Come viene implementato conditional_wait() a livello di kernel e hardware/assembly?


Comprendo che il thread che attende una variabile condizionale, rilascia atomicamente il blocco e va a dormire fino a quando non viene riattivato da un segnale condizionale da un altro thread (quando viene soddisfatta una condizione particolare). Dopo essersi riattivato, riacquisisce atomicamente il blocco (in qualche modo magicamente) e si aggiorna come richiesto e sblocca la sezione critica.


Sarebbe bello se qualcuno potesse spiegare come questa procedura conditional_wait() sia implementata a livello di kernel e hardware/assembly?


In che modo il blocco viene rilasciato e riacquisito atomicamente? In che modo il kernel lo garantisce?


Cosa significa davvero dormire qui? Significa un passaggio di contesto a un altro processo/thread?


Durante il sonno del thread, in che modo questo thread si risveglia segnalando implementato a livello di kernel e se viene fornito un supporto specifico per l'hardware per questi meccanismi?


Modifica:


Sembra che "futex" sia il ragazzo che gestisce questa roba di attesa/segnale. Per restringere la mia domanda:
In che modo la chiamata di sistema futex per l'attesa e la notifica delle variabili di condizione viene implementata/funziona a basso livello?


Risposte:


Ad alto livello (e dal momento che stai ponendo questa domanda, alto livello è ciò di cui hai bisogno) non è così complicato. Innanzitutto, è necessario conoscere i livelli di responsabilità. Ci sono fondamentalmente 3 livelli:



  • Livello hardware - di solito qualcosa che può essere codificato in una singola istruzione ASM

  • Livello kernel - qualcosa che fa il kernel del sistema operativo

  • Livello dell'applicazione - qualcosa che fa l'applicazione


In genere, queste responsabilità non si sovrappongono:il kernel non può fare ciò che solo l'hardware può fare, l'hardware non può fare ciò che solo il kernel può fare. Tenendo questo in mente, è utile ricordare che quando si tratta di bloccare, l'hardware ne sa molto poco. Praticamente si riduce a



  • aritmetica atomica:l'hardware può bloccare una particolare regione di memoria (assicurarsi che nessun altro thread vi acceda), eseguire operazioni aritmetiche su di essa e sbloccare la regione. Questo può funzionare solo sull'aritmetica supportata nativamente dal chip (nessuna radice quadrata!) e sulle dimensioni supportate nativamente dall'hardware

  • Barriere o barriere di memoria, ovvero introducono una barriera all'interno di un flusso di istruzioni, in modo che quando la CPU riordina le istruzioni o utilizza le cache di memoria, non attraversi tali barriere e la cache sarà fresca

  • Impostazione condizionale (confronta e imposta) - imposta la regione di memoria sul valore A se è B e segnala lo stato di questa operazione (è stata impostata o meno)


Questo è praticamente tutto ciò che la CPU può fare. Come vedi, qui non ci sono variabili futex, mutex o condizionali. Questa roba è fatta dal kernel che ha a disposizione operazioni supportate dalla CPU.


Diamo un'occhiata a un livello molto alto come il kernel potrebbe implementare la chiamata futex. In realtà, futex è leggermente complicato, perché è un misto di chiamate a livello di utente e chiamate a livello di kernel, se necessario. Esaminiamo il mutex "puro", implementato esclusivamente nello spazio del kernel. Ad alto livello, sarà abbastanza dimostrativo.


Quando mutex viene inizialmente creato, il kernel associa ad esso una regione di memoria. Questa regione conterrà un valore di mutex bloccato o sbloccato. Successivamente, al kernel viene chiesto di bloccare il mutex, prima indica alla CPU di emettere una barriera di memoria. Un mutex deve fungere da barriera, in modo che tutto ciò che viene letto/scritto dopo l'acquisizione (o il rilascio) del mutex sia visibile al resto delle CPU. Quindi, utilizza l'istruzione di confronto e impostazione supportata dalla CPU per impostare il valore della regione di memoria su 1 se era impostato su 0. (ci sono mutex rientranti più complicati, ma non complichiamo l'immagine con essi). È garantito dalla CPU che anche se più di un thread tenta di farlo contemporaneamente, solo uno riuscirà. Se l'operazione riesce, ora 'manteniamo il mutex'. Una volta che al kernel viene chiesto di rilasciare il mutex, la regione di memoria viene impostata su 0 (non è necessario farlo in modo condizionale, poiché sappiamo di tenere il mutex!) e viene emessa un'altra barriera di memoria. Il kernel aggiorna anche lo stato del mutex nelle sue tabelle - vedi sotto.


Se il blocco del mutex fallisce, il kernel aggiunge il thread alle sue tabelle che elencano i thread in attesa di rilascio di un particolare mutex. Quando il mutex viene rilasciato, il kernel controlla quali thread sono in attesa su questo mutex e 'pianifica' (cioè prepara per l'esecuzione) uno di quelli (nel caso ce ne sia più di uno, quale sarà programmato o risvegliato dipende da moltitudine di fattori, nel caso più semplice è semplicemente casuale). Il thread pianificato inizia l'esecuzione, blocca nuovamente il mutex (a questo punto può fallire di nuovo!) e il ciclo della vita continua.


Spero che abbia almeno un senso parziale :)