CygwinのnanosleepはECHILDを返すことがある"疑惑"
yuniのフロントエンド実装を進めるにあたって、core Scheme処理系としてはGambitを選ぶことにした。別にR5RSかつ非R6RS/R7RSであればMIT-SchemeなりSCMなりなんでも良いが、GitHubにリポジトリが有り、かつ、他と違ってコンパイラなので。
で、Gambitはその移植性( http://www.iro.umontreal.ca/~gambit/Gambit-inside-out.pdf#page=18 )の割にCygwinで良く動かなかったのでパッチを2つ書いた。
- https://github.com/feeley/gambit/pull/191
- Cygwin libcのselect()ではなくWinSockのselect()を使っちゃう問題の修正
- https://github.com/feeley/gambit/pull/192 - ★ これは正しくなかった(後述)
- Cygwinではselect()中にnanosleep()しない
前者は多分autotools移行時のleftoverなので気にしないことにして、後者がなかなか絶妙と言える。いづれも未パッチの状態では、
(shell-command "echo hoge")
のような単純な shell-command 手続きの実行に失敗してしまう。
$ ../gsi/gsi.exe check.scm hoge *** ERROR IN ##thread-report-scheduler-error! -- Scheduler reported the exception: #<os-exception #2>
これはGambitのスレッドサポートが、nanosleepがEINTR以外を返したら異常と判定していることに依る。
- https://github.com/feeley/gambit/blob/dc9820362c1e461f3bd07819062590220a511229/lib/_thread.scm#L1074
;; ##thread-check-devices! only returns after a device ;; becomes ready or the timeout is reached or an error is ;; detected (##thread-check-timeouts!) ;; ★ nanosleep()を持っているカーネルではnanosleep、そうでなければ空select(2) (cond ((##not (##fx< code 0)) ;; no error? #f) ((##fx= code ##err-code-EINTR) ;; ★ これがハンドルされている唯一のエラー ;; an interrupt may need to be serviced, so make ;; sure at least one thread is runnable - snip - #f))))))) (else ;; there was an error that cannot be handled, so ;; force the primordial thread to wakeup (it can't ;; be currently runnable) and raise a "scheduler ;; error" exception ;; ★ nanosleep()がECHILDを返すとここに来る (##thread-report-scheduler-error! code))))
nanosleepはECHILDを返しても良いのか
実際常識的に考えてnanosleepは(EINVALやEFAULTを除くと)EINTR以外のエラーでは失敗のしようがなく、POSIXでもLinuxでもマニュアルの記述はそうなっている。
- https://linuxjm.osdn.jp/html/LDP_man-pages/man2/nanosleep.2.html
- Linux JM
- http://pubs.opengroup.org/onlinepubs/9699919799/functions/nanosleep.html
- Issue7
LinuxがPOSIXに無いエラーEFAULTを返すことからわかるように、POSIXはエラーコードの拡張を一応許可している。このため、nanosleepがECHILDを返しても別にPOSIXとしては文句の無い実装と言える。
Implementations shall not generate a different error number from one required by this volume of POSIX.1-2008 for an error condition described in this volume of POSIX.1-2008, but may generate additional errors unless explicitly disallowed for a particular function.
実際にECHILDを返しているのか
nanosleepがECHILDを返しているかどうかは、(Cygwinでもちゃんと使える)straceで確認できる:
$ strace ../gsi/gsi.exe check.scm - snip - --- Process 95356 exited with status 0x0 ★ shell-commandで起動したプロセスが終了した 1979 69611 [waitproc] gsi 149568 pinfo::maybe_set_exit_code_from_windows: pid 50620, exit value - old 0x8000000, windows 0xDEADBEEF, cygwin 0x8000000 17 69628 [waitproc] gsi 149568 sig_send: sendsig 0x84, pid 149568, signal 20, its_me 1 16 69644 [waitproc] gsi 149568 sig_send: Not waiting for sigcomplete. its_me 1 signal 20 11 69655 [waitproc] gsi 149568 sig_send: returning 0x0 from sending signal 20 ★ SIGCHLDを送信 11 69666 [waitproc] gsi 149568 proc_waiter: exiting wait thread for pid 50620 11 69677 [sig] gsi 149568 sigpacket::process: signal 20 processing 14 69691 [sig] gsi 149568 init_cygheap::find_tls: sig 20 11 69702 [sig] gsi 149568 sigpacket::process: using tls 0xFFFFCE00 28 69730 [sig] gsi 149568 sigpacket::process: signal 20, signal handler 0x10041920E 11 69741 [sig] gsi 149568 sigpacket::setup_handler: controlled interrupt. stackptr 0xFFFFE460, stack 0xFFFFE458, stackptr[-1] 0x1004189ED 12 69753 [sig] gsi 149568 proc_subproc: args: 5, 1 10 69763 [sig] gsi 149568 proc_subproc: clear waiting threads 11 69774 [sig] gsi 149568 proc_subproc: finished clearing ★ シグナルハンドラの起動準備完了 12 69786 [main] gsi 149568 clock_nanosleep: 4 = clock_nanosleep(1, 0, 0.003582000, 0.d) ★ nanosleepがEINTRで中断 14 69800 [sig] gsi 149568 proc_subproc: returning 1 13 69813 [main] gsi 149568 __set_errno: int nanosleep(const timespec*, timespec*):145 setting errno 4 ★ errnoをEINTRにセット (!) 12 69825 [sig] gsi 149568 _cygtls::interrupt_setup: armed signal_arrived 0x150, signal 20 11 69836 [sig] gsi 149568 sigpacket::setup_handler: signal 20 delivered - snip - 10 70116 [main] gsi 149568 proc_subproc: released waiting thread 10 70126 [main] gsi 149568 proc_subproc: finished processing terminated/stopped child 10 70136 [main] gsi 149568 proc_subproc: returning 1 10 70146 [main] gsi 149568 wait4: 0 = cygwait (...) 10 70156 [main] gsi 149568 wait4: 50620 = wait4(-1, 0x0, 1, 0x0) ★ シグナルハンドラがwait(2)を発行 11 70167 [main] gsi 149568 wait4: calling proc_subproc, pid -1, options 1 12 70179 [main] gsi 149568 proc_subproc: args: 6, -7152 14 70193 [main] gsi 149568 proc_subproc: wval->pid -1, wval->options 1 10 70203 [main] gsi 149568 checkstate: nprocs 0 18 70221 [main] gsi 149568 checkstate: no matching terminated children found 13 70234 [main] gsi 149568 checkstate: returning 0 12 70246 [main] gsi 149568 proc_subproc: waiting thread found no children 21 70267 [main] gsi 149568 proc_subproc: finished processing terminated/stopped child 69 70336 [main] gsi 149568 proc_subproc: returning 1 61 70397 [main] gsi 149568 __set_errno: pid_t wait4(int, int*, int, rusage*):91 setting errno 10 17 70414 [main] gsi 149568 wait4: -1 = wait4(-1, 0x0, 1, 0x0), errno 10 ★ waitが完了してerrnoにECHILDを設定 (!) 67 70481 [main] gsi 149568 set_signal_mask: setmask 80000, newmask 0, mask_bits 80000 55 70536 [itimer] gsi 149568 timer_thread: 0x180213A80 waiting for 10 ms
... なんとnanosleepは正しくEINTRを返しており、シグナルハンドラがerrnoをセーブせずにsyscallしてる。。
___HIDDEN void sigchld_signal_handler ___P((int sig), (sig) int sig;) { #ifdef USE_signal ___set_signal_handler (SIGCHLD, sigchld_signal_handler); #endif - snip - for (;;) { int status; ___device *head; pid_t pid = waitpid_no_EINTR (-1, &status, WNOHANG); ★ if (pid <= 0) break;
Issueにコメントしたように、ここは本来errnoをセーブするのが正しい。
- syscallを発行する
- errnoがセットされる
- syscallのライブラリ関数から返る
- (ここでシグナルが発生し、シグナルハンドラが同じスレッドで起動される)
- シグナルハンドラで更にsyscallする
- シグナルハンドラの発行したsyscallでerrnoが上書きされる
のような順序によって、errnoがシグナルハンドラが発行したsyscallによって上書きされる危険がある。ここではEINTR以外のエラーがwaitによって発生することは期待していないかもしれないが、1. で発行したsyscallがEINTR以外のエラーを返していた場合に悲惨なことになる。
というわけでGambitはerrnoをセーブするように修正するとして、次の疑問は、このコードが何故他のカーネルでは動作するのかというポイントと言える。Cygwin以外では、SIGCHLDによってsyscallが中断されても、シグナルハンドラが完了するまで(元のsyscallの)errnoがセットされない実装になっていることが多いのかもしれない。つまり、他の大多数のOSではnanosleepがSIGCHLDによって中断され、そのシグナルハンドラでerrnoをcrobberしたとしてもちゃんとerrno == EINTRが見え、CygwinだけがnanosleepがECHILDを返しているように見えるという状況なのではないだろうか。