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

Sempre que dois threads de recursos de compartilhamento de execução que podem ser acessados ao mesmo tempo, em um computador uniprocessador ou em um computador SMP (multiprocessador simétrico), 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 executada em um IRQL mais alto, como um ISR, o recurso compartilhado deverá ser protegido para evitar condições de corrida que deixam o 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 gerar temporariamente o IRQL do código IRQL 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 marcar seu dispositivo. Ou um driver nic (cartão de interface de rede) pode ter que executar uma operação periódica, como sondagem. 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 miniporte pode testar a interrupção em uma NIC gravando no dispositivo. O driver deve aguardar uma interrupção para notificar o driver 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 recursos compartilhados por threads no modo kernel em execução no IRQL > PASSIVE_LEVEL 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 mantém qualquer thread, mas aquele que impede o bloqueio de rotação de usar o recurso. Em um computador SMP, um thread que está aguardando os loops de bloqueio de rotação tentando adquirir o bloqueio de rotação até que ele seja liberado pelo thread que mantém o bloqueio.

Outra característica dos bloqueios de rotação é o IRQL associado. A tentativa de aquisição de um bloqueio de rotação gera 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 preemptem o thread em execução. Os threads, no mesmo processador, em execução em um IRQL mais alto podem antecipar o thread em execução, mas esses threads não podem adquirir o bloqueio de rotação 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 devem proteger a fila com um bloqueio de rotação para que apenas um thread de cada 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 de fila enquanto o pacote é adicionado com segurança à fila. Quando o driver de miniporto tira os pacotes da fila, esse acesso é protegido pelo mesmo bloqueio de rotação. Ao executar instruções que modificam o cabeçalho 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 bloqueio de rotação, 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 remover 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 cumprir 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 B do NDIS chamando a função NdisAcquireSpinLock .

  2. O Thread 2, que contém o bloqueio de rotação B do NDIS, chama uma função NdisXxx que tenta adquirir o bloqueio de rotação do NDIS A chamando 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 de manter simultaneamente mais de um bloqueio de rotação. No entanto, se uma seção do driver tentar adquirir o bloqueio de rotação A enquanto mantém o bloqueio de rotação B, e outra seção tentar adquirir o bloqueio de rotação B enquanto mantém o bloqueio de rotação A, o deadlock resultará. Se ele adquirir mais de um bloqueio de rotação, um driver deverá evitar deadlock aplicando uma ordem de aquisição. Ou seja, se um driver impor 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 bloqueio de rotação eleva o IRQL para DISPATCH_LEVEL e armazena o IRQL antigo no bloqueio de rotação. 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 liberar o bloqueio de rotação A e liberar o bloqueio de rotação B, o código está em execução em PASSIVE_LEVEL em vez de DISPATCH_LEVEL e está sujeito a interrupções inadequadas.

  • Depois de liberar o bloqueio de rotação B, o código é executado em DISPATCH_LEVEL o que pode fazer com que o chamador falha muito mais tarde com um erro de parada de 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 geralmente são distintas (por exemplo, funções de envio e recebimento) têm sobreposições secundárias para as quais dois bloqueios de rotação podem ser usados. O uso de mais de um bloqueio de rotação pode ser uma compensação que vale a pena para permitir que as duas funções operem independentemente em processadores separados.

Temporizadores

Os temporizadores são usados para operações de sondagem ou de tempo limite. 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 de um tiro 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 limpo. Um temporizador de um tiro deve ser redefinido sempre que é acionado.

Os temporizadores são criados e inicializados chamando NdisAllocateTimerObject e definidos chamando NdisSetTimerObject. Se um temporizador não periódico for usado, ele deverá ser redefinido chamando NdisSetTimerObject. Um temporizador é limpo 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 em IRQL = PASSIVE_LEVEL chama NdisWaitEvent para se colocar em um estado de espera. Quando um thread de driver aguarda em um evento, ele 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 de tempo de espera máximo especificado expirar, o que ocorrer primeiro.

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

Suporte a vários processadores em drivers de rede