前回(2012/08/27)から、だいぶ間が空いてしまいましたが、続きです。
■サービスとユーザモードドライバの起動の仕組み
前回の説明で、telneted や ftpd などのサービスは、それらの DLL のホストプロセスが servicesd.exe であり、ユーザモードドライバのホストプロセスは udevice.exe だということを述べました。そして、各サービスは、ユーザモードのデバイスドライバとして実装されており、そのため、サービスのホストである servicesd.exe と、通常のユーザモードドライバのホストである udevice.exe は、中核部の実装を共有していることも説明しました。
今回は、両者について、もう少し詳しく見てみます。
まず、サービスとユーザモードドライバが、それぞれ、どのようにしてロードされて起動するのかを見てみましょう。両者ともに、OS の起動時に自動的に起動するように設定することもできれば、アプリケーションからの要求に応じて起動することもできます。
(OS の起動時に自動起動されない設定の)ユーザモードドライバを起動するには、カーネルモードのドライバと同じく、ActivateDevice() または ActivateDeviceEx() を使います。前回のエントリでも紹介したリファレンスのページに書かれている通り、ActivateDevice() や ActivateDeviceEx() の第一引数に渡すドライバのレジストリキーにおいて、Flags の値が DEVFLAGS_LOAD_AS_USERPROC ビット(0×10)を含んでいると、そのドライバは、カーネルではなく、udevice.exe プロセスによってロードされ、ユーザ空間で動作します。
一方、サービスを起動するには、ActivateService() という関数を使います:
ActivateService (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee501277.aspx
ActivateDevice[Ex]() に渡すレジストリキーは、HKEY_LOCAL_MACHINE\Drivers\ 配下のサブキーですが、ActivateService() に渡すレジストリキーは、HKEY_LOCAL_MACHINE\Services\ 配下のサブキーです。つまり、レジストリ設定においては、ユーザモードドライバとサービスは、HKEY_LOCAL_MACHINE\Drivers\ 配下に記述されるのか HKEY_LOCAL_MACHINE\Services\ 配下に記述されるのかによって区別されます。この区別は、OS の起動時における自動起動処理においても使われます。
■デバイスドライバ(ユーザモードドライバ)の自動起動処理
OS 起動時のデバイスドライバの自動起動処理は、デバイスマネージャの働きによって行われます。具体的には、次の手順で、レジストリキー KEY_LOCAL_MACHINE\Drivers\BuiltIn\ 配下に記述されたビルトインのドライバ群が起動されます。この起動手順は、カーネルモードドライバとユーザモードドライバで共通です。上述したように、各ドライバのレジストリキーにおいて、Flags キーの値に DEVFLAGS_LOAD_AS_USERPROC ビットを含むドライバが、ユーザモードドライバとしてロードされます。
1.) デバイスマネージャ(device.dll)のエントリルーチンである DevMainEntry() において、StartDeviceManager() を呼び出す。
2.) StartDeviceManager() は、DevLoadInit() を呼び出すことにより、ブートの第1フェーズでロードされるデバイスドライバ群をロードして起動する。
3.) StartDeviceManager() は、その後、InitDevices() を呼び出して、ブートの第2フェーズでロードされるデバイスドライバ群をロードして起動する。
上の (2) および (3) において、レジストリキー HKEY_LOCAL_MACHINE\Drivers\BuiltIn を指定して ActivateDevice() または ActivateDeviceEx() が呼び出されます。その結果、指定されたレジストリキー配下のドライバ群を数え上げて起動する BusEnum という特殊なドライバが起動し、このドライバが、HKEY_LOCAL_MACHINE\Drivers\BuiltIn\ 配下に記述されたドライバ群をロードして初期化します。
ここで、「ブートの第1フェーズ」と「第2フェーズ」については、このブログの 2011/02/21 のエントリ(「レジストリ変更内容の永続化(2/2)」)をご覧下さい。
BusEnum は、ブートの第1フェーズと第2フェーズのそれぞれにおいてインスタンスが生成されます。以下に、ブートの第1フェーズにおいて BusEnum が生成・起動された時の呼び出し履歴(コールスタック)を示します。興味のある方は、これを手掛かりにして、デバイスマネージャのソースコードをご覧になるのも面白いでしょう:
BUSENUM!BusEnum::BusEnum()
BUSENUM!Init()
DEVMGR!DriverFilterMgr::DriverInit()
DEVMGR!DeviceContent::EnableDevice()
DEVMGR!DeviceContent::InitialEnable()
DEVMGR!I_ActivateDeviceEx()
DEVMGR!DM_ActivateDeviceEx()
K.COREDLL!xxx_ActivateDeviceEx()
DEVMGR!InitDevices()
DEVMGR!DevloadInit()
DEVMGR!StartDeviceManager()
DEVICE!DevMainEntry()
K.COREDLL!ThreadBaseFunc()
注意:ただし、ユーザモードドライバは、ブートの第1フェーズではロードできません。ユーザモードドライバをロードできるのは、ブートの第2フェーズおよび、ブート完了後です。
■サービスの自動起動処理
サービスの自動起動処理は、serviceStart.exe によって行われます。serviceStart.exe は、レジストリキー HKEY_LOCAL_MACHINE\init\ 下に登録され、device.dll 、つまりデバイスマネージャの起動完了後に起動されるように設定されています。具体的には、次の手順で、レジストリキー HKEY_LOCAL_MACHINE\Services\ 配下に記述されたサービス群が起動されます。
1.) serviceStart.exe は、レジストリキー HKEY_LOCAL_MACHINE\Services を指定して ActivateDevice() または ActivateDeviceEx() を呼び出す。
HKEY_LOCAL_MACHINE\Services を指定した ActivateDevice[Ex]() の呼び出しの結果、このレジストリキー配下のサービス群を数え上げて起動する ServicesEnum という特殊なドライバが起動し、ServicesEnum が、HKEY_LOCAL_MACHINE\Services\ 配下に記述されたサービス群をロードして初期化します。
この動作は、”HKEY_LOCAL_MACHINE\Drivers\BuiltIn” が “HKEY_LOCAL_MACHINE\Services” に代わり、そして BusEnum が ServiceEnum に代わった以外は、デバイスドライバの場合と概ね同じです。デバイスドライバ(ユーザモードドライバ)やサービスをロードするホストプロセスは、ユーザモードドライバが udevice.exe でサービスが servicesd.exe という違いはありますが、大枠の処理の流れは同じです。そのため、実装上も、前回のエントリから述べてきたように、udevice.exe と servicesd.exe が中核機能を共有している、というわけです。
以下に、serviceStart.exe から ServiceEnum が呼び出された時のコールスタックを示します:
SERVICESENUM!ServicesEnum::ServicesEnum()
SERVICESENUM!Init()
DEVMGR!DriverFilterMgr::DriverInit()
DEVMGR!DeviceContent::EnableDevice()
DEVMGR!DeviceContent::InitialEnable()
DEVMGR!I_ActivateDeviceEx()
DEVMGR!EX_DM_ActivateDeviceEx()
COREDLL!xxx_ActivateDeviceEx() ★
SERVICESSTART!WinMain()
SERVICESSTART!WinMainCRTStartupHelper()
SERVICESSTART!WinMainCRTStartup()
COREDLL!MainThreadBaseFunc()
ちなみに、上のコールスタックで★を付けた行の、coredell.dll の ActivateDeviceEx() の呼び出しまでがユーザランドで、それより上、つまり、devmgr.dll の EX_DM_ActivateDeviceEx() 以降は、カーネルランドにおける呼び出しです。ActivateDeviceEx() から EX_DM_ActivateDeviceEx() の間には、システムコールが介在しているのですが、カーネルデバッガは、その遷移を通常の関数呼び出しのように見せてくれるのです。
■ホストプロセスを指定する仕組み
次に、ユーザモードドライバやサービスが、どのようにしてホストプロセスに割り当てられるのかを見てみます。前回のエントリで紹介した、WinCE 6.0 のリファレンスにある User Mode Driver Framework のアーキテクチャ図を見て下さい。
User Mode Driver Framework Architecture (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee486510(v=winembedded.60)
この図にある Reflector が、ユーザモードドライバやサービスをホストプロセスに割り当てる処理を実行します。Reflector を呼び出すのは、上のページのアーキテクチャ図にある通り、デバイスマネージャです。
デバイスマネージャは、デバイスドライバの DLL をロードするよう要求された際、その DLL に対して DEVFLAGS_LOAD_AS_USERPROC が指定されている場合は、Reflector のインスタンスを生成します。サービスの場合も、同様にデバイスマネージャに対して DLL のロードが要求され、その際、DEVFLAGS_LOAD_AS_USERPROC が指定されます。その結果、ユーザモードドライバに対してもサービスに対しても、それらの DLL ごとに Reflector のインスタンスが生成されて、DLL に割り当てられます。そして、デバイスマネージャは、Reflector を介して、ユーザモードドライバやサービスとやり取りします。つまり、Reflector が proxy の役割を担います。
Reflector は、自身に割り当てられた DLL をロードさせるホストプロセスを探し、存在しない場合は、それを起動します。DLL をロードさせるホストプロセスが何かというのは、レジストリの設定によって決まります。具体的には、ユーザモードドライバやサービスのレジストリキーにおける UserProcGroup の値によって、ホストプロセスの実体が決まります。WEC 7 のリファレンスですと、次のページに説明があります:
User Mode Driver Framework Registry Settings (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee482921.aspx
ただし、上のページには、UserProcGroup ではなく ProcGroup と記載されています。これは間違いだと思われます。実際、後述する
%_WINCEROOT%/public/common/oak/files/common.reg
では、ホストプロセスの ID を指す値は UserProcGroup となっています。また、’ProcGroup’ という値を指定しても、その値は無視されてしまいます。Reflector のソースファイルにある CreateReflector() の実装を見ても、’UserProcGroup’ という値を参照していますから、上のページの説明が間違っているのだと思います。
Reflector によって生成されたホストプロセスの情報は、デバイスマネージャ内の大域変数に束縛された連結リストに格納され、それぞれのホストプロセスには ID が付けられます。そして、UserProcGroup で指定された ID のホストプロセスが既に生成済みであれば、そのプロセスを呼び出して、ユーザモードドライバやサービスの DLL をロードさせます。UserProcGroup で指定された ID のホストプロセスが、未だ生成されていなければ、生成したうえで、DLL をロードさせます。これらの処理の詳細に興味のある方は、Reflector のソースコードをご覧になってみて下さい。Reflector のソースファイルは、
%_WINCEROOT%/private/winceos/COREOS/device/devcore/reflector.cpp
です。
参考までに、serviceStart.exe によってサービス群がロード・起動される際の、一つのサービスに対して Reflector が生成されるまでの呼び出しのコールスタックを以下に示します:
DEVMGR!CreateReflector()
DEVMGR!Reflector_Create()
DEVMGR!DeviceContent::LoadLib()
DEVMGR!I_ActivateDeviceEx()
DEVMGR!DM_ActivateDeviceEx()
K.COREDLL!xxx_ActivateDeviceEx() ★★
SERVICESENUM!DeviceFolder::LoadDevice()
SERVICESENUM!ServicesEnum::ActivateAllChildDrivers()
SERVICESENUM!ServicesEnum::PostInit()
SERVICESENUM!DefaultBusDriver::FastIOControl()
SERVICESENUM!ServicesEnum::FastIOControl()
SERVICESENUM!DefaultBusDriver::IOControl()
SERVICESENUM!IOControl()
DEVMGR!DriverFilterMgr::DriverControl()
DEVMGR!DriverControl()
DEVMGR!IoPckManager::DevDeviceIoControl()
DEVMGR!DevDeviceIoControl()
DEVMGR!DM_DevDeviceIoControl()
KERNEL!MDCallKernelHAPI()
KERNEL!NKHandleCall()
K.COREDLL!DirectHandleCall()
K.COREDLL!xxx_DeviceIoControl()
DEVMGR!DevicePostInit()
DEVMGR!DeviceContent::EnableDevice()
devmgr.dll の DeviceContent::EnableDevice() が呼び出されるまでの経路は、前項の「サービスの自動起動処理」に示したコールスタックと同じですから、ここでは省略します。上のコールスタックでは、★★を付けた行に注目して下さい。ServiceEnum.dll の DeviceFolder::LoadDevice() から ActivateDeviceEx() が呼び出されています。実は、サービスに対しても、デバイスマネージャ内部では、ロードする際には ActivateDeviceEx() が呼び出されるのです。これは、アプリケーションから ActivateService() を呼び出してサービスを起動する場合も同じです。ActivateService() によってデバイスマネージャにサービスのロードが要求されると、デバイスマネージャは、上のコールスタックと同様、ServiceEnum を介して ActivateDeviceEx() を呼び出すのです。
■ホストプロセスを複数起動する
ユーザモードドライバやサービスを割り当てるホストプロセスは、デバイスマネージャ内の Reflector において ID により識別され、必要に応じて(つまり、UserProcGroup で指定された ID のホストプロセスが起動済みでなければ)起動されることを上で述べました。ユーザモードドライバとサービスを割り当てるホストプロセスの ID は、それぞれデフォルト値があり、ユーザモードドライバのホストプロセス(udevice.exe)は 3 で、サービスのホストプロセスは 2 です。このデフォルト値は、
%_WINCEROOT%/public/common/oak/files/common.reg
で定義されています。common.reg の中にある、PROCGROUP_DRIVER_MSFT_DEFAULT というのが udevice.exe のデフォルト ID で、PROCGROUP_SERVICE_MSFT_DEFAULT が、servicesd.exe のデフォルト ID です。
ユーザモードドライバやサービスに対するレジストリ設定で、デフォルトの ID 以外の値を UserProcGroup に指定すると、デフォルトのものとは別にホストプロセスが生成・起動されます。たとえば、FTP サーバ(ftpd)を組み込んだ OS イメージにおいて、 OS Design のレジストリ設定ファイル(OSDesign.reg)に次の行を追加すると、ftpd 専用の servicesd.exe が起動します。
[HKEY_LOCAL_MACHINE\Services\FTPD]
"UserProcGroup"=dword:8
[HKEY_LOCAL_MACHINE\Drivers\ProcGroup_0008]
"ProcName"="servicesd.exe"
"ProcVolPrefix"="$services"
"ProcTimeout"=dword:20000
上の例では、ID が 8 で servicesd.exe を実行するホストプロセスを設定して、そのプロセスに ftpd.dll がロードされるように、FTPD の UserProcGroup の値に 8 を指定しています。このようにすると、servicesd.exe のプロセスが二つ起動されて、二番目の方には ftpd.dll だけがロードされて動きます。下の図は、カーネルデバッガの「スレッド」ウィンドウで、ftpd.dll だけがロードされた servicesd.exe を表示した画面です。
![2012-12-03.threads ftpd.dll だけをロードした servicesd.exe]()
ftpd.dll だけをロードした servicesd.exe
■Reflector によるホストプロセスの呼び出し
ところで、カーネルの一部であるデバイスマネージャ内の Reflector から、ホストプロセスのユーザモードドライバやサービスを呼び出す処理は、どうなっているのでしょうか?
アプリケーションからカーネルモードのデバイスドライバを呼び出す場合であれば、DeviceIoControl() の呼び出しによってシステムコールが実行され、カーネルに制御が移ったのちに、カーネル内部でデバイスマネージャからデバイスドライバが呼び出されます。しかし、ユーザモードドライバの場合には、カーネルモードからユーザモードへの遷移が必要です。
カーネル内部からユーザプロセス内の DLL を呼び出す機能は、カーネル内部で実装されており、システムコールと似た仕組みです。カーネルのソースコードでいうと、
%_WINCEROOT%/private/winceos/coreos/nk/kernel/apicall.c
にある NKHandleCall() の中で呼び出している MDCallUserHAPI() という関数が、カーネルからユーザプロセスを呼び出すためのものです。この MDCallUserHAPI() は、MD (Machine Dependent) という接頭辞の通り、CPU アーキテクチャごとに異なる実装となり、アセンブラで書かれています。WEC 7 の場合ですと、ARM, MIPS, x86 用のソースが、それぞれ次の場所にあります:
%_WINCEROOT%/private/winceos/coreos/nk/kernel/arm/armtrap.s
%_WINCEROOT%/private/winceos/coreos/nk/kernel/mips/except.s
%_WINCEROOT%/private/winceos/coreos/nk/kernel/x86/fault.c (※インラインアセンブラ)
MDCallUserHAPI() を呼び出している NKHandleCall() は、API の実体を呼び出すための関数ですが、API の「ハンドル」がユーザモードに所属している場合は、MDCallUserHAPI() によってカーネルモードからユーザモードへの遷移を伴う呼び出しを行い、それ以外の場合は、カーネル内部での呼び出しを実行します。カーネルモードからユーザモードへの遷移処理の実体は、apicall.c にある SetupCallToUserServer() (および、この関数から呼び出される、各種プロセッサ依存の実装を持つ関数)です。
■WinCE 6.0 以前の仕組み
さて、WinCE 6.0 以前、つまり WinCE 5.0 までは、デバイスドライバは、全てユーザモードで動作していました。
WinCE 5.0 までは、純粋なマイクロカーネル構造であり、デバイスマネージャは、カーネルにロードされる DLL ではなく、マイクロカーネルとは独立したプロセス(device.exe)だったのです。各デバイスドライバは、ユーザプロセスで動作するため、通常のアプリケーションと同様に API を呼び出すことができ、デバイスドライバが直接 GUI 表示を行うことも可能だったようです。そして、ユーザモードで動作するデバイスドライバがハードウェアを直接制御できるように、ユーザモードから物理アドレスを直接アクセスすることが可能になっていました。
WinCE 5.0 から WinCE 6.0 への移行において、この構造に見直しが加えられ、カーネルランドとユーザランドを明確に区別して、ユーザモードから物理アドレスを直接アクセスできないようになったのです。それに合わせて、仮想記憶機構にも大幅な変更が加えられています。WinCE 6.0 において、デバイスマネージャが、カーネルから独立したプロセスではなくカーネルにロードされる DLL となったことに伴い、デバイスドライバは、デフォルトではカーネルモードで動作するようになりました。これは、パフォーマンスの面では有利である一方、システムの堅牢性という観点から見ると、好ましくない面があります。前回のエントリでも述べたように、デバイスドライバのバグによって、カーネル全体が障害を起こしてしまう可能性があるからです。
そのため、WinCE 6.0 では、(WinCE 5.0 までと同様に)ユーザモードでデバイスドライバを動かすことも可能なように、User Mode Driver Framework が導入されたのです。WinCE 5.0 から WinCE 6.0 への移行における、デバイスドライバ回りのアーキテクチャの変更については、WinCE 6.0 のβ版がリリースされた頃に提供されたと思われるドキュメントが Microsoft 社のサイトからダウンロードできますので、そちらをご覧になると、参考になるでしょう。
Future Directions For The Windows CE Device Driver Architecture
http://download.microsoft.com/download/5/b/9/5b97017b-e28a-4bae-ba48-174cf47d23cd/WCE030_WH06.ppt
WinCE 5.0 までのデバイスドライバと、WinCE 6.0 以降のユーザモードドライバを比べると、物理アドレスを直接アクセスできるかどうかという点が異なります。また、アプリケーションからデバイスドライバを呼び出す場合のオーバーヘッドを考えると、WinCE 6.0 以降のユーザモードドライバは、Reflector を介するために、WinCE 5.0 までのドライバ呼び出しに加えると、若干オーバーヘッドが大きくなっていると思われます(※一方、WinCE 6.0 のカーネルモードドライバは、WinCE 5.0 までとは異なり、カーネルからデバイスマネージャのプロセスを呼び出すシーケンスがありませんから、上述したように、オーバーヘッドが小さくなっていると考えられます)。
デバイスドライバから GUI 表示を行う場合のことを考えると、WinCE 6.0 以降では、ハードウェアを制御する部分をカーネルモードのドライバ、GUI 表示を行う部分をユーザモードドライバとして、分割しなければならず、WinCE 5.0 までのデバイスドライバに慣れ親しんだ開発者にとっては、不便に思える変更だったことでしょう。
なお、堅牢性と柔軟性の観点から考えると、今回のエントリで説明したように、WinCE 6.0 で導入された User Mode Driver Framework では、個々のユーザモードドライバを、それぞれ異なるホストプロセス(udevice.exe)に割り当てることも可能なため、WinCE 5.0 までのドライバアーキテクチャよりも強力だと言えるんじゃないかと思います。開発途上の、不安定なデバイスドライバは、レジストリ設定で専用のホストプロセスを割り当てて動かし、安定した時点で、他のユーザモードドライバと同じホストプロセスへ移す、といった開発の進め方も可能になっているからです。
■マイクロカーネルの考え方~Android との比較
今回のエントリを終える前に、マイクロカーネルの考え方を採用した他の OS として、Android について書いてみます。皆さんご存じの通り、Android は、カーネルに Linux カーネルを用いて構築された OS であり、マイクロカーネルの OS ではありません。WinCE 5.0 を除いて、現在市場で広く使われている純粋マイクロカーネル構造の OS といえば、前回も述べた QNX があります。
しかし、Android という OS の設計には、マイクロカーネルの考え方を踏襲している部分があると僕は思うのです。おそらくは30代以上の、マニアックな OS を好きな方なら、もしかすると BeOS という OS のことをご存じかも知れません。1990年代の後半、SMP 型のマルチプロセッサ機に対応し、パーソナルコンピュータの分野で普及することを目指した OS が BeOS です。実は、Android の 1.0 が発表された頃、公表されていた Android 開発チームのコアメンバー10数人の半分は、その BeOS に関わっていたエンジニアでした。そのためか、Android の内部には、BeOS に由来する仕組みが残っているようです。
特に、Linux カーネルに独自の改変を加える形で実現されている、”binder” という名前の軽量なプロセス間通信機構および、binder を支える “ashmem” という名前の共有メモリ機構は、BeOS の考え方を踏襲したものだと僕には思えます。Android では、Linux カーネルを採用してはいるものの、ユーザランドは全く独自であり、Linux ディストリビューションと呼べる存在では、ありません。Java API として提供される各種ミドルウェアの内部実装を見ると、Linux カーネルを一種のマイクロカーネルとして使い、各種ミドルウェア内部で動作するサービスモジュール(サービスプロセス)群が、軽量プロセス間通信機構と共有メモリ機構を駆使して連携する仕組みとなっているように思われます。
WinCE 5.0 までのデバイスマネージャのような、デバイスドライバをホストするサービスプロセスこそありませんが、音声や映像の入出力・レンダリング処理を司る media server というサービスプロセスなどは、BeOS のファンだった僕にとって、その世界を彷彿とさせるものです。
Android では、それらのサービスプロセス群を起動・監視する役割を担うプロセスとして init が動作し、あるサービスが障害により動作を停止したり強制終了してしまった場合には、それを再起動する仕組みになっているようです。これは、OS が提供するシステム機能を、複数のサービスプロセスに分割して、堅牢性を高めるという、マイクロカーネル構造の考え方に通じるものだと思います。
Android は、Linux カーネルを採用することで、Linux 用に開発されたデバイスドライバをそのまま流用し、そのうえで、開発チームが慣れ親しんでいた BeOS の構造を踏襲した設計を行った OS ではないかというのが、僕の想像です。
振り返って WEC/WinCE を見てみると、WinCE 5.0 までの純粋マイクロカーネル構造から、UNIX 系 OS に近い(そして、WindowsNT 系統のカーネルにも近い)メモリモデルやデバイスドライバモデルに移行しつつも、純粋マイクロカーネル構造の時からモジュール同士のインタフェースを大幅に変更することなく(※実際、デバイスマネージャは、.exe から .dll に変わったものの、インタフェースと内部の構造には、必要最小限の変更しか加わっていません)、移行前の資産のうち活かせる部分は残して、堅牢性を高める工夫をしたと言えるんじゃないかと思います。そして、前回と今回の二回にわたってとりあげた User Mode Driver Framework の設計は、可用性を高める効用があったと評価できる、と思うのです。