高度な検索
Developer Connection
Member Login ログイン | ご入会 ADC連絡先

Technote 1067

Traditional Device Drivers: Sync or Swim
or, "I've Got the vSyncWait Blues"


目次

デバイスドライバを管理する 2 つのルール

事例による証明

デッドロックの回避

ルールを証明する例外

要約
般的なソフトウェアシステムと同様、誰もがルールにしたがっている限り、Mac OS は正常に動作します。一般的な Toolbox オペレーションの場合は、これらのルールはかなり容易に理解することができます。しかし、伝統的な Mac OS デバイスドライバ (DRVR) は、その本来の性質からして、複雑なプログラムであり、それらの動作を管理するルールはなかなか理解できません。

この TECHNOTE では、伝統的な Mac OS デバイスドライバを書くときの危険の一つ、つまり、あるデバイスドライバから別のデバイスドライバを同期的に呼び出すときに発生するデッドロックの可能性について説明します。こうしたデッドロックが発生するいくつかのケースを具体的に説明し、さらにデッドロックの発生を回避する方法についても解説します。

この TECHNOTE では、読者として、伝統的な Mac OS デバイスドライバ、特に、他のデバイスドライバを呼び出すドライバを書いているデベロッパを想定しています。また、伝統的な Mac OS デバイスドライバの内部構造に関心を持っている人にも参考になると思います。


デバイスドライバを管理する 2 つのルール
伝統的な Macintosh デバイスドライバを書くときには、2 つの重要なルールにしたがう必要があります。

ルール #1
デバイスドライバを非同期的に呼び出すことができ、かつ別のデバイスドライバを呼び出す場合、そのドライバも非同期的に呼び出す必要があります。

ルール #1 にしたがわないと、システムをデッドロックさせてしまう危険があり、ユーザにとって破局的な結果が生じる可能性があります。

注意:
Andrew S. Tanenbaum の『Modern Operating Systems』によると、「プロセスのセットがデッドロックに陥るのは、そのセットに含まれる各プロセスがそのセット内の別のプロセスによってのみ発生するイベントを待ち続けるときです」たとえば、読者の信頼できるディスクドライバを得るまで私がこの TECHNOTE を書き終えることができず、同時にこの TECHNOTE を読み終えるまで、読者が信頼できるディスクドライバを開発することができない場合、われわれはデッドロックに陥ることになります。人間にとって、デッドロックを回避したり、デッドロックから抜け出すことは必ずしも不可能ではありませんが、コンピュータにはそんな芸当はできません。



ルール #2
デバイスドライバを非同期的に呼び出すことができる場合は、あたかも非同期的に呼び出されているように、常にオペレーションを実行してください。

言い換えれば、このオペレーションが同期的であるか非同期的であるかを確認するためにテストを行わないでください。また、それぞれの場合で異なる処理を実行しないでください。

ルール #2 にしたがわないと、システムは期待に反して、割り込み時に不正な動作を実行する可能性があります。たとえば、ルール #1 に対する違反のような動作です。


事例による証明
ここでは、2 つの事例を通して、ルールに違反すると、どのようにしてシステムがデッドロックに陥るかを説明します。これら 2 つの事例は、次のようなシナリオに基づいています。

"旧" AppleTalk デバイスドライバを使って、ネットワーク経由でディスクブロックをフェッチするブロックデバイスドライバを書いたとします。Finder を使って、ファイルをディスクイメージにコピーするとき、システムが vSyncWait という名前のルーチンでときどきデッドロックに陥ります。このような事態は、旧ネットワークを実行しているときよりも、Open Transport を実行しているときの方がより頻繁に発生します。

最近、このようなドライバを書いている何人かの Macintosh デベロッパからの問合せに対応した経験から、この事例を選びました。これらのデベロッパはルールに違反していましたが、すべてが正常に動作すると思い込んでいました。残念ながら、Open Transport は、AppleTalk がインプリメントされる方法を大幅に変更しました。その結果、われわれは、予期しないシステムのデッドロックという、ルール違反のペナルティを突然発見することになりました。

