Async訪ねて3000里 (3) : ハードウェア割り込みとDPC

前回、カーネルモードでのデータ読み取り要求が、最終的にディスクコントローラーにコマンドとして発行されるところまでを解説しました。ほとんどの場合、コマンドの発行は瞬時に完了するため、ディスクデバイスドライバーも「STATUS_PENDING」をすぐに返却します。

全体を通して、ReadFileEx APIの呼び出しから、ERROR_IO_PENDINGの返却までにかかる時間は一瞬、と見て良いでしょう。そして、ユーザーモードのコードは、直後から自由に別の処理を開始する事が出来ます。裏ではディスクコントローラーがハードディスクからデータを読み取るために、ヘッドを目的のトラックに移動しようとしている所です。

Async3

ここから、CPUは自由に別の処理を実行する事が出来ます。


さて、しばらくして、ディスクコントローラーがハードディスクからデータを読み取った、としましょう。読み取ったデータはSATA等のインターフェイスでコントローラーに送られてきますが、最初のコマンドに記述されていた通り、所定の物理メモリにこのデータを格納します。これはCPUではなく、ディスクコントローラーが自力でメモリに格納します。この動作を「DMA」(ダイレクトメモリアクセス)と呼びます。

そして、すべからく処理が完了した時点で、CPUに対して「俺、用事あるんだけど?」と通知を行います。この事を「ハードウェア割り込み」と呼びます。
殆どの場合、この通知を受け取ったCPUは、すぐに現在処理中の作業を中断し(後で何事もなく作業続行出来るようにするため、状態を全部記憶し)、「割り込み処理」と呼ばれる特別なメソッドの実行を開始します。

割り込み処理のコードは、ディスクデバイスドライバ内にあります。ここで、ディスクデバイスドライバは、改めてディスクコントローラーに状態を確認し、「あ、さっき要求したコマンドが完了したんだ」と言う事が分かります。なので、すぐにユーザーモードのコードに完了を通知したい…のですが、事は単純ではありません。

CPUは直前まで実行していた「何らかの処理」を放り出して、現在の割り込み処理コードを実行しています。つまり、直前までどのような状態であったのかを知ることは不可能に近いのです。例えば、

  • ユーザーモードの.NET ILコードを実行中
  • ユーザーモードの.NET CLRのガベージコレクタを実行中
  • ユーザーモードのエクスプローラーのアドインコードを実行中
  • ユーザーモードのサービスプロセスを実行中
  • ユーザーモードのDirectXライブラリを実行中
  • カーネルモードのWindowsメモリマネージャを実行中
  • カーネルモードのUSBデバイスドライバを実行中
  • カーネルモードのプロセススケジューラーを実行中
  • カーネルモードのディスプレイドライバを実行中
  • カーネルモードで完全にCPUがアイドル状態

など、他にも数限りない状態が考えられます。このような状況で、例えば、処理結果を返却する為に多少のメモリが必要で、動的に確保したいとします。

メモリを確保するには、「ExAllocatePool」カーネルモードAPIを使う事が出来ますが、このAPIの内部処理は、恐らくはWindowsメモリマネージャと、ページング・スワッピングカーネルコードが連携する必要があります。

しかし、上記リストにも示したように、Windowsメモリマネージャの何らかの処理を実行中に、その処理を放り出して割り込み処理を行っているかもしれません。このような状況下でメモリ割り当て処理をそのまま実行しようとすると、ステート管理が破たんする可能性があります。このような問題を「再入処理問題」と呼びます。

Async5従って、割り込み処理の実行中にはメモリ確保は出来ない事になります。他の処理なら可能なのでしょうか? 少し考えると分かりますが、結局、割り込み処理が実行される直前に何が行われていたのかが分からない限り、あらゆる処理は安全に処理できない可能性が非常に高い事となります。また、分かったとしても、あらゆる状況下を判断して個別に対応する事も、殆ど不可能と言って良いでしょう。

