一个测试OOM killer的程序未触发OOM所带来的问题

时间:2024-03-06 19:52:56

概述

我们知道,由于MMU实现了虚拟地址到物理地址的转换,所以我们在申请虚拟地址时往往可以申请一大块内存,这实际上是对资源的有效利用,毕竟只有内存真正被投入使用时(如memset)才会实际分配物理内存,虚拟内存需要物理内存作为支撑,当分配了太多虚拟内存,导致物理内存不够时,就发生了Out Of Memory。

介绍下overcommit-memory机制

Linux 内核支持以下内存过度承诺处理模式:

vm.overcommit_memory = 0
启发式内存过度承诺处理。显而易见的地址空间过度承诺会被拒绝。适用于典型系统。它确保严重的过度承诺会失败,同时允许过度承诺减少交换使用量。在此模式下,root 用户可以分配略多一些内存。这是默认设置。(太明显的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存,会上报给应用程序申请失败

vm.overcommit_memory = 1
总是过度承诺。适用于某些科学应用程序。经典示例是使用稀疏数组的代码,并依赖几乎完全由零页面组成的虚拟内存。(malloc申请内存可以被申请到,但是内存耗光会触发oom killer

vm.overcommit_memory = 2
不过度承诺。系统的总地址空间承诺不得超过交换空间加上可配置数量(默认为物理内存的 50%)。根据您使用的数量,在大多数情况下,这意味着进程在访问页面时不会被杀死,但在内存分配时会适当地收到错误信息。(通过overcommit_ratio,overcommit_kbytes可以修改这个比率,来限制可以申请的内存)

适用于希望保证其内存分配将来可用而无需初始化每个页面的应用程序。
内存过度承诺策略通过 sysctl 的 vm.overcommit_memory 来设置。

过度承诺数量可以通过 vm.overcommit_ratio(百分比)或 vm.overcommit_kbytes(绝对值)来设置。

当前的过度承诺限制和已承诺的数量可以通过 /proc/meminfo 中的 CommitLimit 和 Committed_AS 查看。

接下来举个例子:
当vm.overcommit_memory = 2时;

//系统默认情况如下:
# free
              total        used        free      shared  buff/cache   available
Mem:         196732       33028      141076          60       22628      156752
Swap:             0           0           0

cat /proc/meminfo | grep ommit
CommitLimit:       98364 kB
Committed_AS:       5832 kB

# cat /proc/sys/vm/overcommit_ratio
50
# cat /proc/sys/vm/overcommit_memory
0
# cat /proc/sys/vm/overcommit_kbytes
0

//cat test_50M.c 申请50M空间的测试程序
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
        long *p = NULL;
        while(1) {
                p = (long *)malloc(0x3200000);
                if(!p){
                        printf("p is null\n");
                        return 0;
                }
                memset(p, 0x55, 0x3200000);
                printf("50M success\n");
                usleep(10000);
        }
        return 0;
}
//执行,结果发现,申请第二个50M的时候申请失败
./test_50M
50M success
p is null

/ # echo 30 > /proc/sys/vm/overcommit_ratio
/ # cat /proc/meminfo | grep ommit
CommitLimit:       59016 kB
Committed_AS:       5432 kB
//上述操作可以看到过度承诺的内存限制变化了,变小了

/ # echo 70 > /proc/sys/vm/overcommit_ratio
/ # cat /proc/meminfo | grep ommit
CommitLimit:      137712 kB
Committed_AS:       5408 kB
/ # ./test_50M
50M success
50M success
p is null
//上述操作可以看到申请第二个50M的时候申请成功了,第三个失败了


介绍下panic_on_oom

该选项用于启用或禁用内存耗尽时的系统紧急处理功能。

  • 若设置为 0,内核将终止某些恶意进程,称为 oom_killer。通常情况下,oom_killer 能够终止恶意进程,系统将能够继续运行。
  • 若设置为 1,当内存耗尽发生时,内核会发生紧急情况。然而,如果一个进程受到内存策略/内存掩码的限制,并且这些节点出现内存耗尽状态,oom-killer 可能会终止一个进程。在这种情况下不会发生紧急情况。因为其他节点的内存可能是空闲的。这意味着系统整体状态可能还未达到灾难性程度。
  • 若设置为 2,即使在上述情况下也会强制发生内核紧急情况。即使在内存控制组下发生内存耗尽,整个系统也会发生紧急情况。

系统默认值为 0。

1 和 2 是用于集群容错的故障切换。请根据您的故障切换策略选择其中一种。
panic_on_oom=2+kdump 可以提供非常强大的工具来调查内存耗尽的原因。您可以获得快照。

介绍下oom_kill_allocating_task

该选项用于启用或禁用在内存耗尽情况下终止触发OOM的任务。

  • 若设置为零,OOM killer 将扫描整个任务列表,并根据启发式选择一个任务进行终止。通常情况下,它会选择一个占用大量内存的恶意任务,在被终止时释放大量内存。
  • 若设置为非零值,OOM killer 将直接终止触发内存耗尽条件的任务。这避免了昂贵的任务列表扫描操作。

如果选择了 panic_on_oom,它将优先于 oom_kill_allocating_task 中使用的任何值。

默认值为 0。

总结panic_on_oom和oom_kill_allocating_task关系

当系统发生OOM的时候,根据panic_on_oom配置,走系统奔溃还是杀进程
panic_on_oom=0:杀进程,此时根据oom_kill_allocating_task的配置选择进程赴死

  • oom_kill_allocating_task=0,扫描所有进程,根据算法对进程打分,分高者赴死,此时可以通过oom_score_adj选项控制进程oom_score,手动干预算法。
    早期选项(已失效):文件在/proc//oom_adj。范围是[-17 ~ 15],数值越大表示越容易被oom
    killer杀死。如果进程的oom_adj配置为-17,表示进程禁止被OOM killer杀死。
    现在选项:文件在/proc//oom_score_adj。范围是[-1000 ~ 1000],数值越大表示越容易被oom
    killer杀死。oom_score_adj=-1000,表示完全禁止进程被oom杀死。
  • oom_kill_allocating_task非0,直接杀死触发OOM的进程;

如果panic_on_oom的配置不为0,那么oom_kill_allocating_task的配置就没什么用了;

======================================================================
说了这么多我们还没有进入正题,接下来我们说下标题的问题

问题描述

测试给了一段下面的代码,说为什么没有报OOM killer

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
        long *p = NULL;
        while(1) {
                p = (long *)malloc(0x3200000);
                usleep(10000);
        }
        return 0;
}

