復刻:キーワードガイダンス第13回「リード・モディファイ・ライト」 ― 2014年05月10日 01時04分18秒
初出:技術評論社「組込みプレスVol.15」(平成21(2009)年5月12日発売)
●内容の不定な上書き
ある会社では社員の行動予定の管理を共有エリアに置いた「予定表」というEXCELファイルにより行っていました。
社員全員の行動予定が記入できるようになっているファイルです。
このファイルの書き換えは基本的にはAさんだけが行う約束です。
社外からは電話をAさんにかけて予定表を書き換えてもらいます。
さて、Aさんはこの予定表の書き換えを以下のように行っていました。
(1) 共有エリアのファイルを自分のパソコンにコピーする。
(2) 自分のパソコンにコピーするのは出社直後の9時過ぎ
(3) 電話連絡は概ね10時前には終わるので、10時までは自分のパソコン上でファイルを修正します。
(4) 念のため10時過ぎまで待って、10時10分頃、自分のパソコンのファイルを共有エリアにコピーします。
●約束違反
そんなある日、営業のBさんが行方不明になりました。
予定表に何も書かれていないのに、出社してこないのです。
もちろん、Aさんにも予定を告げる連絡は入っていません。
この事件は営業担当Bさんの約束違反によるものでした。
パソコンやネットワークに詳しいBさんは会社の外から社内の共有エリアのファイルを読み書きできるようにしていました。
Bさんは客先に出かける予定をAさんに連絡するのを忘れていたため、当日の朝9時半頃に移動中のパソコンでネットワーク接続して、会社の予定表ファイルを書き換えたのです。
Bさんはこれで連絡できたつもりでいました。
ところがAさんはBさんに書き換えられる前のファイルを9時過ぎには自分のパソコンにコピーしていますから、Bさんの予定が入っていないファイルを編集して、10時過ぎには予定表ファイルを上書きしてしまいました。
当然Bさんの予定内容はなくなってしまいました。
このような経緯により、Bさんは行方不明となったのです。
Bさんの約束違反が全ての原因です。
●本当に約束違反か
実はBさんは、中途入社後半年も経っていたのですが、Aさんしか予定表ファイルを修正してはいけないという「約束事」を知らされていませんでした。
先輩社員はみんな「当たり前のこと」として周知すべき項目から抜けていたからです。
とりあえず、Aさんに連絡を入れるように、ということだけは聞かされていました。
そんなBさんは、いつもはAさんに予定の連絡はするものの、それは単なる「慣習」程度にしか考えていなかったのです。
実はBさんはこの半年の間、直接予定表ファイルを修正してしまっていました。
Bさんが予定表ファイルを修正するのは大抵、帰宅前。17時頃でしょうか。
そんな行き違いもあり、事件は発生したのです。
●リード・モディファイ・ライトとは
以上の事例がリード・モディファイ・ライトの全てです。
要するにリード・モディファイ・ライトは以下のように整理できます。
(1) データを別の領域にコピーする(リード)
(2) コピーしたデータの内容を修正する(モディファイ)
(3) 修正した内容のデータを元のデータに上書きする(ライト)
この操作を行うのが一つのタスク、一つの割り込み処理など、同一コンテキストだけであれば問題ありません。
問題が発生するのは複数のタスク、複数の割り込み処理で同一のデータの書き換えを行う必要がある場合です。
●リード・モディファイ・ライトの注意事項
以下の様なデータに対してはアクセス時の十分な注意が必要です。
・CPUの制御レジスタの書き換え
・ワンチップマイコン内蔵のI/O制御用レジスタなどの書き換え
・タスク関で情報伝達するために用意したグローバル変数の書き換え
リード・モディファイ・ライトが回避できないような処理にせざるを得ない場合には以下の対応策が必要です。
・リード・モディファイ・ライトを行う区間を割り込み禁止にする
・セマフォなどを使って、データの排他制御を行う
・アクセス関数を用意して、その関数経由でしかデータを操作できないようにする
リード・モディファイ・ライトが原因の組込みシステムの「原因不明の誤作動」は枚挙に暇がないくらいです。
●内容の不定な上書き
ある会社では社員の行動予定の管理を共有エリアに置いた「予定表」というEXCELファイルにより行っていました。
社員全員の行動予定が記入できるようになっているファイルです。
このファイルの書き換えは基本的にはAさんだけが行う約束です。
社外からは電話をAさんにかけて予定表を書き換えてもらいます。
さて、Aさんはこの予定表の書き換えを以下のように行っていました。
(1) 共有エリアのファイルを自分のパソコンにコピーする。
(2) 自分のパソコンにコピーするのは出社直後の9時過ぎ
(3) 電話連絡は概ね10時前には終わるので、10時までは自分のパソコン上でファイルを修正します。
(4) 念のため10時過ぎまで待って、10時10分頃、自分のパソコンのファイルを共有エリアにコピーします。
●約束違反
そんなある日、営業のBさんが行方不明になりました。
予定表に何も書かれていないのに、出社してこないのです。
もちろん、Aさんにも予定を告げる連絡は入っていません。
この事件は営業担当Bさんの約束違反によるものでした。
パソコンやネットワークに詳しいBさんは会社の外から社内の共有エリアのファイルを読み書きできるようにしていました。
Bさんは客先に出かける予定をAさんに連絡するのを忘れていたため、当日の朝9時半頃に移動中のパソコンでネットワーク接続して、会社の予定表ファイルを書き換えたのです。
Bさんはこれで連絡できたつもりでいました。
ところがAさんはBさんに書き換えられる前のファイルを9時過ぎには自分のパソコンにコピーしていますから、Bさんの予定が入っていないファイルを編集して、10時過ぎには予定表ファイルを上書きしてしまいました。
当然Bさんの予定内容はなくなってしまいました。
このような経緯により、Bさんは行方不明となったのです。
Bさんの約束違反が全ての原因です。
●本当に約束違反か
実はBさんは、中途入社後半年も経っていたのですが、Aさんしか予定表ファイルを修正してはいけないという「約束事」を知らされていませんでした。
先輩社員はみんな「当たり前のこと」として周知すべき項目から抜けていたからです。
とりあえず、Aさんに連絡を入れるように、ということだけは聞かされていました。
そんなBさんは、いつもはAさんに予定の連絡はするものの、それは単なる「慣習」程度にしか考えていなかったのです。
実はBさんはこの半年の間、直接予定表ファイルを修正してしまっていました。
Bさんが予定表ファイルを修正するのは大抵、帰宅前。17時頃でしょうか。
そんな行き違いもあり、事件は発生したのです。
●リード・モディファイ・ライトとは
以上の事例がリード・モディファイ・ライトの全てです。
要するにリード・モディファイ・ライトは以下のように整理できます。
(1) データを別の領域にコピーする(リード)
(2) コピーしたデータの内容を修正する(モディファイ)
(3) 修正した内容のデータを元のデータに上書きする(ライト)
この操作を行うのが一つのタスク、一つの割り込み処理など、同一コンテキストだけであれば問題ありません。
問題が発生するのは複数のタスク、複数の割り込み処理で同一のデータの書き換えを行う必要がある場合です。
●リード・モディファイ・ライトの注意事項
以下の様なデータに対してはアクセス時の十分な注意が必要です。
・CPUの制御レジスタの書き換え
・ワンチップマイコン内蔵のI/O制御用レジスタなどの書き換え
・タスク関で情報伝達するために用意したグローバル変数の書き換え
リード・モディファイ・ライトが回避できないような処理にせざるを得ない場合には以下の対応策が必要です。
・リード・モディファイ・ライトを行う区間を割り込み禁止にする
・セマフォなどを使って、データの排他制御を行う
・アクセス関数を用意して、その関数経由でしかデータを操作できないようにする
リード・モディファイ・ライトが原因の組込みシステムの「原因不明の誤作動」は枚挙に暇がないくらいです。
復刻:キーワードガイダンス第12回「assert禁止」 ― 2014年05月09日 00時03分59秒
初出:技術評論社「組込みプレスVol.14」(平成21(2009)年2月21日発売)
●論理エラー検出手法
C言語およびC++言語のプログラミングテクニックの一つとしてassert()マクロの利用があります。
例えば、ポインタを引数とする関数を利用する場合、引数となるポインタがNULLではないということを「期待」しているような場合、以下のようになります。
void funcp ( void * p )
{
assert( NULL != p );
//ポインタ正常時処理
}
このように作られた関数に対して引数にNULLを与えて関数コールすると以下のようなエラーメッセージが出力されます。
atest: asserttest.c:22: funcp: Assertion `((void *)0) != p' failed.
assert()マクロはこのように論理的に期待している状態と異なる状態を検出してプログラムの実行を停止させることが出来るのです。
非常に便利な機能であることは確かで、デバッグ後には「-DNDEBUG」をCコンパイラの引数に指定するなどの方法で簡単にassert()が生成するコードを削除することができます。
ソースコード上にassert()を挿入して単体テストの効率を上げるというような発想の下に積極的な活用が薦められることも少なくありません。
●性善説のassert
先の例のようにポインタがNULLになるような条件というものは一過性のデバッグ時期にのみ発生する不具合と言い切ることが出来るでしょうか。
呼び出し側で値を保証すべきであり、呼び出された側ではそのようなチェックを行う必要は本当にないのでしょうか。
これは不具合のないプログラム実装が行われるべき、という考えが前提の「性善説」が基本になっているという言い方もできます。
仮想記憶システムではない環境では複数のタスクから共通のRAM領域へのアクセスが可能であることから、関係のない処理の想定外の不具合によって自関数のみが使用しているはずのポインタ変数がクリアされるような現象が発生しないとは言い切れません。
「いつの間にか」、「突然に」、「想定外に」NULLに変わってしまうというような現象が発生することになります。
assert()でチェックしている「デバッグ時期」には現象が発生せず、assert()を無効にした「リリース後」に発生する不具合かもしれません。
●assertとfail-safe
一方で、組込み機器は実行時に不正な状況に陥ってもシステムを停止させずに安定稼動させる必要があるという命題を持っています。
「デバッグ時にのみ存在する不正な状態」とは限定すべきではなく、「常に不正な状態を回避する実装」が要求されます。
いわゆるfail-safe(フェイルセーフ)の概念が必要で、不正な状況に陥っても単純にプログラムを停止させて「プログラマに知らせる」のではなく、安全な状態でシステムを落ち着かせる必要があります。
この意味においてはassertの概念と組込みシステム開発におけるfail-safeの概念は根本から食い違っていると言えます。
したがって、デバッグ時にのみ論理的に不正な状態を検出するassert()は組込みシステム開発では使用すべきではなく、常に論理的な不正を検出して対応を行うための機能をリリース版として実装する必要があるわけです。
最後に先ほどの関数のassert()を使わない場合のコードは以下のようになります。
void funcp ( void * p )
{
if( isvalid(p) ) {
//ポインタ正常時処理
}
else {
//ポインタ不正時処理
}
}
●論理エラー検出手法
C言語およびC++言語のプログラミングテクニックの一つとしてassert()マクロの利用があります。
例えば、ポインタを引数とする関数を利用する場合、引数となるポインタがNULLではないということを「期待」しているような場合、以下のようになります。
void funcp ( void * p )
{
assert( NULL != p );
//ポインタ正常時処理
}
このように作られた関数に対して引数にNULLを与えて関数コールすると以下のようなエラーメッセージが出力されます。
atest: asserttest.c:22: funcp: Assertion `((void *)0) != p' failed.
assert()マクロはこのように論理的に期待している状態と異なる状態を検出してプログラムの実行を停止させることが出来るのです。
非常に便利な機能であることは確かで、デバッグ後には「-DNDEBUG」をCコンパイラの引数に指定するなどの方法で簡単にassert()が生成するコードを削除することができます。
ソースコード上にassert()を挿入して単体テストの効率を上げるというような発想の下に積極的な活用が薦められることも少なくありません。
●性善説のassert
先の例のようにポインタがNULLになるような条件というものは一過性のデバッグ時期にのみ発生する不具合と言い切ることが出来るでしょうか。
呼び出し側で値を保証すべきであり、呼び出された側ではそのようなチェックを行う必要は本当にないのでしょうか。
これは不具合のないプログラム実装が行われるべき、という考えが前提の「性善説」が基本になっているという言い方もできます。
仮想記憶システムではない環境では複数のタスクから共通のRAM領域へのアクセスが可能であることから、関係のない処理の想定外の不具合によって自関数のみが使用しているはずのポインタ変数がクリアされるような現象が発生しないとは言い切れません。
「いつの間にか」、「突然に」、「想定外に」NULLに変わってしまうというような現象が発生することになります。
assert()でチェックしている「デバッグ時期」には現象が発生せず、assert()を無効にした「リリース後」に発生する不具合かもしれません。
●assertとfail-safe
一方で、組込み機器は実行時に不正な状況に陥ってもシステムを停止させずに安定稼動させる必要があるという命題を持っています。
「デバッグ時にのみ存在する不正な状態」とは限定すべきではなく、「常に不正な状態を回避する実装」が要求されます。
いわゆるfail-safe(フェイルセーフ)の概念が必要で、不正な状況に陥っても単純にプログラムを停止させて「プログラマに知らせる」のではなく、安全な状態でシステムを落ち着かせる必要があります。
この意味においてはassertの概念と組込みシステム開発におけるfail-safeの概念は根本から食い違っていると言えます。
したがって、デバッグ時にのみ論理的に不正な状態を検出するassert()は組込みシステム開発では使用すべきではなく、常に論理的な不正を検出して対応を行うための機能をリリース版として実装する必要があるわけです。
最後に先ほどの関数のassert()を使わない場合のコードは以下のようになります。
void funcp ( void * p )
{
if( isvalid(p) ) {
//ポインタ正常時処理
}
else {
//ポインタ不正時処理
}
}
復刻:キーワードガイダンス第11回「ハイブリッドOS」 ― 2014年05月08日 22時55分51秒
初出:技術評論社「組込みプレスVol.13」(平成20(2008)年11月7日発売)
●いいとこどり
「ハイブリッド」という言葉が一般的に使われるようになった代表としては「ハイブリッドカー」(Hybrid Car)が挙げられます。
電気で動作する「モーター」とガソリンなどの燃料を使った「エンジン」(内燃機関)の複数の動力源を使い分けるなど、複数の技術を応用した車のことです。
エンジンとモータを駆動源として切り替えるタイプとエンジンを単なるモータ駆動のための発電機として使うタイプに大きく分けられます。
前者を「パラレル方式」、後者を「シリーズ方式」と呼びます。
ハイブリッドカーはエンジンと電気モータの「いいとこどり」をした車ということができます。
同じように開発効率および移植性の高さとリアルタイム性能の追求という二つの「いいとこどり」を狙った技術が「ハイブリッドOS」です。
●ハイブリッド化の意義
ハイブリッドOSはひとつのプロセッサでμITRONとLinuxを共存させたり、μITRONとWindowsCEを共存させるというようなことが行われます。
つまり、リアルタイム性能が高いとされる、μITRONと他のOSとの組み合わせで語られることが多いようです。
この場合のLinuxまたはWindowsCEなどのOSをここでは便宜的に「ゲストOS」と呼びましょう。
このようなことを行う視点は二つあります。
(1) μITRON資産重視
これはμITRONで開発した資産をそのまま使い、μITRON上で対応していないプロトコルスタックなどの追加開発を行うのが大変という場合。
この大変な開発がゲストOSでは既に解決済みであることが重要です。
TCP/IP関連のプロトコルスタック、ファイルシステムとの連携などについて目を向けられることが多くなります。
(2) リアルタイム性能補完重視
ゲストOSでの豊富な開発資産があるが、製品性能的にリアルタイム性能が要求されるイベント処理を追加実装したい場合。
●ハイブリッド化が必要なシステム
(1)の視点でハイブリッド化を行う場合、既にリアルタイム性能が要求されるデバイス制御を伴った実装が行われているということが重要です。
つまり、ベースシステムがそれほどリアルタイム性能を追求していないμITRONシステムなのであれば、ハイブリッド化するまでもなく、ゲストOS環境に乗り換えれば済む話です。
(2)の視点でも同様の側面があります
リアルタイム性能を引き上げたいというのは既存の機能の性能改善という意味ではなく、独立したハードウェアイベントの処理について語る必要があるというところが重要です。
つまり、例えばTCP/IPの通信性能を上げたいとか、WWWサーバの応答性を上げたいという意味のリアルタイム性能補完ではなく、特定の追加デバイスの割り込み応答性能がLinuxのデバイスドライバでは満たせない、というような場合にのみ有効だということです。
そのような特殊デバイスや特殊イベント処理の対応予定もないのにハイブリッドOSを使う必要はありません。
●ハイブリッドOSの実装の基本は「パラレル方式」
以上のようにハイブリッドOSの利用目的のポイントを考えるとハイブリッドカーでの「シリーズ方式」は選択肢からは外れます。
μITRON上でイベント検知してその応答処理をLinux側のプロセスで処理するというような構成はあまり意味がないからです。
リアルタイム性能が要求されるデバイスに対する処理は全てμITRONで完結させて、WWWサーバ経由での表示処理などの少しくらい遅れても構わない処理のためにその情報をμITRONからLinuxのプロセスに通知するというような使い方になります。
このような目的を踏まえてハイブリッドOSには主に以下の機能が実装されます。
(a) ゲストOS起動機能
(b) 割り込み調停機能
(c) OS間通信機能
割り込み調停機能部分でデバイス単位の割り込み処理対象OSを確定するようにパラレル制御します。
●いいとこどり
「ハイブリッド」という言葉が一般的に使われるようになった代表としては「ハイブリッドカー」(Hybrid Car)が挙げられます。
電気で動作する「モーター」とガソリンなどの燃料を使った「エンジン」(内燃機関)の複数の動力源を使い分けるなど、複数の技術を応用した車のことです。
エンジンとモータを駆動源として切り替えるタイプとエンジンを単なるモータ駆動のための発電機として使うタイプに大きく分けられます。
前者を「パラレル方式」、後者を「シリーズ方式」と呼びます。
ハイブリッドカーはエンジンと電気モータの「いいとこどり」をした車ということができます。
同じように開発効率および移植性の高さとリアルタイム性能の追求という二つの「いいとこどり」を狙った技術が「ハイブリッドOS」です。
●ハイブリッド化の意義
ハイブリッドOSはひとつのプロセッサでμITRONとLinuxを共存させたり、μITRONとWindowsCEを共存させるというようなことが行われます。
つまり、リアルタイム性能が高いとされる、μITRONと他のOSとの組み合わせで語られることが多いようです。
この場合のLinuxまたはWindowsCEなどのOSをここでは便宜的に「ゲストOS」と呼びましょう。
このようなことを行う視点は二つあります。
(1) μITRON資産重視
これはμITRONで開発した資産をそのまま使い、μITRON上で対応していないプロトコルスタックなどの追加開発を行うのが大変という場合。
この大変な開発がゲストOSでは既に解決済みであることが重要です。
TCP/IP関連のプロトコルスタック、ファイルシステムとの連携などについて目を向けられることが多くなります。
(2) リアルタイム性能補完重視
ゲストOSでの豊富な開発資産があるが、製品性能的にリアルタイム性能が要求されるイベント処理を追加実装したい場合。
●ハイブリッド化が必要なシステム
(1)の視点でハイブリッド化を行う場合、既にリアルタイム性能が要求されるデバイス制御を伴った実装が行われているということが重要です。
つまり、ベースシステムがそれほどリアルタイム性能を追求していないμITRONシステムなのであれば、ハイブリッド化するまでもなく、ゲストOS環境に乗り換えれば済む話です。
(2)の視点でも同様の側面があります
リアルタイム性能を引き上げたいというのは既存の機能の性能改善という意味ではなく、独立したハードウェアイベントの処理について語る必要があるというところが重要です。
つまり、例えばTCP/IPの通信性能を上げたいとか、WWWサーバの応答性を上げたいという意味のリアルタイム性能補完ではなく、特定の追加デバイスの割り込み応答性能がLinuxのデバイスドライバでは満たせない、というような場合にのみ有効だということです。
そのような特殊デバイスや特殊イベント処理の対応予定もないのにハイブリッドOSを使う必要はありません。
●ハイブリッドOSの実装の基本は「パラレル方式」
以上のようにハイブリッドOSの利用目的のポイントを考えるとハイブリッドカーでの「シリーズ方式」は選択肢からは外れます。
μITRON上でイベント検知してその応答処理をLinux側のプロセスで処理するというような構成はあまり意味がないからです。
リアルタイム性能が要求されるデバイスに対する処理は全てμITRONで完結させて、WWWサーバ経由での表示処理などの少しくらい遅れても構わない処理のためにその情報をμITRONからLinuxのプロセスに通知するというような使い方になります。
このような目的を踏まえてハイブリッドOSには主に以下の機能が実装されます。
(a) ゲストOS起動機能
(b) 割り込み調停機能
(c) OS間通信機能
割り込み調停機能部分でデバイス単位の割り込み処理対象OSを確定するようにパラレル制御します。
復刻:キーワードガイダンス第10回「再起呼び出し禁止」 ― 2014年05月06日 06時46分24秒
初出:技術評論社「組込みプレスVol.12」(平成20(2008)年8月8日発売)
●スマートなプログラミング
プログラミングテクニックとして「再帰呼び出し」という方法があります。
「リカーシブコール(recursive call)」とも呼びます。
関数などの処理の中で自分自身(の関数)を呼び出すという方法で、
抽象的な概念のアルゴリズムを表現するのに便利です。
例えば1からnまでの整数の足し算を行うC言語関数については
再帰呼び出しを使うと以下のようになります。
int SumFunc(int n)
{
if( n<=1 ) return n;
return(n+SumFunc(n-1));
}
再帰呼び出しではない方法で上記の処理を行う場合はforループなどを
使用した処理が必要になります。
例えば、以下のようになります。
int SumFunc1(int n)
{
int sum;
for(sum=0;n>0;n--)
{
sum += n;
}
return sum;
}
●無限と有限
再帰呼び出しのメリットは以下になります。
(1) 複雑なアルゴリズムを少ないソースコードで表現できる
(2) 概念的には無限のパターンの処理を記述できる
少ないソースコードで任意の引数に応じた結果を返す処理を記述できるため、ソフトウェアの開発効率としては非常に高いことになります。
同じアルゴリズムの考え方は言語にもCPUにも依存しません。
メリットだけを考えると非常に有効な手法となります。
しかし、再帰呼び出しには以下のデメリットがあります。
(a) 関数コールを繰り返すため、スタック領域を大量に消費する
(b) 例題のような処理の場合には単なるループ処理よりも処理時間を消費するつまり、メモリ領域や処理時間の制限がある組込みシステム開発では使ってはいけないテクニックの一つです。
組込みシステムでは常に「有限」を意識する必要があるからです。
●アルゴリズムの移植の問題
なお、自ら新規に開発する場合には再帰呼び出しを封印することは可能ですが、他のシステム開発で実装されたアルゴリズムを流用するような場合には注意が必要です。
複雑なアルゴリズムが再帰呼び出しで実装される可能性として例えば、画像処理、圧縮処理、暗号処理などが考えられます。
このようなアルゴリズムの流用を行う場合には、予めスタック使用量が
変動するかどうか、スタックの最大消費量はどれ位になるかなどの検討を事前に行う必要があります。
●組込み開発流実装例
さて、再帰呼び出しの例として1からnまでの整数の足し算を挙げました。
組込みソフトウェア開発で頻繁に行われる実装例を以下に示します。
int SumFunc2(int n)
{
#define MAX (sizeof(tbl)/sizeof(int))
#define ERROR -1
const int tbl[] = { 1,3,6 };
if( n>=1 && n<=MAX )
return tbl[n-1];
else
return ERROR;
}
CPUでは計算させずに予め計算結果を格納した配列を確保して、
計算結果は取り出すだけとする方法です。
この方法のメリットは以下になります。
(1) ループ処理を行わないため、処理速度がほぼ一定
(2) スタック消費量が一定
デメリットとしては以下になります。
(a) 処理できるパターンが有限
(b) 処理パターンとROMの使用量が比例して増加する
実際にはそれぞれの手法のメリットとデメリットを考慮して実装すること
になりますが、再帰呼び出しは許容できないケースが多くなるでしょう。
●スマートなプログラミング
プログラミングテクニックとして「再帰呼び出し」という方法があります。
「リカーシブコール(recursive call)」とも呼びます。
関数などの処理の中で自分自身(の関数)を呼び出すという方法で、
抽象的な概念のアルゴリズムを表現するのに便利です。
例えば1からnまでの整数の足し算を行うC言語関数については
再帰呼び出しを使うと以下のようになります。
int SumFunc(int n)
{
if( n<=1 ) return n;
return(n+SumFunc(n-1));
}
再帰呼び出しではない方法で上記の処理を行う場合はforループなどを
使用した処理が必要になります。
例えば、以下のようになります。
int SumFunc1(int n)
{
int sum;
for(sum=0;n>0;n--)
{
sum += n;
}
return sum;
}
●無限と有限
再帰呼び出しのメリットは以下になります。
(1) 複雑なアルゴリズムを少ないソースコードで表現できる
(2) 概念的には無限のパターンの処理を記述できる
少ないソースコードで任意の引数に応じた結果を返す処理を記述できるため、ソフトウェアの開発効率としては非常に高いことになります。
同じアルゴリズムの考え方は言語にもCPUにも依存しません。
メリットだけを考えると非常に有効な手法となります。
しかし、再帰呼び出しには以下のデメリットがあります。
(a) 関数コールを繰り返すため、スタック領域を大量に消費する
(b) 例題のような処理の場合には単なるループ処理よりも処理時間を消費するつまり、メモリ領域や処理時間の制限がある組込みシステム開発では使ってはいけないテクニックの一つです。
組込みシステムでは常に「有限」を意識する必要があるからです。
●アルゴリズムの移植の問題
なお、自ら新規に開発する場合には再帰呼び出しを封印することは可能ですが、他のシステム開発で実装されたアルゴリズムを流用するような場合には注意が必要です。
複雑なアルゴリズムが再帰呼び出しで実装される可能性として例えば、画像処理、圧縮処理、暗号処理などが考えられます。
このようなアルゴリズムの流用を行う場合には、予めスタック使用量が
変動するかどうか、スタックの最大消費量はどれ位になるかなどの検討を事前に行う必要があります。
●組込み開発流実装例
さて、再帰呼び出しの例として1からnまでの整数の足し算を挙げました。
組込みソフトウェア開発で頻繁に行われる実装例を以下に示します。
int SumFunc2(int n)
{
#define MAX (sizeof(tbl)/sizeof(int))
#define ERROR -1
const int tbl[] = { 1,3,6 };
if( n>=1 && n<=MAX )
return tbl[n-1];
else
return ERROR;
}
CPUでは計算させずに予め計算結果を格納した配列を確保して、
計算結果は取り出すだけとする方法です。
この方法のメリットは以下になります。
(1) ループ処理を行わないため、処理速度がほぼ一定
(2) スタック消費量が一定
デメリットとしては以下になります。
(a) 処理できるパターンが有限
(b) 処理パターンとROMの使用量が比例して増加する
実際にはそれぞれの手法のメリットとデメリットを考慮して実装すること
になりますが、再帰呼び出しは許容できないケースが多くなるでしょう。
復刻:キーワードガイダンス第9回「リエントラント性とスレッドセーフ」 ― 2014年05月05日 21時46分04秒
初出:技術評論社「組込みプレスVol.11」(平成20(2008)年5月9日発売)
●Linuxライターの作業手順とリエントラント性
突然ですが、「組込みプレス」と「Software Design」という2種類の雑誌があります。
通常はこれら各々の読み手となるターゲットは異なるのですが、たまたまどちらの雑誌もLinuxの特集を行うことになったとします。
さらに、両方の編集部から一人のLinuxを得意とするライターに執筆の
依頼が届いたとします。
大雑把にはLinuxというカテゴリの記事ですが、それぞれ詳細な記事の目的は異なります。
締め切り日も近く、どうしても並行して執筆作業をする必要がありましたが、どちらの仕事もそのライターは引き受けてしまいました。
そのLinuxライターは一人、執筆に使う道具も同じです。
ですが、それぞれの記事はそれぞれの依頼元の意向に沿った形で執筆し、また、提出する原稿の内容はそれぞれ独立している必要があります。
もし同じ原稿用紙に1行ごとにそれぞれの雑誌の記事を書き込んでいく
などということをしたらどうなるでしょうか。
記事の内容としてはとても脈絡のないものになってしまい、依頼元の要求どおりの出力は得られないことでしょう。
一人のライターがそれぞれの雑誌の記事をそれぞれの意向に沿った形で提出できる状態がリエントラント性を持った処理であるということが言えます。
●複数のアプリケーションから同じ処理を呼び出す
ソフトウェアの開発に話を戻します。
ここで、ライターを共通のプログラム、依頼元の意向をプログラムに対しての引数、独立した出力結果を得る為に内部で使用するメモ用紙を独立したリソース、出力結果もまた独立したリソースと読み替えて見ます。
つまり、プログラム実行コードの実体は一つしかなくとも、それぞれの引数を独立して解釈でき、それに応じたリソースを使用して、複数の独立した結果を出せることが「リエントラント性がある」ということが出来ます。
それぞれの処理の手続きの流れを以下に示します。
<組込みプレス出版処理>
①プログラム(kpress.exe)開始
②処理Aを行う
③サブルーチンLinuxWriter(組込みプレス用執筆依頼)を呼び出す
④出力結果を貰う
⑤処理Bを行う
⑥プログラム(kpress.exe)終了
<Software Design出版処理>
①プログラム(sdesign.exe)開始
②処理αを行う
③サブルーチンLinuxWriter(Software Design用執筆依頼)を呼び出す
④出力結果を貰う
⑤プログラム(sdesign.exe)終了
最初のプログラムkpress.exeと次のプログラムsdesign.exeは共に同じ手続きを処理するサブルーチンLinuxWriter()を呼び出しています。
●共通のプログラムはどこに実体があるか
ここで、2つのプログラムを「プロセス」と表現しなおしましょう。
プロセスの場合、kpress.exeとsdesign.exeの両方のプロセスの中にLinuxWriter()がクローンとしてそれぞれに実体が存在し、静的なメモリはもちろんのこと、スタック上の変数などは独立した仮想メモリ空間に存在できます。
この時、LinuxWriter()はスレッドセーフである必要はありません。
ところが「技術評論社プロセス」という一つのプロセスが「組込みプレス出版スレッド」と「SoftwareDesign出版スレッド」を生成、実行してLinuxWriter()を使用する場合はLinuxWriter()はスレッドセーフである必要があります。
では、この2つのプログラムがスレッドや仮想記憶をサポートしないRTOS上で動作するタスクだったとします。
同様に呼び出されるLinuxWriter()はスレッドセーフである必要があります。
更に、LinuxWriter()も独立したタスクとして扱うことにすると、「スレッドセーフ」という表現は使わず、「リエントラント性がある」という言葉を使います。
●まとめ
最後にリエントラント性とスレッドセーフについて整理します。
・プロセス単位で使う共通処理は必ずしもスレッドセーフである必要はない。
・同じメモリ空間を共有しているマルチタスク環境や同じプロセスから生成される複数のスレッドから呼び出させる共通処理はスレッドセーフである必要がある。
・スレッドセーフという言葉はプログラムの一部に存在する共通処理部分に対して使われる。
・リエントラント性という言葉は抽象的な話の中やタスクやスレッド自体が 複数の処理依頼を同時に受け付けることが出来るような場合に使用する。
※最近はこのような場合でもスレッドセーフを使う事も多い。
●Linuxライターの作業手順とリエントラント性
突然ですが、「組込みプレス」と「Software Design」という2種類の雑誌があります。
通常はこれら各々の読み手となるターゲットは異なるのですが、たまたまどちらの雑誌もLinuxの特集を行うことになったとします。
さらに、両方の編集部から一人のLinuxを得意とするライターに執筆の
依頼が届いたとします。
大雑把にはLinuxというカテゴリの記事ですが、それぞれ詳細な記事の目的は異なります。
締め切り日も近く、どうしても並行して執筆作業をする必要がありましたが、どちらの仕事もそのライターは引き受けてしまいました。
そのLinuxライターは一人、執筆に使う道具も同じです。
ですが、それぞれの記事はそれぞれの依頼元の意向に沿った形で執筆し、また、提出する原稿の内容はそれぞれ独立している必要があります。
もし同じ原稿用紙に1行ごとにそれぞれの雑誌の記事を書き込んでいく
などということをしたらどうなるでしょうか。
記事の内容としてはとても脈絡のないものになってしまい、依頼元の要求どおりの出力は得られないことでしょう。
一人のライターがそれぞれの雑誌の記事をそれぞれの意向に沿った形で提出できる状態がリエントラント性を持った処理であるということが言えます。
●複数のアプリケーションから同じ処理を呼び出す
ソフトウェアの開発に話を戻します。
ここで、ライターを共通のプログラム、依頼元の意向をプログラムに対しての引数、独立した出力結果を得る為に内部で使用するメモ用紙を独立したリソース、出力結果もまた独立したリソースと読み替えて見ます。
つまり、プログラム実行コードの実体は一つしかなくとも、それぞれの引数を独立して解釈でき、それに応じたリソースを使用して、複数の独立した結果を出せることが「リエントラント性がある」ということが出来ます。
それぞれの処理の手続きの流れを以下に示します。
<組込みプレス出版処理>
①プログラム(kpress.exe)開始
②処理Aを行う
③サブルーチンLinuxWriter(組込みプレス用執筆依頼)を呼び出す
④出力結果を貰う
⑤処理Bを行う
⑥プログラム(kpress.exe)終了
<Software Design出版処理>
①プログラム(sdesign.exe)開始
②処理αを行う
③サブルーチンLinuxWriter(Software Design用執筆依頼)を呼び出す
④出力結果を貰う
⑤プログラム(sdesign.exe)終了
最初のプログラムkpress.exeと次のプログラムsdesign.exeは共に同じ手続きを処理するサブルーチンLinuxWriter()を呼び出しています。
●共通のプログラムはどこに実体があるか
ここで、2つのプログラムを「プロセス」と表現しなおしましょう。
プロセスの場合、kpress.exeとsdesign.exeの両方のプロセスの中にLinuxWriter()がクローンとしてそれぞれに実体が存在し、静的なメモリはもちろんのこと、スタック上の変数などは独立した仮想メモリ空間に存在できます。
この時、LinuxWriter()はスレッドセーフである必要はありません。
ところが「技術評論社プロセス」という一つのプロセスが「組込みプレス出版スレッド」と「SoftwareDesign出版スレッド」を生成、実行してLinuxWriter()を使用する場合はLinuxWriter()はスレッドセーフである必要があります。
では、この2つのプログラムがスレッドや仮想記憶をサポートしないRTOS上で動作するタスクだったとします。
同様に呼び出されるLinuxWriter()はスレッドセーフである必要があります。
更に、LinuxWriter()も独立したタスクとして扱うことにすると、「スレッドセーフ」という表現は使わず、「リエントラント性がある」という言葉を使います。
●まとめ
最後にリエントラント性とスレッドセーフについて整理します。
・プロセス単位で使う共通処理は必ずしもスレッドセーフである必要はない。
・同じメモリ空間を共有しているマルチタスク環境や同じプロセスから生成される複数のスレッドから呼び出させる共通処理はスレッドセーフである必要がある。
・スレッドセーフという言葉はプログラムの一部に存在する共通処理部分に対して使われる。
・リエントラント性という言葉は抽象的な話の中やタスクやスレッド自体が 複数の処理依頼を同時に受け付けることが出来るような場合に使用する。
※最近はこのような場合でもスレッドセーフを使う事も多い。
最近のコメント