2015年1月7日水曜日

ウォッチドッグタイマーの盲信(湘南モノレール出発信号冒進事故から学ぶ)

今回はちょっといつもとはテイストの違う記事を書いてみようかなって思います。

もう7年近く前の事故になりますが、湘南モノレールが本来停止すべき西鎌倉駅を止まれずに出発信号を冒進し、対向列車とあわや正面衝突になりかけた事故がありました。

http://jtsb.mlit.go.jp/jtsb/railway/detail.php?id=1744

VVVFインバーターが誤作動して力行状態のままに固定されてしまい、最終的に運転士が非常ブレーキ保安ブレーキを取り扱ったにもかかわらず十分な減速が得られなかったというのがこの事故の原因です。
幸い、死傷者等はいませんでしたが、何重にも安全装置が付けられている鉄道で安全に止まれなかったのかというのは非常に興味深いところではあります。運転士が異常を感じたにもかかわらずただちに停止しようとしなかったというヒューマンエラー的なところから、ノイズを受けやすい配線構造、VVVFインバーターの制御装置がノイズによって異常な信号を受信したときにフェイルセーフにならないプログラムのバグなど、さまざまな原因が絡み合って、最終的にこのような事故といった形で表面に現れてきました。

ここはプログラミング系のブログということもあり、特にそのプログラムのバグのあたりから得られる教訓と設計思想について考えていきたいと思います。


VVVFインバーターの制御プログラムは下図のようになっていたようです。

 (事故調査報告書 P.10図1より引用)

ウォッチドッグタイマー(WDT)はこの手のソフトウェア監視システムとしては非常に一般的なものです。ウォッチドッグ=番犬がメインプロセッサーやクロックから分離された別系統として独自に動作しており、一定時間以上メインプロセッサ側からWDTリセット命令が実行されなかったらシステム全体にリセットを掛ける仕組みになっています(文中で使われる『リセット』という言葉が、WDTをリセットすることなのか、WDTがシステム全体をリセットすることなのかがわかりにくくなっていますが、是非混同しないようにお願いします)。万が一ソフトウェアがハングアップしたり、その他のハードウェア故障等でクロックが停止した場合にリセットを掛けられるということですね。

さて、フローチャートを見ると割り込みによって時間をカウントし、そのカウントされた時間によって10msごとにメインプログラムで加減速シーケンスを処理するようになっています。至ってシンプルで、誰でも作りそうなプログラムです。ここに一体どういう問題が起きたのでしょう。

右側に不正割り込みがあったときの処理が書いてあります。ここで、他の割り込みを禁止するようになっています。どういう意図で割り込みを禁止して戻るだけにしたのか、もしくはハードウェアの制約等でそうなってしまったのかはわかりませんが、こういう仕様になっていたそうです。
また、3両編成の車両の1両のみケーブルの断面積が小さいためノイズがアースに流れにくいなどといった条件も組み合わさった結果、電源装置から発生したノイズがVVVFインバーターの制御装置へ入り込み、それを不正な割り込みとして拾ってしまいました。そうなったら最後、タイマーはインクリメントされないがWDTがリセットされ続けるという状態になってしまい、加減速シーケンスが今後一切処理されることの無いまま、WDTも自らが監視している装置に不具合が起きていることを認識できない状態になってしまいました。そのため、ノイズによる不正割り込みが起こる直前の状態、すなわちこの時は力行状態に固定されてしまったのです。


さて、このトラブルからどういった教訓が得られるでしょうか。

「ウォッチドッグタイマーは一定時間以上リセットされなくなったとき、すなわちハングアップしたときにリセットを掛ける機能」という理解だけでは甘いことがあるということです。「一定時間以上リセットされないのならハングアップした」は必要十分条件ではなく十分条件でしかないということですね。すなわち、「ループのいつも通るところにリセット命令を置いておけばいいや」と機械的にプログラムを書くのでは、本当に有事のときにWDTが正常に作動してくれないことがあるということです。

特に、割り込みはいわばマルチスレッドプログラミングです。例えばタイマー割り込みで一定時間ごとにWDTをリセットするプログラムが意味が無いということはわかるでしょう。なぜならば、メインプログラムがハングしてもプロセッサーが動いてさえいれば割り込みは受け付けられますから延々とWDTはリセットされ続けます。
またその逆でも問題は起こりえます。割り込みですべての処理をしていたとして、メインプログラムでWDTをリセットし続けたら、例えば何らかの要因で割り込みが発生しなくなってしまって一切の処理が行われない状態になってしまっても、WDTは延々とリセットされ続けます。
また、今回のように処理をするにあたる条件かどうかの分岐の外でWDTのリセットを掛けるプログラムにしても問題が起きます。
なので、このような状態ではWDTは何も本来の意味をなさないわけです。WDTのリセット命令を置く位置は、本当に慎重に吟味しなければなりません。


今回のこの事故を受けて、湘南モノレールの車両メーカーは非常ブレーキが投入された場合に主回路を遮断する処理の追加と、WDTのリセット処理を加減速シーケンスの直後への変更を制御プログラムに行ったようです。(事故報告書 P.67 6.2)

