菜鸟的 linux 学习笔记 -- OOM

缘起

作为一个菜鸟,扒代码是提升自己内功的必修课,因此,本弱菜也没事扒一把代码学习。今儿,扒的是 openssh 的代码中的 sshd 部分的代码。这部分的代码不难理解,但是其中有个 oom_adjust_setup 的函数引起了俺的兴趣。(openssh 6.3p1 openbsd-compat/port-linux.c:262) 想起之前也见过 syslog 里面出现 oom-killer 的记录,但是究竟这背后意味着什么?linux 是怎么选择被 kill 的进程的捏?还有待研究一番。

从 oom_adjust_setup 开始

好,我们来看看这个函数究竟想干啥?诚如这个函数上面的注释所说的一样 Tell the kernel's out-of-memory killer to avoid sshd.  也就是说经过这个函数一番捣鼓,偶们的 sshd 进程就死活不会被 linux 系统给 kill 掉啦,很好很强大。那,让我们看看它是怎么做到的。(// 后面的注释是俺加的)

/*
 * Tell the kernel's out-of-memory killer to avoid sshd.
 * Returns the previous oom_adj value or zero.
 */
void
oom_adjust_setup(void)
{
        int i, value;
        FILE *fp;

        debug3("%s", __func__);
         for (i = 0; oom_adjust[i].path != NULL; i++) {
                oom_adj_path = oom_adjust[i].path;
                value = oom_adjust[i].value;
                if ((fp = fopen(oom_adj_path, "r+")) != NULL) {
                        // read value from
                        //     "/proc/self/oom_score_adj" (kernels >= 2.6.36)
                        // or  "/proc/self/oom_adj" (kernels <= 2.6.35)
                        // save it to variable oom_adj_save
                        if (fscanf(fp, "%d", &oom_adj_save) != 1)
                                verbose("error reading %s: %s", oom_adj_path,
                                    strerror(errno));
                        else {
                                // the same as fseek(stream, 0L, SEEK_SET)
                                rewind(fp);
                                // rewrite
                                if (fprintf(fp, "%d\n", value) <= 0)
                                        verbose("error writing %s: %s",
                                           oom_adj_path, strerror(errno));
                                else
                                        verbose("Set %s from %d to %d",
                                           oom_adj_path, oom_adj_save, value);
                        }
                        fclose(fp);
                        return;
                }
        }
        oom_adj_path = NULL;
}

其实呢就是修改两个文件(/proc/self/oom_score_adj 和 /proc/self/oom_adj)的值,下面就是上面这个函数中用到的结构体变量的定义

/*
 * The magic "don't kill me" values, old and new, as documented in eg:
 * http://lxr.linux.no/#linux+v2.6.32/Documentation/filesystems/proc.txt
 * http://lxr.linux.no/#linux+v2.6.36/Documentation/filesystems/proc.txt
 */

static int oom_adj_save = INT_MIN;
static char *oom_adj_path = NULL;
struct {
        char *path;
        int value;
} oom_adjust[] = {
        {"/proc/self/oom_score_adj", -1000},    /* kernels >= 2.6.36 */
        {"/proc/self/oom_adj", -17},            /* kernels <= 2.6.35 */
        {NULL, 0},
};

咦,这个结构体中的两个数字(-1000 和 -17)是怎么来的,代码中附上的注释中已经解释得很清楚了(就是那两个 url 链接)。大意就是说捏,通过不同的数字控制自己本身被 linux oom-kill 的优先级,代码中的这两个数字就是别杀我的意思。OK,看来到这里就差不多可以结束了,但是还是有问题 linux 到底是怎么决定去 kill 哪个进程来释放内存的捏?咱继续往下扒-

刨根问底

那么打开 linux kernel 代码开始扒(这里用的是linux-3.12.5 的代码)。内存相关的代码都在 linux-3.12.5/mm/ 目录下,其中有一个叫做 oom_kill.c 就是 kill 掉消耗太多内存的幕后凶手。这里面干活的是 oom_kill_process 这个函数。这个函数的主要流程如下

1)使用 oom_badness 函数去计算每个进程的分数, 取出分数值最高的进程

2)把上面分数最高的进程 kill 掉(do_send_sig_info(SIGKILL, SEND_SIG_FORCED, victim, true);)