ファイルをコピーするとき、Finder は、ソースボリュームからデータのチャンクを読み込み、それらをデスティネーションボリュームに書き込む、連続する ioCompletion のシーケンスを作成します (詳細については、develop 13 の "Asynchronous Routines on the Macintosh" を参照してください)。Finder は実際には、File Manager 呼び出しを実行しますが、このような場合、File Manager は効率を上げるために、これらの呼び出しを適切なブロックデバイスドライバに直接渡します。

次の 2 つの事例では、ブロックデバイスドライバが前述のルールにしたがわないことで、どのようにしてシステムがデッドロックに陥るかを示します。


ルール #1 に対する違反の事例
デバイスドライバがルール #1 に違反している、つまりデバイスドライバが別のデバイスドライバを同期的に呼び出すと仮定します。次に、この違反がどのようにシステムのデッドロックに至るかを段階的に説明します。

1. Finder は PBReadAsync を呼び出して、ローカルハードディスクからファイルのチャンクを読み込みます。これにより File Manager が呼び出されます。File Manager はデバイスドライバを呼び出し、デバイスドライバは SCSI Manager を呼び出し、SCSI Manager はさらに SCSI ハードウェアを起動します。

2. SCSI オペレーションが完了します。SCSI ハードウェアは CPU に割り込み、オペレーションが完了したことを CPU に通知します。割り込みサービスルーチンは、SCSI デバイスドライバに対する ioCompletion を呼び出します。SCSI デバイスドライバは File Manager に対する ioCompletion ルーチンを呼び出し、さらに File Manager は Finder に対する ioCompletion ルーチンを呼び出します。割り込みサービスルーチンの性質上、この時点では割り込みはまだ無効にされたままです。

3. Finder の ioCompletion ルーチンは PBWriteAsync を呼び出して、ブロックデバイスドライバ上にマウントされているボリュームにデータを書き込みます。これにより File Manager が呼び出され、File Manager はデバイスドライバを呼び出します。

4. デバイスドライバが AppleTalk デバイスドライバを同期的に呼び出します。これをリクエスト A とします。残念ながら、AppleTalk デバイスドライバは、完全に独立したプロセスからの別のリクエスト (リクエスト B) を処理するためにビジーです。このため、リクエスト A はキューに入り、リクエスト B が完了するのを待ちます。コードは Device Manager の vSyncWait ループの内部に入り、リクエスト B の完了を待ちます。

5. 先行するネットワーキングオペレーション (リクエスト B) が完了します。ネットワーキングハードウェアは CPU に割り込んで、リクエスト B が完了したことを CPU に知らせようとします。しかし残念ながら、CPU は割り込みが無効にされている vSyncWait の内部に入っており (SCSI ハードウェアがステップ 2 で CPU に割り込んだとき、割り込みは SCSI ハードウェアによって無効にされたままです)、ネットワーキングハードウェアからの割り込みを認識しません。システムはデッドロックに陥ります。

この場合の基本的な問題点は、伝統的な Mac OS デバイスドライバがシングルスレッドである、つまり、一度に 1 つのリクエストしか処理できないことです。第 2 のリクエストを行い、ドライバがビジーで、ドライバにリクエストを完了する機能を与えていない場合、デッドロックに陥ります。


ルール #2 に対する違反の事例
デバイスドライバがルール #2 に違反している、つまりデバイスドライバが同期的に呼び出されているか、非同期的に呼び出されているかをテストし、それぞれの場合で異なる動作を実行すると仮定します。次に、この違反がどのようにシステムのデッドロックに至るかを段階的に説明します。
1. Finder は PBWriteAsync を呼び出して、データのチャンクをブロックデバイスドライバに書き込みます (リクエスト A)。このリクエストが非同期的であると判定されたので (ParamBlockioTrap をテストして)、このリクエストを非同期的に AppleTalk に送信し、その後 Finder に戻ります。

