Async訪ねて3000里 (2) : ファイルシステム・パーティションマネージャ・物理ディスクデバイスドライバ

前回は、TaskCompletionSourceクラスの触りと、Win32 APIのReadFileExを使った非同期処理について書きました。ここまではいわゆる「ユーザーモード」プロセスでの処理ですが、この先はWindows内部へと入っていきます。

ReadFileExの呼び出しでは、ユーザーモードからカーネルモードへの遷移が行われた後、ntoskrnl.dll(プラットフォームによって異なる場合がある)内の「ZwReadFile」カーネルモードAPIが呼び出されます。このAPIは最初から非同期処理が前提です。

Async2

殆どのカーネルモードAPIは非同期で処理を行う事を前提としていて、俗にいう「NTSTATUS」という型の戻り値として「STATUS_PENDING」を返すことで、非同期処理を開始した事を示します。勿論、NTSTATUSを扱うすべてのAPIが非同期処理を可能としているわけではありませんが、とりわけ外部リソース(ディスクやネットワークなど)にアクセスする類の呼び出しは、非同期処理を行う事が出来るように設計されています。

ZwReadFileが非同期処理前提であると言う事は、ユーザーモードのReadFileExやReadFile APIを同期モードで使用した場合はどうなるのでしょうか? そのような場合は、ZwReadFileの呼び出しは非同期処理として実行しつつ、その非同期処理が完了するのを、「手元」のスレッドをハードブロックする事で同期的に処理されるようにしています。

ハードブロックとは、「WaitForSingleObject」APIを使用して、完了が通知されるまでスレッドを停止させる事です。但し、今回の例では最終的にAPCコールバックを使用するため、実際にはなじみの薄い「WaitForMultipleObjectsEx」APIを使用する事になります(APCコールバックについては、復路で説明します)。


カーネルモードでの入り口は、ファイルシステムドライバです。ほとんどの場合、NTFSドライバが処理します。ZwReadFile APIが受け取った、対象のファイル・対象のファイル中の位置を元に、ディスク(実際にはパーティション)上のどこにそのデータがあるのかを特定します。この時、カーネルのメモリマネージャと裏取引(公開されていない)を行い、空きメモリをキャッシュ用に割り当てて、結果を保持しておくか、以前に保持したデータがキャッシュ上に存在しないかを確認し、存在すればI/Oを発生させず、非同期処理をその場で完了させる、などの処理を行います。

そのため、実際にディスクからデータを非同期で読み取るには、これらの条件に引っかからずに通過する必要があります。CreateFile APIのFILE_FLAG_NO_BUFFERINGフラグは、この処理を確実にバイパスさせます。

ファイルシステムドライバを通過した要求は、パーティション上の相対位置でデータの位置が示されます。つまり、次の窓口は「パーティションマネージャ」です。目的とするパーティションと相対位置を示すアドレス値から、物理ディスク上の絶対アドレス値への変換を行います。また、より複雑な、Windows標準のソフトウェアRAID処理(0・1・5)もここで行われます。

どのような場合であれ、これらの処理もまた非同期操作が前提となっているので、要求は更に下層にそのまま非同期処理として送られる可能性があります。なお、ファイルシステムドライバから先の要求には、明確なAPIシンボル名が存在しません。WDM(Windows Driver Model)は、各ドライバが階層化されたスタック構造となっていて、要求がバケツリレーのように次のドライバへと送られます。エントリポイントはただ一つですが、データの読み取り要求として、「IRP_MJ_READ」という識別子が割り当てられています。


そして、いよいよ物理ディスクのデバイスドライバまで要求が到達します。この先にも更にスタックされたドライバが存在する可能性があるのですが、話が複雑になるので割愛します。少なくともここに到達すると、物理ディスクの読み取り対象アドレス値が明確になっている筈です。従って、この値を物理ディスクに指示する事で、データを読み取る事が出来ます。

その場で応答が返って来ればの話ですが。

そうです。例えばハードディスクドライブの場合、「セクタ123456から4セクタ読み取ってほしい」と指示しても、データの読み出しが完了するのは、途方もなく未来の話です、CPUにとっては。

そこで、次のようにします。「セクタ123456から4セクタ読み取ってほしい。読み取ったデータは物理メモリ空間の0x12345000に格納して欲しい。終わったら呼び出してね」とディスクコントローラーに指示しておきます。そして、例の「STATUS_PENDING」を戻り値として返します。

STATUS_PENDINGを戻り値として受け取ったパーティションマネージャは、そのまますぐにファイルシステムドライバにもSTATUS_PENDINGを返します。ファイルシステムドライバもまた、STATUS_PENDINGを返します。この値はZwReadFile APIの戻り値として、やはりそのまま返され、カーネルモードからユーザーモードに遷移し、ReadFileEx APIが「ERROR_IO_PENDING」という、STATUS_PENDINGのユーザーモード表現の値として返します。

ReadFileEx APIがERROR_IO_PENDINGを返したと言う事は、非同期処理が開始されたことを意味します。つまり、バックグラウンドでのデータ読み取りが始まったことになります。今までの経路を見た通り、この状態に至るまでにワーカースレッドは生成も使用もされていません。従って、CPUはその間に自由に動く事が出来るようになります。

実際に非同期の処理を行っているのは、正にハードウェアそのもの、と言う事が出来ます。
これが、非同期処理がもっとも効率が高いと考えられる理由です。また、async/awaitによる記述だけで、ここまでデバイスに密着した動きをさせながら、かつ、低レベルAPIの直接使用を不要とする事が出来る事が画期的です。

次回は、復路の複雑な動きを解説します。