NDIS 6.80 の同期 OID 要求インターフェイス

Windows ネットワーク ドライバーは、OID 要求を使用して NDIS バインド スタックに制御メッセージを送信します。 TCPIP や vSwitch などのプロトコル ドライバーは、基になる NIC ドライバーの各機能を構成するために数十もの OID に依存しています。 Windows 10 バージョン 1709 以前では、OID 要求は標準と直接の 2 つの方法で送信されていました。

このトピックでは、OID 呼び出しの 3 番目の方法である「同期」について説明します。 同期呼び出しは、待ち時間が短く、非ブロッキングで、スケーラブルで信頼性が高い方法です。 同期 OID 要求インターフェイスは、Windows 10 バージョン 1709 以降に含まれる NDIS 6.80 以降で使用できます。

標準OID 要求と直接 OID 要求との比較

同期 OID 要求では、呼び出しのペイロード (OID 自体) は、通常の OID 要求と直接 OID 要求とまったく同じです。 唯一の違いは、呼び出し自体にあります。 したがって、3 種類の OID すべての役割は同じであり、方法だけが異なります。

次の表では、標準 OID、直接 OID、同期 OID の違いについて説明します。

Attribute 標準 OID 直接 OID 同期 OID
Payload NDIS_OID_REQUEST NDIS_OID_REQUEST NDIS_OID_REQUEST
OID の種類 Stats、Query、Set、Method Stats、Query、Set、Method Stats、Query、Set、Method
発行 プロトコル、フィルター プロトコル、フィルター プロトコル、フィルター
完了 ミニポート、フィルター ミニポート、フィルター ミニポート、フィルター
フィルターは変更可能 はい はい はい
メモリを割り当て可能 各フィルター (OID クローン) 各フィルター (OID クローン) フィルターの数が非常に多い場合のみ (コンテキストを呼び出す)
Pend 可能 はい はい いいえ
ブロック可能 はい いいえ いいえ
IRQL == PASSIVE <= DISPATCH <= DISPATCH
NDIS でシリアル化 はい いいえ いいえ
フィルターが呼び出される Recursively Recursively Iteratively
フィルターは OID を複製する はい はい いいえ

フィルター処理

他の 2 種類の OID 呼び出しと同様に、フィルター ドライバーは同期呼び出しの OID 要求を完全に制御できます。 フィルター ドライバーは、同期 OID を観察、傍受、変更、発行できます。 ただし、同期 OID は効率を高めるために、その仕組みが多少異なります。

パススルー、インターセプト、配信元

概念的には、すべての OID 要求は上位ドライバーから発行され、下位ドライバーによって完了されます。 その過程で、OID 要求はいくつものフィルター ドライバーを通過する可能性があります。

最も一般的なケースでは、プロトコル ドライバーが OID 要求を発行し、すべてのフィルターは OID 要求を変更することなくそのまま渡します。 次の図に、この一般的なシナリオを示します。

Typical OID path originated from a protocol.

ただし、すべてのフィルター モジュールは OID 要求をインターセプトし、完了できます。 その場合、要求は次の図に示すように下位ドライバーに渡されることはありません。

Typical OID path originated from a protocol and intercepted by a filter.

場合によっては、フィルター モジュール自身が OID 要求を発信することを決定する場合もあります。 次の図に示すように、この要求はフィルター モジュールのレベルから開始され、下位のドライバーをトラバースするだけです。

Typical OID path originated from a filter.

すべての OID 要求は、上位のドライバー (プロトコル ドライバーまたはフィルター ドライバー) が要求を発行し、下位ドライバー (ミニポート ドライバーまたはフィルター ドライバー) が完了するという基本的なフローに沿って処理されます。

通常 OID 要求と直接 OID 要求の仕組み

通常 OID 要求または直接 OID 要求は再帰的にディスパッチされます。 次の図は、関数呼び出しシーケンスを示しています。 シーケンス自体は、前のセクションの図で説明したシーケンスとよく似ていますが、要求の再帰的な性質を示す配置になっていることに注意してください。

Function call sequence for Regular and Direct OID requests.

多くのフィルターがインストールされている場合、NDIS は、より深い再帰を続けるために新しいスレッド スタックを割り当てる必要があります。

