WorkPad 用プログラムの組み方(6)

Prev 1 , 2 , 3 , 4 , 5 , 6, 7 , Next


このページの目次


Alarm Manager を使ってはいけない

もし、あなたが Application をつくるならば、 Alarm Manager を使ってはいけない。

もし、あなたが Application を使うならば、 Alarm Manager を使ったものをインストールするべきではない。

一見、何の問題もない Alarm Manager は、 深刻な問題を引き起こす元になる。 この問題は「めったに起こらない」ため、 確実に問題が発生したときにはその理由を忘れている。 そのため、ユーザーがこの点を指摘することはなかろう。 しかし、ここには確実に問題が存在する。 これを判っていて使うのは、技術者として恥知らずである。


PalmOS Reference Manual によると、

Alarm Manager は特定の時刻になったことを検出し、 必要なアプリケーションを特定の起動コードで起動する、

とある。 各 Application ごとに 1つづつ時刻を指定することができる。

Application が使って良い API は

   ULong AlmGetAlarm( cardNo, dbID, refP );
   Err   AlmSetAlarm( cardNo, dbID, ref, alarmSeconds, quiet );

の2つだけである。 一応、内部 API としてこれらの他に

   Err   AlmInit(void);
   void	 AlmEnableNotification(Boolean enable);
   void	 AlmDisplayAlarm(Boolean okToDisplay);
   void	 AlmCancelAll(void);
   void	 AlmAlarmCallback(void);

が存在する。

Alarm Manager は、 Reference Manager の曖昧な記載とは異なり、 特定の時刻を 1sec ごとの real time 割り込みで検出し、 Alarm Event Queue に登録されている Application のどれかが 引っ掛かった場合、
『割り込みハンドラー上で』
その Alarm を起動する。 Alarm Manager 用 thread 等、 Alarm 用 thread resource があるわけではない。

Alarm 起動は2段階あり、 1つ目は GUI を使ってはいけない火急勝つ速やかに終了するべきコール、 2つ目は GUI などを使っても良いコール、ということになっている。 しかしどちらも割り込みハンドラー上で実行されているのに変りはない。

1-5 までに書かれていた情報と以上の情報、 ならびに Palm device の普通の挙動だけから、 Alarm Manager は使ってはいけないことがわかる。 最低2通りの問題が発生しうることが判るし、 その中の一つが時刻問題を引き起こしている要因だろう、とも判る。


日付問題

最初に来るのは俗に「日付問題」という名で知られている問題である。

Alarm が起動すると、Application は起動される。 最初に起動された場合、起動コードは sysAppLaunchCmdAlarmTriggered である。 この起動キーでは一切の「待ち」なしで終了しなくてはいけない。 Alarm が発生したときに User に何も表示できないのでは意味がないので、 大抵の場合、ここでは sysAppLaunchCmdDisplayAlarm で再度起動してもらえるように、設定して終了する。

Alarm Manager は起動するべき Application を一度すべて起動したあと、 sysAppLaunchCmdDisplayAlarm で再度起動するべき Application について 各 Application を起動し直す。 このコードで起動された Application は GUI などを使った、 User Interface を使ってユーザーに状態を報告する。 このように動くもっとも典型的なプログラムが「予定表」だろう。

さて、ではここで
「ユーザーがこれに対して何も反応しなかったら」
何が起こるだろうか?

User Interface が起動するということは、 PalmOS は表示能力を on にする。 これと同時に、PalmOS の内部で時限タイマーが動き出す。 このタイマーによって一定の時間が経つと PalmOS は表示能力を off にし、 sleep モードに陥る。

Alarm の UI に対してユーザーが反応しない場合も PalmOS はこれと全く同じ動作をする。 というのは、User Interface は基本的にユーザーからの Action を 受け付けるために普通の Event 処理ルーチンが動いており、 SysHandleEvent() に到達した段階で、
「Sleep モードに陥れ」
イベントが処理されるからだ。

