ネットワーク ドライバーの同期と通知

ユニプロセッサ コンピューターでも対称型マルチプロセッサ (SMP) コンピューターでも、2 つの実行スレッドが同時にアクセスできるリソースを共有する場合は、常に同期する必要があります。 たとえば、ユニプロセッサ コンピューターでは、1 つのドライバー関数が共有リソースにアクセスしているときに、ISR などの高い IRQL で実行される別の関数によって中断された場合、リソースが不確定な状態になる競合状態を防ぐために、共有リソースを保護する必要があります。 SMP コンピューターでは、2 つのスレッドが異なるプロセッサー上で同時に実行され、同じデータを変更しようとしている可能性があります。 このようなアクセスは同期する必要があります。

NDIS には、同じ IRQL で実行されるスレッド間で共有リソースへのアクセスを同期するために使用できるスピン ロックが用意されています。 リソースを共有する 2 つのスレッドが異なる IRQL で実行される場合、NDIS には、共有リソースへのアクセスをシリアル化できるように、低い IRQL コードの IRQL を一時的に上げるメカニズムが用意されています。

スレッドがスレッド外のイベントの発生に依存している場合、スレッドは通知に依存します。 たとえば、ドライバーがデバイスをチェックできるように、ある期間が経過したときに通知する必要がある場合があります。 あるいは、ネットワーク インターフェイス カード (NIC) ドライバーが、ポーリングなどの定期的な操作を実行する必要があるかもしれません。 タイマーはそのような場合に必要なメカニズムを提供します。

イベントは、2 つの実行スレッドが操作を同期するために使用できるメカニズムを提供します。 たとえば、ミニポート ドライバーは、デバイスに書き込むことで NIC の割り込みをテストできます。 ドライバーは、操作が成功したことをドライバーに通知する割り込みを待機する必要があります。 イベントを使用して、割り込みの完了を待つスレッドと割り込みを処理するスレッドの間で操作を同期できます。

このトピックの次のサブセクションでは、これらの NDIS メカニズムについて説明します。

スピン ロック

スピン ロックは、IRQL> PASSIVE_LEVEL で実行されるカーネル モード スレッドによって共有されるリソースを、ユニプロセッサ コンピューターまたはマルチプロセッサ コンピューターで保護するための同期メカニズムを提供します。 スピン ロックは、SMP コンピューターで同時に実行されるさまざまな実行スレッド間の同期を処理します。 スレッドは、保護されたリソースにアクセスする前にスピン ロックを取得します。 スピン ロックは、スピン ロックを保持しているスレッド以外のスレッドがリソースを使用しないようにします。 SMP コンピューターでは、スピン ロックを待機しているスレッドは、スピンロックを保持しているスレッドがスピンロックを解放するまでループして取得を試みます。

スピン ロックのもう 1 つの特徴は、関連する IRQL です。 スピン ロックの取得を試みると、要求スレッドの IRQL がスピン ロックに関連付けられた IRQL に一時的に上げられます。 これにより、同じプロセッサ上のすべての下位 IRQL スレッドが実行中のスレッドを先取りすることを防ぐことができます。 同じプロセッサ上で、より高い IRQL で実行されているスレッドは実行中のスレッドを先取りできますが、実行中のスレッドの IRQL は低いため、スピン ロックを取得できません。 したがって、あるスレッドがスピン ロックを取得してから解放するまで、他のスレッドはスピン ロックを取得できません。 適切に記述されたネットワーク ドライバーは、スピン ロックを保持する時間を最小限に抑えます。

スピン ロックの一般的な用途は、キューを保護することです。 たとえば、ミニポート ドライバーの送信関数 MiniportSendNetBufferLists は、プロトコル ドライバーによって渡されたパケットをキューに入れます。 他のドライバー関数もこのキューを使用するため、MiniportSendNetBufferLists は、一度に 1 つのスレッドのみがリンクまたはコンテンツを操作できるように、スピン ロックでキューを保護する必要があります。 MiniportSendNetBufferLists はスピン ロックを取得し、キューにパケットを追加した後でスピン ロックを解放します。 スピン ロックを使用することで、ロックを保持しているスレッドがキュー リンクを変更する唯一のスレッドであることが保証され、パケットをキューに安全に追加できます。 ミニポート ドライバーがキューからパケットを取り出す際のアクセスも同じスピン ロックによって保護されます。 キューの先頭またはキューを構成するリンク フィールドのいずれかを変更する命令を実行する場合、ドライバーはスピン ロックでキューを保護する必要があります。

ドライバーは、キューを過剰に保護しないように注意する必要があります。 たとえば、ドライバーは、パケットをキューに入れる前に、パケットのネットワーク ドライバー予約フィールドで一部の操作 (長さを含むフィールドへの入力など) を実行できます。 ドライバーは、スピン ロックで保護されたコード領域の外部でこれを行うことができますが、これはパケットをキューに入れる前に行う必要があります。 ドライバーは、パケットがキューにある状態で実行中のスレッドがスピン ロックを解放すると、他のスレッドがすぐにパケットをデキューできると想定しておく必要があります。

