菜鸟的 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 的种种到这里也差不多可以告一个段落了,休息一下休息一下。