LinuxにおけるOOM発生時の挙動

はじめに

これはLinux Advent Calendar 2015 3日目の記事を2016/2/2に編集したものです。

Linuxにおいてシステムの物理メモリが枯渇したOut-Of-Memory(OOM)という状態になった際の挙動について説明しています。OOMに関連が深いsysctlパラメタを紹介するとともに、カーネルの内部論理についても触れました。

本記事に記載されているファイル名は、とくに断りが無ければカーネルソースのトップディレクトリからの相対パス名です。調査に使用したカーネルバージョンは4.3です。

本書は話を単純化するために、細かい動作論理については説明を省いていることをご承知おきください。また、本書の中に誤りを見つけたかた、および、私が追いきれなかったソースについての詳細をご存知のかたは、指摘していただけると助かります。

Out-Of-Memory(OOM)とOOM-killer

OOM発生時は、デフォルトでは適当に選んだプロセスを殺して物理メモリを空けることにより、なんとかしてシステムを生き延びさせようとします。この仕組みをOOM-killerと呼びます。

OOM-killerの挙動には次のような問題があるため、下手にシステムを生き延びさせるよりも、即座にシステムをpanicさせたいことがあります。

  • 運用に必須なプロセスが殺されてしまった場合は、システムが生きていてもしょうがない
  • OOM発生後にシステムが生き続けると、そこに至った根本原因を突き止めにくくなる。panicした場合は、その時に採取したカーネルダンプを解析することによって、原因がわかることがある

このように、OOM発生時にOOM-killerを発動させるのか、それともシステムをpanicさせるのかを選択するsysctlパラメタがvm.panic_on_oomです。

vm.panic_on_oom

vm.panic_on_oomパラメタは 0, 1, 2 という3つの値をとることができます。それぞれの意味は次の通りです。

OOM発生時の挙動
0 (デフォルト)必ずOOM-killerが発動
1 システムの物理メモリがまだ残っている場合はOOM-killerが発動。それ以外はpanic
2 必ずpanic

0と2はわかりやすいのですが、1の、システムの物理メモリがまだ残っているのにOOMが発生するとは、一体どういうことでしょう。それは、以下のように、プロセスのメモリ獲得に制限がかかっている場合です。

制限の名前 意味 OOM-killer発動時に殺すプロセス 詳細情報のありか
memory cgroup 特定cgroup内プロセスのメモリの使用量を制限 同じmemory cgroup内に存在するプロセス Documentation/cgropus/memory.txt
cpuset 特定NUMAノードからのみメモリを獲得できる ソースを追っていないので不明 man cpuset
mempolicy 同上 同上 man {set,get}_mempolicy

vm.panic_on_oom=1の用途として次のようなものが考えられます。

  • 基本的にはOOM発生時にはシステムをpanicさせたい。および、
  • memory cgroupよるメモリ使用量を制限したDockerコンテナが複数ある。および、
  • あるコンテナ内のプロセスがメモリを獲得した際にOOMが発生した場合、そのコンテナ内に影響を留めたい。OOM-killerによって、そのコンテナ内のプロセスのみを殺したい。

vm.oom_kill_allocating_task

OOM-killer発動時にシステム内の適当なプロセスを殺すのではなく、メモリを獲得しようとしたプロセスのみを殺害対象にしたい場合があります。そのときはこのsysctlパラメタの値を1にします。デフォルトは0です。

カーネル内の実装

ここからは、上述の振る舞いについてのカーネル内実装の簡単な解説をします。

OOM発生時におけるpanic有無の判断

OOM発生時におけるpanic有無の判定をする関数はmm/oom_kill#check_panic_on_oom()です。ソースは以下の通りです。

 void check_panic_on_oom(struct oom_control *oc, enum oom_constraint constraint,
                        struct mem_cgroup *memcg)
 {
         if (likely(!sysctl_panic_on_oom)) // ...(1)
                 return;
         if (sysctl_panic_on_oom != 2) { // ...(2)
                 /*                                                                                                                                     
                  * panic_on_oom == 1 only affects CONSTRAINT_NONE, the kernel                                                                          
                  * does not panic for cpuset, mempolicy, or memcg allocation                                                                           
                  * failures.                                                                                                                           
                  */
                 if (constraint != CONSTRAINT_NONE) ...(3)
                         return;
         }
         ...
         panic("Out of memory: %s panic_on_oom is enabled\n",
                sysctl_panic_on_oom == 2 ? "compulsory" : "system-wide"); ...(4)
 }

