文章目录
概述
Linux 操作系统有特定的内存管理方法。 其中一项策略是overcommit,它允许应用程序提前预订所需的内存。 然而,承诺的内存在实际使用时可能并不可用。 然后,系统必须提供一种特殊的手段来避免内存不足。
在本教程中,我们将了解内存不足 (OOM) killer,这是一个为了系统稳定性而消除应用程序的过程。
当 OOM Killer 被调用时
为了让killer发挥作用,系统必须允许overcommit。 然后,根据系统从kill该进程中获得的收益来对每个进程进行评分。
最后,当遇到低内存状态时,内核会kill分数最高的进程。
我们可以通过 /proc/PID/oom_score 文件中的 PID 找到进程的分数。 现在启动一个终端并打印其进程分数,因为 $$ 变量保存其 PID:
$ cat /proc/$$/oom_score
0
接下来,使用 oom_score_reader 脚本列出所有进程及其 PID 和名称,按 oom_score 从最低到最高排序:
#!/bin/bash
while read -r pid comm
do
printf '%d\t%d\t%s\n' "$pid" "$(cat /proc/$pid/oom_score)" "$comm"
done < <(ps -e -o pid= -o comm=) | sort -k2 -n
这里使用进程替换将 ps 的结果提供给 read 命令。
检查一下结果:
10 0 rcu_sched
102 0 kswapd0
...
97 0 devfreq_wq
99 0 watchdogd
1051 1 upowerd
1114 1 sddm-helper
1126 1 systemd
...
1147 2 pulseaudio
2005 2 gnome-shell-cal
2172 2 gsd-datetime
...
4329 6 gedit
2186 7 evolution-alarm
5300 9 qterminal
875 9 Xorg
9215 10 Web Content
3527 17 Privileged Cont
6353 19 Web Content
1679 20 gnome-shell
6314 21 Web Content
8625 21 Web Content
4070 22 Web Content
7753 23 Web Content
3170 27 gnome-software
3615 41 WebExtensions
3653 41 Web Content
3160 62 firefox
分数的取值范围是 0 到 1000。我们注意到 oom_score 的值为零意味着该进程不会受到 OOM killer的攻击。
3. 保护进程免受 OOM Killer 的侵害
现在,我们将尝试尽量减少进程被淘汰的可能性。对于长时间运行的进程和服务尤其重要。因此,对于这样的进程,应该设置 oom_score_adj 参数。
该参数的取值范围为 -1000 到 1000(包含 -1000 和 1000)。 因此,它的负值会降低 oom_score,从而降低该过程OOM Kiler的吸引力。 相反,正值会导致分数上升。 最后, oom_score_adj = -1000 的进程不会被kill。
检查文件/proc/PID/oom_score_adj中的参数:
$ cat /proc/$$/oom_score_adj
0
3.1. 手动设置 oom_score_adj
最简单的方法,可以手动调整 oom_score_adj 文件。 首先,让我们检查一下 Firefox 进程的分数。 我们需要 pgrep 来获取它的 PID:
$ cat /proc/$(pgrep firefox)/oom_score
60
接下来,让我们让其更容易被kill:
echo 500 > /proc/$(pgrep firefox)/oom_score_adj
cat /proc/$(pgrep firefox)/oom_score
562
最后,让我们再增强其安全性:
sudo echo -30 > /proc/$(pgrep firefox)/oom_score_adj
cat /proc/$(pgrep firefox)/oom_score
31
注意,需要 sudo 权限才能将调整因子降低到零以下。
3.2. choom 命令
使用 choom 来报告分数并修改其调整值。 该命令是 util-linux 软件包的一部分。使用 p 开关再次检查 Firefox 进程:
choom -p $(pgrep firefox)
pid 3061's current OOM score: 40
pid 3061's current OOM score adjust value: -30
然后,让我们通过使用 n 开关提供 oom_score_adj 的新值来增加其分数:
choom -p $(pgrep firefox) -n 300
pid 3061's OOM score adjust value changed from -30 to 300
choom -p $(pgrep firefox)
pid 3061's current OOM score: 371
pid 3061's current OOM score adjust value: 300
最后,使用 choom,我们可以使用给定的 oom_score_adj 立即启动一个进程:
choom -n 300 firefox
choom -p $(pgrep firefox)
pid 3061's current OOM score: 346
pid 3061's current OOM score adjust value: 300
3.3. 配置服务
对于服务,我们可以永久调整位于/etc/systemd 文件夹中或链接的服务配置中的分数。 因此,我们需要编辑“服务”部分中的 OOMScoreAdjust 条目。 作为示例,我们来看看 snapd 服务的配置:
[Unit]
Description=Snap Daemon
some output skipped
[Service]
# some output skipped
OOMScoreAdjust=-900
ExecStart=/usr/lib/snapd/snapd
# more output skipped
4. oom_score 是如何计算的
我们应该注意,得分的计算方式取决于内核版本。除了内存占用之外,旧版本可能会考虑运行时间、nice优先级或root拥有权。
然而,在版本5中,只有总内存使用量才重要。要找出这个,我们应该检查 oom_kill.c 源文件中的 oom_badness 函数。
首先,该函数检查进程是否具有免疫性。通常情况下,这要归功于 oom_score_adj = -1000。在这种情况下,任务获得零分。
否则,任务的RAM、虚拟内存和交换空间大小被加总。然后,将结果除以总可用内存。最后,函数将比率归一化为1000。
此时,oom_score_adj 开始发挥作用。因此,它会添加到得分中。因此,其负值实际上会减少此值。
现在我们应该意识到,如果对我们来说进程很重要,我们需要自己确保其生存能力。因此,我们应该适当调整其 oom_score_adj 参数
补充([原文出处]
在 David 的补丁集中,旧的 badness() 启发法几乎完全消失了。 相反,计算变成了一个简单的问题:进程正在使用多少百分比的available memory(可用内存)。 如果系统整体内存不足,那么“available memory”就是系统可用的所有 RAM 和swap空间的总和。 相反,如果 OOM 情况是由于耗尽给定 cpuset/控制组(cgroup)允许的内存而引起的,则“available memory”是分配给该控制组的总量。 如果超出内存策略施加的限制,则会进行类似的计算。 在每种情况下,进程的内存使用量都被视为其 resident set(驻留集,正在使用的 RAM 页数)与其swap使用量的总和。
该计算产生一个百分比乘以十的数字; 使用available memory的每个字节的进程将获得 1000 分,而根本不使用内存的进程将获得零分。 对这个分数的heuristic (启发式)调整很少,但代码仍然从 root-ownered(root 拥有)的进程的分数中减去少量 (30),因为它们比user-owned (用户拥有)的进程稍微更有价值。
另一项应用的调整是添加存储在每个进程的 oom_score_adj 变量中的值,该变量可以通过 /proc 进行调整。 该变量允许调整每个进程对用户空间中 OOM killer的吸引力; 将其设置为 -1000 将完全禁用 OOM 终止,而设置为 +1000 相当于在关联进程上绘制一个大目标。
5. 用于控制overcommit的系统范围参数
我们可以通过设置overcommit_memory参数来改变Linux系统的overcommit’(过度使用)策略。 该参数位于 /proc/sys/vm/overcommit_memory 文件中,采用:
- 0 允许适度overcommit(过度使用)。 但是,不合理的内存分配将会失败。 这是默认设置
- 1 总是overcommit(过度使用)
- 2 不允许overcommit(过度使用)。 进程通常不会被 OOM Killer 终止,但内存分配尝试可能会返回错误
我们应该意识到,使用默认策略以外的策略对应用程序处理内存的方式提出了更高的要求
6.演示
现在让我们展示一下 OOM 杀手是如何工作的。 因此,让我们模拟一下内存消耗较高的进程。 然而,他们不应该单独耗尽系统的功能。 然后,只有启动下一个进程才会被检测为内存不足威胁。
那么,让我们使用test脚本来吃掉内存:
!/bin/bash
for x in {0..6999999}
do
y=$x$y
done
我们注意到该脚本分配了大量内存,但没有任何尖峰需求。 因此,其内存使用水平很快。
该演示在 Ubuntu 20.04 LTS 上进行,内核为 5.4.0-58-generic,内存约为 4 GB,交换空间为 4 GB。 通常,系统最多可以维持三个测试应用程序同时运行。 然后,启动第四个实例唤醒 OOM killer。
6.1. 记录进程的 oom_score
由于进程的 oom_score 不会出现在内核日志中,因此我们需要安装一个基于 cron 的简单记录器。 因此,让我们使用 oom_score_reader 记录每分钟得分最高的五个进程:
crontab -l
some output skipped
OOMTEST=/home/joe/prj/oom
*/1 * * * * $OOMTEST/./oom_score_reader | tail -n 5<br /> >> $OOMTEST/oom_score.log && echo "-------------" >> $OOMTEST/oom_score.log
6.2. 追踪 OOM killer
由于我们已经在终端中开始了任务,因此在某个时候,我们将在其中一个终端中获取一条消息:
./test
Killed
我们在kern.log中查找对应的事件:
$ grep -ia "Killed process" kern.log
Jul 7 18:40:56 virtual kernel: [ 7269.971178] Out of memory: Killed process 20257 (test) total-vm:1980996kB,<br /> anon-rss:21128kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:3920kB oom_score_adj:0
然后让我们获取有关 PID 20257 的更多信息:
Jul 7 18:40:56 virtual kernel: [ 7269.971162] oom-kill:constraint=CONSTRAINT_NONE,<br /> nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice/user-1000.slice/user@1000.service,task=test,pid=20257,uid=1000
Jul 7 18:40:56 virtual kernel: [ 7269.971178] Out of memory: Killed process 20257 (test) total-vm:1980996kB,<br /> anon-rss:21128kB, file-rss:0kB, shmem-rss:0kB, UID:1000 pgtables:3920kB oom_score_adj:0
Jul 7 18:40:56 virtual kernel: [ 7270.002859] oom_reaper: reaped process 20257 (test),<br /> now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
这里,oom-reaper是一个回收内存的辅助进程。 最后,我们可以在日志文件中找到进程的最后一个 oom_score:
1537 13 gnome-shell
2361 24 gnome-software
20256 235 test
20257 235 test
24214 236 test
让我们看看20257的最后得分235是记录时的第二大得分。 然而,这是因为 cron 的粒度高达 1 分钟。
7. 结论
在本教程中,我们了解了 Linux 管理内存的方法。 首先,我们研究了overcommit策略,它允许任何合理的内存分配。 然后我们遇到了OOM killer,这个进程在内存不足的情况下守护系统的稳定性。
接下来,我们通过内存使用情况查看进程的评分,并了解如何保护它们免受 OOM killer的攻击。 此外,我们还查看了系统范围的overcommit设置。
最后,我们提供了 OOM killer如何工作的示例。