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つ書いた。

前者は多分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以外を返したら異常と判定していることに依る。

              ;; ##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でもマニュアルの記述はそうなっている。

LinuxPOSIXに無いエラー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をセーブするのが正しい。

  1. syscallを発行する
  2. errnoがセットされる
  3. syscallのライブラリ関数から返る
  4. (ここでシグナルが発生し、シグナルハンドラが同じスレッドで起動される)
  5. シグナルハンドラで更にsyscallする
  6. シグナルハンドラの発行した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を返しているように見えるという状況なのではないだろうか。