当应用程序申请内存时,如果系统内存不足或者到达了memory cgroup设置的limit,会触发内存回收,如果还是得不到想要的数量的内存,最后会出发oom,选择一个进程杀死,释放其占用的内存。
下面通过实验来学习一下通过memory cgroup触发oom。
内核:linux-5.14
发行版:ubuntu20.4 (默认使用的是cgroup v1)
发行版:ubuntu22.04 (默认使用的是cgroup v2)
作者: pengdonglin137@163.com
oom对应的源码是mm/oom_kill.c。
cgroup v1
在memory cgroup的挂载目录下创建一个子目录,然后内核会自动在这个目录下创建一些预定义的文件,下面主要用到如下几个节点:
memory.oom_control # 控制是否启动oom,以及查看当前的oom状态 memory.limit_in_bytes # 用于设置一个memcg的内存上限 memory.usage_in_bytes # 用于查看一个memcg当前的内存使用量 cgroup.procs # 用于将某个进程加入到这个memcg
- 关闭swap,防止内存回收时匿名内存被交换出去影响测试
swapon -s swapoff <swap_file>
- 在memory cgroup顶层目录新建一个oom_test控制组
cd /sys/fs/cgroup/memory mkdir oom_test
- 建立三个终端,然后将三个终端进程加入到oom_test控制组中
分别在三个终端中执行如下命令:
echo $$ > /sys/fs/cgroup/memory/oom_test/cgroup.procs
- 设置内存限制
echo 80M > memory.limit_in_bytes
- 分别在三个终端里执行三个不同的命令
- 终端1: 执行
alloc_memory_1
,这个程序每敲一次回车申请10MB的内存 - 终端2: 执行
alloc_memory_2
,这个程序每敲一次回车申请10MB的内存 - 终端3: 执行
print_counter.sh
,每秒打印一行log,表示还活着
- 在另外一个终端观察内存使用
watch -n1 cat /sys/fs/cgroup/memory/oom_test/memory.usage_in_bytes
- 在终端1和终端2中交替敲回车
观察oom_test的变化,看哪个进程被杀死。 - 结果
观察到如下现象,当在终端2中敲回车后,内存超过设置的limit时,终端1中的进程alloc_memory_1被kill掉了,然后控制的内存使用量瞬间降了下来。而终端2和终端3中的
进程还活着。同时内核输出如下的log:
oom的内核log
- 查看节点
memory.oom_control
root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control oom_kill_disable 0 # 表示当前控制组的oom kill功能处于开启状态 under_oom 0 # 用于计数是否正处于oom,内核提供了两个接口函数mem_cgroup_mark_under_oom和mem_cgroup_unmark_under_oom来操作这个计数 oom_kill 1 # 表示已经杀死了一个进程
- 关闭oom_test控制组的oom功能
执行下面的命令关闭oom_test的oom功能:
root@ubuntu:/sys/fs/cgroup/memory/oom_test# echo 1 > memory.oom_control root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control oom_kill_disable 1 under_oom 0 oom_kill 1
然后重新在终端1中运行alloc_memory_1
,然后继续在终端1中敲回车,观察控制组的内存占用以及alloc_memory_1进程的状态。
可以看到如下结果:
当控制组的内存使用到达limit后,不再增长,并且alloc_memory_1也不动了,通过ps查看进程状态,发现alloc_memory_1编程了D
状态,即不可中断睡眠。
root@ubuntu:/home/pengdl/work/oom_test# ps -aux | grep alloc_me root 343405 0.0 0.2 43472 42096 pts/4 S+ 00:36 0:00 ./alloc_memory_2 root 369713 0.0 0.2 43472 41252 pts/2 D+ 01:12 0:00 ./alloc_memory_1
此时查看memory.oom_control
的内容:
root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control oom_kill_disable 1 under_oom 1 oom_kill 1
其中,under_oom
是1,因为此时alloc_memory_1在处理oom时被阻塞了,没有处理完oom。
内核源码流程:
handle_mm_fault -> __set_current_state(TASK_RUNNING) -> __handle_mm_fault(vma, address, flags) -> handle_pte_fault -> do_anonymous_page(vmf) -> alloc_zeroed_user_highpage_movable -> mem_cgroup_charge -> __mem_cgroup_charge -> try_charge -> try_charge_memcg -> mem_cgroup_oom -> 如果memcg->oom_kill_disable是1的话: if (memcg->oom_kill_disable) { if (!current->in_user_fault) return OOM_SKIPPED; css_get(&memcg->css); current->memcg_in_oom = memcg; current->memcg_oom_gfp_mask = mask; current->memcg_oom_order = order; return OOM_ASYNC; } -> return -ENOMEM; -> return VM_FAULT_OOM -> mem_cgroup_oom_synchronize -> prepare_to_wait(&memcg_oom_waitq, &owait.wait, TASK_KILLABLE)
在mem_cgroup_oom_synchronize会将当前进程设置为TASK_KILLABLE
状态,被称为中度睡眠,是(TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
两种状态的组合,即还不完全是D
,还可以响应kill信号。
此时,我们可以在终端1中用ctrl c
杀死alloc_memory_1
进程。
- 接着上一步,重新开启oom_test的oom功能
执行下面的命令开启oom_test的oom,然后观察现象,注此时alloc_memory_1处于D
。
root@ubuntu:/sys/fs/cgroup/memory/oom_test# echo 0 > memory.oom_control root@ubuntu:/sys/fs/cgroup/memory/oom_test# cat memory.oom_control oom_kill_disable 0 under_oom 0 oom_kill 2
可以看到,此时oom被打开,然后终端2中的alloc_memory_2进程被杀死了,节点memory.oom_control
的under_oom
变成了0,表示oom处理完了,而终端1中的进程alloc_memory_1的状态变成了S
:
root@ubuntu:/home/pengdl/work/oom_test# ps -aux | grep alloc_me root 369713 0.0 0.3 63960 62632 pts/2 S+ 01:12 0:00 ./alloc_memory_1
原因是,当开启oom时,会将memcg_oom_waitq中睡眠的进程唤醒,当alloc_memory_1唤醒后,继续运行,因为上次没有处理成功缺页,所以mmu中的pte的还不是有效的映射,所以还是会触发跟上一次相同的缺页
终端,由于此时开启了oom,会选中alloc_memory_2并且杀掉以释放出内存。
mem_cgroup_oom_control_write -> memcg->oom_kill_disable = val -> memcg_oom_recover -> __wake_up(&memcg_oom_waitq, TASK_NORMAL, 0, memcg)
alloc_memory_2被杀死的内核log
cgroup v2
cgroup v2默认也被挂载在/sys/fs/cgroup/
下,用到的文件:
cgroup.procs # 用于将进程加入到控制组 memory.max # 用于设置控制组的内存上限 memory.current # 用于查看控制组的内存消耗 memory.oom.group # 用于控制oom发生时,是否回收控制组中的所有进程
cgroupv2中删除了控制oom是否开启的文件,即oom始终开启。并且多了一个memory.oom.group的文件,表示当发生oom时,是否杀死这个控制组下的所有进程。
- 创建oom_test控制组
- 上前面同样的操作步骤,关闭全局swap,创建三个终端,将终端进程加入到oom_test控制组,然后分别运行alloc_memory_1、alloc_memory_2以及print_counter.sh
- 设置内存上限
root@ubuntu2204:/sys/fs/cgroup/oom_test# echo 80M > memory.max root@ubuntu2204:/sys/fs/cgroup/oom_test# cat memory.max 83886080
- 在终端1和终端2中交替敲回车
可以看到跟之前的一样,当在终端2中输入回车后,导致内存超过max,从而触发了内存回收,由于没有swap,并且页缓存也不够,这样会触发oom,选中了终端1中的进程alloc_memory_1,杀死后,
控制组的内存占用降了下来,终端2和终端3终端的进程还在运行。
回收alloc_memory_1的内核log
- 开启oom.group,观察是否会杀死控制组中的所有进程
在终端1中重新启动alloc_memory_1,然后执行下面的命令开启oom.group
root@ubuntu2204:/sys/fs/cgroup/oom_test# cat memory.oom.group 0 root@ubuntu2204:/sys/fs/cgroup/oom_test# echo 1 > memory.oom.group root@ubuntu2204:/sys/fs/cgroup/oom_test# cat memory.oom.group 1
- 在终端1中连续敲回车,观察现象
可以看到,当控制组的内存占用超过max后,会将控制组中的进程全部杀死:
root@ubuntu2204:/sys/fs/cgroup/oom_test# cat cgroup.procs
可以看到,这个控制组下一个进程也没有了。在看看内核log:
控制组中的所有进程全部被杀死的log
代码流程:
out_of_memory -> select_bad_process -> oom_kill_process -> mem_cgroup_get_oom_group -> __oom_kill_process -> mem_cgroup_scan_tasks C 复制 全屏
完。