WDM(Windows Driver Model)では、ハードウェア割り込みの割り込み処理中でも安全に処理が可能なAPIを公開しています。しかし、そのようなAPIはごくわずかであり、やはり実作業的な操作は出来ません。一般的に推奨される割り込み処理は、DPC(Deffered Procedure Call・遅延プロシージャコール)キューに、キューエントリーを追加する事です。この処理のために、「IoRequestDpc」カーネルモードAPIを使う事が出来ます。もちろん、このAPIはハードウェア割り込み処理中でも使用可能です。


DPCとは一体何でしょうか? 何故このような事をする必要があるのでしょうか?

先ほど述べたように、割り込み処理中はあまりに制限が多く、事実上何も出来ないため、「都合のいいタイミングまで処理を遅延」するためにキューを使用します。DPCは割り込み処理コードと同じように、一種のコールバックです。割り込み処理コードでDPCキューにキューエントリーを追加しましたが、このエントリーが「いつかのタイミング」で拾い上げられ、指定されたコールバックが実行されます。このコールバック内なら(若干の制限はありますが)、割り込み処理では実現出来なかった様々な処理を安全に実行する事が出来ます。

Async4

一般のユーザーモードデベロッパーはDPCになじみが薄いと思うので、少しだけ垣間見えるものをお見せします。この図はSysinternals Process Explorerの表示です。赤枠の行が、ハードウェア割り込みとDPCの統計を表しています。1秒間当たりの割り込みとDPCコールバックの発生回数を示していますが、安静にしていても結構な回数発生している事が分かります。

DPCが「いつかのタイミング」で実行される、と書きましたが、具体的にはいつでしょうか? あまり深入りすると複雑なので省きますが、「CPUがアイドル状態」又は「カーネルモードAPI実行の区切りの良いタイミングを通過するスレッド」が、DPCキューからキューエントリを取り出して、コールバックを呼び出します。従って、ここでもワーカースレッドを生成する事はありませんが、後者の場合は、何らかの不特定なスレッド(Arbitrary thread context)を間借りして実行している、と言えます。このタイミングは、Windowsカーネルが完全に管理しているため、タイミングを任意にコントロールする事は出来ず、CPUアイドル実行か不特定スレッドによる実行かを選択する事も出来ません。また、DPCコールバックを実行中は、特定のスレッドに紐づかないため、スレッドを特定する事は無意味となります。

なお、これまでの説明は、CPUが1個のみ(1コア)である事を前提としています。マルチコア・マルチCPUの場合、これらの動作は更に複雑になりますが、基本的には同じ構造を踏襲しています。

さて、これでユーザーモードのコードに処理完了の通知が出来るようになりました。次回はどうやってユーザーモードに通知されるのかを追います。

第8回まどべんよっかいち「LINQ基本のキ」

第8回まどべんよっかいちで、「LINQ基本のキ」というタイトルで登壇してきました。

時間が短かったので、スライドはコンパクトにまとめ、ディスカッションで補足しました。内容としては、LINQというキーワードは知っているが、全然使った事が無い・SQLと何が違うの?それおいしいの?・クエリが全然読める気がしないんだけど… という方向けの、取り掛かり的なものです。

クエリの中から参照するローカル変数の扱い(評価タイミング)や、そもそも集合演算に慣れるにはどうすれば良いか、といった、慣れてしまった側から見ても有意義な質問が頂けて、楽しかったです。

懇親会で、次回にエントリーしてしまったので、次回はasync/awaitについて、同様な話をしたいと思っています。

スライドはこちら(とても重要な所にアニメーションを使っているので、是非オリジナルで見て下さい):LINQ基本のキ_2

#ちなみに、人、来ました(ほっ)

ご清聴ありがとうございました。
それではまた。

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の直接使用を不要とする事が出来る事が画期的です。

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

Async訪ねて3000里 (1) : ユーザーモードのターン

C# 5.0から、本格的に非同期処理を行うための土台が整いました。何回かの連載で、非同期処理の意味を、Windowsのカーネルレベルにまで掘り下げて紹介したいと思います。

