Compartilhar via


Sincronização e notificação em drivers de rede

Sempre que dois threads de execução compartilham recursos que podem ser acessados ao mesmo tempo, seja em um computador uniprocessador ou em um computador multiprocessador simétrico (SMP), eles precisam ser sincronizados. Por exemplo, em um computador uniprocessador, se uma função de driver estiver acessando um recurso compartilhado e for interrompida por outra função que é executada em um IRQL superior, como um ISR, o recurso compartilhado deverá ser protegido para evitar condições de concorrência que resultam no recurso em um estado indeterminado. Em um computador SMP, dois threads podem estar sendo executados simultaneamente em processadores diferentes e tentando modificar os mesmos dados. Esses acessos devem ser sincronizados.

O NDIS fornece bloqueios de rotação que você pode usar para sincronizar o acesso a recursos compartilhados entre threads executados no mesmo IRQL. Quando dois threads que compartilham um recurso são executados em IRQLs diferentes, o NDIS fornece um mecanismo para elevar temporariamente o IRQL do código de prioridade inferior para que o acesso ao recurso compartilhado possa ser serializado.

Quando um thread depende da ocorrência de um evento fora do thread, o thread depende da notificação. Por exemplo, um driver pode precisar ser notificado quando algum período de tempo tiver passado para que ele possa verificar seu dispositivo. Ou um driver NIC (placa de interface de rede) pode ter que executar uma operação periódica, como polling. Os temporizadores fornecem esse mecanismo.

Os eventos fornecem um mecanismo que dois threads de execução podem usar para sincronizar operações. Por exemplo, um driver de miniport pode testar a interrupção em uma placa de rede (NIC) escrevendo no dispositivo. O driver deve aguardar uma interrupção para ser notificado de que a operação foi bem-sucedida. Você pode usar eventos para sincronizar uma operação entre o thread aguardando a conclusão da interrupção e o thread que manipula a interrupção.

As subseções a seguir neste tópico descrevem esses mecanismos de NDIS.

Bloqueios de rotação

Um bloqueio de rotação fornece um mecanismo de sincronização para proteger os recursos compartilhados por threads em modo kernel em execução no IRQL > PASSIVE_LEVEL, seja em um computador uniprocessador ou multiprocessador. Um bloqueio de rotação manipula a sincronização entre vários threads de execução que são executados simultaneamente em um computador SMP. Um thread adquire um bloqueio de rotação antes de acessar recursos protegidos. O bloqueio de rotação impede que qualquer thread, exceto aquele que está segurando o bloqueio, use o recurso. Em um computador SMP, um thread que está aguardando pelo bloqueio de rotação faz loops tentando adquirir o bloqueio até que ele seja liberado pela thread que o mantém.

Outra característica dos spin locks é o IRQL associado. A tentativa de aquisição de um bloqueio de rotação eleva temporariamente o IRQL do thread solicitante para o IRQL associado ao bloqueio de rotação. Isso impede que todos os threads IRQL inferiores no mesmo processador preempram o thread em execução. As threads, no mesmo processador, em execução em um IRQL mais alto podem preemptar o thread em execução, mas essas threads não podem adquirir o spin lock porque ele tem um IRQL inferior. Portanto, depois que um thread tiver adquirido um bloqueio de rotação, nenhum outro thread poderá adquirir o bloqueio de rotação até que ele seja liberado. Um driver de rede bem escrito minimiza a quantidade de tempo em que um bloqueio de rotação é mantido.

Um uso típico para um bloqueio de rotação é proteger uma fila. Por exemplo, a função de envio do driver de miniport MiniportSendNetBufferLists, pode enfileirar pacotes passados para ele por um driver de protocolo. Como outras funções de driver também usam essa fila, MiniportSendNetBufferLists deve proteger a fila com um bloqueio de rotação para que apenas um thread por vez possa manipular os links ou o conteúdo. MiniportSendNetBufferLists adquire o bloqueio de rotação, adiciona o pacote à fila e libera o bloqueio de rotação. O uso de um bloqueio de rotação garante que o thread que mantém o bloqueio seja o único thread que modifica os links da fila enquanto o pacote é adicionado com segurança à fila. Quando o driver de miniporto tira os pacotes da fila, esse acesso é protegido pelo mesmo spinlock. Ao executar instruções que modifiquem a cabeça da fila ou qualquer um dos campos de link que compõem a fila, o driver deve proteger a fila com um bloqueio de rotação.

Um driver deve tomar cuidado para não proteger demais uma fila. Por exemplo, o driver pode executar algumas operações (por exemplo, preenchendo um campo que contém o comprimento) no campo reservado ao driver de rede de um pacote antes de enfileirar o pacote. O driver pode fazer isso fora da região de código protegida pelo spin lock, mas deve fazê-lo antes de enfileirar o pacote. Depois que o pacote estiver na fila e o thread em execução liberar o bloqueio de rotação, o driver deverá assumir que outros threads podem desempor o pacote imediatamente.