NDIS は、 NDIS_OID_REQUEST 構造体をスタックに 沿った 1 つのホップに対してのみ有効だとみなします。 フィルター ドライバーが要求を次の下位ドライバーに渡したい場合 (OID の大部分が該当します)、フィルター ドライバー は、OID 要求を複製するために数十行の定型コードを挿入する必要があります 。 この定型文には、いくつかの問題があります。

  1. OID を複製するためにメモリ割り当てを強制する。 メモリ プールのヒットは時間を要するため、OID 要求の進行を保証できなくなる。
  2. すべてのフィルター ドライバーには、ある NDIS_OID_REQUEST の内容を別の NDIS_OID_REQUEST にコピーするしくみがハード コードされているため、OID 構造体の設計は長期にわたり同じものである必要がある。
  3. 多くの定型文を要求することで、フィルターが実際に何をしているかが不明瞭になる。

同期 OID 要求のフィルター モデル

同期 OID 要求のフィルターモデルは、前のセクションで説明した問題を解決するために、呼び出しの同期性を利用します。

Issue ハンドラーと Complete ハンドラー

通常 OID 要求や直接 OID 要求とは異なり、同期 OID 要求には、Issue ハンドラーと Complete ハンドラーの 2 つのフィルター フックがあります。 フィルター ドライバーは、どちらのフックも登録しないか、一方または両方のフックを登録できます。

Issue 呼び出しは、スタックの上部からスタックの下部に向かって、フィルター ドライバーごとに呼び出されます。 フィルターの Issue 呼び出しは、OID が下方向に継続することを停止し、何らかの状態コードで OID を完了できます。 OID をインターセプトするフィルターがない場合、OID は NIC ドライバーに到達し、NIC ドライバーは OID を同期的に完了する必要があります。

OID が完了すると、Complete 呼び出しが、OID が完了したスタック内のどこからでも、スタックの先頭まで、フィルター ドライバーごとに呼び出されます。 Complete 呼び出しでは、OID 要求を検査または変更したり、OID の完了状態コードを検査または変更したりできます。

次の図は、プロトコルが同期 OID 要求を発行し、フィルターがその要求をインターセプトしない一般的なケースを示しています。

Function call sequence for Synchronous OID requests.

同期 OID の呼び出しモデルは反復的であることに注意してください。 これにより、スタックの使用量は一定に保たれ、スタックを拡張する必要がなくなります。

フィルター ドライバーが Issue ハンドラーで同期 OID をインターセプトした場合、その OID は下位フィルターや NIC ドライバーに渡されません。 ただし、次の図に示すように、上位フィルターの Complete ハンドラーは引き続き呼び出されます。

Function call sequence for Synchronous OID requests with an interception by a filter.

最小限のメモリ割り当て

通常 OID 要求と直接 OID 要求では、NDIS_OID_REQUEST を複製するフィルター ドライバーが必要です。 これに対し、同期 OID 要求は複製できません。 この設計の利点は、同期 OID の待機時間が短く、フィルター スタックを通過する OID 要求は繰り返し複製されないため、障害の機会が少なくなるという点です。

ただし、新しい問題が発生します。 OID を複製できない場合、フィルター ドライバーは要求ごとの状態をどこに保存するのでしょう。 たとえば、フィルター ドライバーがある OID を別の OID に変換するとします。 スタックを下へと進む途中で、フィルターは古い OID を保存する必要があります。 スタックを上へと戻る途中で、フィルターは古い OID を復元する必要があります。

この問題を解決するために、NDIS は処理中の各同期 OID 要求ごとに、各フィルター ドライバーに対するポインター サイズのスロットを割り当てます。 NDIS は、フィルターの Issue ハンドラーから Complete ハンドラーへの呼び出しにわたりこのスロットを保持します。 これにより、Issue ハンドラーは、後で Complete ハンドラーによって使用される状態を保存できます。 次のコード スニペットに例を示します。

