谈谈OOM

作为一个不合格的开发人员多多少少都被 OOM(Out of memory) x 过,只是一般大家对于为什么被选中,可能没太考究。

1) OOM 的起因

没有买卖就没有杀害,这个结论在操作系统也是成立的。如果我们不在分配内存的时候严格控制,那么也就没 OOM 机制什么事情了。但为了内存的最大化使用,一般都愿意来冒这个风险。如何控制内存分配的参数,也就是之前有聊过的一个系统参数 谈谈 Overcommit

简单来说之所以会出现 OOM, 就是已分配的虚拟内存大于物理内存和Swap分区大小,导致需要内存无法分配。如果 overcommit = 2, 在申请虚拟内存时,如果超过限制的内存比例 + Swap 空间会直接返回失败,只要分配虚拟内存不超过物理内存,也就不会有 OOM。

2) 进程选择

既然无法分配内存,就有两种下面的两种选择:

  1. 向申请内存报告申请内存失败
  2. 选择一个杀掉其他的进程来释放内存

内核 2.4 版本之前是第一种做法(走在路上听别人说的), 后面的内核版本才采用第二种。我们这里要来看的是第二种,原因很简单,因为第一种做法没什么好看的...

下面的代码基于内核 2.6.32, 代码在 arch/不同型号/mm/fault.cmm/oom_kill.c:

我们分配物理内存的时候是通过缺页中断来实现的,然后会调用 do_page_fault ,如果内存不足就会通过 mm_fault_error 来处理,调用链如下:

out_of_memory->pagefault_out_of_memory->__out_of_memory
->select_bad_process->badness

select_bad_process 调用 badness 来计算每个进程的得分(范围 0-1000),然后干掉 score 最高的进程。每个进程的 score 我们通过 /proc/pid/oom_score 来查看。我们下面来看这个 score 是如何计算,主要是下面几个纬度:

  • 子进程内存消耗,越多越容易被选中
  • CPU 密集型以及老进程,比刚启动的进程更不容易被选中
  • root 启动的进程更不容易被选中
  • 用户通过控制 oom_adj 来控制进程选中优先级(范围是-17到15)
unsigned long badness(struct task_struct *p, unsigned long uptime)
{
    ...
    // 这个 oom_adj 值是从 /proc/pid/oom_adj 获取
    // 用户可以通过控制 oom_adj 来控制 score
    int oom_adj = p->signal->oom_adj;
    ...

    // 系统总内存大小作为基础分数
    points = mm->total_vm;

    if (p->flags & PF_OOM_ORIGIN)
        return ULONG_MAX;

    // 如果 fork 的进程子进程也太多子进程被选到的概率也比较大。
    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);
    }

    thread_group_cputime(p, &task_time);
    // 进程用户空间消耗的 cpu 分片数
    utime = cputime_to_jiffies(task_time.utime);
    // 进程系统消耗的 cpu 分片数
    stime = cputime_to_jiffies(task_time.stime);
    cpu_time = (utime + stime) >> (SHIFT_HZ + 3);

    // 计算启动时间
    if (uptime >= p->start_time.tv_sec)
        run_time = (uptime - p->start_time.tv_sec) >> 10;
    else
        run_time = 0;

    // 消耗 CPU 越多的进程,降低 score
    if (cpu_time)
        points /= int_sqrt(cpu_time);
    // 根据启动时间降低 score, 所以最新启动的进程最可能被杀
    if (run_time)
        points /= int_sqrt(int_sqrt(run_time));

    // 降低 root 用户启动的进程的 score
    if (has_capability_noaudit(p, CAP_SYS_ADMIN) ||
        has_capability_noaudit(p, CAP_SYS_RESOURCE))
        points /= 4;

    if (!has_intersects_mems_allowed(p))
        points /= 8;

     if (oom_adj) {
        if (oom_adj > 0) {
            if (!points)
                points = 1;
            points <<= oom_adj;
        } else
            points >>= -(oom_adj);
    }
    return points;
}

3) 总结

从整体来看,占用内存越大,非CPU消耗刑,非 Root 启动以及新启动的进程更加容易被选中。而用户可以通过调整 /proc/$pid/oom_adj 来调整进程的优先级。