VMの割り込みが地味に難しい

コードが無限ループしたときにバックトレースすら出ないのは如何なものかということで、CTRL+Cで割り込めるようにしてみた(今のところWin32のみ)。

(import (rnrs)
        (srfi :48)
        (nmosh debugger hooks main-thread))

(define count 0)

(define (loop)
  (format #t "Mainloop! ~a\n" count)
  (set! count (+ 1 count))
  (loop))

(loop)

こういうコードを実行中に、CTRL+Cすると

Mainloop! 247
Mainloop! 248
Mainloop! 249
MINTERRUPT!
TRACE!! :
  1 : ==USRP== (_format port format-string args return-value) @ \\?\c:\repos\mos
h\lib\srfi\%3a48\intermediate-format-strings.sls:54
  2 : ==USRP== (_format port format-string args return-value) @ \\?\c:\repos\mos
h\lib\srfi\%3a48\intermediate-format-strings.sls:54
  3 : ==USRP== (loop) @ \\?\C:\build\nmoshdll64\hook.sps:7

のようにちゃんと止まる。
今の実装は本当にVMのフラグ変数を見ているだけなので、あとでちゃんとatomic命令に直す。Boehm GCを使っているうちはatomic_opsで良いが。。
volatileはこの目的 - 他所のスレッドでフラグを立てる - に使えない。例えばC++11やMSVCのARM版のデフォルトは明示的にこれを禁止している。

When the /volatile:ms compiler option is used―by default when architectures other than ARM are targeted―the compiler generates extra code to maintain ordering among references to volatile objects in addition to maintaining ordering to references to other global objects.

VMデザインの問題

nmosh VMは(サポートされている場合は)direct-threadedなので、ディスパッチの先頭で割り込みフラグを見るわけには行かない。全てのVM命令の処理コードが1ステップ大きくなるのはコストとして高い。
というわけで、今回はAPPLYのタイミングでチェックするようにしてみた。常識的なプログラムは定期的にApplyするし、Apply自体はそれなりにコストの掛かる操作なので全体のコストから見ればフラグのチェックは観測不能なレベルになる。
... つまり、定期的にApplyしないプログラムは止まらない。

  • I/O待ち
  • Mutex等のスレッド同期
  • VM命令のみで構成されるループ

は割り込むことができなくなってしまう。
もっとも、nmoshは有象無象のC/C++ライブラリとともに使われるのが普通なので、そもそもVMスレッドを任意タイミングで停止できる保証はどこにもない。なので、この機能はデバッグと割りきって通常のシグナル処理のような用途には使わない。もちろん、スレッドが止まらなかった場合の考察は必要になるが。。

OS互換性問題

Win32とPOSIXではI/Oやスレッド同期の割り込みに関する考察に全く互換性が無い。

  • I/O

POSIXはsyscall界面で割り込み可能であることが保証されている(EINTRを返す)が、Win32は統一的な割り込み手法が存在しない
Win32のコンソールはCTRL+Cのようなキーシーケンスに対するコールバックを登録できるが、コールバックは別スレッドで実行される。POSIXのシグナルのように自分のスレッドでハンドラが実行されるわけではない。

Because the system creates a new thread in the process to execute the handler function, it is possible that the handler function will be terminated by another thread in the process.

対して、POSIXは複数の継続をsignalハンドラとして一つのスレッドに登録できる。
Win32は様々な方法でスレッドをブロックできるので、スレッドがどの関数でブロックしているのかを何らかの方法でVM構造体の方に記録してinspectできるようにしないとPOSIXと同レベルの割り込みは実現できない。...もちろん、この方法は外部のC/C++ライブラリ関数がブロックしているときは役に立たない。

  • スレッド同期

スレッド同期(MutexやCondition variables)はもっと頭の痛い問題で、端的に言えばポータブルな方法では割り込むことはできない。一応、pthreadsでもWin32でもスレッドを停止(cancel)すること自体は可能だが、そのセマンティクスを理解するのは非常に難しい。
例えば、pthread_cancelによってキャンセルできる地点(cancellation points)はPOSIXで規定されているが、pthreads(7)を見ると( http://linuxjm.sourceforge.jp/html/LDP_man-pages/man7/pthreads.7.html )usleepのような非常に基本的な関数でもPOSIX2001とPOSIX2008で互換性が無い。(もっともusleepに限って言えば常識的なOSではキャンセルできる)
Win32のTerminateThreadはもっと強烈に非推奨になっている: XP以前のWindowsではユーザランドスタックの開放は手動でやらないといけない( http://www.nicklowe.org/2012/01/thread-termination-dont-leak-the-stack/ )
Win32の場合、MutexをWaitForMultipleObjectsで待てるので、キャンセル用のオブジェクトも一緒に待たせたりUser mode APCのような手法で割り込みを掛けることが出来る。特にUser mode APCは強力な手法で、Sleepのような待ち状態も同様に割り込める。もっとも、これも他所のライブラリでブロックしている場合は無力(どの方法でブロックしているのか分からない)。