Solaris 8 のソフトウェア開発 (追補)

第 3 章 高可用性ドライバ

Solaris 8 10/00 リリースでドライバの強化が新規に追加されました。Solaris デバイスドライバの作成方法についての詳細は、『Writing Device Drivers』を参照してください。

可用性 (availability) は、障害の発生率と修復速度の両方により決定されます。多くの場合、個々のデバイスで障害が発生しても、トータルなシステム障害に至るわけではありません。冗長構成のハードウェアコンポーネントを高可用性をサポートする設計のドライバと組み合わせることで、個々のコンポーネントで障害が発生しても、システム動作を継続することができます。このようなドライバを使用している場合は一般に、サービスを継続したまま、システムを修復できます。

デバイス障害がもたらすドライバ障害をプログラミングによって排除することを、ドライバの強化 (driver hardening) といいます。強化されたドライバは、故障したデバイスからシステムの他の部分にエラーが拡がらないように、エラーに耐え、他を守ることができます。

障害の特定および回復/修理所要時間の短縮に役立つ、ドライバ内部の機能により、システムのサービス利用可能性が向上します。その結果、修復所要時間が短縮され、可用性が向上します。


注 –

最新のマニュアルページを参照するには、man コマンドを使用してください。Solaris 8 Update リリースのマニュアルページには、「Solaris 8 Reference Manual Collection」には記載されていない新しい情報も提供されています。


ドライバの強化

強化 (hardening) とは、ドライバが制御する入出力デバイスで障害が発生しても、またはシステムコアの外部に起因するその他の障害が発生しても、そのドライバが正常に動作するようにするプロセスです。強化されたドライバは、パニックに陥ったり、システムをハングさせたり、またこのような障害の結果として壊れたデータを無秩序に拡散させたりしてはなりません。

ドライバの開発者は、次の役割を担います。

すべての Solaris ドライバは強化されるべきです。強化されたドライバは、次の規則を守ります。

デバイスドライバのインスタンス

Solaris カーネルは、複数のドライバのインスタンスの使用を認めます。インスタンスごとに専用のデータ領域を持ちますが、テキストおよび一部のグローバルデータは、他のインスタンスとの共有になります。デバイスはインスタンス単位で管理されます。強化されたドライバは、ドライバがフェイルオーバーを内部的に処理する設計になっていない限り、ハードウェアの部分ごとに別々のインスタンスを使用すべきです。たとえば、多機能カードなど、スロットごとに複数のドライバインスタンスが存在することになりますが、これは Solaris デバイスドライバの標準の動作です。

DDI アクセスハンドルの排他的使用

強化されたドライバによるすべてのプログラムされた入出力 (PIO) アクセスでは、必ず、ddi_getXddi_putXddi_rep_getX、および ddi_rep_putX ルーチンファミリーに含まれる Solaris DDI アクセス関数を使用しなければなりません。ドライバが、ddi_regs_map_setup(9F) から返されたアドレスを使用して、対応付けされたレジスタに直接アクセスしてはなりません。アクセスハンドルを使用することによって、入出力エラーが確実に制御され、その影響が戻り値に限定されるので、マシン状態の他の部分を壊す可能性が減少します (アクセスハンドルを使用しないため、ddi_peek(9F) および ddi_poke(9F) ルーチンは避けてください)。

DDI アクセスメカニズムは、カーネルにデータを読み込む方法を制御する機会を提供するので重要です。DDI アクセスルーチンは、バスタイムアウトトラップの影響を限定することによって、保護能力を提供します。

破壊されたデータの検出

以下の節では、どこでデータ破壊が発生する可能性があるか、およびそれらを検出するにはどのような手順が必要かについて説明します。

デバイス管理および管理データの破壊

ドライバは、PIO によるか DMA によるかを問わず、デバイスから取得したデータは壊れている可能性があるということを想定しなければなりません。特に、デバイスから得たデータから読み取った、または計算されたポインタ、メモリーオフセット、配列インデックスは、慎重に扱ってください。この種の値は有害な場合があり、参照に利用される場合、カーネルパニックを引き起こす可能性があります。この種の値はすべて、範囲および整列 (必要な場合) をチェックしてから使用してください。

