Multimedia Class Scheduler Service (MMCSS) (1)

今回は,Windows Vista で導入される Multimedia Class Scheduler Service (以下 MMCSS) について取り上げてみたいと思います.その前に,Windows のスレッドスケジューリングについて復習といきましょう.
現在の Windows は,実行スレッドを頻繁に入れ替えることで,いわゆるプリエンプティブなマルチタスク環境を実現している,という話は世間でも広く知られているかと思います.
しかし,実際にどのようなタイミングで実行スレッドが切り替えられているかは,意外と知られていないのではないでしょうか.ヒントは例によって『インサイド Microsoft Windows 第4版〈上〉』に見つかります.

Windowsのスケジューリングコードは、カーネルに実装されています。しかし、単一の「スケジューラ」モジュールやルーチンというものはありません。実装コードは、カーネル内に散在するように置かれ、その中でスケジューリング関連イベントを発生させる方式になっています。これらの機能を実行するルーチンは、集合的に、カーネルディスパッチャと呼ばれています。次のイベントはスレッドディスパッチャを必要とします。

  • スレッドがレディ状態に入ったとき。たとえば、あるスレッドが新規に作成されたり、待機状態から抜け出した直後など。
  • タイムスライスの経過、動作の完了、実行権の放棄、あるいは待機状態に入ったスレッドが実行状態を抜け出したとき。
  • スレッドの優先度が変化したとき。この変化は、システムサービスが呼び出されたときなどにも発生する。また、Windows自身が優先度を変更することもある。
  • スレッドのプロセッサアフィニティが変化し、実行中のプロセッサ上でそれ以上動作できなくなったとき。

アプリケーションから見れば,大まかに 2 種類のスレッド切り替えが存在します.

  1. アプリケーションが API を呼び出し,カーネルモードに移行した結果,カーネルディスパッチャが実行される
  2. タイマ割り込みの結果,カーネルディスパッチャが実行される

前者は,例えばスレッド操作 API であったり,Sleep(0) や SwitchToThread のような実行権を手放す API であったり,WaitMessage や WaitForSingleObject のような明示的な待機 API であったり,ReadFile API のような読み取りが完了するまで内部的に待機する API などが該当するでしょう.これらの状況に共通するのは,あるイベントが発火するのを待っていたり,HDD からのデータ読み取りが完了するのを待っていたりと,そのスレッドの実行をしばらく中断しても問題がない場合です.このような場合,カーネルディスパッチャは,処理の実行を希望している別のスレッドに実行権を移動させます.
ソースコード中にこれらの API が見あたらなくても,例えば C 言語の fread 関数は内部で ReadFile API を呼び出しているでしょうから,単純な C 言語のプログラムでもカーネルディスパッチャの動作について注意を払っておくのことには意味があります.
一方,今回の話で興味があるのは後者の方です.Windows カーネルは,一定時間ごとに割り込みを発生させる周期タイマをカーネルディスパッチャのトリガに使用することで,前述のような API 呼出しが一切行われなかった場合でも強制的にスレッド実行権を移動させられるようにしています.いわゆるプリエンプティブなマルチタスク環境の解説で意図されているのはこちらの仕組みの方でしょう.また,タイマ割り込みは経過時間のカウントにも使用されているので,時間指定された待機動作にも大きく関係しています.
周期タイマは HAL が隠蔽していますが,多くの x86 Windows 環境では 8254 CMOS チップ互換の PIT が使用されています.Local APIC タイマや High Precision Event Timer (HPET) も同用途に使用可能ですが,最近の Windows 環境でどこまで使われるようになってきているのかはよく分かりません(文章末の参考資料[2],[3]を参照してください).
『インサイド Microsoft Windows 第4版〈上〉』の「図3-7 DPC 処理の概要」にあるように,PIT がハードウェア割り込みを発生させると,経過時間の計測が行われ,時間待ちをしていたスレッドの解放が行われます.ただしスレッドディスパッチ処理はすぐには行われず,カーネルは DPC (遅延プロシージャコール) を待ち行列に入れます.その後,IRQL が DPC/ディスパッチレベル以下になったタイミングで,未処理の DPC 行列が全て処理されます.このタイミングで,カーネルディスパッチャは必要に応じて実行スレッドの切り替えを行います.つまり,周期タイマをトリガとしたプリエンプティブなスレッド切り替えの時間粒度は,周期タイマの割り込み間隔に依存することになります.例えば 15 msec の周期でタイマ割り込みが発生する環境で,Sleep(3) (最低約 3 msec スリープせよ) を実行した場合,そのスレッドが活性化すべきかどうかのチェックが最大 15 msec 後になる可能性があるわけです.
いわゆる「Sleep の精度問題」google:Sleep 精度 は,この現象について逆さまから見たものと考えられます.
「Sleep の精度問題」では,「timeBeginPeriod / timeEndPeriod API を使用することで Sleep の精度が向上する」とまとめられていることが多いですが,因果関係としては次のようになっているのでしょう.

  1. timeBeginPeriod(1) のような小さい値を設定することで,PIT の割り込み間隔が短くなる
  2. 割り込みによってトリガされる経過時間の計測間隔・カーネルディスパッチャの実行間隔が短くなる
  3. Sleep によって待機していたスレッドの活性化タイミングが,意図していたタイミングに近くなる

注意して欲しいのは,timeBeginPeriod は Sleep の精度を向上させるための API ではないということです.あるプロセスが timeBeginPeriod を呼び出した結果,システム全体のスレッドスケジューリングのパターンが一変してしまい,あくまでその結果として Sleep の精度が向上したと見るべきでしょう.
身近な例として,Windows Media Player で音声ファイルやムービーを再生すると,他のアプリケーションが滑らかに動くようになったという話などは,このことが影響しているものと考えられます.
一般に,PIT の割り込みタイミングを短くした方が個々の処理のレイテンシは改善されますが,コンテキストスイッチが多発することで全体のスループットは悪化する恐れがあります(文章末の参考資料[1],[2]を参照してください).その点を考慮して,一般的な Windows 環境では 10 msec または 15 msec 程度の値がデフォルト値となっています(参考資料[2]).
Windows ゲームについても,レスポンスを向上させたり,時間揺らぎを減らしたりする目的で,しばしば timeBeginPeriod(1) が使用されます.Direct3D 9 では,D3DPRESENT_PARAMETERS::PresentationInterval に D3DPRESENT_INTERVAL_ONE を指定することで,割り込み間隔を短くするために内部的に timeBeginPeriod が呼び出されます.

Full-screen mode supports similar usage as windowed mode by supporting D3DPRESENT_INTERVAL_IMMEDIATE regardless of the refresh rate or swap effect. D3DPRESENT_INTERVAL_DEFAULT uses the default system timer resolution whereas the D3DPRESENT_INTERVAL_ONE calls timeBeginPeriod to enhance system timer resolution. This improves the quality of vertical sync, but consumes slightly more processing time. Both parameters attempt to synchronize vertically.



参考資料.

  1. id:NyaRuRu:20051226
  2. http://www.microsoft.com/whdc/system/CEC/mm-timer.mspx
  3. http://mowamowa.p.utmc.or.jp/~amedama/cgi-bin/wiki/wiki.cgi?page=Kernel%A5%E1%A5%E2+%BB%FE%B4%D6%B4%C9%CD%FD%CA%D4