Sleep 状態に陥るために必要な処理は、 実は SysHandleEvent() システムコールを呼び出した段階で 保存されている。 Application thread は他の thread と同様 CPU の現状を保存され、 thread ID を持たない場合は保存された後、 パワーセービングモードに陥る。

あとで、電源を入れると、『最後に保存された thread が』起動される。 Alarm Manager で起動されたのは「thread ではないので」、 thread resource としては影響を受けない。


そう、影響を受けないのは「thread resource だけ」なのだ。 Alarm 割り込み処理を行う間マスクされた割り込みは、復元されない。 それは PalmOS の『グローバルリソース』であって、 thread resource ではないからだ。

Alarm Manager がまだ、ある Alarm 似ついての処理を終了していないうちに、 次の Alarm 処理が発生したら、困る。 Alarm 処理、特に sysAppLaunchCmdDisplayAlarm 処理中は、ユーザーからのアクションを受け付ける必要があるため、 すべての割り込みをブロックすることはできない。 そこで、Alarm は自分を起動する元である、 Real Time Clock による割り込みをブロックする。 Real Time Clock の割り込みは、「日」割り込みと「秒」割り込みが 使われており、 その両方がブロックされる。

Alarm 割り込みがあろうがなかろうが、 「日付」管理処理はソフトウェアで実行する以外に手がない。 このため、割り込みルーチンは『日の割り込み』はブロックされていない、 と想像している。

一方で、「秒」の割り込みは、電源を切った場合、 Alarm Manager が管理するべき割り込みが存在しない場合、 ブロックした方が効率が良い。 このため、「電源が入った場合」に「秒の割り込み」は復元される。

以上ですべての情報はそろった。

Alarm Manager は「日の割り込み」と「秒の割り込み」の両方をブロックし、 User Interface モードに突入する。
ユーザーはこれに一切反応せず、 Sleep モードに突入する。 このとき、 「日の割り込み」と「秒の割り込み」は両方ともブロックされている。
電源が入ると、「秒の割り込み」は復元する。 そしてほぼすぐ、「秒の割り込み」である level 4 の割り込みが処理される。

もし、「日の割り込み」が2度以上本来発生するはずだったとしても、 電源を入れる前には何も処理されていない。 電源を入れると「秒の割り込み」が起動され、同じ level 4 の割り込みである 「日の割り込み」があったことは検知できる。 検知はできるが「何度」発生したのかは判らない。 そこで、日付処理ルーチンは(常識的に)1度発生したのだろうと判定し、 日付を1つ進める。

これが、「日付問題」と呼ばれる問題が発生する理由である。


これで、なぜ日付問題が発生するのかは判ったが、さて…。 では、PalmOS はどうあれば良かったのだろうか? Alarm Manager の構造を変更することなく、 割り込みマスクをどのように操作すれば良いのだろうか?

熟慮すれば判るが(というか、これは総当たり戦になってしまうので、 説明するとめちゃくちゃ長くなり、説明する気になれないが)、
「この問題は解決できない」
が答だ。

Alarm Manager が専用の thread を持ち、これが UserInterface を自由に 操作する権限を持っていれば、この問題は解決できる。 sysAppLaunchCmdDisplayAlarm は、Alarm Manager thread が処理するべき問題でしかなく、 PalmOS が GUI をマルチウィンドウシステムとして作り上げておけば、 単に「メッセージウィンドウが1つ増えた」だけでしかないのだ。 これがいかに便利に動くか、は unix を使っていれば良く判るだろう。 cron や at コマンドなど、unix にはこの手の
「時間が来たら処理をしろ」
系の process が山のように動いている。 それらが適宜動き回って、便利な環境を作り上げているわけだ。

しかし、PalmOS は GUI 回りを「Application Thread」という1つの thread しか操作できないようにすることで、 排他制御機能をサボってしまった。
160x160 という狭い画面でマルチウィンドウでもなかろう、 とウィンドウシステムの作りをサボってしまった。
このため、UI を必要とする Alarm Manager は、 割り込みルーチンの中で実行する、 という形で排他制御を実装するしかなくなってしまった。