前者はよりフェイルセーフを確固なものにするための変更ですね。今まで独立していた非常ブレーキとモーター制御に対して、非常ブレーキ作動時にモーターへ電流が流れないようにプログラムを変更したということですね。

後者はWDTのリセット位置の再検討ですね。これは当然の対応です。

(事故調査報告書 P.10図1を一部編集)

おそらく、事故報告書の文面から上図のようなフローチャートのプログラムに変更されているはずです。これによって、加減速シーケンス処理が一定以上行われていないことがすなわちWDTへのリセット命令が実行されないということになり、今回のケースにおけるバグは解消されました。


また、これ以外にも、運転の取り扱いマニュアルに「非常ブレーキを掛けても減速感が無い場合はレバースハンドル(進行方向を逆転するためのスイッチ)を「切」にする(筆者注:おそらくモーターへの電流を遮断するという意味だと思われる)」といったことも付け加えられたようです。(事故報告書 P.67 6.1)
実はこれはとても重要です。プログラミングをする人ならば、プログラムへのバグの混入は避けられないものであることは承知しているかと思います。どんなにデバッグしても、どんなに動作検証をしても、思いもよらないところでたまにバグることがあるかと思います。もちろん、ソフトウェアの構造設計やプログラミングのスキルによってそれはある程度低減させることはできますが、「100%完璧なプログラムでいかなる状態でも確実に動作します」なんて言える人はどこにもいません。なので、このようにソフトウェアの外で安全を担保する仕組みはとても重要なのです。



さて、それではWDT使用時の注意点をいくつか私なりにまとめてみました。今回の事故から得られる教訓以外にも、重要だと思う点は挙げたつもりです。

プログラムの検証段階からWDTを使用しない

これはとても重要です。まだ検証が十分に行われていないプログラムは余裕でバグります。
しかし、WDTを使用していたら、そのバグによって起こった異常状態からすぐさまリセット処理を行い回復するでしょう。そうなった場合、プログラムのバグが人間の目に届きにくくなります。
WDTによるリセット処理が行われるのは正常な状態ではありません。そのようなリセット処理は出番が無いのが一番です。 なので、WDTの導入はソフトウェアが完成した、もうこれでリリースできるぞというときに導入すべきです。

WDTのリセット命令を複数か所に入れない

これもとても重要です。複数か所にリセット命令を入れてしまうと、万が一1か所で問題が起きても他の場所に設置されたリセット命令でWDTがリセットされ続け、正常にWDTによるリセット処理がされないという状態になりえます。WDTのリセット命令は1か所だけ、これは鉄則です。

割り込み処理中にWDTリセット命令を置かない

これは上でも少し触れました。割り込みはメインプログラムで何が起きていようと発生するため、割り込み処理中にWDTリセットを行うとメインプログラムでのハングアップに気づけないことが十分にあり得ます。なので、WDTのリセット命令はもっとも優先度の低いプログラムに置くというのは鉄則です。
また、今回の事故のように、メインプログラムでも割り込みに依存する場所で問題が起きる場合が考えられるので、そのあたりも含めて慎重に検討しましょう。

WDTリセット命令はクリティカルな処理の近くに置く(不用意にリセットしまくらない)

これが今回の湘南モノレールの事故を踏まえた教訓ですね。
クリティカルな処理を行うかどうかの分岐の外側にWDTリセット命令を設置してしまったがために、 クリティカルな処理が行われないままWDTがリセットしまくられるということになってしまいました。なので、クリティカルな処理の近くで、かつ周期的にリセットがかけられる位置に置くべきです。
「クリティカルな処理は不定期に行われる」という場合は困りますね。ケースにもよるでしょうが、どこかその周辺の定期的に呼ばれる場所に置くしかないんですかね。ちなみに、「クリティカルな処理が行われる周期が大きすぎてWDTのリセット周期に間に合わない」とか「複数クリティカルな処理がある」といったケースはもはや信頼性を求められる機器では設計を考え直せという話になるかと思います。

WDTを盲信しない

最後にこれです。異常をソフトウェア外で感知した場合、もしくは人間が緊急停止の操作をした場合などは、ハードウェアレベルでそれを優先し安全に停止するようなシステムを検討すべきです。WDTによって想定しえないトラブルから回復できるかもしれないですが、でもそれは万能ではありません。あくまでも保険だという気持ちを持って、WDTを信用しすぎないことが重要です。想定しえないようなトラブル時に復帰する機能が正常に動くかなんて検証することすら難しいですもんね。


いかがでしょうか。

まあ、私は趣味のプログラマーなのでそこまで人命にかかわるような装置の組み込みプログラムを書くことは滅多に無いのですが、でも安全は大切です。WDTを導入するようなプログラムを書くときは、是非ともこれくらいは頭に入れながら設計したいものですね。
もしかしたら、現場のプロのプログラマーの間ではこれくらい当たり前で、さらにもっと体系化された知識があるのかもしれません。そういう情報を入手できる機会があったら是非ともまた吸収していきたいなと思います。


あと、盲信は禁物です。この記事を読んでWDTを導入したけどうまく動作しなくて命の危機に見舞われたとか、そんなことの無いように、あくまでも自分で責任を持って考えながら、もしくは勉強しながらやっていただけたらなと思います。

0 件のコメント:

コメントを投稿