ふと openSUSE 13.1 を install してみた。lsb_release の Description と /etc/os-release の PRETTY_NAME は一致しているようだ。
% lsb_release -a LSB Version: n/a Distributor ID: openSUSE project Description: openSUSE 13.1 (Bottle) (x86_64) Release: 13.1 Codename: Bottle % cat /etc/os-release NAME=openSUSE VERSION="13.1 (Bottle)" VERSION_ID="13.1" PRETTY_NAME="openSUSE 13.1 (Bottle) (x86_64)" ID=opensuse ANSI_COLOR="0;32" CPE_NAME="cpe:/o:opensuse:opensuse:13.1" BUG_REPORT_URL="https://bugs.opensuse.org" HOME_URL="https://opensuse.org/" ID_LIKE="suse"
なお、サーバ (テキスト環境) をインストールしたのだが、lsb_release はなかったので、zypper install lsb-release として lsb-release パッケージをインストールした。
また、/etc/SuSE-release と /etc/SUSE-brand というものもあった。
% cat /etc/SuSE-release openSUSE 13.1 (x86_64) VERSION = 13.1 CODENAME = Bottle # /etc/SuSE-release is deprecated and will be removed in the future, use /etc/os-release instead % cat /etc/SUSE-brand openSUSE VERSION = 13.1
Ruby で system, spawn メソッドなどコマンドを起動するところに vfork システムコールをを使ってみた。(現在の trunk に入っており、消されなければ Ruby 2.2 に入る。)
vfork というのは危険だけれど速い fork システムコールである。
Unix におけるコマンドの起動は、まず親プロセスが fork でプロセスを複製し、そうやってできた子プロセスが execve でプロセスを別のプログラムに入れ替える。
ここで、たいていは fork してすぐに execve するので、時間をかけてプロセスのメモリをコピーしたあげくにすぐに捨てる、という動作が無駄で遅い、というのが古代の Unix では問題だったそうな。
そこで BSD のひとが、子プロセスが execve するまでは親のメモリをそのまま使えばいいじゃない、と考えてそういう動作を行う vfork を作った。これは無駄な動作がなくなるので実際に速くなった。なお、親子が同じメモリで同時に動作するとまともに動かないのが明らかなので、子プロセスが execve する (あるいは _exit などで終了する) まで親プロセスは停止する。
とはいえ、子プロセスがメモリを書き換えた結果が親プロセスから見えるとか、ちょっとありえないと言いたくなるような動作で、vfork がよろしくないことは初めから分かっていた。だから仮想メモリで copy-on-write を実現して、あたかもコピーしたかのように見えるけれども実際にはコピーしていないので速い、という動作を fork で実現したら vfork を捨てようという話ではあったようだ。
そして時が経ち、現代では fork が copy-on-write にするのは普通で、速くなった。だから vfork は忘れましょう、というのが普通の認識だろう。
しかし、仮想メモリで copy-on-write とはいえ、子プロセスのメモリをそういうふうに設定しないといけないので、親プロセスがメモリを使えば使うほど fork が遅くなるという傾向は変わっていない。vfork にその傾向はない (あるいは低い) ので、親プロセスが大きくなればそのうち vfork のほうが明確に速くなるだろう。
もうひとつの問題はメモリのオーバーコミット (利用できるよりも多くのメモリをプロセスに割り当てること) を許していない場合に、巨大なプロセスの fork が失敗しがちになるということである。fork すると親プロセスと同じ量のメモリが子プロセスに割り当てられるので、必要なメモリは 2倍になる。極端な場合として、利用できるメモリの半分よりも大きなプロセスは fork できないことになる。vfork であれば、子プロセスは親プロセスのメモリをそのまま利用するので、この問題は発生しない。
というわけで、vfork を使ってみたわけだが、まず現実的なメモリ量で速くなるのか、という疑問を解決するために測定してみた。現実的なメモリ量で速くならないなら、あまり魅力はない。
以下のようにして測定してみた。
% uname -mrsv Linux 3.14-2-amd64 #1 SMP Debian 3.14.15-2 (2014-08-09) x86_64 % ./miniruby -Ilib -rbenchmark -e ' str = "a" * 1000; 23.times { mem = File.read("/proc/self/status")[/^VmSize:\s*(\S+)/, 1] str << str time = Benchmark.realtime { system("true") } puts "#{mem} #{time}" }'
で、プロットしてみた。
結果としては、vfork のほうがあからさまに速い。ruby (miniruby) が起動したあたりのメモリ量 (21MB 強) ですでに数倍の速度が出ている。メモリが大きくなるにつれて差は広がり、プロセスが 4GB くらいになるまで測っているが、そこでは 200倍以上になっている。
というわけで、速度を考えるとぜひ vfork を使いたい。
しかし、vfork は危険である。わかりやすいのは CERT の Secure Coding で、 POS33-C. Do not use vfork() と明確に「使うな」と書かれていることだろう。(JPCERT による和訳)
この危険性を確信を持って避けられるか、というのが問題である。
fork に対する vfork の違いは以下の 2点である。
メモリの共有で問題が起きないように、メモリの使い方を以下のように制限する。
親プロセスで制限を守るのは難しくない。しかし、子プロセスについては簡単ではない。
いずれにせよ制限を守れなかった場合には、子プロセスが親プロセスに影響を及ぼす、あるいはその逆が起こり得る。このとき、子プロセスと親プロセスの権限が異なるとセキュリティ問題に発展するかもしれない。このため、権限が異なることになるかもしれないときには vfork は使わないことにする。これは vfork した直後は親子の権限は同じなので、その後でどちらかが setuid などで権限を変化させる可能性がある場合である。つまり、setuid などが可能なプロセス (root のプロセスや、setuid/setgid されたコマンドから起動されたプロセス) では vfork を使わないことにする。
具体的に、子プロセスでスタック以外のメモリを変更するコードが動くことを防ぐことについては以下のように考えてみた。
他に、意図せざるコードが動く可能性はあるだろうか。
調べると、vfork を実際に使う話はいくつか見つかる。libc で posix_spawn を実装する時に使う、というのが多い。
glibc の posix_spawn の実装 sysdeps/posix/spawni.c vfork が (POSIX_SPAWN_USEVFORK を指定しなくても) 有効になるのは以下の条件が成り立ったとき
(flags & (POSIX_SPAWN_SETSIGMASK | POSIX_SPAWN_SETSIGDEF | POSIX_SPAWN_SETSCHEDPARAM | POSIX_SPAWN_SETSCHEDULER | POSIX_SPAWN_SETPGROUP | POSIX_SPAWN_RESETIDS)) == 0 && file_actions == NULL
せっかくなのでもっと細かくデータをとってみた。
% ./miniruby -Ilib -rbenchmark -e ' str = "a" * 1000; while str.length < 5_000_000_000 mem = File.read("/proc/self/status")[/^VmSize:\s*(\S+)/, 1] str << str[0, str.length/100] time = Benchmark.realtime { system("true") } puts "#{mem},#{time}" end'
[latest]