2. 別のプロセスが PBWriteSync を呼び出して、一部のデータをドライバに書き込みます (リクエスト B)。ドライバが現在ビジーであるため、このリクエストはドライバのキューに格納されます。

3. AppleTalk ドライバは ioCompletion ルーチンを呼び出して、リクエストが完了したことを示します。これに応答して jIODone を呼び出します。jIODone はリクエスト A を完了し (さらに、Finder の ioCompletion ルーチンを呼び出し)、さらにドライバのキューをチェックして、ペンディングになっているリクエストを探します。リクエスト B がペンディングになっていることが検出され、jIODone はデバイスドライバの Prime を呼び出して、そのリクエスト B を開始します。まだ割り込み時であることを思い出してください。

4. デバイスドライバが呼び出され、リクエスト B が開始されます。デバイスドライバは、ioTrap をテストして、リクエスト B が同期的であることを認識します。その結果、AppleTalk を同期的に呼び出します。この時点で、ドライバは割り込み時にデバイスドライバを同期的に呼び出すことになり、ルール #1 の違反の事例と同様に、システムがデッドロックに陥ることになります。
この場合の基本的な問題点は、ParamBlockioTrap ワードがリクエストが同期的に行われたかどうかを示すだけで、リクエストが非割り込み時に実行されているかどうかを示さないことです。


デッドロックの回避
前述の問題を回避する唯一の方法は、デバイスドライバの内部で非同期的に実行されるデバイスドライバ呼び出しを発行することです。

別のドライバ (DRVR B) を呼び出す伝統的な Mac OS デバイスドライバ (DRVR A) が、次のようなオペレーションを実行するようにします。
1. リクエスト (リクエスト A) を受け付けます。

2. リクエストが同期的であるか非同期的であるかは無視します。

3. 非同期的に DRVR B へのサブリクエスト (リクエスト B1) を発行します。

4. 呼び出し元に戻ります。
サブリクエストは ioCompletion ルーチンを持っているとします。その ioCompletion ルーチンのコードは次のように動作する必要があります。
1. これが、リクエスト A を遂行するために実行する必要のある最後のオペレーションである場合、jIODone にジャンプします。

2. そうでない場合は、同じ ioCompletion ルーチンを使って、DRVR B への別のサブリクエスト (リクエスト Bn) を発行し、その後、呼び出し元に戻ります。
このような方法でデバイスドライバを構造化すると、デバイスドライバをステータスマシンに変えることができます。たとえば、受け取ったそれぞれのリクエストに対して2 つのサブリクエストを発行する必要がある場合、デバイスドライバは、最終的にリクエストB1、およびリクエストは B2 という 2 つのステータスを処理します。完了ルーチンは次のように動作することになります。
1. 完了したサブリクエストからメインリクエストのリザルトバッファの中にデータをコピーします。

2. ステータスが Request B2 である場合、jIODone にジャンプすることで最初のリクエストを完了します。

3. そうでない場合は、ステータスをインクリメントし、次のリクエストを非同期的に発行し、呼び出し元に戻ります。
この構造は、ドライバ (DRVR A) が同期的に呼び出されるか非同期的に呼び出されるかに関係なく、また呼び出しているドライバ(DRVR B) が同期的であるか非同期的であるかに関係なく、正常に動作します。

図 1、2、3、および 4 は、これら 4 つのケースを図示しています。

図 1 ドライバが同期的に呼び出され、同期ドライバを呼び出すケース
figure1

図 2 ドライバが非同期的に呼び出され、同期ドライバを呼び出すケース
figure2

図 3 ドライバが同期的に呼び出され、非同期ドライバを呼び出すケース
figure3

