Sincronizzazione e notifica nei driver di rete

Ogni volta che due thread di condivisione di esecuzione possono essere accessibili contemporaneamente, in un computer uniprocessore o in un computer multiprocessore simmetrico (SMP), devono essere sincronizzati. Ad esempio, in un computer uniprocessor, se una funzione driver accede a una risorsa condivisa e viene interrotta da un'altra funzione eseguita in un sistema IRQL superiore, ad esempio un ISR, la risorsa condivisa deve essere protetta per impedire condizioni di gara che lasciano la risorsa in uno stato indeterminato. In un computer SMP, due thread possono essere eseguiti simultaneamente in processori diversi e tentare di modificare gli stessi dati. Tali accessi devono essere sincronizzati.

NDIS fornisce blocchi di rotazione che è possibile usare per sincronizzare l'accesso alle risorse condivise tra thread eseguiti nello stesso irQL. Quando due thread che condividono una risorsa vengono eseguiti in irQLs diversi, NDIS fornisce un meccanismo per generare temporaneamente il codice IRQL del codice IRQL inferiore in modo che l'accesso alla risorsa condivisa possa essere serializzato.

Quando un thread dipende dall'occorrenza di un evento all'esterno del thread, il thread si basa sulla notifica. Ad esempio, un driver potrebbe dover ricevere una notifica quando un periodo di tempo è passato in modo che possa controllare il dispositivo. O un driver della scheda di interfaccia di rete (NIC) potrebbe dover eseguire un'operazione periodica, ad esempio il polling. I timer forniscono un meccanismo di questo tipo.

Gli eventi forniscono un meccanismo che due thread di esecuzione possono usare per sincronizzare le operazioni. Ad esempio, un driver miniport può testare l'interruzione in una scheda di interfaccia di rete scrivendo nel dispositivo. Il driver deve attendere un interruzione per notificare al driver che l'operazione ha avuto esito positivo. È possibile usare gli eventi per sincronizzare un'operazione tra il thread in attesa del completamento dell'interruzione e il thread che gestisce l'interruzione.

Le sottosezioni seguenti in questo argomento descrivono questi meccanismi NDIS.

Blocchi di rotazione

Un blocco spin fornisce un meccanismo di sincronizzazione per proteggere le risorse condivise dai thread in modalità kernel in esecuzione in IRQL > PASSIVE_LEVEL in un uniprocessor o in un computer multiprocessore. Un blocco di rotazione gestisce la sincronizzazione tra vari thread di esecuzione che vengono eseguiti simultaneamente in un computer SMP. Un thread acquisisce un blocco spin prima di accedere alle risorse protette. Il blocco di rotazione mantiene qualsiasi thread ma quello che tiene premuto il blocco di rotazione dall'uso della risorsa. In un computer SMP, un thread in attesa dei cicli di blocco spin tenta di acquisire il blocco di spin finché non viene rilasciato dal thread che contiene il blocco.

Un'altra caratteristica dei blocchi di rotazione è l'IRQL associato. Il tentativo di acquisizione di un blocco spin genera temporaneamente l'IRQL del thread di richiesta al irQL associato al blocco spin. Ciò impedisce a tutti i thread IRQL inferiori nello stesso processore di impedire il thread in esecuzione. I thread, nello stesso processore, in esecuzione in un irQL superiore possono precorrere il thread in esecuzione, ma questi thread non possono acquisire il blocco di spin perché ha un IRQL inferiore. Pertanto, dopo che un thread ha acquisito un blocco di spin, nessun altro thread può acquisire il blocco di spin finché non è stato rilasciato. Un driver di rete ben scritto riduce al minimo la quantità di tempo in cui viene mantenuto un blocco di rotazione.

Un uso tipico per un blocco di rotazione consiste nel proteggere una coda. Ad esempio, la funzione di invio del driver miniport, MiniportSendNetBufferLists, potrebbe passare pacchetti di coda da un driver di protocollo. Poiché altre funzioni driver usano anche questa coda, MiniportSendNetBufferLists devono proteggere la coda con un blocco di rotazione in modo che solo un thread alla volta possa modificare i collegamenti o il contenuto. MiniportSendNetBufferLists acquisisce il blocco spin, aggiunge il pacchetto alla coda e quindi rilascia il blocco di spin. L'uso di un blocco di rotazione garantisce che il thread che mantiene il blocco sia l'unico thread che modifica i collegamenti alla coda mentre il pacchetto viene aggiunto in modo sicuro alla coda. Quando il driver miniport rimuove i pacchetti dalla coda, tale accesso è protetto dallo stesso blocco di rotazione. Quando si eseguono istruzioni che modificano l'head della coda o uno dei campi di collegamento che costituiscono la coda, il driver deve proteggere la coda con un blocco di rotazione.

