From: KOSAKI Motohiro Date: 2013-08-19T02:42:23-04:00 Subject: [ruby-dev:47639] Re: [ruby-trunk - Bug #8711] 最近NoMemoryErrorが多い 2013/8/19 SASADA Koichi : > (2013/08/01 20:18), naruse (Yui NARUSE) wrote: >> http://u32.rubyci.org/~chkbuild/ruby-trunk/log/20130801T103302Z.log.html.gz >> で 32bit でも安定したような気がします。 >> >> もうしばらく様子を見ます。 >> >> より多くのアドレス空間を必要とするようになった事自体は仕様って理解でいいんですよね>ささださん > > こちら、だいたいわかったんじゃないかなぁ、と思うので調査結果を報告します > (日本語)。小崎さん、チェックしてくれると助かります。 調査お疲れ様です。 こちらで確認した限りでは、この内容で間違いないと思われます。 > > 簡単な報告: > TestFiber#test_many_fibers が沢山仮想メモリを確保し、それを(なぜか)解 > 放しないため、その後の fork が ENOMEM で失敗します。 > > 詳細な報告: > > 前提: > 64bit OS で、2GB の物理メモリ+500MB の swap を持つシステム(VM)で検証 > しています。Linux のバージョンは 3.2.0-51-generic。virtualbox 上で実行し > ています。 > > (1) TestFiber#test_many_fibers が仮想メモリを沢山確保してしまう > > 今は、Fiber のスタックは mmap で確保しています。Fiber が GC されると > munmap で解放するはずなんですが、何かの拍子にプロセスのアドレス空間が広 > がったままになっているようです。これについては要検証。単に Fiber が GC > されない、というか、GC されたけど、実際の解放は遅延している、という気が > します。 > > Linux 側の RSS を見ると、1GB 弱しか使っていないことがわかりますが、仮想 > メモリは 4GB ほど確保していることが観測できました。 > > Ruby 2.0 から、Fiber のためのスタックサイズが広がったため、この問題が生 > じました(多分)。 > > (2) fork が ENOMEM で失敗 > > 物理メモリ(+swap)以上の仮想メモリを持つプロセスを fork しようとする > と、ENOMEM が起って失敗するようです。 > > (3) ENOMEM になってしまう理由 > > Linux は、(デフォルトでは)一度に物理メモリ(+α)以上の mmap は出来な > いようになっているようです。 そうですね。 > > http://passingloop.tumblr.com/post/11957331420/overcommit-and-oom-killer >> 0 >> オーバーコミット有効 >> 一回の malloc で確保できるのは実際に利用可能なメモリの大きさまで > > 多分、この制限で fork() が失敗してるんじゃないかなぁ、と思います。ちなみ > に、このページの malloc って、mmap かなぁ。 はい。そうです > Linux kernel のソースをおおざっぱに追ったのですが、fork() がこのチェック > をするところにどう到達するかは見ることが出来ませんでした。ので、推測にな > ります。 してます。 do_fork() copy_process() copy_mm() dup_mm() dup_mmap() security_vm_enough_memory_mm() __vm_enough_memory() というルートになります。 > 考えられる解決策: > - TestFiber#test_many_fibers を削除 > - TestFiber#test_many_fibers を別プロセスで実行 > - TestFiber#test_many_fibers 後にちゃんと物理メモリを解放するように頑張る システム全体で制限がかかっているので別プロセスで実行は本質的では ないように思います。 ちゃんと開放するようにテスト(と、GC)を変えて、そもそもシステムの メモリが小さすぎるときはテストをスキップするようにしないと、 不思議な箇所でエラーを吐くと思います。 > 余談: > fork では、CoW になるから、ページテーブル操作だけ必要で、物理メモリの制 > 限はないだろー(fork 出来るだろう)、と思っていたんですが、そうでもない > んですね。 > > しかし、この Linux の制限って少し不思議ですね。mmap(4GB) を行うと、(3) > の条件で失敗しますが、mmap(2GB) を 2 回やると成功します。これだけだと連 > 続領域 4GB の確保が出来ないのかな、とも思いますが、実は MAP_WRITE を外す > と mmap(4GB) は成功します。どうやら、書き込み可能な部分だけ、この(3) の > チェックが入るようです。で、mprotect() で 2GB 単位でこの 4GB の連続領域 > を書き込み可能にすることが可能です。つまり、 > > size = 4GB; > addr = mmap(0, size, PROT_EXEC | PROT_READ, ...); > mprotect(addr , size/2, 書き込み可能に); > mprotect(addr+size/2, size/2, 書き込み可能に); > > とすると、書き込み可能な 4GB の連続領域を得ることが出来ます。もちろん、 > 実際に書き込んでいくとページが割り当てられてメモリ足りなくなりますが。 全くその通りです。 > こういう設計なのは、なにがしかの理由があるんでしょうけれども、意外でした。 いや、これは歴史的なゴミなので、あんまり気にしなくていいです。 overcommit_always がきちっとした管理で、overcommit_guessは基本的に チェックしないけど、あまりにもアホなサイズはきっとオペミスだから 助けてやろう。ぐらいのノリ ちゃんと設計されてないので、mprotectまわりで矛盾が発生しており、 最初に PROT_READでmapしてから、PROT_WRITEに変えると アカウントされない → アカウントされる と遷移するけど、 逆にPROT_WRITEでmapしてPROT_READに変更するとアカウントされる ままなのだよ。一回write modeになってしまうと何が書いてあるか 分からないからそうするしかないんだけど、汚い。 そして数々の勘違いアプリコードを生んだのであった。