(1)のsysctl_panic_on_oomという変数が、vm.panic_on_oom sysctlパラメタに対応します。panic有無は、この値と、メモリ獲得時に存在した制限を示す第二引数constraintによって決まります。constraintが取りうる値はinclude/linux/oom.hに定義されています。

 enum oom_constraint {
         CONSTRAINT_NONE, // メモリ獲得時の制限なし
         CONSTRAINT_CPUSET, // cpusetによる制限あり
         CONSTRAINT_MEMORY_POLICY, // mempolicyによる制限あり
         CONSTRAINT_MEMCG, // memory cgroupによる制限あり
 };

sysctl_panic_on_oomが2の際は、(2)のif文の条件が満たされず、必ず(4)においてpanicが発生します。また、CONSTRAINT_NONE以外の場合、つまりメモリ獲得時に何らかの制限があった場合は(3)の条件を満たすため、panicせずに復帰します。そうでなければ(4)においてpanicします。

OOMが発生した場合に呼ばれる関数

OOM発生時には、その原因によって入り口となる関数が2つ存在します。

OOM発生原因 関数
memory cgroupの場合 mm/memcontrol.c#mem_cgroup_out_of_memory()
それ以外 mm/oom_kill.c#out_of_memory()

それぞれの場合についてソースを追ってみます。

memory cgourpの場合

以下はmem_cgroup_out_of_memory()のソースの抜粋です。

 static void mem_cgroup_out_of_memory(struct mem_cgroup *memcg, gfp_t gfp_mask,
                                     int order)
 {       
         ...
         struct task_struct *chosen = NULL; // ...(1)
         ...
         check_panic_on_oom(&oc, CONSTRAINT_MEMCG, memcg); // ...(2)
         ...
         for_each_mem_cgroup_tree(iter, memcg) { // ...(3)
                ...
         }
         ...
         if (chosen) { 
                 ...
                 oom_kill_process(&oc, chosen, points, totalpages, memcg, 
                                  "Memory cgroup out of memory"); // ...(4)
         } 
         ...
 } 

(1)のchosenは、殺害対象となるプロセスを示します。(2)でpanic有無を判定し、panicしなかった場合は(3)に進んでchosenプロセスを選択します。この後(4)においてOOM-killerが発動し、chosenプロセスを殺します。

どのような場合にchosenがNULLになるのか、また、このとき何故プロセスを殺さないままで処理を先に進められるのかについてはソースを追っていないため、不明です。

それ以外の場合

以下、out_of_memory()のソースの抜粋です。

 bool out_of_memory(struct oom_control *oc)
 {
        ...
        constraint = constrained_alloc(oc, &totalpages); // ...(1)
        ... 
        check_panic_on_oom(oc, constraint, NULL); // ...(2)
        
        if (sysctl_oom_kill_allocating_task && current->mm && // ...(3)
             !oom_unkillable_task(current, NULL, oc->nodemask) &&
             current->signal->oom_score_adj != OOM_SCORE_ADJ_MIN) {
                 get_task_struct(current);
                 oom_kill_process(oc, current, 0, totalpages, NULL,
                                  "Out of memory (oom_kill_allocating_task)"); // ...(4)
                 return true;
         }
 
         p = select_bad_process(oc, &points, totalpages); // ...(5)
         /* Found nothing?!?! Either we hang forever, or we panic. */
         if (!p && !is_sysrq_oom(oc)) {
                 dump_header(oc, NULL, NULL);
                 panic("Out of memory and no killable processes...\n"); // ...(6)
         }
         if (p && p != (void *)-1UL) {
                 oom_kill_process(oc, p, points, totalpages, NULL,
                                  "Out of memory"); // ...(7)
                 ...
         }
         return true;
 }

(1)のconstrained_alloc()は、メモリを獲得しようとしたプロセスの制限を判定し、CONSTRAINT_NONE, CONSTRAINT_CPUSET, CONSTRAINT_MEMPOLICYのいずれかを返します。(2)においては、その結果を用いてpanic有無を判定します。

(3)においては、vm.oom_kill_allocating_taskが1の場合(他にも条件は色々ありますが、ここでは省略)には対応するカーネル内変数sysctl_oom_kill_allocating_taskが1になります。値が1の場合は(4)においてメモリを獲得しようとしたプロセスを殺します。

(3)の条件を満たさなかった場合は、(5)において、全プロセスの中からOOM-killerによる殺害対象となるプロセスpを適当に選択します。そのようなプロセスが見つからなかった場合は、これ以上システムを生き延びさせることができないため、(6)においてpanicします。見つかった場合は、(7)において選択されたプロセスを殺します。

プロセスpが(void *)-1ULになった場合はプロセスを殺すことなく先の処理に進んでいますが、どのような場合にこうなるのか、また、なぜそれでよいのかについてはソースを追っていないので不明です。