ポインタが有害でなくても、誤った結果を導く可能性もあります。たとえば、オブジェクトの有効なインスタンスを指し示していても、該当する正しいインスタンスではないことがあります。可能であれば、ドライバはポインタと指示先オブジェクトをクロスチェックするか、またはポインタによって取得したデータの妥当性を検証すべきです。

他のタイプのデータ (パケット長、ステータスワード、チャネル ID など) も誤った結果を導く可能性があります。データの各タイプを可能な範囲でチェックすべきです。たとえば、パケット長の場合は、範囲をチェックして、負ではないか、収容バッファより大きくないかどうかを確認できます。また、ステータスワードの場合は、「不可能」ビットの有無をチェックできます。チャネル ID は有効 ID のリストと突き合わせます。

値を使用して Stream を識別する場合、ドライバは Stream が実際に存在しているかどうかを確認しなければなりません。STREAMS 処理は非同期の性質があるので、デバイスの割り込みがまだ処理されないうちに、Stream が取り除かれてしまうことがあります。

ドライバはデバイスからデータを再読み込みしてはなりません。データは 1 回だけ読み取って、妥当性を検証し、ドライバのローカル状態で格納します。こうすることによって、最初に読み取って検証したときには正しかったが、後で再読み取りしたときには正しくないというデータによる危険性を回避することができます。

さらに、連続して BUSY 状態を返すデバイス、または別のバッファ処理を要求するデバイスがシステム全体をロックアップすることがないように、ドライバで、すべてのループがバインドされていることも確認すべきです。

受信したデータの破壊

デバイスエラーによって、受信バッファに壊れたデータが格納されてしまうことがあります。このような破壊を、デバイスドメインの外 (たとえば、ネットワーク内など) で発生した破壊と区別することはできません。通常は、プロトコルスタックのトランスポート層やデバイスを使用するアプリケーション内部での整合性チェックなど、この種の破壊に対処する既存のソフトウェアが既に配備されています。

ディスクドライバなどのように、受信したデータの整合性を上位層でチェックしない場合は、ドライバ内部で整合性をチェックすることができます。受信したデータの破壊を検出する方法は、一般にデバイス固有です (チェックサム、CRC など)。

障害の検出

従来のデバイスドライバでは、障害を検出した場合に、デバイスまでのデータパスを無効にできます。PIO アクセスが無効にされている場合にそのデバイスから読み取りを行おうとすると、undefined 値が返り、書き込みが無視されます。DMA アクセスが無効にされている場合は、デバイスはメモリーへのアクセスが禁止される可能性があり、または読み取り時に未定義データを受け取り、書き込みが廃棄される可能性があります。

デバイスドライバは次の DDI ルーチンを使用することによって、データパスが無効にされているかどうかを検出することができます。

各関数は、指定のハンドルによって示されたデータパスに影響を与えるような障害が検出されているかどうかを調べます。いずれかの関数が DDI_FAILURE を返した場合、そのデータパスに障害があります。ドライバは ddi_dev_report_fault(9F) を使用してその障害を報告し、必要なクリーンアップを実行し、さらに可能であれば、呼出側に適切なエラーを返す必要があります。

障害の封じ込め

システムの整合性を維持するには、システム状態が変更されないうちに障害を検出する必要があります。したがって、ドライバは、システムが使用する予定のデータがデバイスから返されるたびに、障害の有無をテストしなければなりません。

DMA の切り離し

障害のあるデバイスは、バス上で不適切な DMA 転送を開始する可能性があります。このデータ転送によって、配信済みの正常データが破壊されることがあります。障害のあるデバイスは、そのデバイスのドライバに属さないメモリーにまで悪影響をおよぼすような破壊されたアドレスを生成しかねません。

IOMMU が備わったシステムでは、デバイスは DMA 用に書き込み可能として対応付けされたページに限って書き込むことができます。したがって、DMA 書き込みのターゲットとなるページは、1 つのドライバインスタンスが単独で所有し、他のカーネル構造体と共有することはありません。該当するページが DMA 用に書き込み可能として対応付けされている場合、ドライバはそのページのデータを疑ってみるべきです。ページをドライバの外部に渡す前に、またはデータを検証する前に、ページと IOMMU の対応付けを解除しなければなりません。