図 4 ドライバが非同期的に呼び出され、非同期ドライバを呼び出すケース
figure4

 同期的に呼び出されているのか、非同期的に呼び出されているのかを検出し、それぞれのケースでオペレーションを変えると、事態を単純化できるとも考えられます。たしかにこの方法は理論的にはエレガントなアイデアに聞こえるかもしれませんが、次の2 つの理由から実際には最悪のアイデアです。
1. いずれにしても、非同期のケースを適切に処理する必要があります。そして、非同期のケースを適切に処理すれば、同期のケースも正常に動作します。半分で十分に動作するコードをわざわざ2 度書くのは無意味です。

2. ルール #2 に違反することになり、結果的にデッドロックの可能性が残されたままになります。

ルールを証明する例外
"ルールを証明する例外" という表現は、実際には例外がルールをテストするという意味です。ルール#1 と #2 には、2 つの重要な例外があり、このトピックを完璧に理解するためには、これらの例外を詳しく調べてみる必要があります。


非同期的に呼び出されないドライバ: 例外 #1
多くの人は、モデルに合致しないことのために伝統的な Mac OS デバイスドライバを使用しています。明らかな例をあげるとすれば、それはデスクアクセサリです。しかし、古いプログラムでは、しばしばデバイスドライバを共有ライブラリメカニズムとして使用しています。この方法で使用されるドライバは、実際にはI/O システムの一部ではなく、一般に非同期的に呼び出されません。ルールの中に、"非同期的に呼び出される場合"という一節があるのはこのためです。

共有ライブラリメカニズムとしてデバイスドライバを使用することはできるかぎり避けてください。現在のMacintosh には必要に応じた、いくつかの現実の共有ライブラリメカニズムが用意されています。

注意:
DRVR を疑似共有ライブラリとして使用する場合は、クライアントにドライバの呼び出しを即座に発行させてください (たとえば、PBControlImmed)。これらの呼び出しは直接ドライバに送信され、いかなる場合もキューに格納されることはありません。



クラシック SCSI Manager: 例外 #2
ルール #1 に矛盾するように見えるケースの一つとして、File Manager と "クラシック" SCSI デバイスドライバとの関係があります。"クラシック" という名称は、SCSI Manager 4.3 以前に書かれた SCSI デバイスドライバを指しています。SCSI Manager が非同期オペレーションをサポートしていなかったため、これらのドライバは必然的に同期ドライバでした。

そうすると、SCSI Manager が必然的に同期的であっても、割り込み時に File Manager を呼び出し、File Manager がさらにデバイスドライバを呼び出し、デバイスドライバが SCSI Manager を呼び出すことはそもそも可能なのでしょうか。

この問いに対する答は簡単です。File Manager には、これをサポートするための特殊なコードが含まれています。非同期リクエストを発行するとき、File Manager は、SCSI ハードウェアがすでに使用されているかどうかをチェックします。すでに使用されている場合、File Manager は、SCSI ハードウェアが再度解放されるまで、コマンドのオペレーションを遅延させます。これにより潜在的なデッドロックを回避することはできますが、ルール #1 の精神には矛盾することになります。

リクエストは非同期であるため、File Manager だけがリクエストを遅延させることができます。File Manager を同期的に呼び出すと、デッドロックに陥ることになります。

要約
大部分の Macintosh プログラマは、ルールにしたがってプログラミングを行おうとしています。しかし残念なことに、伝統的な Mac OS デバイスドライバの場合、そのルールはほとんどマニュアル化されておらず、ときによっては理解するのも困難な状況です。この TECHNOTE では、伝統的な Mac OS デバイスドライバにとって重要な 2 つのルールについて説明しました。これらのルールにしたがうことで、システムを全体としてより信頼性の高いものにすることができます。

参考文献
  • develop 13, Asynchronous Routines on the Macintosh, by Jim Luther
  • Modern Operating Systems, by Andrew S. Tanenbaum, Prentice-Hall, 1992, ISBN 0-13-588187-0