Posts filed under 'OS の内部動作'
前回の続きです。アプリケーションとカーネルランドを一緒にデバッグするやり方に関連して、いくつか補足します。
■カーネルデバッガと一緒に動かす場合の設定(※ネットワークインタフェースが一つだけの場合)
さて、カーネルデバッガとアプリケーションデバッガを一緒に動かす場合、一つ注意しなければいけません。それは、WinCE/WEC に固定 IP アドレスを設定する場合のレジストリキーです。前回の「ターゲットボードとホスト PC を直結する場合」の説明で、IP アドレスを設定する場合のレジストリキーが
[HKEY_LOCAL_MACHINE\Comm\<アダプタ名>\Parms\Tcpip]
だと書きました。カーネルデバッガを動かす場合、この「アダプタ名」が、動かさない時と違う場合があるのです。それは、ターゲットボードにネットワークインタフェースが一つしかなく、アプリケーションデバッガの Ethernet 接続と、カーネルデバッガの Ethernet 接続が、一つのネットワークインタフェースを共有する(つまり、Ethernet ケーブルを一本しか使わない)場合です。
その場合、カーネルデバッガを動かす時は、アダプタ名として “VMINI1″ を指定しなければいけません。VMINI1 というのは、前回の例で示した “FEC1″ のような、Ethernet コントローラを直接制御するデバイスドライバではなく、仮想のネットワークデバイス(論理デバイス)のアダプタ名です。この仮想ネットワークデバイス(VMini)は、リファレンスの次のページで説明されています:
Ethernet Debugging Services (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-us/library/ee478406(v=winembedded.60).aspx
WEC 7 のリファレンスには、VMini について説明した個所が見当たりません。もしかすると、WEC 7 からは、VMini の使用を推奨しておらず、そのため、説明を省略しているのかも知れません。なお、KITL についてご存じの方は、ここと、次の説明(「KITL について、もう少しだけ」)は飛ばして下さい。
以下は、KITL について詳しくない方のための説明です。
上のページの図を見ると、”VBridge” というモジュールが、”Vmini.dll” と “EDBG” の二つのモジュールに繋がっており、”Vmini.dll” は、NDIS インタフェースを介して TCP/IP スタックと繋がっています。この “VBridge” が、カーネルデバッガとアプリケーションデバッガが一つのネットワークインタフェースを共有するためのモジュールで、”Vmini.dll” が VMini のことです。VMini は、ネットワークインタフェース、つまり Ethernet コントローラを直接制御せず、VBridge に委譲して間接的に制御します。VBridge は、VMini に加えて、”EDBG”、つまり、カーネルデバッガが使用する Ethrnet 通信機能のモジュールに対してもネットワークインタフェース機能を提供します。
この VBridge の仕組みから分かるように、カーネルデバッガは、Ethernet 経由でホスト PC と通信しますが、TCP/IP スタックは使用せず、独自のプロトコルで通信します。TCP/IP スタックは、カーネルの上位にありますから、カーネルデバッガが TCP/IP スタックを利用することは、できないのです。カーネルデバッガがホスト PC と通信するプロトコルは、KITL (Kernel Independent Transport Layer) と名付けられており、上のページの一つ上のドキュメント階層で説明されています。
Kernel Independent Transport Layer (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-us/library/ee479323(v=winembedded.60).aspx
Kernel Independent Transport Layer (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee479323.aspx
KITL の説明は、WEC 7 のリファレンスにもあり、どちらも同じ図が載っています。この図の “KITL” の、”Device hardware” に近い側の部分(下側の部分)に先ほどの図の “VBridge” があり、そして、”Kernel debugger” に近い側の部分(上側の部分)に “EDBG” があると考えて下さい。
本題に戻ります。ネットワークインタフェースが一つしかなく、それを KITL と TCP/IP スタックで共有する場合、通常の Ethernet コントローラのドライバでは対応できません。そのため、通常の Ethernet コントローラのドライバ(先の例の “FEC1″)は使われず、VMini が代わりに OS イメージに組み込まれ、VBridge の働きにより、TCP/IP スタックと KITL がネットワークインタフェースを共有する、というわけです。そのため、固定 IP アドレスを設定する場合、レジストリキーで指定する TCP/IP スタックのアダプタ名を、VMini のアダプタ名である “VMINI1″ にしなければいけないのです。
■KITL について、もう少しだけ
ところで、KITL は Ethernet 接続でしか使えないかといえば、そうではありません。Kernel *Independent* Transport Layer という名前の通り、Ethernet 以外の様々な伝送路に対応できます。そして、Ethernet の KITL を使用して、TCP/IP スタックと Ethernet コントローラを共有する場合は、TCP/IP スタックが Ethernet コントローラを占有する場合に比べて、Ethernet による通信のパフォーマンスが低下します。これは、同時に二種類の通信が同じ伝送路を使うせいでもでありますし、また、VMini と VBrige、および、VBridge が呼び出す Ethernet コントローラの制御ルーチン(※この制御ルーチンは、OAL の一部として実装されます)の組み合わせによる動作は、通常の Ethernet コントローラのドライバの単体動作に比べてオーバーヘッドが大きいため、パフォーマンスを出しづらい、という事情もあると思われます。
そのため、WinCE/WEC のネットワークのパフォーマンス評価を行う場合は、VMini を無効にした状態で行うようにという注意書きがリファレンスに書かれています。上の KITL の説明ページにも、KITL と OS(TCP/IP スタック)がハードウェアを共有するとパフォーマンスに悪影響がある、という但し書きがあります。
年々、組み込み機器用のプロセッサは高機能化が進み、多くの周辺機器のコントローラを内蔵するようになってきています。そのため、カーネルデバッガ/KITL と TCP/IP スタックが一つのネットワークインタフェースを共有しなくても、たとえば KITL にはシリアルや USB での接続を割り当て、Ethernet コントローラは TCP/IP スタックに占有させる、といった構成は可能です。逆に、プロセッサに内蔵された Ethernet コントローラはデバッグ専用にして KITL に割り当て、TCP/IP スタック用には、USB や SD の WiFi モジュールを使う、というような構成や、あるいは、安価な Ethernet コントローラを外付けして、そちらをデバッグ専用に使う、という方策も考えられます。
いずれにせよ、VMini による、TCP/IP スタックと KITL の共存は、デバッグ専用のものであり、通常構成で使うものではありません。もちろん、Ethernet コントローラのドライバをデバッグする場合には、VMini は利用できません(※Ethernet コントローラのドライバをデバッグしようとすると、同じコントローラを使う KITL の動作に影響を与えるから・・・ではなく、そもそも、デバッグしたいドライバの代わりに VMini が動作するので、デバッグする方法がありません)。
■ソースコードも、見てみましょう
上で述べた VBridge は、KITL (kitl.dll) に組み込まれるモジュールとなっており、そのソースコードは、WEC 7 と WinCE 6.0 のソースツリーの、それぞれ次の場所にあります:
・WEC 7
%_WINCEROOT%/platform/common/src/common/kitl/
・WinCE 6.0
%_WINCEROOT%/PUBLIC/COMMON/OAK/DRIVERS/ETHDBG/VBRIDGE/
WinCE 6.0 では、VBridge は vbridge.lib という static library でしたが(※ソースファイルが vbridge.c 一つだけのライブラリ)、WEC 7 では、oal_kitl.lib という static library にまとめられています。oal_kitl.lib は、WinCE 6.0 にもありますが、VBridge は別のライブラリになっていました。WEC 7 になって、VBridge(vbridge.c) が oal_kitl.lib にまとめられた、というわけです。
ちなみに、カーネルデバッガの OS 側モジュール(kd.dll)のソースコードは、WEC 7 も WinCE 6.0 も、次の場所にあります:
%_WINCEROOT%/private/winceos/COREOS/nk/kdstub/
興味のある方は、どのような実装になっているのか、ソースコードをご覧になってみると面白いんじゃないかと思います。
■おまけ:ネイティブコードのアプリケーションの場合
さて、.NET CF アプリケーションではなく、ネイティブコード(アンマネージドコード)のアプリケーションをデバッグする場合は、どうなのでしょうか?
Visual Studio の「スマートデバイス」プロジェクトで作成したネイティブコードのアプリケーションをデバッグする手順は、.NET CF アプリケーションの場合と同様です。前回と今回で説明した手順を使えば、ネイティブコードのアプリケーションも、カーネルランドと一緒にデバッグできます。試してみて下さい。
ここで、注意深い方であれば、「あれ?」と思われたかも知れませんね。前回の説明では、冒頭で次のように書いたからです:
皆さんご存じの通り、ネイティブコードのアプリケーションの場合には、アプリケーションもカーネルランドも、どちらもカーネルデバッガでデバッグできます。
これは、何だったのでしょうか?前回の冒頭で書いたのは、OS Design のサブプロジェクトとしてアプリケーションを作成した場合のことを念頭に置いていました。つまり、OS イメージをビルドする際に、OS と一緒にビルドして、デフォルト設定では OS イメージに組み込まれるようにした場合のことを指していたのです。その場合は、カーネルデバッガを使って、カーネルランドと同時にアプリケーションをデバッグできます。
では、OS Design のサブプロジェクトで作成したアプリケーションではなく、Visual Studio の「スマートデバイス」プロジェクトで作成したネイティブコードのアプリケーションは、カーネルデバッガを使ってデバッグできないのでしょうか?実は、できます。
Visual Studio の「スマートデバイス」プロジェクトで作成したネイティブコードのアプリケーションを、カーネルデバッガでデバッグするには、アプリケーションの実行ファイル(.exe)とプログラムデータベースファイル(.pdb)を、カーネルデバッガが認識できる場所に置けばよいのです。OS Design のカタログ項目で、”Target Control Support(Shell.exe)” か “Release Directory File System” を選択して OS イメージをビルドしておき、Flat Release Directory に、デバッグしたいアプリケーションの .exe と .pdb をコピーする、というのが一番お手軽だと思います。こうしておけば、わざわざアプリケーションデバッガを別に動かさなくとも、カーネルデバッガだけで、アプリケーションとカーネルランドを一緒にデバッグできます。
たとえば、OS を開発するチームとアプリケーションを開発するチームが分かれていて、アプリケーションで不具合が起きた場合に、不具合の要因がアプリケーション側にあるのか OS 側にあるのか切り分け調査を行うなどの場合、この方法は便利かも知れません。アプリケーション開発チームは、OS Design のサブプロジェクトではなく、Visual Studio の「スマートデバイス」プロジェクトでアプリケーションを作成するのが一般的だと思います。そのような場合に、カーネルランドとアプリケーションを同時または一緒にデバッグする方策は、次のものが考えられます:
- カーネルデバッガとアプリケーションデバッガを各々動かして、カーネルランドとアプリケーションを一緒にデバッグする。
- 「スマートデバイス」プロジェクトを OS Design のサブプロジェクトに作り直し、アプリケーションを OS Design に組み込んだうえで、カーネルデバッガを使ってカーネルランドとアプリケーションを一緒にデバッグする。
- 「スマートデバイス」プロジェクトは、そのままにして、アプリケーションの .exe と .pdb を Flat Release Directory にコピーしてからカーネルデバッガを動かす(上述した方法)。
上の1は、前回と今回の主題である、.NET CF アプリケーションをカーネルランドと一緒にデバッグするのと同じ方策です。2と比べると、3の方が手間が少ないのは明らかです。1と3を比べても、カーネルデバッガだけで作業できるという点で、3の方が、より手間が少ない方法です。
■おまけ2:その他の参考資料
前回と今回では、カーネルランドと一緒(または同時に)アプリケーションをデバッグするにはどうすればよいか、ということを説明しました。「スマートデバイス」プロジェクトで作ったアプリケーションだけをデバッグする場合については、Visual Studio のリファレンスに説明があります。いくつかのトピックが取り上げられていますので、ご覧になってみて下さい:
Debugging Device Projects
http://msdn.microsoft.com/en-us/library/ms180772(v=vs.90).aspx
ネイティブコードのアプリケーションに関しては、MFC や ATL を使ったアプリケーションをビルド・デバッグする場合の注意点や手順についても、解説されています。
Building and Debugging Visual C++ Device Projects
http://msdn.microsoft.com/en-us/library/c5fc53wa(v=vs.90).aspx
2012/04/13
koga
今回は、マネージドコードのアプリケーション、つまり、.NET CF のアプリケーションとカーネルランドを、Ethernet 経由で一緒にデバッグする方法を説明します。「一緒にデバッグする」というのは、”「同時にデバッグする」ことはできないが、アプリケーションデバッガとカーネルデバッガを各々動かすことにより、アプリケーションとカーネルランドを、それぞれのデバッガでデバッグする”という意味です。
皆さんご存じの通り、ネイティブコードのアプリケーションの場合には、アプリケーションもカーネルランドも、どちらもカーネルデバッガでデバッグできます。たとえば、アプリケーションからデバイスドライバを呼び出した場合、デバイスドライバのソースコードにブレークポイントを設定してブレークした時、アプリケーションからデバイスドライバまでのコールスタック(「呼び出し履歴」)を見ることができます。アプリケーションがデバイスドライバを呼び出す場合、システムコールを経由しますが、カーネルデバッガは、その経路を把握して、あたかも通常の関数呼び出しのように、コールスタックを表示してくれるのです。僕は、このように、一つのデバッガだけでアプリケーションとカーネルランドをデバッグすることを指して「同時にデバッグする」と表現しています。
一方、マネージドコード(.NET CF)のアプリケーションの場合には、一つのデバッガだけでアプリケーションとカーネルの両方をデバッグすることは、できません。アプリケーションのデバッガとカーネルデバッガを各々動かす必要があるのです。このことを指して、ここでは「一緒にデバッグする」と表現します。
■Ethernet 経由でのアプリケーションデバッガ接続
Ethernet 経由で WinCE/WEC のアプリケーションデバッガを接続する場合は、ActiveSync は使用しません。ActiveSync は、2005年にリリースされた 4.0 から TCP/IP 接続をサポートしていませんから、使えないのです。ActiveSync を使わずに、TCP/IP で WinCE/WEC に接続する手順は、Visual Studio のリファレンスのページで説明されています:
How to: Connect to Windows CE Device Without ActiveSync
http://msdn.microsoft.com/en-US/library/ms228708(v=vs.90).aspx
上のページは、Visual Studio 2008 のリファレンスです。日本語の説明を読みたい場合は、Visual Studio 2005 ですが、ユニダックス社の「CE6.0 技術情報」に日本語の説明があります:
11. アプリケーションのリモートデバッグ
http://www.unidux.co.jp/embedded/techinfo/ce6/000437.php
上のページで説明されているように、Visual Studio に付属している次の5つのファイルを、WinCE/WEC の /Windows ディレクトリへコピーして、Visual Studio のデバイス設定を行うことにより、TCP/IP でアプリケーションデバッガを接続できます。
Clientshutdown.exe
ConmanClient2.exe
CMaccept.exe
eDbgTL.dll
TcpConnectionA.dll
WEC 7 で、AMR9 コアの CPU のボードに接続する場合ですと、上記のファイルは、次の場所にあります。
C:\Program Files\Common Files\microsoft shared\CoreCon\1.0\Target\wce400\armv5\
これらのファイルを、WinCE/WEC が起動した後に、/Windows ディレクトリに USB メモリなどを使ってコピーするか、または、OS Design の project.bib ファイルを編集して OS イメージに組み込めば、使えます。
または、OS Design のカタログ項目で、”Target Control Support(Shell.exe)” か “Release Directory File System” を選択している場合は、Flat Release Directory(環境変数 _FLATRELEASEDIR が指すディレクトリ)に上記5つのファイルを置いても、使えます。Target Control Support を有効にしてカーネルデバッガを使っている場合には、これが一番お手軽でしょう。
■OS Design から作った SDK との関連づけ
マネージドコードのアプリケーションも、ネイティブコードのアプリケーションも、WinCE/WEC のアプリケーションは、Visual Studio の「スマートデバイス」プロジェクトで作成します。それらのアプリケーションをデバッグする場合は、上で紹介したページで説明されている手順で、WinCE/WEC と接続する必要があります。
Visual Studio 2008 日本語版と WEC 7 の場合ですと、以下のような手順になります。
- 上記の5つのファイルを、WEC 7 の /Windows ディレクトリへコピーする。
- WEC 7 を DHCP 有効で動かしている場合は、コマンドプロンプトで ipconfig を実行して、WEC 7 の IP アドレスを調べる。
- Visual Studio 2008 の [ツール] > [オプション...] メニューを選択し、「オプション」ダイアログを開く。
- 「オプション」ダイアログの左端にあるツリービューで、[デバイスツール」を展開し、その下にある「デバイス」を選択する。
- 「オプション」ダイアログの右端に表示された「デバイス」画面にある、「デバイス(V):」というリストボックスから、アプリケーションのビルドに使った SDK のデバイスを選択する。
- 「デバイス(V):」リストボックスの右側にある「プロパティ」ボタンをクリックして、選択したデバイスのプロパティダイアログを開く。
- プロパティダイアログの「トランスポート(R):」というドロップダウンリストから、「TCP 接続トランスポート」を選択する。
- 「トランスポート(R):」ドロップダウンリストの右側にある「構成(C)...」ボタンをクリックして、「TCP/IP トランスポートの構成」ダイアログを開く。
- 「TCP/IP トランスポートの構成」ダイアログにある二つのラジオボタンのうち、「特定の IP アドレスを使用(S):」を選択する。
- 「特定の IP アドレスを使用(S):」の下にあるエディットフィールド兼ドロップダウンリストに、WEC 7 の IP アドレスを入力して設定する。
- 「TCP/IP トランスポートの構成」ダイアログ、プロパティダイアログ、および「オプション」ダイアログの OK ボタンを順にクリックして、設定内容を確定する。
- WEC 7 で、ConmanClient2.exe と CMaccept.exe を順に実行する。
- Visual Studio 2008 の [ツール] > [デバイスへの接続...] メニューを選択する。
■ターゲットボードとホスト PC を直結する場合
さて、上の手順では、WinCE/WEC を DHCP 有効で動かしている場合(デフォルトでは、DHCP 有効です)、WinCE/WEC を起動するたびに IP アドレスを調べて Visual Studio の「TCP/IP トランスポート構成」の設定に反映しなければいけません。DHCP 有効ではなく、固定 IP アドレスで動かせば、WinCE/WEC を起動するたびに設定する必要がなくなり、少しだけ手間を減らせます。
あるいは、WinCE/WEC とホスト PC をハブ経由で接続せず、クロスの Ethernet ケーブル(*)で直接接続する場合は、DHCP サーバにアクセスできませんから、WinCE/WEC とホスト PC 共に、固定 IP アドレスを割り当てる必要があります。
※最近のネットワークインタフェースは、ストレートの Ethernet ケーブルで直接接続しても、自動認識してくれるものが多くなっています。従って、WinCE/WEC とホスト PC を Ethernet ケーブルで直接接続する際、クロスケーブルがなければ、ストレートケーブルで接続しても大丈夫な場合があります。手元にクロスケーブルがない場合は、試してみて下さい。
なお、Ethernet 経由でリモートデバッグする場合、WinCE/WEC とホスト PC をハブ経由で接続するよりも、直接接続する方が、一般的には通信速度が上がりますので、より快適にデバッグできます。
Visual Studio を動かすホスト PC、つまり、Windows 7 や Vista/WindowsXP に固定 IP アドレスを設定する手順は、皆さんご存じだと思いますので、ここでは、WinCE/WEC に固定 IP アドレスを設定する方法を述べます。
WEC 7 と WinCE 6.0 の IP アドレス設定のレジストリは、リファレンスの以下のページで説明されています:
TCP/IPv4 Configurable Registry Settings (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee494881.aspx
TCP/IPv4 Configurable Registry Settings (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee494881(v=winembedded.60).aspx
上のページの中ほどにある、”Adapter-specific Values” という表を見て下さい。ここに、”EnableDHCP”, “DefaultGateway”, “IpAddress”, “Subnetmask” が載っています。これらのレジストリ項目を使って、WinCE/WEC に固定 IP アドレスを設定できます。レジストリキーは、これらのページに書かれている通り、
[HKEY_LOCAL_MACHINE\Comm\<アダプタ名>\Parms\Tcpip]
です。たとえば、弊社が提供している Armadillo-400 シリーズ用の BSP であれば、以下の行を .reg ファイルに書いて OS イメージをビルドすると、クラス C の固定 IP アドレス 192.168.0.50 が設定されます:
[HKEY_LOCAL_MACHINE\Comm\FEC1\Parms\Tcpip]
"EnableDHCP"=dword:0
"DefaultGateway"="192.168.0.1"
"IpAddress"="192.168.0.50"
"Subnetmask"="255.255.255.0"
ここまでの設定は、Visual Studio のスマートデバイスプロジェクトのアプリケーションを Ethernet 経由でデバッグするための手順です。次に、今回の本題である、マネージドコード(.NET CF)のアプリケーションをカーネルランドと一緒にデバッグする方法を説明します。
■カーネルデバッガも一緒に動かす方法
今回の冒頭で、「.NET CF のアプリケーションとカーネルランドを一緒にデバッグするとは、アプリケーションデバッガとカーネルデバッガを各々動かすことにより、それぞれをデバッグすること」だと述べました。アプリケーションデバッガとカーネルデバッガを各々動かすというのは、どういうことかと言えば、Visual Studio を二つ起動して、一方でアプリケーションデバッガ、もう一方でカーネルデバッガを動かす、ということです。
以下に、もう少し具体的な手順を述べます。以下のようにすれば、アプリケーションデバッガとカーネルデバッガを各々動かして、.NET CF のアプリケーションとカーネルランドを一緒にデバッグできます。
- Visual Studio を起動して、OS Design のプロジェクトを開く。
- OS Design のプロジェクトをビルドしてできた OS イメージを、ホスト PC と Ethernet 接続したターゲットボードに転送し、カーネルデバッガを有効にして WinCE/WEC を起動する。
- Visual Studio をもう一つ起動して、デバッグしたいアプリケーションのプロジェクトを開く。
- 上で述べた手順を使って、アプリケーションのプロジェクトを開いた Visual Studio から WinCE/WEC に接続する。
- アプリケーションのデバッグを開始する。
このようにすれば、二つ起動した Visual Studio のうち、OS Design のプロジェクトを開いた方でカーネルデバッガが動き、アプリケーションのプロジェクトを開いた方でアプリケーションデバッガが動きます。なお、アプリケーションデバッガで設定したブレークポイントでアプリケーションが停止した場合、OS(WinCE/WEC)は動作し続けていますが、カーネルデバッガで設定したブレークポイントで停止した場合は、OS 全体が停止しますので、アプリケーションの動作も停止します。これは、アプリケーションデバッガとカーネルデバッガで違う点です。
以上で、アプリケーションとカーネルランドを一緒にデバッグする方法の説明は、一応終わりです。ただし、いくつか補足が必要なので、次回に続けます。もし、上の手順を試してみたが、アプリケーションデバッガとカーネルデバッガを一緒に動かせない、という方は、次回の説明もあわせて読んでみて下さい。
2012/04/03
koga
前回のエントリでは、WinCE/WEC のウォッチドッグタイマ機能について、ハードウェアタイマとソフトウェアタイマの二種類があることを述べました。そして、ハードウェアタイマとソフトウェアタイマでは、ソフトウェアタイマの方が、より柔軟であり、以下の点が異なると説明しました:
- ソフトウェアタイマは、複数の監視対象を設定できる(タイマを複数作成できる)
- ソフトウェアタイマは、タイマ満了時に発火させるアクションとして、デバイスをリセットする以外の動作を設定できる
- ソフトウェアタイマの場合は、カーネルではなく、監視対象のアプリケーションやスレッドが、タイマのリフレッシュ動作を行う
今回のエントリでは、上に示した、ハードウェアタイマとソフトウェアタイマの違いのうち、2. について補足します。ウォッチドッグタイマ機能において、ソフトウェアタイマとハードウェアタイマのどちらも、タイマの満了時にデバイスをリセットできますが、リセット動作の詳細は、異なるのです。
■ハードウェアタイマについての注意点
前回のエントリでは、ソフトウェアタイマがタイマ満了時に発火させることのできる三通りのアクションのうち、WDOG_RESET_DEVICE のことを、「OS をリブート(デバイスをリセット)する」動作だと説明しました。この「OS をリブート」というのが、ハードウェアタイマとは違う点です。ハードウェアのウォッチドッグタイマは、プロセッサを強制リセットします。強制リセットする際、OS がどのように動作しているのかは関知しません。
ここで、ハードウェアタイマの場合には、WinCE/WEC のカーネルが(より正確には、ウォッチドッグタイマ用のカーネルスレッドが)タイマのリフレッシュ動作を行います。従って、ハードウェアタイマが満了したということは、カーネルが動作を停止しているということですから、プロセッサを強制リセットするのが妥当です。通常は、その通りです。しかし、何らかの不具合で OS 全体がフリーズしているわけではないのに、カーネルが動作を停止する場合もあるのです。どんな場合でしょうか?
それは、カーネルデバッガを使っている時です。カーネルデバッガを使ってデバッグ動作している状態では、ブレークポイントにヒットすると、OS 全体の動作が停止します。OS 全体の動作が停止した状態であっても、ハードウェアタイマは動作し続けますから、ハードウェアタイマの満了時間が過ぎると、その時点で、ハードウェアのウォッチドッグタイマの働きにより、プロセッサが強制リセットされてしまうのです。ソフトウェアタイマの場合には、タイマの動作も停止していますから、そのようなことは、起きません。
カーネルデバッガを使ったデバッグ中に、予期せぬ強制リセットが起きしてしまわないよう、カーネルデバッガを使う時にはハードウェアタイマを動かさないか、または、タイマの満了時間をできるだけ長い値に設定して下さい。
■ソフトウェアタイマによるリセット動作
さて、ソフトウェアタイマのリセット動作は、「OS をリブート」する点がハードウェアタイマと違うと上で述べました。これについて、説明します。
まず、ハードウェアタイマのリセット動作では、プロセッサのリセット信号線を使って、ハードウェアによるリセットを行います。つまり、ハードウェアのウォッチドッグタイマが、プロセッサに対してリセット信号を出力する(プロセッサのリセット入力信号線に供給する電圧の論理値を、リセット用の値に変化させる)ことによって、プロセッサを強制リセットします。
一方、ソフトウェアタイマでは、ウォッチドッグタイマ用のカーネルスレッドが、OS のリブート処理を始動することによって、リセット動作を起こします。具体的には、IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出します。興味のある方は、ソースコードをご覧になってみて下さい。ウォッチドッグタイマ用のカーネルスレッドのソースは、前回述べた通り、
%_WINCEROOT%/PRIVATE/WINCEOS/COREOS/NK/KERNEL/watchdog.c
にあり、WatchDogTimerThrd() が、そのスレッドが実行する手続きです。WatchDogTimerThrd() を見ると、ソフトウェアタイマのどれか一つが満了すると、タイマの状態が WD_STATE_SIGNALED になり、その結果、そのタイマに設定されたアクションが実行されることが分かります。タイマに設定されたアクションの実行は、WDTakeDfltAction() を呼び出すことによって行われますが、WDTakeDfltAction() では、タイマに設定されたアクションが WDOG_RESET_DEVICE の場合、IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出す実装になっています。
IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出すことによって OS のリブート動作が起きる仕組みについては、2009/07/20 のエントリ(「アプリケーションからリブート(または電源 OFF)~その1(1/2)」)に書いています。もし興味があれば、お読みになってみて下さい。
2009/07/20 のエントリで書いたように、カーネルスレッドが、IOCTL_HAL_REBOOT を引数として KernelIoctl() を呼び出した場合には、レジストリやファイルシステムのフラッシュ動作が実行されたのち、OALIoCtlHalReboot() が呼び出されます。OALIoCtlHalReboot() は、カーネル移植レイヤ(OAL)で実装されており、プロセッサごとに異なるリセット動作を実行します。一般的には、ハードウェアのウォッチドッグタイマの発火時間を短い値にセットしたのち、無限ループを実行し、ハードウェアタイマによってプロセッサを強制リセットさせます。
■ハードウェアタイマについて、もう一度だけ注意
ここで重要なことは、ソフトウェアタイマによるリセット動作では、プロセッサをリセットする前に、レジストリやファイルシステムのフラッシュ動作を実行する、という点です。このことにより、ウォッチドッグタイマが発火してリセット動作が起きた際に、永続記憶域上のレジストリ内容やファイルシステムが、壊れてしまう危険性が抑えられるのです。ソフトウェアタイマが発火する原因になった監視対象のアプリケーションが、ファイル操作を行っていた場合には、ファイル内容が壊れてしまう可能性はあります(ファイルの書き込みを行っている途中でフリーズした場合など)。しかし、ディレクトリが壊れて読めなくなってしまったりするといった致命的なことは、起きないようにするというわけです。
この点も、ハードウェアタイマとは違う点です。通常動作中に OS 全体がフリーズしてしまった場合は、強制リセットが起きるのは仕方ありません。しかし、カーネルデバッガを使ったデバッグ中に、ハードウェアタイマが満了してしまい、予期せぬ強制リセットによってレジストリやファイルシステムが壊れてしまう、というようなことが起きないよう、ハードウェアタイマを使う場合には、ご注意ください。
2012/03/27
koga
■ウォッチドッグタイマの必要性
組み込み機器、特に、PC のような対話的な操作ができない、ヘッドレスのデバイスなどでは、より高い信頼性や耐障害性が求められます。逆に、タッチパネルディスプレイのような、表示と入力のインタフェースを備えたデバイスであれば、PC と同様に、アプリケーションがフリーズするなど異常が起きた時に、デバイスを操作するユーザが異常に気づき、アプリケーションを終了したり、デバイスをリセットしたりして、すぐに対処できます。
しかし、ユーザが対話的に操作せずに、常時稼働するタイプの組み込み機器の場合には、アプリケーションや OS がフリーズした場合、自動的に異常を復旧する仕組みがないと、機器が異常を起こしたままになってしまいます。その結果、深刻な問題を引き起こすおそれがあります。たとえば、自動販売機がフリーズして動かなくなってしまったり、工場のラインの自動制御システムで、制御装置のアプリケーションがフリーズした場合のことを、考えてみて下さい。
アプリケーションや OS がフリーズした場合に、デバイスをリセットしたり、あるいは、フリーズしたアプリケーションを終了して再始動したりするための仕組みとして、ウォッチドッグタイマがあります。Linux であれば、/dev/watchdog として提供されており、ウォッチドッグタイマを更新する watchdog デーモンも提供されています:
The Linux Watchdog driver API
http://kernel.org/doc/Documentation/watchdog/watchdog-api.txt
watchdog(8)
http://linux.die.net/man/8/watchdog
組み込み機器向けのプロセッサは、ウォッチドッグタイマを内蔵しているものが多く、それを利用するのが一般的です。ハードウェアのウォッチドッグタイマ機能がない場合には、OS のカーネルで、ソフトウェアによるウォッチドッグタイマ機能が提供されることもあります。Linux の場合、/dev/watchdog のウォッチドッグタイマ機能は、ハードウェアで実装されることもあれば、/dev/watchdog はデフォルト実装のカーネル内部のソフトウェアタイマとして提供し、プロセッサ内蔵のウォッチドッグタイマに対しては、別途ブートローダで設定する、という仕組みで提供されることもあるようです。PC 用の Linux の場合、/dev/watchdog は、BIOS の機能を使ったハードウェアタイマとして実装されているようです。
■ハードウェアタイマとソフトウェアタイマ
ウォッチドッグタイマ機能の、ハードウェアによる実装(ハードウェアタイマ)とソフトウェアによる実装(ソフトウェアタイマ)の違いは、何でしょう?
答えは、OS がフリーズした場合に動作できるかどうかです。ハードウェアタイマの場合は、OS が動作しているかどうかに関係なく、独立したハードウェアの機能として動作しますから、たとえ OS 全体がフリーズしていても、設定した通りに動作します。つまり、設定したタイマ満了時間以内にタイマを更新(リフレッシュ)しなければ、タイマが満了した時点で、プロセッサが強制リセットされます。一方、ソフトウェアタイマの場合は、OS 全体がフリーズすると、タイマの動作も停止してしまいますから、タイマ満了時間が過ぎても、何も起きません。つまり、ソフトウェア実装のウォッチドッグタイマ機能は、OS がフリーズした場合には、役に立たないのです。
■ソフトウェアタイマの長所
OS 全体がフリーズした場合(カーネルのスケジューラも停止した場合)には機能しない、という点は、ソフトウェアタイマの短所です。一方、長所もあります。ハードウェアタイマは、一つしか満了時間を設定できませんが、ソフトウェアタイマには、ハードウェアの制約がありませんので、複数のタイマを設定できるように実装することが可能です。
ウォッチドッグタイマを使う目的は、OS や、特定のアプリケーション(プロセス)、あるいはスレッドが、正常に動作し続けているかどうかを監視することにあります。ウォッチドッグタイマが満了した場合、何らかの障害が起きて、それらの監視対象が動作していないということを意味します。そのため、ウォッチドッグタイマが満了した場合は、OS をリブートするなどの、異常からの復旧処理を発火させる、というわけです。
さて、ソフトウェアタイマによって、複数のウォッチドッグタイマを設定できるということは、監視対象を複数にできる、ということです。つまり、複数のウォッチドッグタイマを設定できるなら、常時稼働していなければならないアプリケーション(プロセス)やスレッドが複数ある場合に、それらに対して個別にウォッチドッグタイマを設定し、どれか一つでも動作していないことが検出されたら、復旧処理を発火できます。WinCE/WEC では、それが可能です。さらに、ウォッチドッグタイマの満了時に発火させる復旧処理として、OS をリブートする以外の動作を設定できます。
■WinCE/WEC のウォッチドッグタイマ機能
WinCE/WEC のウォッチドッグタイマ機能には、ハードウェアタイマとソフトウェアタイマがあり、ソフトウェアタイマは、カーネルの API を使って利用できます。ソフトウェア実装のウォッチドッグタイマについては、リファレンスの次のページをご覧下さい:
CreateWatchDogTimer (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee482966(v=winembedded.60).aspx
CreateWatchDogTimer (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee482966.aspx
上のページで説明されている CreateWatchDogTimer() を使って、ソフトウェア実装のウォッチドッグタイマを設定できます。この関数では、タイマの満了時間に加え、タイマ満了時に発火させるアクションを指定できます。指定できるアクションは、次の三つです:
- WDOG_KILL_PROCESS 監視対象のプロセスを強制終了する
- WDOG_NO_DFLT_ACTION 何もしない
- WDOG_RESET_DEVICE OS をリブート(デバイスをリセット)する
CreateWatchDogTimer() は、戻り値として、ウォッチドッグタイマのハンドルを返します。このハンドルは、non signaled な状態ですが、タイマが満了した時点で、signaled な状態にセットされます。タイマ満了時に発火させるアクションとして、WDOG_NO_DFLT_ACTION を指定すると、ウォッチドッグタイマのハンドルに対して WaitForSingleObject() を呼び出すことにより、タイマが満了したことを検出できます。アプリケーション内で、特定のスレッドが動作しなくなったことを検出したいなどの場合には、WDOG_NO_DFLT_ACTION を使うのが便利でしょう。
CreateWatchDogTimer() で生成したウォッチドッグタイマを更新(リフレッシュ)するには、RefreshWatchDogTimer() を呼び出します。ウォッチドッグタイマを生成した後、StartWatchDogTimer() でタイマを始動したら、CreateWatchDogTimer() の dwPeriod 引数で指定したタイムアウト時間よりも短い間隔で、RefreshWatchDogTimer() を繰り返し呼び出さなければいけません。そうしないと、タイマが満了してしまいます。
次に、ハードウェアタイマについて見てみましょう。
ハードウェアタイマに対しては、API は提供されておらず、実装用のインタフェースが、カーネル移植レイヤ(OAL)で定義されています。ハードウェアがウォッチドッグタイマをサポートしていない場合は、OAL の初期化関数である OEMInit() の中で、ウォッチドッグタイマのインタフェース設定を変更せず、デフォルトのままにします。デフォルトでは、ハードウェアタイマ機能は、使用されません。ハードウェアタイマを有効にする場合は、OAL において、ハードウェアタイマの初期化処理とリフレッシュ処理を実装し、ウォッチドッグタイマのインタフェースを設定します。具体的には、OEMGLOBAL 構造体の dwWatchDogPeriod と pfnRefreshWatchDog を、OEMInit() で設定し、pfnRefreshWatchDog に設定した関数で、リフレッシュ処理(および、必要ならハードウェアタイマの初期化処理も)を実行します。 dwWatchDogPeriod は、デフォルトでは 0 に設定されており、この値が 0 の場合は、ハードウェアタイマが存在しないものとして扱われます。なお、pfnRefreshWatchDog のデフォルト値は、NULL ではなく、何もしない関数を指す関数ポインタです。
dwOEMWatchDogPeriod (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee479342(v=winembedded.60).aspx
pfnOEMRefreshWatchDog (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee479294(v=winembedded.60).aspx
OEMGLOBAL (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee478176(v=winembedded.60).aspx
上は、WinCE 6.0 のリファレンスです。WEC 7 のリファレンスは、以下にあります。
dwWatchDogPeriod (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee479342.aspx
OEMRefreshWatchDog (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee479294.aspx
OEMGLOBAL (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee478176.aspx
ハードウェアタイマに対しては、OAL で設定したリフレッシュ間隔(dwWatchDogPeriod)以内の間隔で、WinCE/WEC カーネルが定期的に pfnRefreshWatchDog の関数を呼び出すことにより、リフレッシュ動作を行います。一方、ソフトウェアタイマの場合は、CreateWatchDogTimer() を呼び出したアプリケーション自身が、リフレッシュ動作を行います(あるいは、監視対象のアプリケーションと、監視するアプリケーションの二つに役割を分割し、監視する役割のアプリケーションが CreateWatchDogTimer() を呼び出し、監視対象のアプリケーションが RefreshWatchDogTimer() を呼び出す、というやり方も考えられます)。
■ウォッチドッグタイマ機能の実装を見てみる
上で述べた、WinCE/WEC のウォッチドッグタイマ機能について、どのように実装されているのか、カーネルのソースコードを見てみるのも、面白いでしょう。カーネルが提供するウォッチドッグタイマ機能のソースコードは、
%_WINCEROOT%/PRIVATE/WINCEOS/COREOS/NK/KERNEL/watchdog.c
にあります。wachdog.c で実装されている WatchDogTimerThrd() が、ウォッチドッグタイマのカーネルスレッドが実行する手続きです。
ウォッチドッグタイマのカーネルスレッドは、ハードウェアタイマが存在する場合か、または、CreateWatchDogTimer() が呼び出された場合に生成されます(*1)。ハードウェアタイマが存在する場合、つまり、OEMInit() の中で、OAL によって OEMGLOBAL 構造体の dwWatchDogPeriod と pfnRefreshWatchDog が設定された場合は、カーネルのスケジューラが初期化時に呼び出す InitWatchDog() の中で、ウォッチドッグタイマのカーネルスレッドを生成・始動します。ハードウェアタイマが存在しない場合には、CreateWatchDogTimer() が最初に呼び出された時に、ウォッチドッグタイマのカーネルスレッドが生成・始動されます(*1)。
(2012-04-19 追記)
*1: これは、WinCE 6.0 の場合です。WEC 7 のカーネルでは、ハードウェアタイマが存在するか否かに関わらず、
InitWatchDog() において、ウォッチドッグタイマのカーネルスレッドが生成されます。
ウォッチドッグタイマのカーネルスレッドは、ハードウェアタイマとソフトウェアタイマの両方を管理します。WatchDogTimerThrd() の実装を見ると分かりますが、システムが終了するまで実行を続けるループの中で、ハードウェアタイマと全てのソフトウェアタイマのタイマ満了時刻に従って待ち動作を行い、適切なタイミングで、各タイマをリフレッシュします。ハードウェアタイマもソフトウェアタイマも存在しない状態では、待ち時間を無限に設定して待ち動作を行います。この待ち動作は、CreateWatchDogTimer() や StartWatchDogTimer(), StopWatchDogTimer() を呼び出した際に解除され、ウォッチドッグタイマのカーネルスレッドが、待ち時間を再調整します。
ハードウェアタイマについては、たとえば、WinCE 6.0 のデバイスエミュレータであれば、以下のソースコードを見て下さい:
%_WINCEROOT%/PLATFORM/DEVICEEMULATOR/src\oal/oallib/watchdog.c
%_WINCEROOT%/PLATFORM/DEVICEEMULATOR/src\oal/oallib/timer.c
%_WINCEROOT%/PLATFORM/DEVICEEMULATOR/src\oal/oallib/init.c
上のソースファイルのうち、watchdog.c にある SMDKInitWatchDogTimer() と RefreshWatchdogTimer() が、ハードウェアタイマ用の初期設定と、ハードウェアタイマのリフレッシュ処理を行う関数です。SMDKInitWatchDogTimer() は、timer.c にある OALTimerInit() を経由して、init.c にある OEMInit() から呼び出されます。つまり、OEMInit() の中で、(SMDKInitWatchDogTimer() によって)OEMGLOBAL 構造体の dwWatchDogPeriod と pfnRefreshWatchDog が設定されます。RefreshWatchdogTimer() は、最初に呼び出された時は、プロセッサ(S3C2410)内蔵のウォッチドッグタイマを初期化し、それ以降の呼び出しでは、ウォッチドッグタイマをリフレッシュ(タイマのカウンタをリセット)します。
なお、SMDKInitWatchDogTimer() を見ると、dwWatchDogPeriod と pfnRefreshWatchDog という変数に対して代入を行っていますが、これらの名前は、OEMGLOBAL 型の大域変数のメンバを指すように #define されています。興味のある方は、
%_WINCEROOT%/PUBLIC/COMMON/OAK/INC/bcoemglobal.h
をご覧になってみて下さい。
2012/03/19
koga
Windows Embedded CE/Compact には、PC 用の Windows と同様のコマンドプロセッサ(cmd.exe)が付属しているのは、皆さんご存じの通りです。今回は、このコマンドプロセッサをシリアルケーブル経由で PC から操作する手順について述べます。
さて、Platform Builder のカタログ項目ビューで依存項目を見ると、コマンドプロセッサには、コンソールウィンドウと Telnet サーバーが依存していることが分かります。コンソールウィンドウは、「コマンドプロンプト」のウィンドウですが、コンソールアプリケーションから printf() などで標準出力に出力した場合も、コンソールウィンドウが開いて出力内容が表示されます。PC 用の Windows でも、同様にコンソールウィンドウが開きますよね。つまり、コンソールウィンドウは、標準出力と結びつく(同様に、標準入力とも結びつく)コンソールデバイスとして機能するというわけです。Telnet サーバについては、後で述べます。
■コンソールの出力先に対するレジストリ設定
コマンドプロセッサを、シリアルケーブル経由で PC から操作するには、コンソールの出力先の設定を変更します。この設定は、リファレンスの次のページで説明されています:
Command Processor Registry Settings (Windows Embedded CE 6.0)
http://msdn.microsoft.com/en-US/library/ee506287(v=winembedded.60).aspx
Command Processor Registry Settings (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee506287.aspx
上のページで説明されているように、[HKEY_LOCAL_MACHINE\Drivers\Console] キー直下の OutputTo の値により、コンソールの出力先を、コンソールウィンドウ以外に設定できるのです。たとえば、2番のシリアルポート(COM2)であれば、次のような設定になります。
[HKEY_LOCAL_MACHINE\Drivers\Console]
"OutputTo"=dword:2
"COMSpeed"=dword:1C200
上の例は、COM2 をコンソールの出力先とし、COM2 のボーレートを 115200 としています。
上記のように設定することで、シリアルケーブルで接続した PC から、ターミナルソフトを使ってコマンドプロセッサを操作できます。このように設定しておくと、コマンドプロセッサ(cmd.exe)を起動すると、コンソールウィンドウは開かず、代わりに、シリアルケーブルで接続した PC のターミナルソフトのウィンドウに、コマンドプロセッサのプロンプトが出力されます。そして、コマンドプロセッサのプロンプトに対して、コマンド名やプログラム名をタイプ入力すれば、Windows Embedded CE/Compact 上のコンソールウィンドウの場合と同様、コマンドやプログラムを実行できます。
なお、コンソール出力をデフォルト(0)以外に設定すると、「コマンドプロンプト」を起動しても、ウィンドウが開かず、コンソール出力に設定したデバイスにプロンプトが出力されます。この点は、注意して下さい。
■シリアルコンソールに関する注意点
シリアルケーブル経由でコマンドプロセッサを使う設定については、たとえば、次の Blog エントリでも紹介されています:
Command line (Console) over serial
http://ce4all.blogspot.com/2007/06/command-line-console-over-serial.html
このエントリには、いくつか読者コメントが付いており、最後のコメントにある「入力がエコーバックされない」という質問には、回答が付いていないままです。実は、エコーバックされないのが通常動作です。コマンドプロセッサ(cmd.exe)のソースを見ると分かりますが、cmd.exe 自身は、コンソールに対して入力をエコーバックしません。バッチファイル(.bat, .cmd)の中で、echo コマンドによるエコーバックの ON/OFF 切り替えは出来ますが、コンソールに対しては、echo コマンドの実行有無とは関係無く、エコーバック動作は行いません。
cmd.exe のソースコードは、WinCE/WEC のソースツリーの、以下の場所にあります:
%_WINCEROOT%/private/winceos/UTILS/cmd2/
コンソールに対する入力処理は、cmd.cxx にある cmd_GetInput() で実装されていますが、バッチファイル(.bat, .cmd)の実行ではなく、コンソールからの入力、つまり標準入力に対する入力処理では、エコーバックは行いません。そもそも、コマンド入力を _fgetts() で取得していますので、タイプ入力の1文字ごとにエコーバックする仕組み自体がないのです。従って、シリアルコンソールで入力のエコーバックを実現するには、自前でエコーバック処理を実装する必要があります。
■タイプ入力に対するエコーバック動作
コマンドプロセッサのプロンプトに対してエコーバックする処理は、コマンドプロセッサが標準入出力を行う先の、「コンソールデバイス」が担当する役割です。たとえば、コンソールウィンドウは、コマンドプロセッサがデフォルトで使用するコンソールデバイスであり、コンソールウィンドウにおけるタイプ入力のエコーバック処理は、コンソールウィンドウが行います。
もう一つの例は、Telnet サーバです。以下のページでも言及されていますが、Telnet サーバは、自身をコンソールデバイスとして登録したうえで、コマンドプロセッサ(cmd.exe)を起動します。
http://msdn.microsoft.com/en-us/library/aa446909.aspx (Implementing a Network Service on Windows CE)
そして、Telnet クライアントでのタイプ入力を受け取り、それを、コンソールデバイスのインタフェースを介してコマンドプロセッサへ渡し、コマンドの実行を依頼します。コマンドプロセッサがコマンドを実行し、標準出力へ出力を行うと、それが(入力とは逆の向きに)コンソールインタフェースを介して Telnet サーバに渡され、Telnet クライアントに送信される、というわけです。なお、Telnet の場合には、Telnet クライアントでのタイプ入力のエコーバック処理は、Telnet クライアント自身が行うことになるはずです。
コンソールウィンドウのソースコードは、開示されていないため、コンソールデバイスとしての振る舞いの詳細を見ることができません。一方、Telnet サーバの方は、
%_WINCEROOT%/public/servers/sdk/samples/telnetd/
にソースコードが収録されていますので、興味のある方は、ご覧になってみて下さい。telndev.cpp において、Stream Interface Driver のインタフェース(TEL_Init(), TEL_Deinit(), TEL_Open(), TEL_Close(), TEL_Read(), TEL_Write(), TEL_Seek(), TEL_IOControl(), TEL_PowerUp(), TEL_PowerDown() 関数)を実装しており、TEL_Read() と TEL_Write() で、コンソール入出力の処理を行います。
Telnet サーバが cmd.exe を起動する処理は、telnetd.cpp にある TelnetLaunchCmd() ですが、ここでは、cmd.exe を起動する前に、SetStdioPathW() を呼び出して標準入出力を変更します。この時、コンソールデバイスとして、自分自身を実体に割り当てたデバイス(”TEL<インデックス>:” というデバイス名)を設定することにより、cmd.exe の標準入出力先として自身を設定します。Telnet サーバは、Windows の流儀でいう「サービス」、つまり、ユーザモードのデバイスドライバとして動作しますから、このような動作が可能なのです。
■エコーバック動作の実現方策
では、シリアルコンソールの場合に、タイプ入力のエコーバック動作を実現するには、どうすればよいのでしょうか?
方策は、二つ考えられます。一つは、Telnet サーバのように、自前でコンソールデバイス機能を実装し、コマンドプロセッサとシリアルポートの間に介在することにより、シリアルポートからの入力を、一文字ごとにエコーバックする、という方策です。
もう一つは、シリアルポートに対して直接入出力を行い、シリアルポートから文字列が入力されたら、入力された文字列をコマンド名として、都度コマンドプロセッサ(cmd.exe)を起動する、という方策です。
二つの方策のうち、後者の方が、より簡単に実装できます。cmd.exe にコマンドを実行させるには、/Q と /C オプション付きで cmd.exe を実行すればよいのです。つまり、
”/Q /C <コマンド名>[ コマンド引数]”
という文字列を、CreateProcess() の第二引数として渡し、第一引数に “cmd.exe” を渡して呼び出せば、cmd.exe がコマンドを実行し、その結果が、レジストリで設定したコマンドプロセッサのコンソール、つまりシリアルポートへ出力されます。その際、CreateProcess() が返したプロセスハンドルに対して WaitForSingleObject() を呼び出せば、コマンド実行の完了を待つことが出来ます。
ただし、二番目の方策については、次の点を注意して下さい:
・コマンド実行のたびに、都度 cmd.exe を起動するので、シリアルコンソールから cd コマンドが実行されても、その結果が保持されない。
(cd コマンドに対応するとしたら、移動先に指定されたディレクトリを、cmd.exe を起動するプログラム自身が記録することによって、履歴動作を実現する必要がある。)
・cmd.exe を実行している間は、シリアルポートを閉じて、cmd.exe がシリアルコンソールを使用できるようにしなければならない。
二番目の注意点ですが、これは、シリアルポートのデバイスドライバの制約によるものです。シリアルポートは、一つの実体に対し、デバイスを一つしかオープンできないのです。たとえば、COM1 であれば、”COM1:” を指定して CreateFile() を呼び出すことにより、シリアルポートのデバイスをオープンします。この時、”COM1:” のデバイスをオープンしたままの状態で、同じシリアルポート(”COM1:”)に対して CreateFile() を呼び出すと、エラーとなり、オープンできません。この制約は、シリアルポートのデバイスドライバの、MDD レイヤの実装によるものです。
シリアルポートのデバイスドライバの MDD レイヤのソースコードは、
%_WINCEROOT%/public/COMMON/oak/drivers/serial/com_mdd2/
に収録されています。このディレクトリにある mdd.c で実装されている COM_Open() を見ると、既にオープン済みのシリアルポートに対して呼び出された場合は、ERROR_INVALID_ACCESS をエラーコードとしてセットして、NULL を返すことが分かります。
ところで、シリアルコンソールにバックスペース(’\b’)が入力された場合、エコーバック処理では、入力を一文字消さなければいけません。単純に ‘\b’ をエコーバックしただけでは、PC のターミナルソフトでは、カーソルが一文字戻るだけで、文字が消えないのではないかと思います。カーソルを戻すだけでなく、直前にあった文字を消したい場合には、’\b’ を単純にエコーバックする代わりに、”\b \b” という文字列を出力すればよいでしょう。つまり、カーソルを一文字分戻した後、空白文字を出力することにより、直前にあった文字を消し、再度 ‘\b’ を出力してカーソルを戻す、というわけです。
■シリアルコンソールを利用した管理機能の実現
上で述べた、シリアルコンソールのタイプ入力に対するエコーバック動作の二つの実現方策のうち、後者の方は、シリアルコンソールを使った管理機能を実装する際には、却って向いているかも知れません。前者の方策の場合、OS (WinCE/WEC) の起動完了直後にコマンドプロセッサを起動するように設定すれば、シリアルコンソールにコマンドプロセッサのプロンプトが出力されて、Linux などと同じ感覚で使うことが可能です。しかし、そのようにしてしまうと、全てのプログラムをコマンドプロセッサから実行できてしまいます。
シェルをカスタマイズして、特定の操作しかできないようにしたデバイスや、あるいは、ヘッドレスのデバイスの場合には、限られたコマンドだけをシリアルコンソールから実行できるように制限したり、また、ログイン動作を実装してセキュリティを確保する必要があるでしょう。そのような場合は、単純にコマンドプロセッサをシリアルコンソールへ割り当てるのではなく、コマンドプロセッサを呼び出す「ラッパー」/「ドライバ」プログラムを割り当てて、そのプログラムが、ログイン動作や、シリアルコンソールから実行可能なプログラムの名前だけを受け付けて実行する、という仕組みにするのが良いと思います。
2012/03/15
koga
前回のエントリでは、ファイルシステム用の永続記憶域を使った部分アップデートの方法を紹介しました。今回は、ROM イメージを複数に分割する方式の部分アップデートについて紹介します。
最初に断っておきますが、この方法は、WinCE/WEC の OS Design のコンフィグレーションでは対応できません。カーネル移植レイヤ(OAL)とブートローダのカスタマイズが必要です。
■複数の ROM イメージ(Multiple Region)
ROM イメージを複数に分割する仕組みは、もともと、WinCE/WEC のカーネルと Platform Builder 内蔵の ROM イメージ生成コマンド(makeimg.exe, romimage.exe)に組み込まれています。ただし、それを有効にするためには、OAL とブートローダが、複数に分割された ROM イメージに対応する必要があるのです。この対応は、必須ではないため、全ての BSP が対応しているわけではありません。対応していない BSP の場合は、OAL とブートローダの実装にカスタマイズを加えなければいけない、というわけです。
複数に分割された ROM イメージに対応した OAL とブートローダは、それぞれ、次の機能を持ちます。
・OAL
ブートローダが Flash メモリなどから RAM にロードした(XIP の場合は、NOR Flash 上の)、複数に分割された ROM イメージのリストを、カーネルに伝える。この通知は、OAL の初期化時に(つまり、OEMInit() の中で)OEMRomChain を構築することで行う。
・ブートローダ
Flash メモリなどから、複数に分割された ROM イメージを RAM へロードする(XIP の場合は、ロードしない)。また、ROM イメージをホスト(開発用 PC)から転送された時は、分割された ROM イメージを各々認識し、それらを、Flash メモリなどに書き込む。
ここで、「分割された ROM イメージ」というのは、ROM イメージファイルを単純に分割したものでは、ありません。「分割された ROM イメージ」の一つ一つは、それぞれが ROM イメージのヘッダを持っていて、個別の ROM イメージとなっています。これらを、region と呼びます。どのように region を構成するのかは、config.bib で設定します。makeimg.exe は、config.bib で複数の region が設定されていれば、それに従って、region ごとに ROM イメージファイル(.bin)を生成するのです。
複数の region を WinCE/WEC カーネルに認識させるために、OAL は、初期化時に region のリストを構築します。このリストが、上で述べた OEMRomChain です。OEMRomChain は、ROMChain_t 構造体のリストで、カーネルのスタートアップルーチンによって、スタートアップルーチンが所属する ROM イメージを指すように初期化されます。つまり、デフォルトでは、region が一つだけとなります。その後、スタートアップルーチンが OAL の OEMInit() を呼び出し、OEMInit() が、OEMRomChain に複数の region を登録する(OEMRomChain を初期化し直す)というわけです。
より正確には、OEMInit() が OEMRomChain をどう構築するかは、OAL における OEMInit() の実装次第です。カーネルのスタートアップルーチンは、OEMInit() を呼び出した後、OEMRomChain の内容をチェックして、自身が所属する region が登録されていない場合には、自身が所属する region を OEMRomChain の末尾に連結します。
ROMChain_t 構造体の説明は、WinCE 6.0/WEC 7 のリファレンスの、次のページをご覧ください:
OEMRomChain
http://msdn.microsoft.com/en-US/library/ee479246(v=WinEmbedded.60).aspx
http://msdn.microsoft.com/en-US/library/ee479246.aspx
■Region ごとの部分アップデート
複数の ROM イメージ(Multiple Region)を使うと、部分アップデートする対象(アプリケーションなど)を収録した ROM イメージと、OS 本体を収録した ROM イメージを分けることができます。そして、ブートローダが対応していれば、アップデートしたい ROM イメージ(region)だけをブートローダへ転送し、書き換えることができます。これが、ROM イメージファイルを複数に分割する方式の部分アップデートです。
この方法の利点は、次の通りです:
・永続記憶域上にルートファイルシステムを構築する必要がないため、より耐障害性が高い。
・OS イメージ中の全てのファイルを、アップデートすることが可能。
ここで、「OS イメージ中の全てのファイルをアップデート可能」というのは、ROM イメージの Region 間で shadowing が作用することによるものです。つまり、二つの region に同じファイルが収録されている場合、上述した OEMRomChain のリストにおいて、より先頭に近い方の region 内のファイルが優先され、その後ろにある region 内のファイルが隠されるのです。
OEMRomChain の region 間での shadowin は、前回紹介した、ルートファイルシステム上のファイルによる ROM イメージ内のファイルの shadowing の仕組みと、本質的には同じものです。どちらも、FSD Manager と、カーネルのローダーによって実現されています。前回、カーネルのローダーが、OpenExecutable() から OpenFileFromFilesys() と OpenFileFromROM() を呼び出し、先に呼び出す OpenFileFromFilesys() で executable ファイル(.exe または .dll)が見つかれば、そちらを優先する、と説明しました。実は、OEMRomChain に複数の region が登録されている場合、OpenFileFromROM() は、OEMRomChain のリスト(つまり、region のリスト)を先頭から順に辿り、最初に見つかったファイルをオープンします。従って、よりリストの先頭に近い region 内のファイルが優先される、というわけです。
たとえば、OS 本体を収録した region(※この region に、カーネルのスタートアップルーチンが含まれます)と部分アップデートする対象を収録した region の二つがある場合について考えましょう。OEMRomChain のリストを構築する際、OS 本体を収録した region がリストの末尾要素となり、部分アップデートする対象を収録した region がリストの先頭要素となるようにすれば、両者に同じファイルが存在する場合、部分アップデート対象を収録した region 内のファイルが優先されます。このため、OS 本体を収録した region に収録したデバイスドライバなどをアップデートする際に、その region を書き換える代わりに、部分アップデート用の region の ROM イメージにアップデートしたいファイルを追加して、部分アップデート用の region だけを書き換える、という方策が可能です。
OS 本体を収録した region のサイズが、部分アップデートする対象を収録した region のサイズよりも、ずっと大きければ、小さい方の region(つまり、部分アップデートする対象を収録した region)の方を書き換える方が、より小さなコストで済みます。
部分アップデート用に、複数の ROM イメージ、つまり region の分割を行う方策としては、region を二つに分ける以外に、次のように、四つの region を設定する方策も考えられます(どちらかといえば、極端な例ですが):
- NK: OS 本体を収録
- EXT: デバイス固有(BSP 固有)のデバイスドライバやアプリケーションなどを収録
- UNK: NK region 内のファイルの更新版を収録する(※出荷時は空)
- UEXT: EXT region 内のファイルの更新版を収録する(※出荷時は空)
このように region を設定すれば、OS 本体(WinCE/WEC のカーネル及び、標準のデバイスドライバやアプリケーション、API の DLL など)と、デバイス(あなたが開発する製品)固有のドライバやアプリケーションを、それぞれ独立してアップデートできます。OS 本体用の ROM イメージと、デバイス固有の ROM イメージは、全体的なアップデートが必要となれば、各々の region(NK, EXT)を書き換えればよいですし、特定のファイルだけをアップデートしたい場合は、各々の部分アップデート用の region(UNK, UEXT)を書き換えればよい、というわけです。
WinCE/WEC は、月例アップデートが公開されます。もし、出荷後に重要な修正がリリースされた場合は、その修正を、出荷済みの製品の OS に反映しなければならないこともあるでしょう。そのような場合に、上の例のような region 分割を設定しておくと、OS 本体の ROM イメージを全て書き換えなくても、修正が加わった .dll や .exe を収録した、部分アップデートの ROM イメージを作り(そのイメージサイズは、OS 本体の ROM イメージに比べて、非常に小さいでしょう)、対応する region を書き換えることでアップデートできます。なお、部分アップデートを繰り返す場合、部分アップデートの ROM イメージには、最新のアップデート対象の .exe や .dll に加えて、以前の部分アップデートの際に収録した .exe や .dll も収録しなければならないことに注意して下さい。そうしなければ、部分アップデートした際に、以前の部分アップデートの内容が消えてしまうからです。
ところで、もし、部分アップデートを繰り返した結果、アップデート対象のファイルが累積して、部分アップデートの ROM イメージが大きくなった場合は、古い部分アップデート内容を破棄して、全体をアップデートする必要が生じる場合も、あると思います。つまり、上の例で言うと、UNK または UEXT region に収録するファイルが増えた場合は、それらの region を空にして、代わりに、NK または EXT region を作り直し、そちらを書き換える必要が生じるでしょう。もちろん、どのように region を分割し、部分アップデートを行うかということは、ケースバイケースですよね。
■Region 分割の手順
ここまでで、ROM イメージを複数に分割する方式の部分アップデートについて、その仕組みと、部分アップデートの方策について説明しました。残りは、実際の手順です。
複数の region に分割した ROM イメージを作成する手順の例は、WinCE 6.0 のリファレンスに載っています。現時点では、WEC 7 のリファレンスには対応するページが見当たりません。
How to Create a Run-Time Image with Multiple XIP Regions
http://msdn.microsoft.com/en-US/library/ee482739.aspx
このページでは、CEPC の場合の手順が載っていますが、他のボード(デバイス)の場合も、基本的には同じです。必要な作業を簡単にまとめると、次の通りです:
・config.bib ファイルの MEMORY セクションの記述を書き換える。その際、環境変数 IMGMULTIBIN に対する条件分岐を書き加えて、複数 region 対応するかどうかを、環境変数(IMGMULTIBIN)の設定有無で切り換えできるようにすると便利。
複数 region 対応用の MEMORY セクションには、NK の他に定義する region を、RAMIMAGE 型の memory region として設定する。また、それらの region の連結内容を記録するための CHAIN region を、RESERVED の memory region として設定する。
http://msdn.microsoft.com/en-US/library/ee482800.aspx (Modifying the Binary Image Builder Files)
・platform.bib ファイルの MODULES または FILES セクションに、NK 以外の region に収録するファイルを記述する。
BSP 固有のデバイスドライバやアプリケーションなどを、NK 以外の region に収録する場合は、環境変数 IMGMULTIBIN に対する条件分岐を書き加えて、複数 region 対応の場合のみ、当該 region(「Region ごとの部分アップデート」で示した例で言えば、EXT region)を指定する。
http://msdn.microsoft.com/en-US/library/ee482800.aspx (Modifying the Binary Image Builder Files)
・OS イメージをビルドする。
・ビルドしてできた .bin ファイルのうち、chain.bib を、Platform Builder から WinCE/WEC デバイスへ転送するファイルとして設定する。
デバイスへ転送するファイルは、Platform Builder のプロパティダイアログで設定できる。Platform Builder の「プロジェクト」 > 「プロパティ」メニューで、プロジェクトのプロパティダイアログを開き、左側にあるツリービューから、「構成プロパティ」 > 「全般」を選択して表示される画面の、「デバッガ用のターゲットファイル名」というラベルのドロップダウンリストで、転送したい .bin ファイルを選択する。
http://msdn.microsoft.com/en-US/library/ee482967.aspx (Building the Run-Time Image and Opening a Workspace with Multiple XIP Regions)
以上が、複数の region に分割した ROM イメージを作成して、WinCE/WEC デバイスのブートローダへ転送するために、Platform Builder で行う手順です。なお、上の最後のページ(“Building the Run-Time Image and Opening a Workspace with Multiple XIP Regions”)の説明では、複数に分割した ROM イメージを全て転送する場合、xip.bin を指定すると書かれていますが、これは、うまくいきません。xip.bin ではなく、CAHIN region の内容を収録した chain.bin を指定して下さい。
xip.bin は、全ての region の内容を一つに連結したファイルなのですが、WinCE 6.0 付属のブートローダのライブラリ(blcommon)の実装は、この構造に対応していないように思われます。xip.bin が、残りの .bin ファイルの内容を単純に連結した内容になっており、その結果、blcommon ライブラリでは、先頭の region しか認識されないのです。blcommon ライブラリのソースを読むと、ROM イメージのヘッダに記すマジックナンバーの値として、xip.bin 専用の値が定義されていたような形跡があるのですが、その値(X000FF)が設定されている場合、blcommon ライブラリは、エラーメッセージを出力して停止するように実装されています。
■OAL とブートローダのカスタマイズ
さて、今回の最初の方で、複数に分割した ROM イメージに対応するには、OAL とブートローダのカスタマイズが必要だと述べました。複数に分割した ROM イメージに対応している OAL とブートローダが、それぞれ、どのような働きをするのかについても、ごく簡単に説明しました。OAL とブートローダに対するカスタマイズの内容について、最後に、もう少しだけ補足します。
・OAL のカスタマイズ
OEMInit() 中で、OEMRomChain を構築しなければならないことは、既に述べました。このことは、次のページに書かれています。
Booting an Image with Multiple XIP Regions
http://msdn.microsoft.com/en-US/library/ee483010.aspx
しかし、このページの説明だけでは、あっさりし過ぎていて、具体的にどうすればよいのか、分かりません。OEMRomChain を構築する例は、x86 用の BSP 共通ソースをご覧になるのが良いでしょう。このソースは、次の場所にあります:
%_WINCEROOT%/PLATFORM/COMMON/src/x86/COMMON/startup/oeminit.c
oeminit.c の中にある、x86InitRomChain() が、OEMRomChain を構築する関数です。なお、x86InitRomChain() では、OEMRomChain という名前の変数の値を設定していますが、実は、OEMRomChain はマクロで、その実体は、OEMGLOBAL 構造体のメンバ pROMChain なのです。
OEMGLOBAL
http://msdn.microsoft.com/en-us/library/ee478176(v=WinEmbedded.60).aspx
http://msdn.microsoft.com/en-us/library/ee478176.aspx
OEMRomChain を定義したヘッダファイルは、次の場所にあります:
%_WINCEROOT%/PUBLIC/COMMON/OAK/INC/bcoemglobal.h
OEMRomChain を構築する処理では、region の連結内容を記録した CHAIN region と、各 regionの ROM イメージヘッダ内容をアクセスする必要がありますが、それらは、以下のヘッダファイルで定義されています:
%_WINCEROOT%/PUBLIC/COMMON/OAK/INC/romldr.h
%_WINCEROOT%/PUBLIC/COMMON/OAK/INC/pehdr.h
CHAIN region の構造については、次のページでも説明されています:
XIP Chain
http://msdn.microsoft.com/en-us/library/ee482877.aspx
・ブートローダのカスタマイズ
ブートローダのカスタマイズについては、次のページをご覧ください。
Adding Support for Multiple-BIN Image Notification
http://msdn.microsoft.com/en-US/library/ee479207.aspx
OEMMultiBINNotify
http://msdn.microsoft.com/en-US/library/ee478943(v=WinEmbedded.60).aspx
http://msdn.microsoft.com/en-US/library/ee478943.aspx
OEMMultiBINNotify() を実装すると共に、CHAIN region の内容および他の region をホストから受け取って Flash メモリなどに書き込む動作、および、region が一つだけ転送された場合(部分アップデートの場合)に、それを正しい場所へ書き込む機能を実装しなければいけません。さらに、ブート動作では、Flash メモリなどに書き込んだ、CHAIN region と他の全ての region を RAM へロードする処理(XIP の場合は、ロードしない)を実装する必要があります。
2012/01/13
koga
WinCE 6.0/WEC 7 では、通常、OS 全体を ROM イメージファイル(nk.bin)に格納し、Flash メモリなどに書き込んで使います。Flash メモリに書き込んだ ROM イメージファイルは、電源投入後に、ブートローダによって RAM にロードされ、ブートローダがカーネルのスタートアップルーチンへ制御を移すことにより、WinCE/WEC が起動する、というわけです(NOR Flash の場合には、XIP; Execute-In-Place 用の ROM イメージファイルを Flash メモリへ書き込むことにより、イメージファイルを RAM にロードせず、Flash メモリから直接カーネルを実行することも可能です)。
ここで、OS 全体を ROM イメージファイルに格納するということは、デバイスドライバやアプリケーションを修正した場合、その修正を反映するには、ROM イメージファイルを作り直して上書きしなければならない、ということを意味します。ROM イメージファイルのサイズは、コンフィグレーション次第で変わりますが、数十MB、場合によっては 64MB を超えることもあります。PC ならばともかく、一般の組み込み機器では、アップデートのたびに ROM イメージファイル全体を書き換えなければならないというのは、大きなコストだと言えるでしょう。
ROM イメージファイル全体を書き変えずに、アップデートしたいアプリケーション(.exe)やデバイスドライバ(.dll)だけを置き換えることは、できないのでしょうか?それは可能です。アップデートしたいものだけを置き換える、つまり部分アップデートには、二通り、ないしは三通りの方法があります。WinCE 6.0/WEC 7 を動かすデバイスのハードウェア構成の違いや、デバイスのメンテナンス方策、および、カーネル移植レイヤとブートローダのカスタマイズが可能かどうかにより、どの方法が最も適しているかは変わりますので、それぞれについて、順に紹介したいと思います。
今回は、ファイルシステム用の永続記憶域を使った部分アップデートの方法を二通り紹介し、次回は、ROM イメージファイルを複数に分割する方式の部分アップデートの方法を紹介します。
■アップデート対象を OS イメージに含めない方法
部分アップデートの最も簡単な方法は、部分アップデートする対象(アプリケーションなど)を OS イメージ(ROM イメージファイル)に含めず、永続記憶域に配置することです。たとえば、WinCE/WEC に付属しない、そのデバイス専用に開発したアプリケーションなどを、OS イメージには含めずに、ハードディスクや microSD カード、USB メモリなどに格納する、という方策です。
この方法の利点は、次の通りです:
・WinCE/WEC に対するカスタマイズの必要がない。
・OS イメージを配置する記憶域と、部分アップデート対象を配置する記憶域を、それぞれ違うものにできる。
たとえば、NOR Flash と NAND Flash を搭載したデバイスの場合であれば、NOR Flash に OS イメージを配置して、NAND Flash 上に部分アップデート対象を配置する、という方法です。NAND Flash 上にファイルシステムが構築されていれば、この方策を使えます。あるいは、NOR Flash と、microSD/SD スロットや USB ポートを搭載したデバイスであれば、microSD/SD カードや USB メモリを常時装着しておき、それらに部分アップデート対象を配置する、という方法が使えます。
ところで、部分アップデートする対象のアプリケーションや DLL を、Platform Builder の OS Design サブプロジェクトにして開発する場合、それらの .exe や .dll は、Platform Builder で設定すれば、OS イメージに格納されません。従って、ビルドや、カーネルデバッガを使ったデバッグまでは、OS イメージに含めるものと同様、Platform Builder で統合して行うことが可能です。
■永続記憶域上にルートファイルシステムを構築する方法
部分アップデートの二番目に簡単な方法は、永続記憶域上のファイルシステムを、ルートファイルシステムとしてマウントすることです。デフォルトのコンフィグレーションでは、WinCE/WEC は、RAMFS (Object Store) をルートファイルシステムとしてマウントしますが、永続記憶域上のファイルシステムをルートファイルシステムにすることも出来るのです。
ルートファイルシステムの設定は、WinCE 6.0 のリファレンスにある、次のページで説明されています:
Mounting an Installable File System as the Root Directory
http://msdn.microsoft.com/en-us/library/ee490194(v=winembedded.60).aspx
WEC 7 のリファレンスでは、上のページの説明に比べると若干分かりづらいですが、次のページで説明されています:
RAM File System Registry Settings (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee490789.aspx
Mount Settings (Windows Embedded Compact 7)
http://msdn.microsoft.com/en-us/library/ee490773.aspx
WinCE 6.0 の場合、上記のページの説明を見て頂くと分かるように、必要な設定は、レジストリ内容を永続化する場合と一部共通します。レジストリ内容を永続化するための設定と、その仕組みについては、2011/02/15 のエントリ(「レジストリ変更内容の永続化(1/2)」)と、2011/02/21 のエントリ(「レジストリ変更内容の永続化(2/2)」)をご覧ください。
WinCE 6.0 の場合に、永続記憶域をルートファイルシステムとしてマウントするためには、レジストリ内容を永続化する場合の手順に加え、以下の手順が必要です:
・Platform Builder のカタログ項目ビューで、次のカタログ項目を選択する:
コア OS
CEBASE
ファイルシステムおよびデータストア
ファイルシステム – 内部(1つ選択)
★ ROM のみに適用されるファイルシステム
・当該ファイルシステムに対するレジストリ設定で、MountAsRoot の値を 1 に設定する。
2011/02/21 のエントリに書いた例(Armadillo-440 の microSD カードを使う場合)だと、次のように設定して下さい。
[HKEY_LOCAL_MACHINE\System\StorageManager\Profiles\SDMemory\FATFS]
"MountPermanent"=dword:1
"MountAsBootable"=dword:1
"MountAsRoot"=dword:1
この方法の利点は、次の通りです:
・WinCE/WEC のカーネル移植レイヤ(OAL)とブートローダをカスタマイズする必要がない。
・OS イメージに含めたアプリケーションやデバイスドライバも、アップデートすることが可能(ただし、出来ないものもある)。
OS イメージに含めたアプリケーションやデバイスドライバをアップデートできるのは、ルートファイルシステムに配置されたファイルによる、”shadowing” の仕組みがあるからです。この仕組みは、WinCE 6.0/WEC 7 の、以下のソースファイルで実装されています。
%_WINCEROOT%/private/winceos/COREOS/storage/fsdmgr/virtroot.cpp
%_WINCEROOT%/private/winceos/COREOS/storage/fsdmgr/pathapi.cpp
virtroot.cpp と pathapi.cpp は、FSD Manager (fsdmgr.dll) のソースファイルの一つですが、CreateFile() によるファイルオープンや、Find{First,Next}File() によるファイル走査の際に、ルートファイルシステム配下のパスが指定された場合は、そのファイルがルートファイルシステム上に存在しなければ、ROM イメージのファイルシステムをアクセスするようになっています。この仕組みにより、同じパス名のファイルが、ルートファイルシステムと ROM イメージのファイルシステム(つまり、OS イメージ)の両方に存在すると、ルートファイルシステム上のファイルが優先される、というわけです。これが、shadowing です。
(※ただし、virtroot.cpp と pathapi.cpp による shadowing は、.exe と .dll 以外のファイルに対して行われるようです。これについては、次の項で述べます。)
永続記憶域上のファイルシステムをルートファイルシステムに設定している場合、アップデート手順は、上で述べた一番目の方法と同じくらい簡単です。たとえば、ディスプレイドライバをアップデートしたい場合は、ディスプレイドライバの新しい .dll を /Windows ディレクトリに上書きコピーして、リブートするだけです。/Windows ディレクトリに上書きコピーした .dll は、永続記憶域上のファイルシステムに書き込まれ、リブートした際に、OS イメージ内の古い .dll を隠します(shadowing)。つまり、OS イメージ(ROM イメージファイル)全体を書き換えることなく、目的のファイルだけをアップデートできる、というわけです。
ただし、この方法で全てのデバイスドライバや DLL をアップデートできるわけでは、ありません。アップデートできるのは、ブートの初期フェーズ以降にロードされるデバイスドライバや DLL だけです。たとえば、microSD カード上にルートファイルシステムを構築する場合、SD ホストコントローラのドライバは、この方法ではアップデートできません。SD ホストコントローラのドライバは、ルートファイルシステムをマウントするために必要なので、ルートファイルシステムがマウントされる前にロードされます。従って、たとえルートファイルシステムに、更新した SD ホストドライバの .dll を配置したとしても、shadowing は行われず、OS イメージ内の「古い」方の .dll が動作します。
■shadowing について、もう少し
ちなみに、ROM イメージのファイルシステムのドライバは、romfsd.dll です。このドライバのソースは、開示されていません。romfsd.dll は、FSD Manager の初期化処理でロードされ、”ROM” という名前のボリュームとしてマウントされます。FSD Manager が romfsd.dll をロードする処理は、
%_WINCEROOT%/private/winceos/COREOS/storage/fsdmgr/fsdmain.cpp
にある InitializeROMFileSystem() で実装されています。InitializeROMFileSystem() は、同じファイルで実装されている STOREMGR_Initialize() の中で、最後に呼び出される関数です。つまり、WinCE/WEC のストレージ機能の中核を初期化する処理の、最終段階で呼び出される関数です。
romfsd.dll のソースコードが開示されていないので、詳細は不明ですが、おそらく、romfsd.dll は、OS イメージ内の全てのファイルに対するファイルシステムではなく、OS イメージの FILES セクションに収録されたファイルに対するものだと思われます。OS イメージの MODULES セクションに収録されたファイル(.dll および .exe)は、カーネルのローダー機能がアクセスする仕組みになっているようです。カーネルのローダー機能のソースは、
%_WINCEROOT%/private/winceos/coreos/nk/kernel/loader.c
です。loader.cpp で実装されている OpenFileFromROM() が、OS イメージ/ROM イメージの MODULES セクションから .exe や .dll をロードする関数です。この関数は、OpenExecutable() から呼び出されるのですが、OpenExecutable() は、 まず OpenFileFromFilesys() を呼び出し、ファイルシステム上にファイルが見つからなければ、OpenFileFromROM() を呼び出します。従って、永続記憶域上のルートファイルシステムに .exe や .dll が存在すれば、そちらが優先されます(shadowing)。
なお、カーネルデバッガで追ってみると、.dll や .exe は、romfsd.dll に対するアクセスでは見つからず、必ず、OpenFileFromROM() が呼ばれるようです。上で、romfsd.dll は、OS イメージの FILES セクションに収録されたファイルに対するものだと思われると書いたのは、この観測結果からの推察です。
■耐障害性の強化に関する考察
さて、永続記憶域上にルートファイルシステムを構築することにより、OAL とブートローダのカスタマイズ無しで部分アップデートできることを説明しました。ただし、ここで一つ問題があります。それは、ルートファイルシステムが破損する可能性です。不意の電源断などにより、ルートファイルシステムが破損してしまうと、WinCE/WEC は、起動できなくなってしまいます。Flash メモリなどに格納された OS イメージが無事でも、永続記憶域上のルートファイルシステムが破損してしまうと起動できない理由については、2011/08/02 のエントリ(「レジストリの永続化~RAM-Based の場合」)をご覧ください。このエントリの、三種類のレジストリ永続化方式の長短を述べた個所で、「(a) Hive-Based のレジストリ」の短所で述べたことが、ここでも当てはまるのです:
短所:レジストリの Hive を配置するファイルシステムを Bootable に設定しなければならないため、そのファイルシステムが破損するなどしてマウントできなくなった場合、ブートの第2フェーズへ遷移できず、第1フェーズの完了待ちで WinCE カーネルの起動が停止してしまう。
この問題を回避する方策としては、次のようなものが考えられます。
・ルートファイルシステム破損時の復旧用に、もう一つ OS イメージを作成し、ブートローダの操作で、復旧用の OS イメージを起動できるようにする。復旧用の OS イメージでは、ルートファイルシステムを永続記憶域に割り当てず、RAMFS (Object Store) をルートファイルシステムに割り当てる。
←復旧用の OS イメージの起動後、破損したファイルシステムをフォーマットするなどの、復旧作業を行えるようにする。
OS イメージを格納するストレージ(Flash メモリやハードディスクなど)の容量が十分大きく、OS イメージを二つ格納できる場合は、この方策が向いているでしょう。
・ファイルシステムを AutoLoad 可能な永続記憶域上に、ルートファイルシステムを割り当て、かつ、ルートファイルシステムを Bootable に設定しない。ルートファイルシステムとして正しく動作できるように、ブートの第1フェーズでファイルシステムをロードさせる。ルートファイルシステムを Bootable に設定しないので、RAMFS (Object Store) が Bootable なファイルシステムとしてマウントされる結果、永続記憶域上のルートファイルシステムをマウントできなくても、WinCE/WEC が起動する。
←この方策は、microSD/SD カードや USB メモリに対しては、使えません。使えない理由は、2011/02/21 のエントリ(「レジストリ変更内容の永続化(2/2)」)をご覧ください。SD メモリカードのドライバや USB メモリカードのドライバは、クライアントドライバなので、ロードするブートフェーズを明示的に指定できないのです。ファイルシステムを AutoLoad 可能な場合は、ブートの第1フェーズでロードされるように、レジストリの BootPhase 値を設定しておけば、たとえ RAMFS が Bootable なファイルシステムとしてマウントされても、ブートの第2フェーズへ遷移する前に、ルートファイルシステムがマウントされる筈です。
この方策は、NAND Flash やハードディスクを搭載したデバイスの場合に使えるでしょう。この方策を使った場合、そのままでは、レジストリが永続化されないという問題が残りますが、Hive ではなく、RAM ベースのレジストリにして、ルートファイルシステムを割り当てるのとは異なるストレージ(NOR Flash など)にレジストリを配置する、といった解決策が考えられます。
2012/01/10
koga
先日書いた「レジストリの永続化~RAM-Based の場合」では、RAM-Based のレジストリを永続化した場合の注意点として、次のことを書きました:
レジストリの変更内容を永続記憶域に保存するためには、RegFlushKey() の呼び出しが必要です。Hive-Based のレジストリとは異なり、RAM-Based のレジストリでは、RegFlushKey() を呼び出さない限り、レジストリの変更内容は保存されません。
これに加えて、もう一つ注意すべきことがあります。それは、OS イメージを更新する場合です。
OS イメージを更新して、NOR Flash などに新しいイメージを書き込む際、新しいイメージに収録されたレジストリの内容が、置き換え前のイメージに収録されたものと異なっていた場合は、Hive-Based のレジストリと RAM-Based のレジストリで振る舞いが異なります。Hive-Based のレジストリでは、新しいイメージに収録されたレジストリ内容が優先されるのに対し、RAM-Based のレジストリでは、永続記憶域に保存されているレジストリ内容が優先されるのです。
■永続化されたレジストリ内容の扱い方の違い
以下に、OS イメージが置き換えられた際に、永続記憶域に保存されているレジストリ内容がどう扱われるのか、Hive-Based の場合と RAM-Based の場合のそれぞれについて、順に述べます。
(1)Hive-Based の場合
Hive-Based レジストリの場合に、OS イメージが置き換えられた直後のブートにおいて、OS イメージに収録されたレジストリの内容が優先される様子は、リファレンスの次のページの説明を見て下さい:
http://msdn.microsoft.com/en-US/library/ee489764(v=WinEmbedded.60).aspx (Hive-based Registry Initialization)
このページの、Filesys.dll による Hive-Based レジストリの初期化手順の 15 には、次の記述があります:
15. If a clean registry is not required, the system registry hive file (System.hv) is loaded from the file system that contains it. A signature in the system hive file is checked against a signature in the ROM portion of the system registry. If the signatures do not match, a clean registry is required, even if this is not indicated by IOCTL_HAL_GET_HIVE_CLEAN_FLAG.
つまり、永続記憶域に保存されているレジストリファイル(System.hv)の signature と、OS イメージに収録されている System.hv の signature を照合し、signature が等しくない場合は、永続記憶域に保存されているレジストリファイルを捨てて初期化する、というわけです。
(2)RAM-Based の場合
RAM-Based レジストリを永続化した場合に、OS イメージが置き換えられても、永続記憶域に保存されているレジストリ内容が優先される様子は、リファレンスの次のページの説明を見て下さい:
http://msdn.microsoft.com/en-US/library/ee490800(v=WinEmbedded.60).aspx (OAL Initialization of RAM-based Registry)
このページの、OAL での対応により RAM-Based のレジストリを永続化した場合の、Filesys.dll によるレジストリの初期化手順の 2 には、次の記述があります:
2. If the boot process is a cold boot, Filesys.dll performs the following steps:
a. Filesys.dll loads the registry data stored in the Default.fdf file from ROM.
b. If pReadRegistryFromOEM is implemented in the OAL and registry data is available, Filesys.dll deletes the registry data that was restored from Default.fdf. Filesys.dll calls pReadRegistryFromOEM until all of the data is returned. If the data retrieval fails, Filesys.dll cleans the registry and restores Default.fdf.
つまり、OAL が ReadRegistryFromOEM() を実装しており、それを呼び出してレジストリ内容を永続記憶域からロードできた場合は、OS イメージから取り出したレジストリ内容を捨てて、ReadRegistryFromOEM() でロードした内容を使う、ということです。
■RAM-Based レジストリの場合の注意
上で述べたように、RAM-Based のレジストリを永続化した場合は、OS イメージを置き換えても、永続記憶域に保存されたレジストリ内容が優先され、新しい OS イメージに収録されたレジストリ内容は、参照されません。このことは、あるレジストリ設定を変更したくて OS イメージを作りなおした場合に、OS イメージを置き換えても、意図したレジストリ設定の変更が行われない、ということを意味します。作り直した OS イメージに収録したレジストリ内容を有効にするには、永続記憶域に保存されたレジストリを消去して、OS イメージに収録されたレジストリ内容が永続記憶域に保存されるようにしなければいけません。
このことは、OS のバージョンアップを行う際、つまり、WinCE 本体に対する QFE の適用や、あるいは、OS イメージに含める独自デバイスドライバの修正などを行う際に、レジストリ内容の更新が必要な場合に問題となります。そのような場合を考慮して、RAM-Basded レジストリを永続化する対応を行う時は、永続記憶域に保存されたレジストリを消去する機能も実装しておく必要があります。
たとえば、OS イメージの置き換えを行う際にレジストリを消去できるように、イメージ置き換え機能を実装する、という方策が考えられます。あるいは、ブートローダにレジストリ初期化機能を追加し、WinCE カーネルを起動する前にレジストリを消去できるようにする、という方策も考えられます。
2011/08/13
koga
2011/02/21 に書いたエントリ(「レジストリ変更内容の永続化(2/2)」では、Hive-Based と RAM-Based の二種類があるレジストリのうち、Hive-Based のレジストリを永続化する仕組みについて説明しました。今回は、RAM-Based のレジストリの永続化について、Hive-Based のレジストリとの比較を交えて書きます。
まず、RAM-Based のレジストリを永続化する方法については、リファレンスの次のページに記載されています:
http://msdn.microsoft.com/en-us/library/ee489958(v=WinEmbedded.60).aspx (Persisting Data with the RAM-Based Registry)
http://msdn.microsoft.com/en-US/library/ee490769(v=WinEmbedded.60).aspx (Data Persistence with the RAM-based Registry Using the OAL)
実は、RAM-Based のレジストリを永続化する方法は二つあり、上の二つのページで、その各々について説明されています。それぞれの概要は、次の通りです。
(1) Oemregistry.dll という DLL を組み込む。この DLL で、ReadRegData(), WriteRegData(), RegistryOperation() を実装する。
(2) OAL (OEM Adaptation Layer) に、WriteRegistryToOEM() と ReadRegistryFromOEM() の実装を追加する。
これら二つの方法のうち、(1) は、永続記憶域としてファイルシステムを使う場合に適しています。一方、(2) は、NOR Flash の特定領域を割り当てるなどして、ファイルシステムを介さず、生の(raw)ストレージメディアにレジストリ内容を保存する場合に適しています。
どちらの場合も、レジストリの変更内容を永続記憶域に保存するためには、RegFlushKey() の呼び出しが必要です。Hive-Based のレジストリとは異なり、RAM-Based のレジストリでは、RegFlushKey() を呼び出さない限り、レジストリの変更内容は保存されません。
さて、上記 (1) と (2) のどちらも、WinCE 6.0 ではサンプル実装が提供されています。(1) は、リファレンスのページでも紹介されているように、%_WINCEROOT%/Public/Common/Oak/Drivers/FSD/Oemfs/ がサンプル実装です。このサンプル(Oemfs)では、永続記憶域として、リリースディレクトリファイルシステムを使用します。つまり、開発ホスト機の Flat Release Directory に、RAM-Based レジストリの内容がファイルとして保存されます。当然ですが、使用する場合は、OS Design のカタログ項目で、「リリースディレクトリファイルシステム」を選択して OS イメージをビルドしなければいけません。また、サンプルのままでは、ビルドの target name が Oemfs になっており、’Oemregistry’ とは違います。さらに、他の Public/ ディレクトリ配下のコンポーネントと同様、target type が LIBRARY になっていて、一旦 static library (.lib) としてビルドされた後、sysgen 変数でビルド対象に設定された場合にのみ、DLL (.dll) が生成されるように sources ファイルが記述されています。従って、sources ファイルを作り直し、target name を Oemregistry、target type を DYNLINK にする必要があります。
(2) のサンプル実装は、%_WINCEROOT%/Platform/Common/Src/Common/PerReg/ です。このソースから、oal_perreg.lib という static lilbrary がビルドされます。NOR Flash を搭載したボードの BSP で、NOR Flash に対する OALFlashErase() と OALFlashWrite() が実装されている場合には、oal_perreg.lib を oal.exe のリンクライブラリに追加して、PerReg の初期化関数を OAL の初期化時に呼び出せば(※OEMInit() の末尾がよいでしょう)、そのまま使用することができます。そのようにして oal_perreg.lib を組み込むだけで、NOR Flash の特定領域を使った永続化が、RAM-Based レジストリに対して可能になります。PerReg の初期化関数は、%_WINCEROOT%/Platform/Common/Src/INC/oal_perreg.h で宣言されている OALPerRegInit() です。
ところで、ここまで見てきて、レジストリを永続化する方法には、次の三つがあることが分かりました:
(a) Hive-Based のレジストリを組込み、永続化の設定を行う。
(b) RAM-Based のレジストリと Oemregistry.dll を組み込む(※配置先ファイルシステムは、適切に変更する必要あり)。
(c) RAM-Based のレジストリを組込み、OAL に WriteRegistryToOEM() と ReadRegistryFromOEM() の実装を追加する。
どれが最も良いのでしょうか?結論としては、WinCE 6.0 を動かすハードウェア次第であり、ケースバイケースだと言えます。ただし、NOR Flash を搭載したハードウェアであり、かつ、レジストリの書き換えが頻繁には起こらず、あらかじめ意図した変更内容だけを永続記憶域に保存したい場合には、(c) の方法が最も良いのではないかと思います。
なお、WinCE 6.0 のリファレンスを見ると、次のように書かれています:
”The preferred method for persisting registry data is to use the hive-based registry.”
(Persisting Data with the RAM-Based Registry)
”The preferred method for persisting data with the RAM-based registry is to include the OEMRegistry.dll sample file in your OS image.”
(Data Persistence with the RAM-based Registry Using the OAL)
これを見ると、最も preferred なのが (a)、その次が (b) で、最後が (c) という順番です。しかし、ここで言う “preferred” の順番は、堅牢性の観点から推奨されるものではなく、より一般的に使える方策の順、および、OS のカスタマイズが必要な度合いが小さい順であると思います。以下に、(a)~(b) の各々について、長所・短所を述べます。
(a) Hive-Based のレジストリ
長所:OS のカスタマイズは最小限。「レジストリ変更内容の永続化(2/2)」 で説明したように、OS イメージに組み込むレジストリの設定だけで対応できる。
短所:レジストリの Hive を配置するファイルシステムを Bootable に設定しなければならないため、そのファイルシステムが破損するなどしてマウントできなくなった場合、ブートの第2フェーズへ遷移できず、第1フェーズの完了待ちで WinCE カーネルの起動が停止してしまう。
その他:レジストリの Hive ファイルが、WinCE によってオープンされたままとなる。このため、WinCE の起動後にバックアップファイルで上書きするといったことができない。また、RegFlushKey() を呼び出さなくても、暗黙裡に Hive ファイルが更新されるため(※試してみたところでは、起動のたびに、ファイルが更新されます)、レジストリ内容の永続記憶域への書き出しを、意図したタイミングでしか行いたくない場合には、不向き。
(b) RAM-Based のレジストリ + Oemregistry.dll
長所:Oemregistry.dll を組み込むだけで対応でき、OAL の改変が不要。レジストリ内容を、ファイルシステム上にファイルとして保存するが、そのファイルシステムは Bootable に設定する必要がない。そのため、保存先のファイルシステムが破損しても、WinCE は起動する。
短所:レジストリ内容をファイルとして保存するため、レジストリ内容を隠したい場合には不向き。
(c) RAM-Based のレジストリ + OAL に WriteRegistryToOEM() と ReadRegistryFromOEM() を追加
長所:レジストリ内容は、ファイルシステムを介さずに保存できるため、ユーザから隠して保護するのが容易。また、NOR Flash を搭載したハードウェアであれば、NOR Flash 用のサンプル実装(PerReg)をそのまま使える。
短所:OAL の改変が必要。NOR Flash を搭載していないハードウェアの場合には、WriteRegistryToOEM() と ReadRegistryFromOEM() の実装は、それなりに手間がかかる。
どうでしょうか?それぞれの長所と短所は、相対的なものですから、どれかが絶対的に優れているとはいえず、ケースバイケースだと思います。
たとえば、ハードディスク(や SSD)を搭載し、そこに OS イメージも配置する設計のデバイス(ハードウェア)であれば、(a) が最も適しているでしょう。あるいは、上述したように、NOR Flash を搭載していて、レジストリの更新内容を特定の場合にしか保存しなくてよい仕様のデバイスであれば、最も適しているのは (c) だと思います。また、CF カードなどのストレージメディアに Bootable なファイルシステムを置くデバイスで、Bootable なファイルシステムへの書き込みを極力抑えたい、という場合には、(b) が適しているのではないかと思います。
※2011/08/13 追記
RAM-Based のレジストリを永続化した場合、OS イメージを書き換える際に問題となることがあります。新しい OS イメージで、レジストリ項目の追加や削除など、レジストリ内容の変更を行っていても、OS イメージを書き換えた際に、その変更が反映されない、という問題です。これについて、「RAM-Based レジストリの永続化に関する補足」に書きました。
2011/08/02
koga
今回は、前回提示したレジストリの設定内容を、レジストリが永続化される仕組みとともに説明します。弊社が提供している WinCE 6.0 の BSP(Lilas-am440-6; Armadillo-440 用の BSP)以外の BSP をお使いの方は、今回の説明を参考にして下さい。
・基本原理
レジストリを永続化する仕組みの基本原理は、次の通りです:
ブートの初期フェーズにおいて、レジストリが初期化される前に、レジストリのファイル(hive)を配置する永続記憶域用のデバイスドライバとファイルシステムをロードして初期化する。その後、レジストリのファイル(hive)が永続記憶域からロードされて、レジストリの初期化が完了する。
これは、前回示したリファレンスのページにも書かれています:
http://msdn.microsoft.com/en-us/library/ee490032.aspx (Implementing the Hive-Based Registry)
以下では、この仕組みを、詳しく見ていきます。
・三つのレジストリファイル
WinCE は、レジストリの配置形式として、RAM-Based と Hive-Based の二種類をサポートしています:
http://msdn.microsoft.com/en-us/library/ee490801.aspx (Registry Types)
前回示した手順では、OS Design で Hive-Based のレジストリを指定しました(カタログ項目の「Hive ベースのレジストリ」を選択)。上のページに書かれているように、WinCE 6.0 では、Hive-Based のレジストリがデフォルトです。上のページの末尾の方にある表で示されていますが、Hive-Based のレジストリでは、レジストリは、次の三つのファイルで構成されます:
- Boot.hv
このファイルは、必ず OS イメージの中にのみ配置され、永続記憶域に配置することは、できません。
- System.hv
- User.hv
これら二つのファイルは、Boot.hv 内での設定により、指定した永続記憶域、つまり OS イメージの外に配置できます。
Boot.hv の内容は、ブートの初期フェーズにのみ参照されます。bootable なファイルシステムがマウントされて、そのファイルシステム上に System.hv (および User.hv)が構築された時点で、破棄されます。その際、Boot.hv の内容に変更が加わっていれば、その変更は、System.hv に引き継がれます。Boot.hv は、ブートの初期フェーズにのみ参照されますので、初期フェーズで必要となる内容だけが収録されます。
Boot.hv の内容と、他の二つのファイルの初期状態の内容は、OS イメージのファイルを Platform Builder で開いてみれば分かります。Platform Builder の [ファイル] → [開く] → [ファイル...] メニューでオープンダイアログを開き、OS イメージのファイル(.bin ファイル)を指定してオープンしてみて下さい。OS イメージの内容を示すツリービューが表示されて、次の階層に、レジストリ内容が表示されます:
<OS イメージのファイル名>(※デフォルトは NK.bin)
NK
* ブートレジストリ
* レジストリ
(すべてのファイル)
「ブートレジストリ」配下が Boot.hv の内容、そして、「レジストリ」配下が System.hv と User.hv の内容です。注意深くみると、「ブートレジストリ」配下の内容と同じ項目が、「レジストリ」配下にもあることに気づくでしょう。これは、OS イメージがビルドされる際に、Boot.hv の内容が System.hv にマージされることによるものです。つまり、Boot.hv の内容は、System.hv の内容のうち、ブートの初期フェーズにだけ必要なサブセットになっているのです。
前回示したレジストリの設定内容は、先頭行が ‘; HIVE BOOT SECTION’ で、末尾行が ‘; END HIVE BOOT SECTION’ でした。.reg ファイルに、この二つの行で囲った記述があると、囲われた部分の内容は、OS イメージがビルドされる際に、Boot.hv に収録されます(結果として、System.hv にも収録されます)。WinCE は、ブートの初期フェーズにおいて Boot.hv を参照し、bootable なファイルシステムをマウントした後、そのファイルシステム上に System.hv を構築します。この時、ファイルシステム上に System.hv が存在していない場合(または、存在していても、その内容が有効でないと判定された場合)は、OS イメージ中の System.hv を、ファイルシステム上の指定されたディレクトリへコピーします。
デフォルトの設定では、RAMFS (Object Store) が bootable なファイルシステムとしてマウントされます。初期状態の RAMFS には、System.hv と User.hv は存在しませんので、コールドブートのたびに、OS イメージ内のものが RAMFS に展開される、というわけです。これが、前回の冒頭で述べた動作です。
参考:Platform Builder の、OS イメージの内容を閲覧する機能は、リファレンスの次のページで説明されています:
http://msdn.microsoft.com/en-us/library/ee482393.aspx (Run-Time Image Viewer)
注意:この節の冒頭で、WinCE は RAM-Based と Hive-Based の二種類のレジストリ配置形式をサポートしていると述べました。RAM-Based のレジストリに関するリファレンスを読むと分かりますが、実は、RAM-Based のレジストリも、永続化することが可能です。ただし、そのためには、BSP のカーネル移植レイヤ(OAL)での対応が必要となります:
http://msdn.microsoft.com/en-us/library/ee489958.aspx (Persisting Data with the RAM-Based Registry)
http://msdn.microsoft.com/en-us/library/ee490769.aspx (Data Persistence with the RAM-based Registry Using the OAL)
弊社が提供している WinCE 6.0 の BSP (Lilas-am440-6) の無償評価版では、現状、RAM-Based のレジストリの永続化には対応していません。
・Storage Manager と Device Manager の起動タイミング
これまでの説明で、「ブートの初期フェーズ」という表現を使いましたが、より正確には、「ブートの第1フェーズ」です。ブートのフェーズには、0, 1, 2 の三つがあります:
http://msdn.microsoft.com/en-us/library/ee490010.aspx (Boot Phases)
フェーズ0は、Boot.hv の内容がロードされる前の状態、フェーズ1は、Boot.hv の内容がロードされて、System.hv と User.hv が構築される前の状態です。System.hv と User.hv が構築されて、レジストリの初期化が完了した後、ブートが完了するまでの間がフェーズ2となります。
さて、前回示したレジストリの設定内容では、先頭の ‘Boot Phases’ 行の直後に、以下の行がありました:
[HKEY_LOCAL_MACHINE\init\BootVars]
"SystemHive"="REG\\system.hv"
"ProfileDir"="REG"
"Flags"=dword:3
"RegistryFlags"=dword:1
この BootVars キーの Flags の値の意味は、前回示したリファレンスのページで説明されています:
http://msdn.microsoft.com/en-us/library/ee490554.aspx (Hive-based Registry Setup)
Flags の値が3というのは、このページの後半にある表で、ビット 0×00000001 と 0×00000002 を共にセットした状態、つまり、Storage Manager と Device Manager を共に、ブートのフェーズ1で起動するという指定です。
BootVars キーの他のレジストリ値についても、上のページで説明されています。SystemHive は、System.hv のファイルパスであり(※この値で、System.hv 以外の名前に設定することが可能です)、ProfileDir は、User.hv を配置するディレクトリです。
(これを書いている時点では、上のページの ProfileDir の説明は、Systgem.hv の配置先と書かれており、正しくないように思われます。)
RegistryFlags については、上のページでは説明されていませんが、この値を1にすると、RegCloseKey() が呼ばれるたびに、レジストリの更新内容が保存されます:
http://msdn.microsoft.com/en-us/library/ee489941.aspx (Flush-On-Close Registry Flushing)
もし、パフォーマンスが問題となる場合には、RegistryFlags を1に設定せず(※デフォルトでは0になります)、必要な時点で、アプリケーションから RegFlushKey() を呼ぶようにすればよいでしょう。
・ストレージ用のデバイスドライバ
冒頭の「基本原理」の節で、”ブートの初期フェーズにおいて、レジストリが初期化される前に、レジストリのファイル(hive)を配置する永続記憶域用のデバイスドライバとファイルシステムをロードして初期化する。”と述べました。前回提示したレジストリ設定内容で、これに対応するのが、’[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\SDBusDriver]‘ 以降の部分です。そこでは、以下の設定を行っています:
- レジストリのファイルを配置する永続記憶域用のデバイスドライバのレジストリ設定を Boot.hv に収録し、かつ、ブートの第1フェーズでロードするよう指定。
- レジストリのファイルを配置するファイルシステムのレジストリ設定を Boot.hv に収録し、かつ、それを bootable に指定。
レジストリ設定を Boot.hv に収録する指定は、設定内容を ‘; HIVE BOOT SECTION’ と ‘; END HIVE BOOT SECTION’ で囲むことによって達成されます。通常、デバイスドライバのレジストリ設定は、System.hv のみに収録されますが、同じ設定内容を ‘; HIVE BOOT SECTION’ と ‘; END HIVE BOOT SECTION’ で囲って追記することにより、Boot.hv にも収録できます。
永続記憶域用のデバイスドライバを、ブートの第1フェーズでロードするのは、各デバイスドライバに対するレジストリの Flags の値で指定しています。具体的には、次の二つのキーの Flags の値に、dword:1000 を指定しています。
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\SDBusDriver]
...
"Flags"=dword:1000
...
[HKEY_LOCAL_MACHINE\Drivers\BuiltIn\ESDHC1_MX25]
...
"Flags"=dword:1000
...
これらは、それぞれ WinCE の SD バスドライバと、Armadillo-440 の SD ホストコントローラのドライバに対するキーです。Flags の値の意味は、リファレンスの次のページで説明されています:
http://msdn.microsoft.com/en-us/library/ee484469.aspx (ActivateDeviceEx)
dword:1000 は、DEVFLAGS_BOOTPHASE_1 であり、そのドライバをブートの第1フェーズでロードするという指定です。デフォルト(DEVFLAGS_BOOTPHASE_1 が指定されていない場合)では、たとえそのレジストリキーが Boot.hv に収録されていても、デバイスドライバは、ブートの第2フェーズ、つまり System.hv の構築が完了した後でロードされます。その逆に、Flags に DEVFLAGS_BOOTPHASE_1 が指定されていても、その設定内容が Boot.hv に収録されていなければ、ブートの第2フェーズでロードされます。
さて、前回示したレジストリ設定内容は、Armadillo-440 に装着した microSD カードを永続記憶域として使う設定でしたが、microSD のメモリカード用のドライバは、SD バスドライバと SD ホストコントローラのドライバに加え、SD メモリカードのドライバがあります。このドライバのキーは、
[HKEY_LOCAL_MACHINE\Drivers\SDCARD\ClientDrivers\Class\SDMemory_Class]
ですが、このキーには、Flags が指定されていません。指定してはいけないのです。SD メモリカードのドライバに対して、”Flags”=dword:1000 を指定すると、指定どおり、ブートの第1フェーズで順にロードされますが、それだとエラーが発生し、microSD のメモリカードを認識できなくなります。SD メモリカードのドライバは、SD カードのクライアントドライバ(クラスドライバ)であり、SD ホストコントローラを通じて microSD のメモリカードが認識された後、SD バスドライバによってロードされます。その際、SD メモリカードのドライバに対して “Flags”=dword:1000 が指定されていると、SD バスドライバがロードする時点で、既に Device Manager によって、SD メモリカードのドライバがロードされてしまっているため、ロードに失敗してしまうのです。バスドライバとクライアントドライバの関係は、他のバス、たとえば USB メモリの場合も、同様です。
Device Manager による、ブートの第1フェーズと第2フェーズでのドライバのロードの詳細に興味のある方は、WinCE のカーネルのソースツリーにある、Device Manager のソースをご覧になってみて下さい。以下が、関連するソースファイルです。
- <WINCE600_Root>/PRIVATE/WINCEOS/COREOS/DEVICE/DEVCORE/devcore.c
- <WINCE600_Root>/PRIVATE/WINCEOS/COREOS/DEVICE/DEVCORE/devload.c
devcore.c にある StartDeviceManager() で、ブートの第1フェーズと第2フェーズの各々での、ドライバのロード処理を行っています。devload.c には、第1フェーズでのロードを行う DevloadInit() と、第2フェーズでのロードに使われる InitDevices() の実装があります。また、ActivateDeviceEx() の実体である I_ActivateDeviceEx() も、devload.c で実装されています。
永続記憶域用のデバイスドライバを、ブートの第1フェーズでロードするよう指定する設定についての説明は、以上です。次に、ファイルシステムに対する設定です。ファイルシステムに対する設定は、Storage Manager に対する設定であり、前回示したレジストリ設定内容では、次の二つのキーです。
[HKEY_LOCAL_MACHINE\SYSTEM\StorageManager\Profiles\SDMemory]
"Folder"="SDMemory"
"Name"="SD Memory Card"
"AutoMount"=dword:1
"DefaultFileSystem"="FATFS"
"PartitionDriver"="mspart.dll"
[HKEY_LOCAL_MACHINE\System\StorageManager\Profiles\SDMemory\FATFS]
"MountPermanent"=dword:1
"MountAsBootable"=dword:1
このうち、一番目の SDMemory キーの内容は、もともと System.hv だけに収録される設定になっている内容を、コピーしたものです。Platform Builder で OS イメージのファイルを開き、「レジストリ」の配下からコピーしたいキーのパスを辿り、そのキーを右クリックして、ポップアップメニューの [テキストで表示] を選択すると、テキストエディタ(メモ帳)が開いて、キーの内容が表示されます。その機能を使ってコピーするのが便利でしょう。
二番目の SDMemory\FATFS キーの内容は、ここでは、必要となる追加の設定のみ行っています。これらのキーに対する説明は、リファレンスの次のページをご覧ください:
http://msdn.microsoft.com/en-us/library/ee490773.aspx (Mount Settings)
MountPermanent は、指定しなくても動作します。これは、マウントする際に、リムーバブルかどうかを指定するフラグ値ですが、レジストリファイルを配置しますので、リムーバブルではないことを示す1を指定しています。MountAsBootable は、bootable であることを示す1を指定しています。MountAsBootable については、次の節で、もう少し詳しく述べます。
・MountAsBootable
このフラグ値は、当該ファイルシステムが bootable かどうかを示す値ですが、レジストリファイルを配置するファイルシステム(今回の設定例では、FATFS)に対して、このフラグ値に1を指定するだけでは、期待通りに動作しません。その理由は、デフォルトでは、bootable と指定されたファイルシステムが他にあり、そちらが先にマウントされるためです。レジストリファイルが配置されるファイルシステムは、bootable と指定されたもののうち、最初にマウントされたものになります。
さて、注意深い方は、ここまでの説明の中で、前回示したレジストリ設定のうち、まだ説明されていない行があることにお気づきだと思います。設定内容のうち、二番目に書かれている次のキーです。
[HKEY_LOCAL_MACHINE\System\StorageManager\AutoLoad\ObjectStore]
"MountAsBootable"=dword:0
このキーが、レジストリファイルを配置するファイルシステムに対するキーに加えて、追加で必要な設定です。「三つのレジストリファイル」の節の末尾の方で、”デフォルトの設定では、RAMFS (Object Store) が bootable なファイルシステムとしてマウントされます。”と述べました。その設定を無効にするのが、上の ObjectStore キーに対する設定なのです。この設定により、RAMFS (Object Store) が bootable でないファイルシステムとしてマウントされますので、レジストリファイルを配置したいファイルシステム(今回の設定例では、FATFS)が、bootable なファイルシステムとしてマウントされる最初のものとなり、無事に、レジストリファイルの配置先として選択される、というわけです。
前節で述べた、Device Manager によるブートの第1フェーズでのドライバのロード処理の後、第2フェーズへ遷移する前に、bootable なファイルシステムがマウントされて、レジストリの初期化が完了するのを、Event オブジェクトを用いて待ち合わせる仕組みになっています。このため、第1フェーズから第2フェーズへの遷移は、次のようになります:
0.) Boot.hv での設定により(BootVars キー)、Device Manager と Storage Manager がブートの第1フェーズで(つまり、Boot.hv の内容のロード後に)起動される。
1.) Device Manager が、ブートの第2フェーズへ遷移する前に、Boot.hv で Flags の値の DEVFLAGS_BOOTPHASE_1 ビットが設定されているドライバをロードする。
→今回の設定例では、SD バスドライバと SD ホストコントローラが、ロードされる。
2.) Device Manager は、ブートの第1フェースでのロードを指定されたドライバ群をロードし終えた後、レジストリの初期化が完了するのを待つ。
3.) SD ホストコントローラが microSD カードを検出し、SD バスドライバへ通知することにより、SD バスドライバが(Boot.hv の設定内容により)SD メモリカードのドライバをロードする。
(※もし、デフォルト設定のまま、ObjectStore が bootable に指定されていると、microSD カードが検出される前に、RAMFS (Object Store) がマウントされるので、RAMFS 上に System.hv と User.hv が構築されて、以降の動作を待たずに、Device Manager が第2フェーズへ遷移してしまう。)
4.) SD メモリカードのドライバが、microSD メモリカードのパーティションを検出して、Storage Manager へ通知することにより、microSD メモリカードのファイルシステムがマウントされる。
5.) Storage Manager が(Boot.hv の設定内容により)microSD カードのファイルシステムを bootable としてマウントし、それがレジストリ機能に通知される結果、microSD カード上に System.hv と Use.hv が構築されて、レジストリの初期化が完了する。
6.) レジストリの初期化完了が、Event オブジェクトを通じて、(2) で待ち動作を行っている DeviceManager に通知されて、待ち動作が解除される。
7.) Device Manager は、ブートの第2フェーズへ遷移して、System.hv にレジストリ設定内容が収録されている、残りのデバイスドライバ(※起動時の自動ロードが指定されているもの)をロードする。
bootable なファイルシステムのうち、最初にマウントされたものだけが bootable として記録される様子は、WinCE のカーネルのソースツリーにある、File System Disk Manager (FSDMGR) のソースを見ると分かります。興味のある方は、次のソースをご覧になってみて下さい。
<WINCE600_Root>/PRIVATE/WINCEOS/COREOS/STORAGE/FSDMGR/mounttable.cpp
mounttable.cpp にある MountTable_t::RegisterVolume() で、MountAsBootable の指定により bootable と設定されたファイルシステムのうち、最初にマウントされたものだけを bootable なボリュームとして記録するようになっています。
・まとめ
以上で、Hive ベースのレジストリを永続化する手順の説明は終わりです。まとめとして、以下に、手順をおさらいしてみます。
1.) OS Design に、「Hive ベースのレジストリ」を追加する。
デフォルトでは「Hive ベースのレジストリ」が選択されますが、念のために指定しておくのがよいでしょう。
2.) <WINCE600_Root>/PLATFORM/AM440/FILES/platform.reg の末尾に、’; HIVE BOOT SECTION’ と ‘; END HIVE BOOT SECTION’ の2行を追加して、追加した2行の間に、以下で述べる内容を挿入する。
3.) [HKEY_LOCAL_MACHINE\init\BootVars] キーの設定。
レジストリファイルのパスと、Flags、および、RegistryFlags の設定を追加する。
4.) [HKEY_LOCAL_MACHINE\System\StorageManager\AutoLoad\ObjectStore] キーの設定。
RAMFS (Object Store) が bootable としてマウントされないように、MountAsBootable に0を指定する。
5.) レジストリを配置する永続記憶域のデバイスドライバ(バスドライバとホストコントローラのドライバ、およびクラスドライバ)の設定。
各々、System.hv に収録されている内容をコピーする。バスドライバとホストコントローラのドライバに対しては、ブートの第1フェーズでロードされるように、Flags の値に 0×1000 (DEVFLAGS_BOOTPHASE_1) を指定する。
6.) レジストリを配置する永続記憶域のデバイスに対する StorageManager の Profiles 配下のキーを設定する。
<プロファイル名> キーの内容は、System.hv に収録されている内容をコピーする。<プロファイル名>\<ファイルシステム名> キー(または、<プロファイル名>\<ファイルシステム名>\<パーティション名> キー)に対しては、MountAsBootable を1に設定する(他は省略可能)。
前回の設定例で対象にした、弊社提供の WinCE 6.0 BSP 以外の BSP をお使いの場合や、また、弊社提供の WinCE 6.0 BSP で、microSD カード以外の永続記憶域(USB メモリ)をお使いの場合には、上のまとめを参考にして下さい。永続記憶域のデバイスドライバに対するレジストリキーが分からない場合は、BSP の提供元に問い合わせしてみるのがよいでしょう。
・おまけ
今回の説明で、「ブートの初期フェーズ」という表現を使いましたが、それは主に、ブートのフェーズ1でした。フェーズ0、つまり、Boot.hv がロードされる前の処理、特にカーネル本体の初期化が完了するまでのブートシーケンスについては、全く触れていません。カーネル本体の初期化が完了するまでのシーケンスに興味のある方は、リファレンスの次のページが、取っ掛かりとして参考になるでしょう:
http://msdn.microsoft.com/en-us/library/ee482950.aspx (Nk.exe Boot Process)
ところで、Windows CE 5.0 までは、WinCE はマイクロカーネル構造を採用しており、マイクロカーネル本体と、Device Manager や Storage Manager は、それぞれ異なるプロセスとして動作する仕組みでした(Device Manager は Device.exe のプロセスに所属、Storage Manager は Filesys.exe のプロセスに所属)。Windows Embedded CE 6.0 において、それらは、カーネルにロードされる DLL となりました(Device Manager は Device.dll に所属、Storage Manager は Filesys.dll に所属)。つまり、純粋なマイクロカーネル構造から、モノリシックカーネルに近い構造に変わったのですが、モジュール分割の構造は、大きくは変わっていません。上のページで説明されているのは、Windows Embedded CE 6.0 のカーネル本体の初期化シーケンスですが、これは、Windows CE 5.0 までのマイクロカーネル本体のブートシーケンスに該当するものとなります。
Windows CE 5.0 と Windows Embedded CE 6.0 の違いは、@IT MONOist の次の記事が参考になるでしょう:
http://monoist.atmarkit.co.jp/fembedded/winembedded/ce6_01/ce02.html (「Windows CE 6で刷新されたアーキテクチャ」)
2011/02/21
koga
Previous Posts