分析如下,我们发现内存并没有被真正的使用,印证了文章开头的一句话”只有内存真正被投入使用时(如memset)才会实际分配物理内存

/ # free
              total        used        free      shared  buff/cache   available
Mem:         196732       33084      143756          60       19892      156700
Swap:             0           0           0
/ #
/ # ./test &
/ # free
              total        used        free      shared  buff/cache   available
Mem:         196732       33676      143156          60       19900      156108
Swap:             0           0           0

修改代码为

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
        long *p = NULL;
        while(1) {
                p = (long *)malloc(0x3200000);
                memset(p, 0x55, 0x3200000);
                usleep(10000);
        }
        return 0;
}

执行如下,出现了Segmentation fault,有经验的工程师就会知道可能是malloc失败,p=NULL了,memset操作空指针了,导致的Segmentation fault (core dumped)。

./test
Segmentation fault (core dumped)

在这里再转移下注意力,linux内核的core dumped如何生成和分析的,可以参考如下帖子
https://zhuanlan.zhihu.com/p/582051703
https://zhuanlan.zhihu.com/p/661430797

我们再改下代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main()
{
	long *p = NULL;
	while(1) {
		p = (long *)malloc(0x3200000);
		if(!p){
			printf("p is null\n");
			return 0;
		}
		memset(p, 0x55, 0x3200000);
		usleep(10000);
	}
	return 0;
}

执行结果是分配内存失败,并没有触发OOM Killer
```c
# ./test
p is null
/ # free
              total        used        free      shared  buff/cache   available
Mem:         196732       33668      155988          72        7076      156828
Swap:             0           0           0

# 介绍min_free_kbytes 
min_free_kbytes 是 Linux 内核参数,用于强制系统保持一定数量的空闲内存(以千字节为单位)。系统会根据这个数值为系统中每个低内存区域计算一个水印值(watermark)。每个低内存区域会根据其大小比例获得一定数量的保留空闲页。

一定量的内存是需要用于满足 PF_MEMALLOC 分配需求的;如果将 min_free_kbytes 设置得低于 1024KB,系统可能会出现隐性问题,并在高负载下容易发生死锁。

而将 min_free_kbytes 设置得过高可能会导致系统立即发生 Out Of Memory(OOM)情况。

因此,在调整 min_free_kbytes 参数时,需要谨慎设置,避免设置过低导致系统问题,同时也要注意不要设置得过高导致系统内存不足。根据系统的实际情况和需求来合理配置这个参数,以确保系统的稳定性和性能表现。

```c
 # cat /proc/sys/vm/min_free_kbytes
1771

我们按照1M内存分配,改下代码,执行发现可以触发oom killer

