Partilhar 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 executada em um IRQL mais alto, como um ISR, o recurso compartilhado deverá ser protegido para evitar condições de corrida que deixem 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 elevar 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 verificar seu dispositivo. Ou um driver de placa de interface de rede (NIC) 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 miniporta pode testar a interrupção em uma NIC gravando no dispositivo. O condutor deve aguardar por uma interrupção que informe ao condutor que a operação foi bem-sucedida. Você pode usar eventos para sincronizar uma operação entre o thread que aguarda 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 NDIS.

Fechaduras Giratórias

Bloqueio de rotação fornece um mecanismo de sincronização para proteger recursos partilhados por threads em modo kernel em execução no IRQL > PASSIVE_LEVEL num computador uniprocessador ou multiprocessador. Um spinlock lida com a sincronização entre vários threads de execução que estão a executar simultaneamente num 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 contém o bloqueio de rotação, use o recurso. Em um computador SMP, um thread que está aguardando nos 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 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 antecipem o thread em execução. Threads, no mesmo processador, rodando em um IRQL mais alto podem antecipar o thread de execução, mas esses threads não podem adquirir o bloqueio de rotação porque ele tem um IRQL mais baixo. Portanto, depois que um thread adquiriu um bloqueio de rotação, nenhum outro thread pode adquirir o bloqueio de rotação até que ele tenha sido liberado. Um driver de rede bem escrito minimiza o 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 pacotes passados para ela por um driver de protocolo. Como outras funções de driver também utilizam esta fila, MiniportSendNetBufferLists deve proteger a fila com um bloqueio rotativo, de forma que apenas um thread de cada vez possa manipular as ligações ou o conteúdo. 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 conté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 miniporta retira os pacotes da fila, esse acesso é protegido pelo mesmo spinlock. 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 motorista 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 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 deve assumir que outros threads podem retirar o pacote da fila imediatamente.

Evitando problemas de bloqueio de rotação

Para evitar um possível impasse, um driver NDIS deve liberar todos os bloqueios de rotação 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 impasse pode ocorrer da seguinte maneira:

  1. A thread 1, que contém o cadeado giratório A do NDIS, chama uma função NdisXxx que tenta adquirir o cadeado giratório B do NDIS chamando a função NdisAcquireSpinLock.

  2. A thread 2, que possui o bloqueio de rotação B do NDIS, chama uma função NdisXxx que tenta adquirir o bloqueio de rotação A do NDIS chamando a função NdisAcquireSpinLock.

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

Os sistemas operacionais Microsoft Windows não restringem um driver de rede de segurar simultaneamente mais de um bloqueio de rotação. 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, isso resulta em deadlock. Se adquirir mais de um bloqueio de rotação, um motorista deve evitar o impasse aplicando uma ordem de aquisição. Ou seja, se um motorista forçar 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 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 liberar o bloqueio de rotação A e liberar o bloqueio de rotação B, o código é executado em PASSIVE_LEVEL em vez de DISPATCH_LEVEL e está sujeito a interrupções inadequadas.

  • Após libertar o bloqueio de spin B, o código está a ser executado no DISPATCH_LEVEL, o que pode levar a que o chamador posteriormente sofra uma falha com um erro de paragem 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 pequenas sobreposições 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 válida para permitir que as duas funções operem independentemente em processadores separados.

Temporizadores

Os temporizadores são usados para sondagem ou operações 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 disparo único ou periódicos. Uma vez que um temporizador periódico é definido, ele continuará a disparar na expiração de cada período até ser explicitamente limpo. Um temporizador de ocorrência única deve ser reposto cada vez que é ativado.

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 reconfigurado 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 em IRQL = PASSIVE_LEVEL chama NdisWaitEvent para se colocar em um estado de espera. Quando uma thread de driver espera por um evento, ela 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 cooperante que chama NdisSetEvent. Os eventos não são sinalizados quando são criados e devem ser ativados para sinalizar threads que estão à espera. Os eventos permanecem sinalizados até que NdisResetEvent seja chamado.

Suporte a multiprocessadores em drivers de rede