那么接着往里挖,咱来看看 oom_badness 是怎么计算分数的(// 后面是俺的注释)

unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
                          const nodemask_t *nodemask, unsigned long totalpages)
{
        long points;
        long adj;

        // some process as follows can not be killed
        // 1) init process
        // 2) kernel thread
        // 3) not the member of oom cgroup
        // 4) TODO: I'm a newbie, so still don't know which type this process is
        // ( use functiong 'has_intersects_mems_allowed' to judge)
        if (oom_unkillable_task(p, memcg, nodemask))
                return 0;

        p = find_lock_task_mm(p);
        if (!p)
                return 0;

        // this is the value of we set in /proc/self/oom_score_adj
        adj = (long)p->signal->oom_score_adj;
        // Aha, OOM_SCORE_ADJ_MIN this is the magic number (-1000) we told in the sshd code
        if (adj == OOM_SCORE_ADJ_MIN) {
                task_unlock(p);
                return 0;
        }

        /*
         * The baseline for the badness score is the proportion of RAM that each
         * task's rss, pagetable and swap space use.
         */
        points = get_mm_rss(p->mm) + p->mm->nr_ptes +
                 get_mm_counter(p->mm, MM_SWAPENTS);
        task_unlock(p);

        /*
         * Root processes get 3% bonus, just like the __vm_enough_memory()
         * implementation used by LSMs.
         */
        // why 3% ? 'Casuse it will be divided by 1000 next
        // root process may be import so lower its priority
        if (has_capability_noaudit(p, CAP_SYS_ADMIN))
                adj -= 30;

        /* Normalize to oom_score_adj units */
        adj *= totalpages / 1000;
        points += adj;

        /*
         * Never return 0 for an eligible task regardless of the root bonus and
         * oom_score_adj (oom_score_adj can't be OOM_SCORE_ADJ_MIN here).
         */
        return points > 0 ? points : 1;
}

从上面的代码可以看出,计算进程得分的依据就是该进程是用了多少的 rss 啦用了多少页啦 swap 空间的使用情况(这些是加分项),如果是 root 进程就稍微降降分数,最后加上偶们自定义的 oom_score_adj 就大功告成啦。

那么知道了 kernel 是这么干的之后偶们还能干啥捏?当然就是动手实践啦。

注1: linux 代码是压缩的,本弱菜一看我擦  xz 格式咋解压捏?xz -d linux-3.12.5.tar.xz;tar -xvf linux-3.12.5.tar

Try

首先声明下咱的实验环境

ubuntu# free -m
total used free shared buffers cached
Mem: 2003 958 1045 0 207 487
-/+ buffers/cache: 263 1739
Swap: 2047 0 2047
ubuntu# cat /proc/sys/vm/overcommit_memory
0
ubuntu# uname -a
Linux ubuntu 3.5.0-23-generic #35~precise1-Ubuntu SMP Fri Jan 25 17:13:26 UTC 2013 x86_64 x86_64 x86_64 GNU/Linux
ubuntu#

在实验过程中,咱又有所发现。咱们常用的 malloc 行为和咱理解的略略有所不同。大家都知道 malloc 失败返回 NULL 指针,成功返回指向一块连续内存的起始地址。那么问题来了,我们是不是可以分配超过物理内存大小的内存空间捏?答:可以

请看下面一段代码

#include <stdio.h>
#include <stdlib.h>

int main (void)
{
    int n = 0;

    while(1)
    {
        if(malloc(1<<20) == NULL)
        {
            printf("malloc failure after %d MiB\n", n);
            return 0;
        }
        printf ("got %d MiB\n", ++n);
    }
    return 0;
}

 

编译后运行,你会发现它会分配远远大于物理内存大小的空间出来而不会被 oom-kill.为啥?咱们先在看另一段代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
    int n = 0;
    char *p;

    while (1) {
        if ((p = (char *)malloc(1<<20)) == NULL) {
            printf("malloc failure after %d MiB\n", n);
            return 0;
        }
        memset (p, 0, (1<<20));
        printf ("got %d MiB\n", ++n);
    }

    return 0;
}

这段代码就比较符合我们的理解了,会被 oom-kill 干掉

got 3729 MiB
got 3730 MiB
zsh: killed ./malloc_use
ubuntu#

 

syslog 中也有相应的 oom 信息

Dec 16 08:13:52 ubuntu kernel: [600632.103656] Out of memory: Kill process 24642 (malloc_use) score 896 or sacrifice child
Dec 16 08:13:52 ubuntu kernel: [600632.103661] Killed process 24642 (malloc_use) total-vm:3895912kB, anon-rss:1906672kB, file-rss:128kB

 

出现这种现象的原因是为啥捏?原来使用 malloc 分配内存的时候,linux 并没有真正的我们分配的内存地址和物理内存关联上,只有在使用的时候才真正的关联上。于是,我们看到在第二段代码中一个 memset 就能引发 oom 啦。这种事情捏就叫做 memory overcommit。那么有木有办法让我们在 malloc 的时候就关联上物理内存捏?有,别忘了 linux 有一堆系统参数可以调,其中就有一个叫做 vm.overcommit_memory  的,其取值范围如下

0: Heuristic overcommit handling(使用启发式算法去控制要不要 overcommit 等,是默认值)

1: Always overcommit.(总是会在真正使用的时候才和物理内存关联上)

2:Don't overcommit(malloc 的时候就和物理内存关联上啦)

注1:关于这个参数值的说明参考 https://www.kernel.org/doc/Documentation/vm/overcommit-accounting

注2: 不会调系统参数?很简单啊两种方法选一种 1)修改 /etc/sysctl.conf 然后 sysctl -p 2) echo "xxx" > /proc/sys/yyyyyy

注3: /proc/[pid]/oom_score 可以查看计算出来的 OOM 的分数

Summary

关于 oom 的种种到这里也差不多可以告一个段落了,休息一下休息一下。 

 

 

 

posted @ 2013-12-16 23:44  saru  阅读(1291)  评论(0编辑  收藏  举报