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, 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 que é executada em um IRQL mais alto, como um ISR, o recurso compartilhado deve ser protegido para evitar condições de corrida que deixam o recurso em um estado indeterminado. Em um computador SMP, dois threads podiam ser executados simultaneamente em processadores diferentes e tentando modificar os mesmos dados. Esses acessos devem ser sincronizados.

A NDIS fornece bloqueios de rotação que você pode usar para sincronizar o acesso a recursos compartilhados entre threads que são executados no mesmo IRQL. Quando dois threads que compartilham um recurso são executados em diferentes IRQLs, a 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 um período de tempo tiver passado para que possa verificar seu dispositivo. Ou um driver de NIC (placa de interface de rede) pode ter que executar uma operação periódica, como sondagem. Os temporizadores fornecem tal mecanismo.

Os eventos fornecem um mecanismo que dois threads de execução podem usar para sincronizar operações. Por exemplo, um driver de miniporta 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 os recursos compartilhados por threads do modo kernel em execução no IRQL > PASSIVE_LEVEL em um computador com um ou dois processadores. 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 os recursos protegidos. O bloqueio de rotação mantém qualquer thread, mas aquele que mantém o bloqueio de rotação usando o recurso. Em um computador SMP, um thread que está aguardando os loops de bloqueio de rotação tenta adquirir o bloqueio de rotação até ser 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 temporariamente gera 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 preempçãom do thread em execução. Threads, no mesmo processador, em execução em um IRQL mais alto, podem preempção do thread em execução, mas esses threads não podem adquirir o bloqueio de rotação porque ele tem um IRQL menor. 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 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 miniporta, MiniportSendNetBufferLists, pode enfileirar os pacotes passados para ele por um driver de protocolo. Como outras funções de driver também usam essa fila, o 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. O MiniportSendNetBufferLists adquire o bloqueio de rotação, adiciona o pacote à fila e, em seguida, 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 miniporta leva os pacotes para fora 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 Superproteger uma fila. Por exemplo, o driver pode executar algumas operações (por exemplo, preencher um campo que contém o comprimento) no campo reservado de 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á pressupor que outros threads possam remover a fila do 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 NDIS antes de chamar uma função NDIS diferente de uma função de SpinLock do NDISxxx . Se um driver NDIS não atender a esse requisito, um deadlock poderá ocorrer da seguinte maneira:

  1. O thread 1, que mantém o bloqueio de rotação de NDIS A, chama uma função NDISxxx que tenta adquirir o bloqueio de rotação NDIS B chamando a função NdisAcquireSpinLock .

  2. O thread 2, que mantém o bloqueio de rotação NDIS B, chama uma função NDISxxx que tenta adquirir um bloqueio de rotação NDIS a chamando a função NdisAcquireSpinLock .

  3. Thread 1 e thread 2, que estão esperando pelo outro para liberar seu bloqueio de rotação, se tornaram deadlock.

os sistemas operacionais Microsoft Windows não impedem que um driver de rede mantenha simultaneamente mais de um bloqueio de rotação. No entanto, se uma seção do driver tentar adquirir um bloqueio de rotação enquanto mantém o cadeado 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, os resultados do deadlock. Se ele adquirir mais de um bloqueio de rotação, um driver deverá evitar o deadlock impondo uma ordem de aquisição. Ou seja, se um driver impõe a aquisição de 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 gera o IRQL para DISPATCH_LEVEL e armazena o IRQL antigo no bloqueio de rotação. Liberar o bloqueio de rotação define o IRQL para 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 está sendo executado em PASSIVE_LEVEL em vez de DISPATCH_LEVEL e está sujeito a uma interrupção inadequada.

  • Depois de liberar o bloqueio de rotação B, o código está sendo executado em DISPATCH_LEVEL, o que pode causar a falha do chamador em um momento mais tarde com um erro de interrupção 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, as funções geralmente 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 vale a pena para permitir que as duas funções operem independentemente em processadores separados.

Temporizadores

Os timers 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 timers podem ser One-shot ou periódicos. Depois que um temporizador periódico for definido, ele continuará a ser acionado na expiração de cada período até que seja explicitamente limpo. Um temporizador One-Shot deve ser redefinido cada vez que é acionado.

Os temporizadores são criados e inicializados chamando NdisAllocateTimerObject e definidos chamando NdisSetTimerObject. Se um temporizador não-period 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 um evento, ele especifica um tempo máximo de espera, bem como o evento a ser aguardado. A espera do thread é satisfeita quando NdisSetEvent é chamado, fazendo com que o evento seja sinalizado ou quando o intervalo de tempo de espera máximo especificado expira, o que ocorrer primeiro.

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

Suporte a multiprocessadores em drivers de rede