前言

偶然在网上看到一个问题: OOM 是按照虚拟内存还是实际内存来打分? 这里“实际内存”表达的意思应该是物理内存,而“打分”想表达的意思应该是 OOM killer 机制里面的 badness score. 当内存吃紧时,假如开启了 OOM killer,OOM killer 会计算进程的 badness score, badness score 越高,就越优先被 OOM killer 杀死.

实验

当内存吃紧,页分配器尝试回收物理Page失败后,会调用 OOM killer,选择 badness score 最高的进程杀死,释放内存. badness score 的分数范围是0-1000, 0表示不杀死, 1000表示总是杀死, 可以直接通过 cat /proc/<pid>/oom_score 查看进程的 badness score.

这个问题的本质是 OOM 机制是基于虚拟内存还是物理内存,我们可以先通过一个实验验证这个问题:

机器内存:

通过free -h可以查看机器的内存情况,物理内存为16GB

free

实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <sys/mman.h>
#include <vector>
#include <thread>
#include <sys/stat.h>
#include <fcntl.h>
#include <chrono>

int main() {
std::string fname = "oom_killer_file";
long long map_size = 4096UL * 1024 * 1024 * 4;
int file_fd = ::open(fname.c_str(), O_RDWR | O_CREAT, 0645);
posix_fallocate(file_fd, 0, map_size);
void* base_ptr = mmap(nullptr, map_size, PROT_READ | PROT_WRITE,
MAP_SHARED,
file_fd,
0);
if (base_ptr == nullptr) {
close(file_fd);
return 0;
}

std::this_thread::sleep_for (std::chrono::seconds(600));
close(file_fd);
return 0;
}

这段代码新建了一个名为 oom_killer_file 的文件,先使用 posix_fallocate() 预分配16GB的大小,然后利用 mmap() 分配16GB的虚拟空间, 这段代码会 sleep 600s, 假如 OOM killer 是基于虚拟内存的,这段代码会被 kill. mmap() 的原理可以查看文章mmap源码分析, 调用 mmap() 会为进程分配虚拟内存,当真正写入触发缺页中断时才分配物理内存页.

编译运行:

1
2
3
g++ -std=c++11 -O2 oom_killer.cc -o oom_killer

./oom_killer

实验结果:

我们通过 htop观察发现进程确实已经分配了16GB的虚拟内存,但物理内存只有1768 bytes, 而经过600s的运行,程序并没有被 kill, 所以可以断定 OOM killer 不是基于虚拟内存而应该是物理内存计算 badness score.

htop

OOM killer源码分析

OOM killer的核心函数是 out_of_memory(), 执行流程如下:

  • 调用 check_panic_on_oom() 检查是否允许执行内核恐慌,假如允许,需要重启系统.
  • 假如打开了/proc/sys/vm/oom_kill_allocating_task 即允许 kill 掉当前正在申请分配物理内存的进程,那么杀死当前进程.
  • 调用 select_bad_process,选择 badness score 最高的进程.
  • 调用 oom_kill_process, 杀死选择的进程.

我们分析 badness score的计算函数来理解 OOM killer如何选择需要被 kill 掉的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
unsigned long oom_badness(struct task_struct *p, struct mem_cgroup *memcg,
const nodemask_t *nodemask, unsigned long totalpages)
{
long points;
long adj;

/* 假如该进程不能被kill, 则分数返回0. */
if (oom_unkillable_task(p, memcg, nodemask))
return 0;

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

/* 获取该进程的 oom_score_adj, 这个是用户为进程设置的 badness score
* 调整值,假如这个值为-1000或者进程被标记为不可被kill,或者进程处于
* vfork()过程,badness score返回0. */
adj = (long)p->signal->oom_score_adj;
if (adj == OOM_SCORE_ADJ_MIN ||
test_bit(MMF_OOM_SKIP, &p->mm->flags) ||
in_vfork(p)) {
task_unlock(p);
return 0;
}

/* badness score分数 = 物理内存页数 + 交换区页数 + 页表Page Table数量. */
points = get_mm_rss(p->mm) + get_mm_counter(p->mm, MM_SWAPENTS) +
mm_pgtables_bytes(p->mm) / PAGE_SIZE;
task_unlock(p);

/* 利用以下公式对 badness score 值进行调整. */
adj *= totalpages / 1000;
points += adj;

/* 返回 badness score, 假如等于0, 则返回 1. */
return points > 0 ? points : 1;
}

总结:

通过分析 badness score 的计算函数,我们可以发现 OOM killer 是基于RSS即常驻的物理内存来选择进程进行kill, 从而释放内存. Linux内核内存管理部分最主要的一个逻辑就是延迟分配.