Un driver deve prestare attenzione a non sovraproteggere una coda. Ad esempio, il driver può eseguire alcune operazioni, ad esempio compilare un campo contenente la lunghezza, nel campo riservato driver di rete di un pacchetto prima di accodarlo al pacchetto. Il driver può eseguire questa operazione all'esterno dell'area di codice protetta dal blocco spin, ma deve eseguirla prima di accodare il pacchetto. Dopo che il pacchetto si trova nella coda e il thread in esecuzione rilascia il blocco di spin, il driver deve presupporre che altri thread possano dequeuere immediatamente il pacchetto.

Evitare problemi di blocco spin

Per evitare un deadlock possibile, un driver NDIS deve rilasciare tutti i blocchi di spin NDIS prima di chiamare una funzione NDIS diversa da una funzione NdisXxxSpinlock . Se un driver NDIS non è conforme a questo requisito, potrebbe verificarsi un deadlock come indicato di seguito:

  1. Thread 1, che contiene il blocco di spin NDIS A, chiama una funzione NdisXxx che tenta di acquisire il blocco spin NDIS B chiamando la funzione NdisAcquireSpinLock .

  2. Thread 2, che contiene il blocco di spin NDIS B, chiama una funzione NdisXxx che tenta di acquisire il blocco spin NDIS A chiamando la funzione NdisAcquireSpinLock .

  3. Thread 1 e thread 2, ognuno in attesa che l'altro rilasci il blocco di spin, diventi deadlock.

I sistemi operativi Microsoft Windows non limitano un driver di rete a mantenere contemporaneamente più di un blocco di rotazione. Tuttavia, se una sezione del driver tenta di acquisire il blocco spin A tenendo premuto il blocco spin B e un'altra sezione tenta di acquisire il blocco spin B mentre mantiene il blocco spin A, i risultati del deadlock. Se acquisisce più di un blocco spin, un driver deve evitare deadlock applicando un ordine di acquisizione. Vale a dire, se un driver impone l'acquisizione del blocco spin A prima del blocco spin B, la situazione descritta in precedenza non si verificherà.

L'acquisizione di un blocco di rotazione genera irQL in DISPATCH_LEVEL e archivia il vecchio IRQL nel blocco di rotazione. Il rilascio del blocco di spin imposta irQL sul valore archiviato nel blocco spin. Poiché NDIS a volte immette i driver in PASSIVE_LEVEL, i problemi possono verificarsi con la sequenza di codice seguente:

NdisAcquireSpinLock(A);
NdisAcquireSpinLock(B);
NdisReleaseSpinLock(A);
NdisReleaseSpinLock(B);

Un driver non deve accedere ai blocchi di spin in questa sequenza per i motivi seguenti:

  • Tra il rilascio del blocco spin A e il rilascio del blocco spin B, il codice viene eseguito in PASSIVE_LEVEL anziché DISPATCH_LEVEL ed è soggetto a interruzioni inappropriati.

  • Dopo aver rilasciato il blocco spin B, il codice viene eseguito in DISPATCH_LEVEL che potrebbe causare l'errore del chiamante in un momento molto successivo con un errore di arresto IRQL_NOT_LESS_OR_EQUAL.

L'uso dei blocchi di rotazione influisce sulle prestazioni e, in generale, un driver non deve usare molti blocchi di spin. In alcuni casi, le funzioni che in genere sono distinte (ad esempio, inviare e ricevere funzioni) hanno sovrapposizioni minori per cui è possibile usare due blocchi di spin. L'uso di più blocchi di rotazione potrebbe essere un compromesso utile per consentire alle due funzioni di operare in modo indipendente sui processori separati.

Timer

I timer vengono usati per eseguire il polling o il timeout delle operazioni. Un driver crea un timer e associa una funzione al timer. La funzione associata viene chiamata quando il periodo specificato nel timer scade. I timer possono essere un'unica operazione o periodica. Una volta impostato un timer periodico, continuerà a essere attivato alla scadenza di ogni periodo fino a quando non viene cancellato in modo esplicito. Ogni volta che viene attivato, è necessario reimpostare un timer uno shot.

I timer vengono creati e inizializzati chiamando NdisAllocateTimerObject e impostati chiamando NdisSetTimerObject. Se viene usato un timer nonperiodic, deve reimpostare chiamando NdisSetTimerObject. Un timer viene cancellato chiamando NdisCancelTimerObject.

Eventi

Gli eventi vengono usati per sincronizzare le operazioni tra due thread di esecuzione. Un evento viene allocato da un driver e inizializzato chiamando NdisInitializeEvent. Un thread in esecuzione in IRQL = PASSIVE_LEVEL chiama NdisWaitEvent per inserirlo in uno stato di attesa. Quando un thread driver attende un evento, specifica un tempo massimo di attesa e l'evento da attendere. L'attesa del thread viene soddisfatta quando viene chiamato NdisSetEvent che causa la segnalazione dell'evento o quando scade l'intervallo di attesa massimo specificato, che si verifica per primo.

In genere, l'evento viene impostato da un thread di collaborazione che chiama NdisSetEvent. Gli eventi non vengono firmati quando vengono creati e devono essere impostati per segnalare i thread in attesa. Gli eventi rimangono segnalati fino a quando non viene chiamato NdisResetEvent .

Supporto multiprocessore nei driver di rete