zoukankan      html  css  js  c++  java
  • 菜鸟的 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
    ", 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);
                return 0;
            }
            printf ("got %d MiB
    ", ++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);
                return 0;
            }
            memset (p, 0, (1<<20));
            printf ("got %d MiB
    ", ++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 的种种到这里也差不多可以告一个段落了,休息一下休息一下。 

     

  • 相关阅读:
    Nginx 静态站点配置不对导致301跳转的坑
    Prometheus(一):Web服务环境监控
    10BASE-T
    计算机网络之物理层:7、物理层设备(中继器、集线器)
    广域网
    转载------对比网络模拟器软件——Cisco Packet Tracer、华为eNSP、H3C Cloud Lab
    二、Python的使用
    MobSF移动安全扫描平台本地化部署与简单汉化
    MobSF移动安全扫描平台环境搭建与试用
    BSTestRunner增加历史执行记录展示和重试功能
  • 原文地址:https://www.cnblogs.com/foreverfree/p/3477659.html
Copyright © 2011-2022 走看看