ddi_umem_alloc(9F) を使用して、整合ページ全体が割り当てられる、または複数のページを割り当てて最初のページ境界より下のメモリーを無視することができます。ddi_ptob(9F) を使用すると、IOMMU ページのサイズを調べることができます。

あるいは、ドライバは、メモリーの安全な部分にデータをコピーしてから、そのデータを処理することもできます。この場合、最初に ddi_dma_sync(9F) を使用してデータを同期させる必要があります。

ddi_dma_sync(9F) を呼び出すときには、DMA を使用してデバイスにデータを転送する前に SYNC_FOR_DEV を指定し、デバイスからメモリーへ DMA を使用してデータを転送した後で SYNC_FOR_CPU を指定する必要があります。

IOMMU が備わった一部の PCI ベースのシステムでは、デバイスは PCI デュアルアドレスサイクル (64 ビットアドレス) を使用して、IOMMU をバイパスすることができます。その結果、デバイスにおいてメインメモリーの領域が破壊されてしまう潜在的可能性が生じます。強化されたデバイスドライバでは、このようなモードを使用しようとしてはならず、使用不可にしておくべきです。

stuck 割り込みの処理

割り込みが絶えないと、システムパフォーマンスが大幅に低下し、シングルプロセッサマシンの場合はほぼ確実に作業が進まなくなってしまうので、ドライバは stuck 割り込みを識別する必要があります。

時にはドライバにとって、特定の割り込みを偽として識別するのが困難な場合があります。ネットワークドライバの場合、受信した割り込みが指示されても、新しいバッファが利用できなければ作業は不要です。これが単独で発生した場合は問題ありません。実際の作業は別のルーチン (たとえば読み取りサービスなど) によってすでに完了している可能性があるからです。

一方、ドライバが処理する作業を伴わない割り込みが連続した場合、stuck 割り込みラインを示していることがあります。そのため、防御手段を講じる前に、すべてのプラットフォームで多数の明らかに偽の割り込みの発生を許してしまうことになります。

処理すべき作業がありそうなのにハングしてしまったデバイスは、対応するバッファ記述子を更新できなかった可能性があります。ドライバは、このような繰り返しの要求に対して防御しなければなりません。

場合によっては、プラットフォーム固有のバスドライバに請求によらない持続的な割り込みを識別する能力があり、攻撃しているデバイスを使用禁止にできます。しかし、これは有効な割り込みを識別して適切な値を返すことができるという、ドライバの能力に依存します。ドライバはしたがって、デバイスが正当な割り込みをかけた (すなわちデバイスがドライバになんらかの有用な作業を行うことを実際に要求している) ことを検出した場合以外は、DDI_INTR_UNCLAIMED を返さなければなりません。

偶発性の高い他の割り込みの正当性を確認することは、さらに困難です。この目的のためには、割り込みが有効かどうかを評価する手段として、割り込み想定フラグが役立ちます。デバイスの記述子が全部すでに割り当てられている場合に生成できる、descriptor free などの割り込みを例として仮定してください。ドライバはカードの最後の記述子を使用してしまったことを検出すると、割り込み想定フラグを設定できます。対応する割り込みが伝達された時にこのフラグが設定されていないと、疑わしいといえます。

メディアが切断された、またはフレーム同期が失われたといったことを伝える、情報通知目的の割り込みには、予測できないものがあります。このような割り込みが stuck かどうかを検出する最も簡単な方法は、最初のイベントでその送信元を次のポーリングサイクルまでマスクすることです。

禁止されている間に割り込みが再び発生した場合は、偽の割り込みとみなすべきです。デバイスによっては、マスクレジスタが対応する送信元を無効にし、割り込みを発生させない場合でも読み取ることのできる、割り込み状態ビットがあります。ドライバの設計者は、それぞれのデバイスに合わせて、より適切なアルゴリズムを工夫することができます。

割り込み状態ビットが無限ループに陥らないようにしてください。パスの最初に設定された状態ビットセットがいずれも実際の作業を必要としない場合は、このようなループを切断してください。