NDIS_STATUS
MyFilterSynchronousOidRequest(
  _In_ NDIS_HANDLE FilterModuleContext,
  _Inout_ NDIS_OID_REQUEST *OidRequest,
  _Outptr_result_maybenull_ PVOID *CallContext)
{
  if ( . . . should intercept this OID . . . )
  {
    // preserve the original buffer in the CallContext
    *CallContext = OidRequest->DATA.SET_INFORMATION.InformationBuffer;

    // replace the buffer with a new one
    OidRequest->DATA.SET_INFORMATION.InformationBuffer = . . . something . . .;
  }

  return NDIS_STATUS_SUCCESS;
}

VOID
MyFilterSynchronousOidRequestComplete(
  _In_ NDIS_HANDLE FilterModuleContext,
  _Inout_ NDIS_OID_REQUEST *OidRequest,
  _Inout_ NDIS_STATUS *Status,
  _In_ PVOID CallContext)
{
  // if the context is not null, we must have replaced the buffer.
  if (CallContext != null)
  {
    // Copy the data from the miniport back into the protocol’s original buffer.
    RtlCopyMemory(CallContext, OidRequest->DATA.SET_INFORMATION.InformationBuffer,...);
     
    // restore the original buffer into the OID request
    OidRequest->DATA.SET_INFORMATION.InformationBuffer = CallContext;
  }
}

NDIS は、呼び出しごとにフィルターごとに 1 つの PVOID を保存します。 NDIS は、一般的なケースではプール割り当てがゼロになるように、スタック上に妥当な数のスロットをヒューリスティックに割り当てます。 これは通常 7 個のフィルターまでです。 ユーザーが異常なケースを設定した場合、NDIS はプール割り当てにフォールバックします。

定型文の削減

標準 OID 要求または直接 OID 要求を処理する定型文の例」の定型文を検討してください。 このコードは、OID ハンドラーを登録するエントリのコストです。 独自の OID を発行する場合は、さらに何十行もの定型文を追加する必要があります。 同期 OID では、非同期完了を処理するために複雑さが増すことはありません。 そのため、定型文の多くを削減できます。

同期 OID を使用する最小限の Issue ハンドラーを次に示します。

NDIS_STATUS
MyFilterSynchronousOidRequest(
  NDIS_HANDLE FilterModuleContext,
  NDIS_OID_REQUEST *OidRequest,
  PVOID *CallContext)
{
  return NDIS_STATUS_SUCCESS;
}

特定の OID をインターセプトまたは変更する場合は、数行のコードを追加するだけで行えます。 最小限の Complete ハンドラーはさらにシンプルです。

VOID
MyFilterSynchronousOidRequestComplete(
  NDIS_HANDLE FilterModuleContext,
  NDIS_OID_REQUEST *OidRequest,
  NDIS_STATUS *Status,
  PVOID CallContext)
{
  return;
}

同様に、フィルター ドライバーは 1 行のコードのみで独自の新しい同期 OID 要求を発行できます。

status = NdisFSynchronousOidRequest(binding->NdisBindingHandle, &oid);

これに対し、標準 OID または Direct OID を発行する必要があるフィルター ドライバーでは、非同期完了ハンドラーを設定し、独自の OID 完了と、複製した OID の完了を区別するコードを実装する必要があります。 この定型文の例は、標準 OID 要求を発行するための定型文の例に示されています。

相互運用性

通常呼び出し、直接呼び出し、同期呼び出しはすべて同じデータ構造を使用しますが、パイプラインはミニポート内の同じハンドラーに移動しません。 さらに、一部のパイプラインでは使用できない OID もあります。 たとえば、OID_PNP_SET_POWER は慎重な同期を必要とし、多くの場合、ミニポートに呼び出しのブロックを強制します。 これにより、 直接 OID コールバックでの処理は困難になり、同期 OID コールバックでは使用できません。

したがって、直接 OID 要求と同様に、同期 OID 呼び出しは OID のサブセットでのみ使用できます。 Windows 10 バージョン 1709 では、受信側スケーリング バージョン 2 (RSSv2) で使用される OID_GEN_RSS_SET_INDIRECTION_TABLE_ENTRIES OID のみが同期 OID パスでサポートされています。

同期 OID 要求の実装

ドライバーでの同期 OID 要求インターフェイスの実装の詳細については、次のトピックを参照してください。