缘起
作为一个菜鸟,扒代码是提升自己内功的必修课,因此,本弱菜也没事扒一把代码学习。今儿,扒的是 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 的种种到这里也差不多可以告一个段落了,休息一下休息一下。