Evitando problemas de bloqueio de rotação

Para evitar um possível deadlock, um driver NDIS deve liberar todos os bloqueios de rotação do NDIS antes de chamar uma função NDIS diferente de uma função NdisXxxSpinlock . Se um driver NDIS não estiver em conformidade com esse requisito, um deadlock poderá ocorrer da seguinte maneira:

  1. O Thread 1, que contém o bloqueio de rotação A do NDIS, chama uma função NdisXxx que tenta adquirir o bloqueio de rotação do NDIS B chamando a função NdisAcquireSpinLock .

  2. Thread 2, que possui o bloqueio de rotação do NDIS B, chama uma função NdisXxx que tenta adquirir o bloqueio de rotação do NDIS A fazendo uma chamada para a função NdisAcquireSpinLock.

  3. O thread 1 e o thread 2, que estão cada um esperando que o outro libere o bloqueio de rotação, ficam em deadlock.

Os sistemas operacionais Microsoft Windows não restringem um driver de rede a manter simultaneamente mais de um spinlock. No entanto, se uma seção do driver tentar adquirir o spin lock A enquanto mantém o spin lock B e outra seção tentar adquirir o spin lock B enquanto mantém o spin lock A, resultará em deadlock. Caso adquira mais de um spin lock, um driver deverá evitar um deadlock aplicando uma ordem de aquisição. Ou seja, se um driver impuser a aquisição do bloqueio de rotação A antes do bloqueio de rotação B, a situação descrita acima não ocorrerá.

A aquisição de um spin lock eleva o IRQL para DISPATCH_LEVEL e armazena o IRQL antigo no spin lock. A liberação do bloqueio de rotação define o IRQL como o valor armazenado no bloqueio de rotação. Como o NDIS às vezes insere drivers em PASSIVE_LEVEL, podem surgir problemas com a seguinte sequência de código:

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

Um driver não deve acessar bloqueios de rotação nesta sequência pelos seguintes motivos:

  • Entre a liberação do bloqueio de rotação A e a liberação do bloqueio de rotação B, o código é executado em PASSIVE_LEVEL em vez de DISPATCH_LEVEL e está sujeito a interrupção inadequada.

  • Depois de liberar o bloqueio de rotação B, o código é executado em DISPATCH_LEVEL o que pode fazer com que o chamador falque muito mais tarde com um erro de parada IRQL_NOT_LESS_OR_EQUAL.

O uso de bloqueios de rotação afeta o desempenho e, em geral, um driver não deve usar muitos bloqueios de rotação. Ocasionalmente, funções que são normalmente distintas (como as funções de envio e recebimento) apresentam pequenas sobreposições, para as quais podem ser utilizados dois spin-locks. O uso de mais de um bloqueio de rotação pode ser uma compensação valiosa para permitir que as duas funções operem independentemente em processadores separados.

Temporizadores

Os temporizadores são usados para operações de sondagem ou para encerrar operações após um tempo determinado. Um driver cria um temporizador e associa uma função ao temporizador. A função associada é chamada quando o período especificado no temporizador expira. Os temporizadores podem ser únicos ou periódicos. Depois que um temporizador periódico for definido, ele continuará a ser acionado no vencimento de cada período até que seja explicitamente desativado. Um temporizador de disparo único deve ser redefinido sempre que for acionado.

Os temporizadores são criados e inicializados chamando NdisAllocateTimerObject e definidos chamando NdisSetTimerObject. Se um temporizador nãoperiódico for usado, ele deverá ser redefinido chamando NdisSetTimerObject. Um temporizador é cancelado chamando NdisCancelTimerObject.

Eventos

Os eventos são usados para sincronizar operações entre dois threads de execução. Um evento é alocado por um driver e inicializado chamando NdisInitializeEvent. Um thread em execução no IRQL = PASSIVE_LEVEL chama NdisWaitEvent para se colocar em um estado de espera. Quando um thread de driver aguarda por um evento, especifica um tempo máximo de espera, bem como o evento a ser aguardado. A espera do thread é atendida quando NdisSetEvent é chamado fazendo com que o evento seja sinalizado ou quando o intervalo máximo de tempo de espera especificado expirar, o que ocorrer primeiro.

Normalmente, o evento é definido por um thread de cooperação que chama NdisSetEvent. Os eventos não estão sinalizados quando são criados e devem ser configurados para sinalizar threads de espera. Os eventos permanecem sinalizados até que NdisResetEvent seja chamado.

Suporte a vários processadores em drivers de rede