ドライバの強化に関するその他の考慮事項

すでに説明した要件のほかに、ドライバの開発者は、次の問題も考慮する必要があります。

スレッドのインタラクション

デバイスドライバのカーネルパニックは、デバイス障害の発生後に生じた予想外のカーネルスレッドのインタラクションによって引き起こされることがよくあります。デバイスで障害が発生すると、設計者が予想しなかった形でスレッドのインタラクションが起きることがあります。

たとえば、処理ルーチンが正常に完了しないうちに打ち切られた場合、条件変数を待機している他のスレッドに知らせることができないことがあります。他のモジュールに障害を知らせようとしたり、または予想外のコールバックを処理しようとしたりすると、望ましくないスレッドのインタラクションが発生する可能性があります。デバイス障害時に発生する可能性のある、相互排他ロックの取得と放棄の順序を検証してください。

アップストリームの STREAMS モジュールを起点とするスレッドは、予想外にそのモジュールをコールバックするために使用された場合、望ましくないパラドックスに陥る可能性があります。代替スレッドを使用して、例外メッセージを処理してください。たとえば、wput 手順では、読み取り側の putnext で直接通信するのではなく、読み取り側のサービスルーチンを使用して M_ERROR と通信することができます。

障害の発生した STREAMS デバイスが、クローズ時に (障害が原因で) 静止できなかった場合、Stream を取り除いた後に割り込みが発生する可能性があります。割り込みハンドラは、古い Stream ポインタを使用して、メッセージを処理しようとしてはなりません。

トップダウン式要求における危険

ドライバの設計者は、ハードウェアの故障からシステムを保護する一方で、ドライバの誤用に対しても防御する必要があります。ドライバは、カーネル基盤は常に正しい (信頼できるコア) ということを前提にできますが、ドライバに渡されるユーザー要求は潜在的な破壊性を有している可能性があります。

たとえば、ユーザーが提供したデータブロック (M_IOCTL) に対してアクションを実行することをユーザーが要求し、そのデータブロックがメッセージの制御部で指示されたサイズより小さいという場合があります。ドライバはユーザーアプリケーションを信用してはなりません。

設計時には、害を引き起こす可能性があるという観点から、受け取ることのできる各タイプの ioctl の構造を検討すべきです。ドライバは、異常な ioctl を処理しないように、チェックしなければなりません。

代替の対応策

ドライバは障害の起きたハードウェアを引き続き使用してサービスを提供し、代替のデバイスアクセス方法を使用することによって、特定された問題への対処を試みることができます。ハードウェアの故障が予測不能で、設計が複雑になるリスクを伴うことを考えると、代替の対応策をとることが常に賢明な選択というわけではありません。せいぜい、周期的な割り込みポーリングと再試行に限定すべきです。周期的にデバイスを再試行することによって、ドライバはデバイスが復旧したかどうかを知ることができます。周期的ポーリングを使用すると、ドライバが割り込みを強制的に禁止された後でも、割り込みメカニズムを制御することができます。

重要なシステムサービスを提供できるように、システムが常に代替デバイスを備えているのが理想です。カーネルまたはユーザースペースのサービスマルチプレクサは、デバイス障害時にシステムのサービスを維持する最良の手段です。しかし、この章ではこの種の方式については記載していません。

サービス利用可能性

サービス利用可能性を確保するには、ドライバが下記を実行できるようにしなければなりません。

現在のデバイス状態のチェック

ドライバはリソースの無駄なコミットを避けるために、適切なポイントでデバイス状態をチェックしなければなりません。ddi_get_devstate(9F) 関数を使用すると、ドライバでフレームワークによって維持されているデバイスの現在の状態を判別できます。


ddi_devstate_t ddi_get_devstate(dev_info_t *dip);

ドライバは通常、OFFLINE のデバイスを扱うときには呼び出されません。デバイス状態は一般に、再構成アクティビティによって変更されたというような、以前のデバイス障害レポートを反映します。

デバイス障害時の適切な動作

システムは、デバイスのサービス利用可能性に対する影響という観点から、障害を報告しなければなりません。一般に次の場合に、サービス損失が予期されます。