このように Alarm Manager は、 PalmOS の割り込みマスク構造を破綻に追い込む構造になっている。 この問題は PalmOS をよほど根刮ぎ直さない限り修正できない。 従って Alarm Manager API は使うべきではないのだ。


削除、update 問題

「予定表」あるいは「Datebook」と言われる、 標準で ROM に載っているアプリケーションの機能に不満があって、 Datebk3 のような Application を使っている人は多いだろう。 さて、そのような人に質問だ。 Datebk3 で Alarm を設定したあと、 Datebk3 を削除してほしい。何が起こる?

PalmOS のバージョンにもよるだろうが、 fatal error が発生するはずだ。なぜだろう? 何が fatal error を起こしているのだろう?

答は API 自身に書かれている。

   Err   AlmSetAlarm( cardNo, dbID, ref, alarmSeconds, quiet );

というこの API から判る通り、 Alarm Manager は Application を発見するのに、 cardNo と dbID を使っている。 cardNo と dbID は Database を一意に指定するためのものだが、 ここに「存在しない Database」を指定すると fatal error が発生するのだ。

仮に CreatorID とか Database 名とか、 そういった「一度検索しなくてはいけない」ものであれば 検索した段階で Database が存在しないことが判る。 しかし dbID ではそのようなことはできないし、そのようにはならない。 Alarm Manager は cardID と dbID が
「Alarm がかかったときでも存在している」
ことを仮定しているから、そのまま fatal error になるのだ。


これは RAM に載せる Application としてはかなりやばい、 ということが判るだろう。

ROM に載っている Application であれば、何ら問題はない。 Application が消されることは(少なくとも ROM が破損しない限り)あり得ない。 dbID は常に存在する Application を指しており、 破綻することはあり得ない。

しかし、RAM 上にある Application は違う。 消されることも、update されることもある。 消されれば指定した dbID をもつ Application は根元的になくなる。 update した場合、Application は存在するかもしれないが、 その 新しい(あるいは古い)バージョンの Application が 同じ dbID を偶発的に持っている、という可能性は低い。

Application が消されても Alarm Manager はそのことを検知しない。 従って、Alarm Manager が管理している Alarm リストから Application が消されることもない。
消される Application に「消されるぞ処理」起動が発生することもない。 インストールされるときには「インストールされたぞ」起動が発生するのに。
従って、Application が消されるときに Application が、 Alarm Manager をどうにかすることはできない。

update された場合でも、 新しい Application は古い Application の dbID を知るすべがない。 従って、

   ULong AlmGetAlarm( cardNo, dbID, refP );

を使って、何か存在するか調べることもできない。

Application は何もできず、ユーザーは何も気が付かず、 ある日、気が付くと fatal error が起こることになる。

Alarm Manager は Alarm List をきちんと管理しているだろうか? つまり、Alarm が発生した場合、まず 対象となる list を Alarm list から消し、その後に Application を起動しようとしているのだろうか? それとも Application を起動し、そのあとで Alarm List をいじるのだろうか?

答は簡単で、「Application を起動し、そのあとで Alarm list をいじっている」。

なぜ判るか?
sysAppLaunchCmdDisplayAlarm で再度呼び出してもらうかどうか、 Application は返事を返してくる。 もし「再度呼び出してほしくない」ものだけを先に消せば、 残っているものが「再度呼び出さなくてはいけない」 Application だからだ。

ということは、 Application の update などが原因で 一旦 Alarm 関係で fatal error が発生すると、 soft reset をかけても Alarm Manager が再び Alarm list を検索し、 問題のある Application を起動し…を繰り返すことになる。

このような長時間かかる問題では、 ユーザーはもちろん、プログラマー自身も、 何が原因で問題が起こっているのか把握することは不可能だ、 というのも判ろう。

robust な Application をつくるにあたっては、 このような危険な API を使うべきではない、 というのも判るだろう。


Prev 1 , 2 , 3 , 4 , 5 , 6, 7 , Next

2000-08-03 むぅ。こんな問題点があるとは思わなかった。