もう周知だと思いますが、C# 5.0では新たに「async」と「await」という予約語が導入され、非同期処理が簡単に書けるようになりました。この効果は絶大で、過去に様々な非同期処理に関する補助ライブラリが出現しては消え、と言う事を繰り返し、結局どれもが市民権を得られなかったのとは対照的です(非同期処理の種類については「避けて通れない「非同期処理」を克服しよう (@IT)」が詳しいので参照して下さい)。

async/awaitが無くても、非同期処理を記述する事は可能です。これは丁度、LINQが使えなくてもループ処理でリストを操作できるのと似ています。そして、async/waitを使えば、実装がはるかに容易になるという点でもまた、LINQとよく似ています。同時に、async/awaitの内部処理(これはC#コンパイラが頑張っている所ですが)もLINQと良く似ています。

async/awaitの予約語を使うと、コンパイラがどのように処理を行うのかは、「非同期メソッドの内部実装とAwaitableパターンの独自実装 (@IT)」の記事に詳しいので説明を省きます。

この連載では、実際に非同期APIを使うと、内部ではどのように処理されるのかに焦点を当てます。なお、.NET CLRやWindowsカーネルの内部実装を「透視」したわけではないので、現実の実装とは異なる場合があります。非同期処理の道筋についての解釈と思って下さい。


私たちはC#の世界に住んでいるのでC#で説明しますが、とりあえずVB.NETについても非同期処理を行うと同じ過程を踏むはずです。他のCLR対応言語は…awaitに対応するものがあるのかどうか、良く分かりません。

Async1

まず、ユーザーコードから、非同期処理対応のAPIを呼び出すことから始まります。これは、.NET 4.0で導入された「Task」クラスのインスタンスを返却するメソッドとして定義されています。例として、ファイルからデータを読み取る「ReadAsync」という架空のメソッドを考えます。

ReadAsyncは、バックグラウンドで非同期的にデータを読み取り、結果を返します。これを従来の技術でユーザーレベルで実現する場合、ワーカースレッドを生成し、その別スレッド内でファイルの読み取り処理を行い、結果が得られた時点で元のスレッドにデータを通知する、という手順を踏むと思います。

しかし、ネイティブな非同期処理(非同期I/O、といった方が良いので、以降では変えます)では、そもそもワーカースレッドに処理を頼ることはしません。ワーカースレッドが処理をせず、どうやって非同期I/Oが実現するのか?は、おいおい明らかになります。

まず、ReadAsyncの「出口」を固めます。出口とは、データの読み取り処理が完了した事をどのようにユーザコードに通知するのか、という点です。
ユーザーコードは単純に「await」予約語を使って待つか、あるいは戻り値の「Task」を使って待機する事が考えられます。

ワーカースレッドの実行と待機を行うのに、「Task.Run」メソッドを使い、返されたTaskインスタンスをWaitメソッドで待機した事があるのでは無いでしょうか。あるいは、ContinueWithメソッドを使って、ワーカースレッドの継続処理を登録して処理させるなど。つまり、Taskクラスのインスタンスは、非同期処理の結果を操作する役目を持っています。

しかし、今回はワーカースレッドは使いません。そのため、別の方法で、非同期I/Oの終了を通知するためのTaskクラスのインスタンスを入手する必要があります。「TaskCompletionSource」クラスは、この目的に使用する事が出来ます。

TaskCompletionSourceクラスは、特定のワーカースレッドに紐づきません。単に、非同期処理の結果だけを管理します。このクラスにはTaskプロパティがあり、TaskCompletionSourceに非同期処理の完了が通知されると、このTaskプロパティのインスタンスを通じて、その結果が(Taskで待っている処理に)通知されます。そして、awaitから処理が継続したり、Waitメソッドから抜けたりするわけです。

この完了時の動作は、いずれ旅路の復路で明らかになるので、ここまでとしておきます。
重要なのは、

  • 非同期I/Oはワーカースレッドを使って実現するわけではない。
  • TaskCompletionSourceを使って、Taskインスタンスを通じて、処理の完了が通知される。

と言う事です。


次に、ReadAsyncメソッドは、P/Invokeなどを使用してWin32の「ReadFileEx」APIを呼び出します。このメソッドは、一般的な「ReadFile」APIに付加機能が付いたものです。C++でWin32レベルのコードを書いた人であれば、ReadFile APIの同期的な使用方法は容易に理解出来ると思います。

ReadFile及びReadFileEx APIには、「OVERLAPPED」構造体が指定出来ます。この構造体は、非同期I/O要求の管理を行うためのもので、ファイル上のデータのオフセットや、完了時のイベント通知の為の情報を保持します。この構造体を指定しない場合は、I/O操作が同期的に実行されます。ほとんどの方はこの引数にNULLを指定したと思います。また、OVERLAPPED構造体を使用する場合は、ファイルハンドルがFILE_FLAG_OVERLAPPEDフラグを指定してオープンされている必要があります

上記のリンクを見て分かる通り、このKBはとても古く、「Windows NT 3.51」と書かれてます。実際、非同期I/Oは最初のWindows NTカーネル(Windows NT 3.1)からサポートされています。つまり、非同期I/Oの土台としては、現在のWindowsが生まれた時から既に存在すると言う事です。

ReadFileEx APIは何が拡張されているのかというと、「完了ルーチン」なる、コールバックメソッドへのポインタが指定可能になっています。これは、非同期I/O処理が完了した際に、ここで指定されたコールバックメソッドが呼び出されると言う事です。

図中では「APC Callback」と書いた箱がそのコールバックメソッドの実装に対応するわけですが、このメソッドがどのようにして呼び出されるのかは、またかなり大がかりな話となるので、復路で述べることにします。

とにかく、これで、ユーザーコードがReadAsyncメソッドを通じて非同期の読み取り操作を開始し、それがReadFileEx APIを通じてWindowsに要求されたところまで来ました。次は更にこの下に潜ります。

SignalR ブートキャンプ on Windows Azureイベント

地理冗長の中心でAzure愛を叫ぶ (名古屋で、Windows Azure ローンチ4周年とJapan Geo誕生を祝うイベント)

という、中部圏のWindows Azureイベントが開催され、登壇してきました。
私のお題は、「SignalR ブートキャンプ」で、SignalRを使った通信の取り掛かりの解説といった内容です。OWINについてもさらっと取り上げています。

本当は、開催と同時にプレゼンとコードを公開したかったのですが、ちょっと未整理が過ぎたので、後日公開のお約束をさせていただきました。で、本日公開いたします。

この発表のハイライトは、ホワイトボードアプリのデモでした。発表中に、実際にAzure上にホストされたサーバーとクライアントアプリ(SilverlightとWPFによるClickOnce、そして今話題沸騰中のWindows Phone :-) をSignalRで接続し、リアルタイムにホワイトボード共有を実演しました。

#クラウディアさんの分身にはお世話になりました
#ライブコーディングは今後の課題ということで(汗

プレゼン作成中はもちろん検証しているのですが、実際に多人数から同時に使用されたのは初めてで、ぶっつけ本番でしたが、何事もなくほっとしています。と同時にあっさり動いてしまう所が、Windows Azure、本当に魅力的です。

プレゼンです:SignalR ブートキャンプ

コードはGitHubで公開しました:AzureSignalRDemonstration

イベント終了後の懇親会も盛り上がりました!
今回のAzureデータセンター日本リージョン開所記念で、Japan Windows Azure User Groupの中部圏「JAZUG名古屋」もお披露目されました。

また、MiCoCiは中部圏のWindows系技術勉強会の開催などやってます。興味のある方はDSTokaiカレンダーあたりをチェックしてみて下さい。

近日では、Bar Windows Azureの開催を計画しています。

それではまた!