Cygwinのfork()はプロセスにロードされているDLLを消すと失敗する

コネタ。

yunibaseをCygwinに移植していてchibi-schemeのtest-allがCygwinで通らないのを見つけた。これは端的に言えばCygwinあるあるで、普通のLinuxでは普通に可能なことがCygwinではできないケースと言える。
Chibi-schemeの当該テストコードは、

  1. FFI用のライブラリを .stub ファイルから .DLLにコンパイル
  2. ロード
  3. テスト
  4. 削除

をいくつかのライブラリに亘って繰り返している。このとき、1. の .stubから.DLLを生成する過程ではコンパイラを起動するためにfork()→exec()することになる。

(define-syntax test-ffi
  (syntax-rules ()
    ((test-ffi name-expr decls tests ...)
     (let* ((name name-expr)
            (stub-file (string-append "tests/ffi/" name ".stub"))
            (c-file (string-append "tests/ffi/" name ".c"))
            (lib-file
             (string-append "tests/ffi/" name *shared-object-extension*)))
       (call-with-output-file stub-file
         (lambda (out) (write 'decls out) (newline out)))
       (let ((res (system  DLLを生成
                   "./chibi-scheme" "tools/chibi-ffi" "-c"
                   "-f" (string-append
                         "-O0 -L. -Iinclude"
                         (cond-expand
                          (boehm-gc
                           " -DSEXP_USE_BOEHM=1 -I/opt/local/include")
                          (else
                           "")))
                   stub-file)))
         (cond
          ((zero? (cadr res))  ビルドに成功した?
           (let ((orig-failures (test-failure-count)))
             (load lib-file)  DLLをロード
             tests ...  テストを実行
             ;; on any failure leave the stub and c file for reference
             (cond
              ((= orig-failures (test-failure-count))  テストが成功なら中間ファイルを消す
               (delete-file stub-file)
               (delete-file c-file)))
             (delete-file lib-file)))  DLLを消去

このとき、chibi-schemeFFIには(というか普通のFFIには)ライブラリのアンロードが無いため、プロセスのメモリ空間にはDLLが残ったままになってしまう。
Linux等の普通のPOSIXでは、ファイルが消えた状態でもfork()でファイルデスクリプタを複製するようなケースでは他のプロセスからもアクセスを継続できる。このため大きな問題にはならない。
しかし、Cygwinのfork()でできるプロセスはアドレスレイアウトを再現しただけの完全に新しいプロセスであるため、新しくファイルのopen()を行うのと等価になり ロードされていたDLLがopenできない → mmapできない → アドレス空間レイアウトを再現できない という流れでfork()に失敗してしまう。

0 [main] chibi-scheme 311940 child_info_fork::abort: unable to map C:\cygwin64\home\oku\repos2\chibi-scheme\tests\ffi\basic.dll, Win32 error 126

個人的にはこの手のCygwinのメッセージはあんまり信頼していないのでstraceコマンドで確認するようにしている。

   15 64828174 [main] chibi-scheme 299940 frok::parent: copying data/bss of a linked dll
   38 64828212 [main] chibi-scheme 299940 child_copy: linked dll data - hp 0x198 low 0x5CFFC7000, high 0x5CFFCCCC8, res 1
   21 64828233 [main] chibi-scheme 299940 child_copy: linked dll bss - hp 0x198 low 0x5CFFD7000, high 0x5CFFD7230, res 1
   15 64828248 [main] chibi-scheme 299940 child_copy: done
   14 64828262 [main] chibi-scheme 299940 resume_child: signalled child
  419 64828264 [main] chibi-scheme 302196 sync_with_parent: awake
   24 64828286 [main] chibi-scheme 299940 child_info::sync: n 2, waiting for subproc_ready(0x150) and child process(0x198)
   27 64828291 [main] chibi-scheme 302196 sync_with_parent: no problems
   11 64828302 [main] chibi-scheme 302196 frok::child: child is running.  pid 302196, ppid 299940, stack here 0x23C3C8
   11 64828313 [main] chibi-scheme 302196 frok::child: hParent 0x178, load_dlls 1
   66 64828379 [main] chibi-scheme 302196 child_info_fork::abort: unable to map C:\cygwin64\home\oku\repos2\chibi-scheme\tests\ffi\basic.dll, Win32 error 126
--- Process 302196 thread 302200 exited with status 0x800000
--- Process 302196 exited with status 0x800000
12276 64840562 [waitproc] chibi-scheme 299940 pinfo::maybe_set_exit_code_from_windows: pid 302196, exit value - old 0x0, windows 0x800000, cygwin 0x8000000
  151 64840713 [main] chibi-scheme 299940 child_info::sync: pid 302196, WFMO returned 1, exit_code 0x800000, res 0
  121 64840834 [waitproc] chibi-scheme 299940 sig_send: sendsig 0x84, pid 299940, signal 20, its_me 1
   83 64840917 [main] chibi-scheme 299940 frok::parent: returning -1
   83 64841000 [waitproc] chibi-scheme 299940 sig_send: Not waiting for sigcomplete.  its_me 1 signal 20
   70 64841070 [main] chibi-scheme 299940 sig_send: sendsig 0x84, pid 299940, signal -73, its_me 1
  116 64841186 [sig] chibi-scheme 299940 sigpacket::process: signal 20 processing
  117 64841303 [main] chibi-scheme 299940 sig_send: wakeup 0x110

... frok??
最近のCygwinにはposix_spawn()が有るためそれを使うのが望ましい気もするが、Cygwinposix_spawnは導入が本当に最近(2013年)なので微妙かもしれない。