/ # ./test
[ 2991.645179] [King]: out_of_memory, oom_killer_disabled:0
[ 2991.650513] CPU: 1 PID: 1618 Comm: test Tainted: G           O      4.19.125 #1
[ 2991.657822] Hardware name: axera,ax620e (DT)
[ 2991.662091] Call trace:
[ 2991.664547]  dump_backtrace+0x0/0x120
[ 2991.668212]  show_stack+0x14/0x20
[ 2991.671529]  dump_stack+0x98/0xbc
[ 2991.674845]  out_of_memory+0x3c/0x350
[ 2991.678509]  __alloc_pages_nodemask+0x7cc/0x928
[ 2991.683043]  __handle_mm_fault+0x5d4/0x1058
[ 2991.687227]  handle_mm_fault+0x70/0xb8
[ 2991.690979]  do_page_fault+0x178/0x488
[ 2991.694729]  do_translation_fault+0x44/0x4c
[ 2991.698913]  do_mem_abort+0x3c/0xc8
[ 2991.702402]  el0_da+0x20/0x24
[ 2991.705448] test invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), nodemask=(null), order=0, oom_score_adj=0
[ 2991.716299] CPU: 1 PID: 1618 Comm: test Tainted: G           O      4.19.125 #1
[ 2991.723619] Hardware name: axera,ax620e (DT)
[ 2991.727899] Call trace:
[ 2991.730369]  dump_backtrace+0x0/0x120
[ 2991.734048]  show_stack+0x14/0x20
[ 2991.737380]  dump_stack+0x98/0xbc
[ 2991.740708]  dump_header.isra.0+0x54/0x1f0
[ 2991.744815]  oom_kill_process+0xa8/0x484
[ 2991.748751]  out_of_memory+0x328/0x350
[ 2991.752511]  __alloc_pages_nodemask+0x7cc/0x928
[ 2991.757057]  __handle_mm_fault+0x5d4/0x1058
[ 2991.761251]  handle_mm_fault+0x70/0xb8
[ 2991.765014]  do_page_fault+0x178/0x488
[ 2991.768775]  do_translation_fault+0x44/0x4c
[ 2991.772970]  do_mem_abort+0x3c/0xc8
[ 2991.776471]  el0_da+0x20/0x24
[ 2991.779487] Mem-Info:
[ 2991.781777] active_anon:39528 inactive_anon:9 isolated_anon:0
[ 2991.781777]  active_file:32 inactive_file:48 isolated_file:0
[ 2991.781777]  unevictable:0 dirty:2 writeback:0 unstable:0
[ 2991.781777]  slab_reclaimable:1076 slab_unreclaimable:4176
[ 2991.781777]  mapped:19 shmem:18 pagetables:135 bounce:0
[ 2991.781777]  free:467 free_pcp:53 free_cma:0
[ 2991.813537] Node 0 active_anon:158112kB inactive_anon:36kB active_file:140kB inactive_file:212kB unevictable:0kB isolated(anon):0kB isolated(file):0kB mapped:148kB dirty:8kB writeback:0kB shmem:72kB shmem_thp: 0kB shmem_pmdmapped: 0kB anon_thp: 0kB writeback_tmp:0kB unstable:0kB all_unreclaimable? no
[ 2991.840178] Normal free:1868kB min:1768kB low:2208kB high:2648kB active_anon:158108kB inactive_anon:36kB active_file:224kB inactive_file:212kB unevictable:0kB writepending:8kB present:229108kB managed:196732kB mlocked:0kB kernel_stack:1344kB pagetables:540kB bounce:0kB free_pcp:148kB local_pcp:0kB free_cma:0kB
[ 2991.867702] lowmem_reserve[]: 0 0
[ 2991.871038] Normal: 82*4kB (UMEH) 39*8kB (UMEH) 12*16kB (UMEH) 11*32kB (UMEH) 3*64kB (MH) 2*128kB (H) 1*256kB (H) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 1888kB
[ 2991.885166] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
[ 2991.893615] 100 total pagecache pages
[ 2991.897320] 57277 pages RAM
[ 2991.900125] 0 pages HighMem/MovableOnly
[ 2991.904011] 8094 pages reserved
[ 2991.907212] 0 pages hwpoisoned
[ 2991.910407] Tasks state (memory values in pages):
[ 2991.915195] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[ 2991.923891] [   1345]     0  1345      930       42    40960        0             0 syslogd
[ 2991.932308] [   1349]     0  1349      930       50    40960        0             0 klogd
[ 2991.941037] [   1369]     0  1369      631       31    45056        0             0 tee-supplicant
[ 2991.950091] [   1390]     0  1390      930       39    45056        0             0 crond
[ 2991.958355] [   1398]     0  1398     1721      139    53248        0         -1000 sshd
[ 2991.966529] [   1403]     0  1403      930       27    40960        0             0 telnetd
[ 2991.974959] [   1405]     0  1405      930       27    45056        0             0 ifplugd
[ 2991.983384] [   1443]     0  1443      506       32    40960        0             0 axsyslogd
[ 2991.992003] [   1447]     0  1447      506       31    40960        0             0 axklogd
[ 2992.001507] [   1449]     0  1449      930       41    40960        0             0 sh
[ 2992.009459] [   1569]     0  1569      930       30    40960        0             0 getty
[ 2992.017646] [   1618]     0  1618    39533    39059   352256        0             0 test
[ 2992.025752] Out of memory: Kill process 1618 (test) score 795 or sacrifice child
[ 2992.033157] Killed process 1618 (test) total-vm:158132kB, anon-rss:156232kB, file-rss:4kB, shmem-rss:0kB
[ 2992.065409] oom_reaper: reaped process 1618 (test), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB

总结,当前内存剩余余量小于min_free_kbytes时就会触发OOM Killer,我们通过malloc 1M内存(相对小颗粒度)可以把内存耗到1771KB以下;