ナビゲーションリンクをスキップ | |
印刷ビューの終了 | |
デバイスドライバの記述 Oracle Solaris 10 8/11 Information Library (日本語) |
この節では、デバイスドライバにおいて、システムのパニックやハングアップ、システムリソースの浪費、データ破壊の拡散を回避するための手法について説明します。エラー処理と診断のための入出力障害サービスフレームワークに加えて、ここで説明する防御的プログラミング手法をドライバで使用すると、そのドライバは強化されていると認識されます。
すべての Solaris ドライバで、次のコーディング手法を実践するようにしてください。
ハードウェアの各部品が、デバイスドライバの別個のインスタンスによって制御されるようにします。「デバイス設定の概念」を参照してください。
プログラム式入出力 (PIO) は、DDI アクセス関数を介し、適切なデータアクセスハンドルを使用する方法でのみ実行される必要があります。第 7 章デバイスアクセス: プログラム式入出力を参照してください。
デバイスドライバは、デバイスから受信するデータが破壊されている可能性を想定する必要があります。データを使用する前に、ドライバでデータの整合性をチェックする必要があります。
ドライバは不正なデータがシステムのほかの部分に流されないようにする必要があります。
ドライバでは、ドキュメント化された DDI 関数およびインタフェースのみを使用します。
ドライバによって全面的に制御される DMA バッファー (DDI_DMA_READ) 内のメモリーページのみにデバイスが書き込みを行うことをドライバで保証する必要があります。これには、DMA の障害によってシステムのメインメモリーの不特定箇所が破壊されることを防ぐ意味があります。
デバイスが動作停止した場合に、デバイスドライバがシステムリソースを際限なく浪費してはなりません。デバイスから継続的にビジー状態の応答がある場合は、ドライバをタイムアウトします。またドライバでは、正常でない (問題のある) 割り込み要求を検出して適切なアクションを実行します。
デバイスドライバは Solaris OS のホットプラグをサポートする必要があります。
デバイスドライバは、リソースを待機する代わりにコールバックを使用する必要があります。
ドライバは障害のあとにリソースを解放する必要があります。たとえば、ハードウェアで障害が発生したあとでも、システムがすべてのマイナーデバイスを閉じてドライバインスタンスを切り離せるようにする必要があります。
Solaris カーネルは 1 つのドライバの複数のインスタンスを許容します。各インスタンスは個別のデータ領域を持ちますが、テキストや一部のグローバルデータをほかのインスタンスと共有します。デバイスはインスタンス単位で管理されます。ドライバでは、フェイルオーバーを内部的に処理するように設計されている場合を除いて、ハードウェアの部品ごとに別個のインスタンスを使用するようにしてください。たとえば、複数の機能を備えるカードの使用時に、1 つのスロットに付き 1 つのドライバの複数のインスタンスが発生する可能性があります。
ドライバによるすべての PIO アクセスでは、次のルーチンファミリに属する Solaris DDI アクセス関数を使用する必要があります。
ddi_getX
ddi_putX
ddi_rep_getX
ddi_rep_putX
ドライバでは、ddi_regs_map_setup(9F) から返されたアドレスによって、対応づけられたレジスタに直接アクセスしないでください。ddi_peek(9F) および ddi_poke(9F) ルーチンはアクセスハンドルを使用しないため、これらのルーチンを使わないようにします。
DDI アクセス機構が重要な理由は、DDI アクセスの利用により、カーネルへのデータ読み込みの形式を制御できるようになるためです。
以降の節では、データ破壊が発生する可能性がある場所と、破壊を検出する方法について説明します。
ドライバでは、PIO によるか DMA によるかを問わず、デバイスから取得するすべてのデータがすでに破壊されている可能性があると想定するようにしてください。特に、デバイスからのデータに基づくポインタ、メモリーオフセット、および配列インデックスについては細心の注意を払う必要があります。そのような値は悪質である、つまり、間接参照された場合にカーネルパニックを引き起こす可能性があります。そのようなすべての値について、使用する前に範囲および配列 (必要な場合) をチェックしてください。
悪質でないポインタであっても、誤動作の原因となる可能性があります。たとえば、有効だが正しくないオブジェクトのインスタンスをポインタが指し示す可能性があります。ドライバでは可能なかぎり、ポインタとその指示先のオブジェクトをクロスチェックするか、それが難しい場合はそのポインタを介して取得したデータを検証するようにしてください。
パケット長、状態語、チャネル ID など、その他の種類のデータも誤動作の原因となる可能性があります。これらの種類のデータを可能な範囲内でチェックするようにしてください。パケット長については、範囲チェックを実行することにより、長さが負ではないこと、格納先バッファーの長さを超えてもいないことを保証できます。状態語については「不可能」ビットのチェックを実行できます。チャネル ID については、有効な ID のリストとの照合を実行できます。
値を使用してストリームを識別する箇所で、ドライバはストリームがまだ存在していることを保証する必要があります。STREAMS 処理の非同期的な性質は、ストリームが分解可能な一方で、デバイス割り込みが未処理であることを意味します。
ドライバでデバイスからデータを再読み取りしないでください。データは 1 回だけ読み取られ、検証され、ドライバのローカル状態に保存されるようにしてください。これにより、データを最初に読み取ったときは正確だが、あとで再読み取りしたときにデータが誤っているという危険性を回避できます。
ドライバでは、すべてのループの境界が確定していることも確認してください。たとえば、継続的な BUSY 状態を返すデバイスによって、システム全体が動作停止されないようにする必要があります。
デバイスエラーの結果、破壊されたデータが受信バッファーに配置される可能性があります。そのような破壊は、デバイスの領域を超えて (たとえば、ネットワークの内部で) 発生する破壊と区別することができません。既存のソフトウェアは通常、そのような破壊を処理するしくみをすでに備えています。1 つの例は、プロトコルスタックのトランスポート層における整合性チェックです。別の例は、デバイスを使用するアプリケーション内部での整合性チェックです。
上位層で受信データの整合性がチェックされない場合、ドライバ自体の内部でデータの整合性をチェックできます。受信データの破壊を検出する方法は通常、デバイスごとに異なります。実行できるチェックの種類の例としては、チェックサムや CRC があります。
障害のあるデバイスは、バス上で不適切な DMA 転送を開始する可能性があります。このデータ転送によって、以前に配信された正常なデータが破壊されてしまう可能性があります。障害のあるデバイスは、そのデバイスのドライバに属さないメモリーにまで悪影響を及ぼすような、破壊されたアドレスを生成する可能性があります。
IOMMU を備えるシステムでは、デバイスは DMA 用に書き込み可能としてマップされたページに限って書き込むことができます。したがって、そのようなページは 1 つのドライバインスタンスが単独で所有するようにしてください。これらのページは、ほかのどのカーネル構造とも共有しないでください。該当するページが DMA 用に書き込み可能としてマップされている場合でも、ドライバではそのページ内のデータを疑うようにしてください。ページをドライバの外部に渡す前に、またはデータを検証する前に、ページと IOMMU のマッピングを解除する必要があります。
ddi_umem_alloc(9F) を使用すると、アライメントされたページ全体が割り当てられることを保証したり、複数のページを割り当てて、最初のページ境界よりも下のメモリーを無視したりできます。ddi_ptob(9F) を使用すると、IOMMU ページのサイズを調べることができます。
別の方法として、ドライバでメモリーの安全な部分にデータをコピーしてから、そのデータを処理することもできます。この場合、最初に ddi_dma_sync(9F) を使用してデータを同期させる必要があります。
ddi_dma_sync() を呼び出すときは、DMA を使用してデータをデバイスに転送する前に SYNC_FOR_DEV を指定し、デバイスからメモリーに DMA を使用してデータを転送したあとに SYNC_FOR_CPU を指定するようにしてください。
IOMMU を備えた一部の PCI ベースのシステムでは、デバイスは PCI デュアルアドレスサイクル (64 ビットアドレス) を使用すると IOMMU をバイパスできます。この機能により、デバイスでメインメモリーのいずれかの領域が破壊される可能性が生じます。デバイスドライバでは、そのようなモードの使用を試みてはならず、モードを無効にしておくべきです。
ドライバでは問題のある割り込みを識別する必要があります。これは、割り込みが際限なく発生し続けるとシステムのパフォーマンスが著しく低下し、シングルプロセッサーのマシンではほぼ確実にストールしてしまうためです。
ドライバで特定の割り込みを無効と識別することが困難な場合もあります。ネットワークドライバの場合は、受信した割り込みが指示されても、新しいバッファーが利用できなければ作業は不要です。この状況が単独で発生した場合は問題ありません。実際の作業は (読み取りサービスなどの) 別のルーチンによってすでに完了している可能性があるためです。
一方、ドライバが処理する作業を伴わない割り込みが連続した場合は、問題のある割り込みの列を示している可能性があります。そのため、防御手段を講じる前に、プラットフォームが明らかに無効な割り込みを多数発生させてしまうことになります。
処理する作業がありそうなのにハングアップしてしまったデバイスは、対応するバッファー記述子を更新できなかった可能性があります。ドライバでは、このような繰り返しの要求を防御するようにしてください。
場合によっては、プラットフォーム固有のバスドライバの側で、要求に基づかない持続的な割り込みを識別し、障害のあるデバイスを無効化できることがあります。ただしこれは、有効な割り込みを識別して適切な値を返すことができるという、ドライバの能力に依存します。ドライバでは、デバイスが正当な割り込みをかけたことを検出した場合を除き、DDI_INTR_UNCLAIMED の結果を返すようにしてください。割り込みが正当であるのは、デバイスが実際に、何らかの有用な処理を行うことをドライバに要求している場合に限られます。
偶発性の高いその他の割り込みの正当性を証明することは、さらに困難です。割り込み想定フラグは、割り込みが有効かどうかを評価するために役立つ手段です。デバイスの記述子すべてがすでに割り当てられている場合に生成できる、記述子なしのような割り込みを例として考えます。ドライバがカードの最後の記述子を使用したことを検出した場合、割り込み想定フラグを設定できます。関連付けられた割り込みが配信されたときにこのフラグが設定されていない場合、その割り込みは疑わしいと判断できます。
メディアが切断されたことやフレーム同期が失われたことを知らせるものなど、情報通知のための割り込みの中には予測できないものがあります。そのような割り込みに問題があるかどうかを検出するもっとも簡単な方法は、最初の発生時にこの特定の送信元を次のポーリングサイクルまでマスクすることです。
無効化されている間にふたたび割り込みが発生した場合、その割り込みを偽とみなすようにします。デバイスによっては、関連付けられた送信元をマスクレジスタが無効にし、割り込みを発生させない場合でも読み取ることのできる、割り込み状態ビットがあります。ドライバの開発者は、デバイスに合わせてより適切なアルゴリズムを工夫できます。
割り込み状態ビットが無限ループに陥らないようにしてください。パスの開始時に設定された状態ビットがいずれも実際の作業を必要としない場合は、このようなループを切断してください。
これまでの節で述べた要件に加えて、次の問題を考慮してください。
スレッドの対話
トップダウン要求の脅威
適応型戦略
デバイスドライバにおけるカーネルパニックの多くは、デバイス障害の発生後の、カーネルスレッドの予期しない対話によって引き起こされます。デバイスで障害が発生すると、開発者が予想しなかった形でスレッドの対話が起きることがあります。
処理ルーチンが早期終了した場合、予期しているシグナルが与えられないことにより、条件変数の待機側がブロックされます。ほかのモジュールに障害を通知しようとしたり、予想外のコールバックを処理しようとしたりすると、望ましくない形でスレッドの対話が発生する可能性があります。デバイス障害の際に発生する可能性がある、mutex の取得と放棄の順序について検討してください。
アップストリームの STREAMS モジュールを起点とするスレッドは、予想に反してそのモジュールをコールバックするために使用された場合、望ましくない矛盾した状況に陥る可能性があります。代替スレッドを使用して例外メッセージを処理することを検討してください。たとえば、プロシージャーでは、読み取り側の putnext(9F) でエラーを直接処理するのではなく、読み取り側のサービスルーチンを使用すると M_ERROR を伝達できます。
障害の発生した STREAMS デバイスが、クローズ時に障害が原因で静止できなかった場合、ストリームが分解されたあとに割り込みが発生する可能性があります。割り込みハンドラは、古いストリームポインタを使用してメッセージを処理しようとしてはなりません。
ドライバの開発者は、ハードウェアの故障からシステムを保護する一方で、ドライバの誤用を防ぐ必要もあります。ドライバは、カーネル基盤は常に正しい (信頼できるコア) ということを前提にできますが、ドライバに渡されるユーザー要求が有害な場合があります。
たとえば、ユーザーが提供したデータブロック (M_IOCTL) に対してアクションを実行することをユーザーが要求し、そのデータブロックがメッセージの制御部で指示されたサイズより小さいという場合があります。ドライバはユーザーアプリケーションを信頼してはなりません。
ドライバが受信できる各タイプの ioctl の構造と、ioctl が引き起こす可能性がある潜在的な損害について検討してください。ドライバでは、不正な形式の ioctl を処理しないようにチェックを実行するようにしてください。
ドライバは、障害の起きたハードウェアを使用することでサービスの提供を継続できます。デバイスにアクセスするための代替的な戦略を用いることによって、特定された問題への対処を試みることができます。ハードウェアの故障が予測不能であることと、設計の複雑さが増すことのリスクを考慮すれば、適応型戦略が常に賢明とは限りません。この戦略は、定期的な割り込みポーリングや再試行といった範囲に限定するようにしてください。デバイスを定期的に再試行することにより、ドライバはデバイスがいつ回復したかを把握できます。定期的なポーリングを使用すると、割り込みの無効化をドライバが強制されたあとでも、割り込み機構を制御できます。
不可欠のシステムサービスを提供するための代替デバイスをシステムが常に備えていることが理想的です。 カーネルまたはユーザー空間でのサービス多重化は、デバイスで障害が起きたときにシステムサービスを維持するための最良の手段です。ただし、この節ではそのような方式について扱いません。