mallocの落とし穴 - 組み込みLinuxでのmemory overcommit

天災は忘れた頃にやって来る
寺田寅彦

組み込みLinuxにおけるメモリ確保のエラーハンドリングに関するお話。

Linuxにはmemory overcommit(オーバーコミット)と呼ばれる仕組みがあります。簡単にいうと、メモリ割り当ての段階では、仮想アドレス空間だけをわりあてて、実際に使われる段階で、実メモリを割り当てるというものです。メモリ割り当て時には、実メモリはまだ割り当てられないので、実際の物理メモリの容量以上のメモリ割当てが行われることを許されることになります。

実例を見た方が早いので、例えば以下の例。
#define BLK_NUM 100
#define BLK_SIZE (100 * 1000 * 1000) /* 100MB */

int
main(int argc, char* argv[])
{
  int i;
  char* blk[BLK_NUM];

  for (i = 0; i < BLK_NUM; i++) {
    blk[i] = malloc(BLK_SIZE);
    assert(blk[i] != NULL);
  }

  return 0;
}
mallocでヒープを100MBx1000 = 10GB取得していますが、これをコンパイルしたものを実メモリ128MBのLinuxで実行してもおそらくassert()にはひっかからず正常終了します。これはmallocの段階では、仮想メモリのみが割り当てられ、実メモリが割り当てられていないためです。つまり、アドレス空間を超えるような異常なmallocでもしない限り、mallocの戻り値は常に成功(NULL以外)で返ります。実際、malloc完了したところでプロセスのstatus値を見てみると、VSSは増えているものの、RSSは増えていないことが確認できると思います。(returnをwhile(1)やsleepにでもして、psやprocでみて見るとよくわかります)。

では、以下の分をmallocのループの下に加えるとどうなるでしょうか。
  for (i = 0; i < BLK_NUM; i++) {
    memset(blk[i], 0, BLK_SIZE);
  }
今後は、メモリが10GBもないようなシステムでは、実行途中に以下のようなログとともにプロセスが強制終了したと思います。
01:00:00 xxxxx kernel: Out of memory: Killed process 10000, UID 100, (xxx)
memsetによって書き込みが発生する段階で、実メモリが割当りあてられ、この時点でプロセスがkernelに強制的にkillされてしまいます)。いわゆるOOM-killer(out of memory killer)という仕組みで、カーネルがメモリを大量消費していると思しきプロセスを強制的にkillする仕組みです。教科書的な「mallocでは、戻り値見てメモリを取得できたか確認しましょう」というのはこの場合通用しないのです。さらに、困ったことに、必ずしも犯人とは限らないプロセスが殺される場合もあります。これだと、個別のユーザーアプリケーション側では対処のしようがありません。

そういうわけで、かなり不評なしくみでもあり、memory overcommitの仕組みについては、仕様バグだという言われようもよく見かけます。そういうことで、最近(というかkernel 2.6からなので結構前から)のLinuxには、overommitの挙動を変更する仕組みがあります。具体的には、sysctl、もしくは、直接proc/sysに値を書き込むことで設定を変更できます。以下、procのman pageから引用、
/proc/sys/vm/overcommit_memory
 このファイルにはカーネル仮想メモリーのアカウントモードが書かれている。 値は以下の通り:
  0: 発見的なオーバーコミット (heuristic overcommit) (これがデフォルトである)
  1: 常にオーバーコミットし、チェックしない。
  2: 常にチェックし、オーバーコミットしない。

/proc/sys/vm/overcommit_ratio (Linux 2.6.0 以降)
 この書き込み可能なファイルは、 オーバーコミットできるメモリーの割合をパーセントで定義する。 このファイルのデフォルト値は 50 である。 /proc/sys/vm/overcommit_memory の説明を参照。
上で、「犯人と思しきプロセス」がkillされるというのが、ここでいうheuristic overcommitというやつですね。どの閾値でoom-killerが呼ばれるかは、 以下の式になります。
CommitLimit = (total_RAM - total_huge_TLB) * overcommit_ratio / 100 + total_swap
組み込みでmallocでとったメモリを確実に確保したいというのであれば、overcommit_memory=2にしてovercommit_ratioを100に近い値にしておけば、基本的にはmallocでNULL以外がかえってくれば、そのメモリは使えるというこが一応保証されることにはなります。

では、組み込みの場合は、overcommit_memory=2を使うべきなんでしょうか?

残念ながらそうとは言い切れません。仮想メモリとして確保した分だけ、実メモリも消費する(実際割り当てられないとしても総和はこえないようコントロールされる)ということになると、以下のような場合に相当の実メモリが必要になります。
  • 各プロセス、スレッド毎のスタック用メモリ
    • 通常スレッドあたり2MB
    • 通常のスレッドならそこまでメモリは使わないのでovercommit=0なら必要量のpageのみが実メモリとして割り当てられるが、overcommit=2ならまるまる必要になる。
  • forkする際のfork元プロセスが使用しているメモリ
    • 通常、forkして時にはfork元プロセスのメモリ空間がコピーされるが、いわゆるcopy on writeという仕組みで、書き込みが行われない限りは、この段階では実際のメモリコピーは発生しない。
    • overcommit=2だと、その時点で実メモリの容量も予約されてしまう
    • 結果としては、fork(あるいは、中でfork実行するsystem関数など)が失敗することがある。

メモリ容量が少ない組み込みでは、こういう隠れた予約メモリがばかにならないので、結果としてovercommit=2で使うのはなかなか厳しいのではないかと。言い換えると、overcommit memoryという仕組みがあるおかげで、こういうケースにもうまく対応できているのだと考えられます。

じゃあ、一回メモリ不足はどうやってハンドリングしたらいいんだという話になりますが、基本的には組み込みなんだからメモリ使用量くらいちゃんと把握しましょうというのが基本ですが、エラー検出の方法としては、メモリ総量をwatchして不適切なプロセスがいればkillする、あるいは、リブートさせるという仕組みを自分でいれる方法になるのかと思います。ちなみに、oom-killerで中途半端にシステムを混乱させるくらいなら、kernel panicに落として、watchdogリセットにでももっていきたいということであれば、以下のproc設定(manpage引用)も検討候補になります。
/proc/sys/vm/panic_on_oom (Linux 2.6.18 以降)
 このファイルは、メモリー不足時にカーネルパニックを 起こすか起こさないかを制御する。
 このファイルに値 0 を設定すると、 カーネルの OOM-killer がならず者のプロセスを kill する。 普通は、OOM-killer がならず者のプロセスを kill することができ、 システムは何とか動き続けることができる。

 このファイルに値 1 を設定すると、 メモリー不足の状況が発生すると、カーネルは普通はパニックする。

memory overcommitにかぎらず、本当に必要なところまで処理を遅らせるという遅延処理は、パフォーマンスを最大化する上で非常に有用ですが、エラー処理が難しくなるので注意が必要ですね。

Linuxカーネル解析入門 (I・O BOOKS)Linuxカーネル Hacks ―パフォーマンス改善、開発効率向上、省電力化のためのテクニック UNIXという考え方―その設計思想と哲学

コメント

非公開コメント

本のおすすめ

4873115655

4274065979

4822236862

4274068579

4822255131

B00SIM19YS


プロフィール

  • Author:proger
  • 組み込み関係で仕事してます

ブログ内検索