OOMキラー
メモリが無くなってpageのアロケートに失敗すると、動作中の任意のプロセスを削除することで、メモリを確保しようといたします。これをOOM(Out Of Memory)キラーと呼ばれるものです。任意ということですが、そこにはある程度の規則があって、できるだけ削除してもよさそうなプロセスを選択します。この処理を行うのがselect_bad_process()です。
out_of_memory()の最初の処理はsysctl_panic_on_oomが2であるなら、"out of memory・・・"を表示して、panic処理となります。panic()関数ではさようならLinuxということです。そうでないならconstrained_alloc()をコールしています。この関数はアロケーションの束縛(?)すなわち、NUMAシステムに掛かる処理でX86では常にCONSTRAINT_NONEを返します。
case CONSTRAINT_NONE以下では再度sysctl_panic_on_oomをチェックします。この場合0でないならpanic()となります。先の処理でも同じような処理をしています。結果的にNUMAシステムの時のみ、sysctl_panic_on_oom == 2は意味を成すものとなり、X86ではsysctl_panic_on_oom!=0なら同じ結果となります。
sysctl_oom_kill_allocating_taskが0でないなら、メモリーをアロケートしようとして出来なかったプロセスがOOMキラー対象のプロセスとなります。そうでないならselect_bad_process()でOOMキラーとなるプロセスを選択します。
oom_kill_process()は対象となるプロセスにシグナルを送信することで実現します。ラベルout:以降は、if (!test_thread_flag(TIF_MEMDIE))でOOKキラー対象プロセスが、カレントプロセスでないなら、OOMキラープロセスが削除されのを、schedule_timeout_uninterruptible()で少しウエイトすることで、カレントプロセスメモリー確保することにあると思います。
なお、oom_kill_process()ではOOMキラーとしてシグナル送信(SIGKILL)されたプロセスは、TIF_MEMDIEがセットされています。
sysctl_panic_on_oom/sysctl_oom_kill_allocating_taskは以下のprocで設定できます。
[root@localhost ~]# ls /proc/sys/vm/* | grep oom
/proc/sys/vm/oom_dump_tasks
/proc/sys/vm/oom_kill_allocating_task
/proc/sys/vm/panic_on_oom
/proc/sys/vm/would_have_oomkilled
select_bad_process()でOOMキラーされるプロセスを選択します。do_each_thread(g, p)マクロでinit_taskをヘッドとするプロセスのリストをすべてなめることでチェックします。まず、メモリー空間を有していないプロセス(カーネルスレッド)言うまでも無く対象外です。
if (is_global_init(p))はプロセスIDが1のプロセスがどうかをチェックします。これはinitプロセスです。(initプロセスもif (!p->mm)でスキップされるのではと・・・)
if (test_tsk_thread_flag(p, TIF_MEMDIE)はそのプロセスがすでにOOMキラーされているプロセスです。これも再度シグナルを送っても意味ありません。この場合エラーで呼び出し元に復帰します。(呼び出し元ではエラーで帰るとschedule_timeout_uninterruptible()で完全に削除されるまでウエイトしていました。)
if (p->flags & PF_EXITING)は対象プロセスがゾンビの場合です。それがカレント(メモリーをアロケートしようとしたプロセス)でないなら、完全に削除するのをウエイトするために、エラーで復帰です。もしそれが自プロセスならchosen = p;*ppoints = ULONG_MAXとし、これがOOMキラープロセスとなるようにしています。自プロセスもエラー復帰させ、EXITされるのを待とうとすると、EXIT処理でページアロケート失敗ですから、永久にOOMキラーが発生することになるかと思います。なおこれは推測ですが、EXITのプロセスをOOMキラーすると、スレッドフラグにTIF_MEMDIEが設定されるだけのようです。ページをアロケートしようとするプロセスがアロケートできなかったわけで、しかもそれがEXIT処理です。TIF_MEMDIEをセットしたプロセスはカーネルがプールしているメモリかなんか優先的に割り当てられるようになっているのでしょうか?
if (p->oomkilladj == OOM_DISABLE)ならそのプロセスは対象外です。OOM_DISABLE=-17で削除されたくないプロセスはあらかじめこの値を設定することでOOMキラーから除外されます。これは以下のように設定することができます。
echo -17 > /proc/〈PID〉/oom_adj
上記以外のプロセスがbadness()で悪いプロセス度(?)が評価され、一番悪いプロセスを選択しています。
まずメモリ空間の有無のチェックです。(select_bad_process()でもやっていますが・・・)そしてそのトータルメモリーをpointsとします。
if (p->flags & PF_SWAPOFF)でスワップしないプロセスなら、無条件にOOMキラー対象プロセスです。
チェックプロセスの子プロセスのメモリ空間をチェックします。それらのメモリー空間の半分+1をpointsに加算です。
CPU使用時間(ユーザ空間+カーネル空間)および動作時間を取得します。この値に重みを付けてこれでpointsを割っています。すなわちこれらの時間が大きいというのは重要なプロセスだと。いう位置づけです。nt_sqrt()はビットシフトおよび加算減算で、ラフにsqrを計算する関数です。
if (task_nice(p) > 0)でプライオリティが0以上なら、pointsは2倍です。次のhas_capability()でプロセスの権限によるチェックです。それがCAP_SYS_ADMIN/CAP_SYS_RESOURCE(たぶんルート権限みたいなもん?)またはIOを処理するものならpointsは1/4です。f (!cpuset_mems_allowed_intersects(current, p))は解りません。
最後にif (p->oomkilladj) で評価しています。oomkilladjはOOMキラー対象外とするだけのパラメータでは無いようです。この値が正の場合pointsをシフトして評価値を上げています。反対に負の場合右にシフトしてpointsを下げています。
どのプロセスが削除されるか解らないという事で、悪名高いと言われているOOMキラーも、可能な限りベストなプロセスを選択しようとしていると言うことでした。
評価ベースはmm->total_vmとなることから、大きなプロセスしかも子プロセスを多く有するものが、その対象に選ばれやすいと言えます。
out_of_memory()の最初の処理はsysctl_panic_on_oomが2であるなら、"out of memory・・・"を表示して、panic処理となります。panic()関数ではさようならLinuxということです。そうでないならconstrained_alloc()をコールしています。この関数はアロケーションの束縛(?)すなわち、NUMAシステムに掛かる処理でX86では常にCONSTRAINT_NONEを返します。
case CONSTRAINT_NONE以下では再度sysctl_panic_on_oomをチェックします。この場合0でないならpanic()となります。先の処理でも同じような処理をしています。結果的にNUMAシステムの時のみ、sysctl_panic_on_oom == 2は意味を成すものとなり、X86ではsysctl_panic_on_oom!=0なら同じ結果となります。
sysctl_oom_kill_allocating_taskが0でないなら、メモリーをアロケートしようとして出来なかったプロセスがOOMキラー対象のプロセスとなります。そうでないならselect_bad_process()でOOMキラーとなるプロセスを選択します。
oom_kill_process()は対象となるプロセスにシグナルを送信することで実現します。ラベルout:以降は、if (!test_thread_flag(TIF_MEMDIE))でOOKキラー対象プロセスが、カレントプロセスでないなら、OOMキラープロセスが削除されのを、schedule_timeout_uninterruptible()で少しウエイトすることで、カレントプロセスメモリー確保することにあると思います。
なお、oom_kill_process()ではOOMキラーとしてシグナル送信(SIGKILL)されたプロセスは、TIF_MEMDIEがセットされています。
void out_of_memory(struct zonelist *zonelist, gfp_t gfp_mask, int order)
{
struct task_struct *p;
unsigned long points = 0;
unsigned long freed = 0;
enum oom_constraint constraint;
if (sysctl_panic_on_oom == 2)
panic("out of memory. Compulsory panic_on_oom is selected.\n");
constraint = constrained_alloc(zonelist, gfp_mask);
read_lock(&tasklist_lock);
switch (constraint) {
case CONSTRAINT_MEMORY_POLICY:
oom_kill_process(current, gfp_mask, order, points, NULL,
"No available memory (MPOL_BIND)");
break;
case CONSTRAINT_NONE:
if (sysctl_panic_on_oom)
panic("out of memory. panic_on_oom is selected\n");
/* Fall-through */
case CONSTRAINT_CPUSET:
if (sysctl_oom_kill_allocating_task) {
oom_kill_process(current, gfp_mask, order, points, NULL,
"Out of memory (oom_kill_allocating_task)");
break;
}
retry:
p = select_bad_process(&points, NULL);
if (PTR_ERR(p) == -1UL)
goto out;
if (!p) {
read_unlock(&tasklist_lock);
panic("Out of memory and no killable processes...\n");
}
if (oom_kill_process(p, gfp_mask, order, points, NULL,
"Out of memory"))
goto retry;
break;
}
out:
read_unlock(&tasklist_lock);
if (!test_thread_flag(TIF_MEMDIE))
schedule_timeout_uninterruptible(1);
}
・補足sysctl_panic_on_oom/sysctl_oom_kill_allocating_taskは以下のprocで設定できます。
[root@localhost ~]# ls /proc/sys/vm/* | grep oom
/proc/sys/vm/oom_dump_tasks
/proc/sys/vm/oom_kill_allocating_task
/proc/sys/vm/panic_on_oom
/proc/sys/vm/would_have_oomkilled
select_bad_process()でOOMキラーされるプロセスを選択します。do_each_thread(g, p)マクロでinit_taskをヘッドとするプロセスのリストをすべてなめることでチェックします。まず、メモリー空間を有していないプロセス(カーネルスレッド)言うまでも無く対象外です。
if (is_global_init(p))はプロセスIDが1のプロセスがどうかをチェックします。これはinitプロセスです。(initプロセスもif (!p->mm)でスキップされるのではと・・・)
if (test_tsk_thread_flag(p, TIF_MEMDIE)はそのプロセスがすでにOOMキラーされているプロセスです。これも再度シグナルを送っても意味ありません。この場合エラーで呼び出し元に復帰します。(呼び出し元ではエラーで帰るとschedule_timeout_uninterruptible()で完全に削除されるまでウエイトしていました。)
if (p->flags & PF_EXITING)は対象プロセスがゾンビの場合です。それがカレント(メモリーをアロケートしようとしたプロセス)でないなら、完全に削除するのをウエイトするために、エラーで復帰です。もしそれが自プロセスならchosen = p;*ppoints = ULONG_MAXとし、これがOOMキラープロセスとなるようにしています。自プロセスもエラー復帰させ、EXITされるのを待とうとすると、EXIT処理でページアロケート失敗ですから、永久にOOMキラーが発生することになるかと思います。なおこれは推測ですが、EXITのプロセスをOOMキラーすると、スレッドフラグにTIF_MEMDIEが設定されるだけのようです。ページをアロケートしようとするプロセスがアロケートできなかったわけで、しかもそれがEXIT処理です。TIF_MEMDIEをセットしたプロセスはカーネルがプールしているメモリかなんか優先的に割り当てられるようになっているのでしょうか?
if (p->oomkilladj == OOM_DISABLE)ならそのプロセスは対象外です。OOM_DISABLE=-17で削除されたくないプロセスはあらかじめこの値を設定することでOOMキラーから除外されます。これは以下のように設定することができます。
echo -17 > /proc/〈PID〉/oom_adj
上記以外のプロセスがbadness()で悪いプロセス度(?)が評価され、一番悪いプロセスを選択しています。
static struct task_struct *select_bad_process(unsigned long *ppoints,
struct mem_cgroup *mem)
{
struct task_struct *g, *p;
struct task_struct *chosen = NULL;
struct timespec uptime;
*ppoints = 0;
do_posix_clock_monotonic_gettime(&uptime);
do_each_thread(g, p) {
unsigned long points;
if (!p->mm)
continue;
if (is_global_init(p))
continue;
if (mem && !task_in_mem_cgroup(p, mem))
continue;
if (test_tsk_thread_flag(p, TIF_MEMDIE))
return ERR_PTR(-1UL);
if (p->flags & PF_EXITING) {
if (p != current)
return ERR_PTR(-1UL);
chosen = p;
*ppoints = ULONG_MAX;
}
if (p->oomkilladj == OOM_DISABLE)
continue;
points = badness(p, uptime.tv_sec);
if (points > *ppoints || !chosen) {
chosen = p;
*ppoints = points;
}
} while_each_thread(g, p);
return chosen;
}
badness()の評価は、メモリ空間の使用状況、CPU使用時間、動作時間、プライオリティをベースにして算出されています。その評価値はpointsに積算され返り値ろしています。まずメモリ空間の有無のチェックです。(select_bad_process()でもやっていますが・・・)そしてそのトータルメモリーをpointsとします。
if (p->flags & PF_SWAPOFF)でスワップしないプロセスなら、無条件にOOMキラー対象プロセスです。
チェックプロセスの子プロセスのメモリ空間をチェックします。それらのメモリー空間の半分+1をpointsに加算です。
CPU使用時間(ユーザ空間+カーネル空間)および動作時間を取得します。この値に重みを付けてこれでpointsを割っています。すなわちこれらの時間が大きいというのは重要なプロセスだと。いう位置づけです。nt_sqrt()はビットシフトおよび加算減算で、ラフにsqrを計算する関数です。
if (task_nice(p) > 0)でプライオリティが0以上なら、pointsは2倍です。次のhas_capability()でプロセスの権限によるチェックです。それがCAP_SYS_ADMIN/CAP_SYS_RESOURCE(たぶんルート権限みたいなもん?)またはIOを処理するものならpointsは1/4です。f (!cpuset_mems_allowed_intersects(current, p))は解りません。
最後にif (p->oomkilladj) で評価しています。oomkilladjはOOMキラー対象外とするだけのパラメータでは無いようです。この値が正の場合pointsをシフトして評価値を上げています。反対に負の場合右にシフトしてpointsを下げています。
どのプロセスが削除されるか解らないという事で、悪名高いと言われているOOMキラーも、可能な限りベストなプロセスを選択しようとしていると言うことでした。
unsigned long badness(struct task_struct *p, unsigned long uptime)
{
unsigned long points, cpu_time, run_time, s;
struct mm_struct *mm;
struct task_struct *child;
task_lock(p);
mm = p->mm;
if (!mm) {
task_unlock(p);
return 0;
}
points = mm->total_vm;
task_unlock(p);
if (p->flags & PF_SWAPOFF)
return ULONG_MAX;
list_for_each_entry(child, &p->children, sibling) {
task_lock(child);
if (child->mm != mm && child->mm)
points += child->mm->total_vm/2 + 1;
task_unlock(child);
}
cpu_time = (cputime_to_jiffies(p->utime) + cputime_to_jiffies(p->stime))
>> (SHIFT_HZ + 3);
if (uptime >= p->start_time.tv_sec)
run_time = (uptime - p->start_time.tv_sec) >> 10;
else
run_time = 0;
s = int_sqrt(cpu_time);
if (s)
points /= s;
s = int_sqrt(int_sqrt(run_time));
if (s)
points /= s;
if (task_nice(p) > 0)
points *= 2;
if (has_capability(p, CAP_SYS_ADMIN) ||
has_capability(p, CAP_SYS_RESOURCE))
points /= 4;
if (has_capability(p, CAP_SYS_RAWIO))
points /= 4;
if (!cpuset_mems_allowed_intersects(current, p))
points /= 8;
if (p->oomkilladj) {
if (p->oomkilladj > 0) {
if (!points)
points = 1;
points <<= p->oomkilladj;
} else
points >>= -(p->oomkilladj);
}
return points;
}
・補足評価ベースはmm->total_vmとなることから、大きなプロセスしかも子プロセスを多く有するものが、その対象に選ばれやすいと言えます。