スピン ロックの問題を回避する

デッドロックの可能性を回避するために、NDIS ドライバーは、 NdisXxxSpinlock 関数以外の NDIS 関数を呼び出す前にすべての NDIS スピン ロックを解放する必要があります。 NDIS ドライバーがこの要件に準拠していない場合、次のようにデッドロックが発生する可能性があります。

  1. NDIS スピン ロック A を保持するスレッド 1 は、 NdisXxx 関数を呼び出します。この関数はNdisAcquireSpinLock を呼び出して、NDIS spin lock B を取得しようとします。

  2. NDIS スピン ロック B を保持するスレッド 2 は、NdisXxx 関数を呼び出します。この関数は NdisAcquireSpinLock を呼び出して、NDIS spin lock A を取得しようとします。

  3. スレッド 1 とスレッド 2 は、それぞれがもう一方のスピン ロックが解放されるのを待つため、デッドロックになります。

Microsoft Windows オペレーティング システムは、ネットワーク ドライバーが同時に複数のスピン ロックを保持することを制限していません。 しかし、ドライバーの 1 つのセクションがスピン ロック B を保持している状態でスピン ロック A を取得しようとし、別のセクションがスピン ロック A を保持している状態でスピン ロック B を取得しようとすると、デッドロックが発生します。 複数のスピン ロックを取得する場合、ドライバーは、取得の順序を強制することによってデッドロックを回避する必要があります。 つまり、ドライバーがスピン ロック B の前にスピン ロック A の取得を強制すれば、上記の状況は発生しません。

スピン ロックを取得すると、IRQL が DISPATCH_LEVEL に上がり、古い IRQL はスピン ロックに格納されます。 スピン ロックを解放すると、IRQL はスピン ロックに格納されている値に設定されます。 NDIS は、PASSIVE_LEVEL でドライバーに入る場合があるため、次のコード シーケンスで問題が発生する可能性があります。

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

次の理由により、ドライバーは、このシーケンスのスピン ロックにアクセスすべきではありません。

  • スピン ロック A を解放してからスピン ロック B を解放するまでの間、コードは DISPATCH_LEVEL ではなく PASSIVE_LEVEL で実行され、不適切な割り込みの対象となります。

  • スピン ロック B を解放した後、コードは DISPATCH_LEVEL で実行されているため、かなり後で呼び出し元が IRQL_NOT_LESS_OR_EQUAL ストップエラーで失敗する可能性があります。

スピン ロックの使用はパフォーマンスに影響を及ぼすため、一般的には、ドライバーはスピン ロックを多用すべきではありません。 場合により、通常は異なる関数 (送信関数と受信関数など) に、2 つのスピン ロックを使用できる小さな重複がある場合があります。 複数のスピン ロックを使用することは、2 つの関数が異なるプロセッサで独立して動作することを可能にするため、価値のあるトレードオフの可能性があります。

タイマー

タイマーは、ポーリングまたはタイムアウト操作に使用されます。 ドライバーはタイマーを作成し、タイマーに関数を関連付けます。 タイマーで指定された期間が経過すると、関連付けられた関数が呼び出されます。 タイマーには、1 回のみのものと周期的なものがあります。 一度設定された周期的なタイマーは、明示的にクリアされるまで、各周期に到達するたびに起動し続けます。 1 回限りのタイマーは、起動するたびにリセットする必要があります。

タイマーの作成と初期化は NdisAllocateTimerObject を呼び出して行い、設定は NdisSetTimerObject を呼び出して行います。 非周期タイマーを使用する場合は、NdisSetTimerObject を呼び出してリセットする必要があります。 タイマーは、NdisCancelTimerObject を呼び出 すことでクリアされます。

Events

イベントは、2 つの実行スレッド間で操作を同期するために使用します。 イベントはドライバーによって割り当てられ、NdisInitializeEvent を呼び出すことで初期化されます。 IRQL = PASSIVE_LEVEL で実行されているスレッドが NdisWaitEvent を呼び出すと待機状態になります。 ドライバー スレッドがイベントを待機する場合は、待機するイベントと同様に、待機する最大時間を指定します。 スレッドの待機が満たされるのは 、NdisSetEvent が呼び出されてイベントが通知されたとき、または指定された最大待機時間が経過した場合のいずれか早い方です。

通常、イベントは NdisSetEvent を呼び出す協調的なスレッドによって設定されます。 作成時点のイベントは通知を行わないため、待機中のスレッドに通知するための設定が必要です。 イベントは、NdisResetEvent が呼び出されるまで通知されたままになります。

ネットワーク ドライバーのマルチプロセッサ サポート