Ruby と exec と fd と

ruby で exec するときにソケットや fd のリークを起こさないために。主に自分用の調査結果のメモ。

結論

ruby 1.9.1 以上で exec するときは、何はなくとも :close_others をつける
ruby 2.0.0 以降ではこれを設定しなくても fd リークはしない
ruby 1.9.0 以前は人力で必死に頑張る

基本的な話

ruby の exec は基本的には POSIX execve のラッパ。execve 前後では多くのものが保持されないが、ファイルディスクリプタは基本的には残る。

デフォルトでは、ファイルディスクリプタは execve() を行った後でもオープンされたままである。 close-on-exec の印が付いているファイルディスクリプタはクローズされる。

http://linuxjm.sourceforge.jp/html/LDP_man-pages/man2/execve.2.html

ということで、fork-exec や exec を行いたい場合は fd の扱いに気を付ける必要がある。
また、exec を行う時点で open しっぱなしになっている可能性のある fd に関しては、 close-on-exec のフラグを付けておけば execve 内で close してもらえる。
ruby 1.9.3 以前では、この close-on-exec は自分で設定する必要がある。

f = File.open("/tmp/hoge")
f.close_on_exec = true

close-on-exec は ruby 2.0.0 以降ではデフォルトになっている*1。そのため、 exec による予期せぬ fd リークは起こりにくくなっている。

困るシーン

他の人が使用するフレームワーク系ライブラリの中で exec を使用する場合、そのライブラリユーザが開いた fd やソケットが開きっぱなしになってしまう可能性がある。やばい。宇宙やばい。マルチスレッドならさらにこのような問題が起こりやすくなる。

ユーザの知恵はそんなものを乗り越えられるかというとそんなことはなくて、ならば今すぐ ruby 1.9.3 ユーザ全員の IO::open に close_on_exec = true を授けて見せろと言われてもそうだよそれはできないから、ruby 2.0.0 になってからそうさせてもらうと言うことになった。

では 1.9.3 ではどうすればよいかというと、 Kernel#exec (及び #spawn) に :close_others というオプションがあり、stdin/stdout/stderr 以外の fd をすべて調べて閉じてくれる仕組みがある。しかしこれは単に ruby 処理系にて C で書かれているだけなので、どうやら標準ではなさそうだ。少なくとも execve にはそのようなオプションはない。

f = File.open("/tmp/hoge")
exec("ls /proc/$$/fd", :close_others = true)

1.9.2 1.9.0 以前は close_others の実装がない(1.9.1 より実装された)。そのため、close_others オプションによる fd リーク防止は効果を持たない。close_othersはつけておいても特に害はない。

2.0.0 以降は、 fd ごとに close_on_exec が設定され、exec 時の close を保証してくれる。したがって、意図的に fd を残したい場合を除いては自動的に fd は exec 時に close されることとなり、 :close_others の設定は不要である。とはいえ、close_others を付けていてもそう問題は起きないだろうし、つけておいても問題はない。むしろ、ユーザの不注意でつけられた close_on_exec = false な fd も閉じてくれるので、使い勝手はよいかもしれないくらいだ。

そういうわけで結論(再掲)は

ruby 1.9.1 以上で exec するときは、何はなくとも :close_others をつける
ruby 2.0.0 以降ではこれを設定しなくても fd リークはしない
ruby 1.9.0 以前は人力で必死に頑張る

ということになった。まあ 1.9.0 以前って普通は 1.8.7 以前のことだけど。

補足

むしろ、ユーザの不注意でつけられた close_on_exec = false な fd も閉じてくれるので、使い勝手はよいかもしれないくらいだ。

とさっき書いた。「fd を勝手に閉じられたら困る、という話があるのでは??」と思うかもしれないが…、

本来、Kernel#exec 前後で開いたままにするべき fd は、exec (spawn) のオプションとして明示的に渡しているはずなので、それ以外の fd はすべて閉じるべき。

# たとえば fd 7 番を残したいのであれば
exec("ls -ls", 7 => 7, :close_others => true)

また、close_on_exec は IO::open 時のオプションに設定できないので、ruby 1.9.3 以前では open と close_on_exec 設定とがアトミックな処理にならない。つまり、その間に exec が発生する可能性があり、fd 漏れの起こりうる箇所になる。
その点 :close_others => true なら exec 時に確実に設定できるので、fd 漏れのタイミングは生まれない。多分。

ただ、IO#close_on_exec は POSIX 標準に対するインターフェイスである一方で、:close_others => true は ruby の独自実装。なので、現実的には :close_others を付けるのが正しそうなんだけど、あんまり美しくない感はあって悔しいかもしれない。

そんな感じ。