ddi_get_decstate(9F) から返されたデバイス状態が、デバイスが使用不能であることを示している場合、ドライバは新規または未処理の入出力要求をすべて拒否し、(可能であれば) 適切なエラーコード (EIO など) を返す必要があります。STREAMS ドライバでは、M_ERROR または M_HANGUP をアップストリームに送り、ドライバが使用不能であることを示さなければなりません。

各メジャーエントリポイントで、またオプションで操作のためにリソースをコミットする前、および障害の報告後に、デバイスの状態をチェックする必要があります。いずれかの段階で、デバイスが使用不能であることが判明した場合は、ドライバで必要なクリーンアップ処理 (リソースの解放など) を実行し、タイミングよく戻る必要があります。再試行または回復処理を試行してはなりません。障害の報告も不要です。状態は障害ではなく、状態はすでにフレームワークと管理エージェントに認識されています。状態は、現在の要求および他の未処理すなわちキュー内の要求に完了マークを付け、可能であれば再びエラー標識を出します。

ioctl() エントリポイントは、デバイスが使用不能で、他 (エラー状態の回復など) が引き続き動作中である場合、所定の ioctl 操作における、デバイスに対する入出力 (たとえば、ディスクのフォーマットなど) 障害を意味する問題を提示します。したがって、状態チェックはコマンド単位で行う必要があります。または、別のエントリポイントまたはマイナーデバイスモードを使用して、あらゆる状態でこれらの操作を実装できますが、この方法は既存アプリケーションとの互換性の点から制約を受ける可能性があります。

close() は、デバイスが使用不能な場合であっても、常に正常に完了しなければなりません。デバイスが使用不能な場合は、割り込みハンドラがその後のすべての割り込みに DDI_INTR_UNCLAIMED を返す必要があります。割り込みが引き続き発生する場合は、最終的に割り込みが禁止されることがあります。

障害の報告

次の関数は、ドライバがデバイス障害を検出したことをシステムに通知します。


void ddi_dev_report_fault(dev_info_t *dip, ddi_fault_impact_t impact,
             ddi_fault_location_t location, const char *message);

impact パラメータは、デバイスの通常のサービス利用可能性に障害が与える影響を示します。このパラメータは、障害に対する適切な処置を決定するために、システムの障害管理コンポーネントが使用します。この処置によってデバイス状態が変化することがあります。サービスが損われる障害が発生すると、デバイス状態が DOWN に変わります。サービスが低下する障害の場合は、デバイス状態が DEGRADED に変わります。

デバイスは次の場合に、障害として報告されなければなりません。

できるだけ、ドライバが同じ障害を繰り返し報告しないようにします。特に、デバイスがすでに使用不能状態になっている場合に、ドライバがエラーを報告しても無駄です (望ましくありません) (ddi_get_devstate(9F) のマニュアルページを参照)。

接続プロセスでハードウェア障害が検出された場合、ドライバは ddi_dev_report_fault(9F) のを使用して障害を報告するとともに、DDI_FAILURE を返さなければなりません。

定期的な健全性チェック

潜在的な障害とは、他のアクションが発生するまで明らかにならない障害のことです。たとえば、コールドスタンバイモードのデバイスで発生したハードウェア障害は、マスターデバイスで障害が発生するまで検出されない可能性があります。マスターデバイスで障害が発生した時点で、システムに 2 つの障害デバイスがあり、動作を継続できない可能性があることが判明します。

一般論として、未検出のままの潜在的障害は、最終的にシステム障害につながります。潜在的な障害を検査しなかった場合は、冗長型システム全体の可用性が損なわれる危険があります。これを回避するには、デバイスドライバで潜在的な障害を検出し、他の障害と同様に報告しなければなりません。

ドライバには、デバイスに対する定期的な健全性チェックを行うメカニズムが必要です。デバイスを第 2 デバイスすなわちフェイルオーバーデバイスにできる、障害耐久 (フォールトトレラント) の状況では、故障した第 2 デバイスを早い段階で検出することが重要です。そうすれば、第 1 デバイスで障害が発生しないうちに修理または交換することができます。

定期的な健全性チェックには、次のものがあります。