先来看代码:这段代码很简单,就是先用mmap为进程分配10GiB的虚拟内存,然后用pagewriting让操作系统为10GiB的虚拟内存,分配对应的物理内存,最后休眠,等待我们的测试。运行:没问题,如我们所料,正常执行。再打开一个终端,执行如下命令,查看其内存使用情况:上图中的VSZ指的是虚拟内存,RSS指的是物理内存,单位是KiB,所以本进程使用的虚拟内存和物理内存,都是大约等于10GiB,没问题。我们再打开一个终端,再次执行这个程序:第二次执行这个程序是没有问题的,但是奇怪的是,第一次执行的程序此时被kill了:这是为什么呢?上面我们说程序的逻辑是分配10GiB的物理内存,所以运行两次,也就是分配20GiB的物理内存。但是在我们的测试机上,总的物理内存只有16GiB,运行两个这样的进程肯定是不够的。当第二次执行程序向操作系统申请物理内存时,操作系统会发现物理内存没有了。这时,为了防止整个系统崩溃,Linux内核会触发OOM/OutofMemory的查杀机制,即按照一定的规则选择一个进程将其杀死,从而回收物理内存,所以从而保证机器整体的稳定运行。同时kill事件也会记录在内核日志中,可以通过dmesg命令等方式查看。比如上面第一个进程被kill掉的事件记录是这样的:看上面红色字体的那一行。这一行意味着进程14134被Linux内核杀死,因为它内存不足。这个过程就是我们上面第一次执行的那个。程序。linux内核的oomkilling机制其实是一种弃车救帅的方法,因为如果我们不杀掉某个进程来释放物理内存,很可能造成后续的系统级崩溃,两害相权最好是Islight,操作系统只能这样处理,归根结底是我们对进程使用的物理内存规划不够,导致出现这种情况。那为什么在第二次执行程序的时候调用mmap分配虚拟内存的时候不直接报错,返回无法分配内存呢?这是因为,经过多年的观察,Linux内核的开发者发现,大部分程序在分配了大量的虚拟内存后,大多数时候,并不会一直使用那么多的物理内存。因此,为了更合理、更高效地使用物理内存资源,linux内核允许虚拟内存overcommit,也就是例如上面执行mmap分配虚拟内存时,linux内核不会严格检查虚拟内存所有正在运行的进程分配的内存加起来,是否超过整个物理内存大小。这也解释了为什么上面第二次运行程序时mmap没有报错。但是mmap的虚拟内存分配虽然成功了,但是当真正使用内存的时候,比如上面的writememory,此时有可能分配物理内存失败,因为虚拟内存的overcommit很可能会导致后续Insufficient物理内存。如果出现这种情况,就会触发linux内核的oomkilling机制,即linux内核中的oomkiller会按照一定的规则选择一个进程进行kill。我们已经在上面证明了这一点。那为什么不杀掉第二个进程,而杀掉第一个呢?这与linux内核中oomkiller的选择策略有关。我们直接看源码:当一个进程请求操作系统为它分配物理内存时,如果这块物理内存没有了,就会触发上图中的out_of_memory函数。在这个函数中,通过select_bad_process选择要杀掉的进程,然后使用oom_kill_process杀掉释放物理内存。在看select_bad_process之前,我们先看oom_kill_process:这个函数调用了__oom_kill_process:在上面的函数中,通过向victim进程发送信号SIGKILL(我们平时使用的kill-9命令就是使用的信号),将其杀死,并且然后kill事件将记录在内核日志中。注意这里记录的日志格式和上面dmesg输出的14134processwaskilledeventlog的格式完全一致。杀死一个进程的过程是这样的。我们看一下select_bad_process函数是如何选择杀死进程的:在这个函数中,会遍历系统中的所有进程,然后使用oom_evaluate_task函数对每个进程进行评估:oom_evaluate_task函数中,oom_badness用于计算过程的坏点。积分越高,越容易被击杀。如果badnesspoint是LONG_MIN的特殊值,则直接跳过进程,即进程不会成为被杀死的对象。如果badnesspoints小于之前选择的process的badnesspoints,则该进程也会被skipped,即kill掉,丢失的processbadnesspoints应该是最大的。遍历中选中的进程,以及它的badnesspoints,会被赋值给oc->chosen和oc->chosen_points,而oc->chosen最终指向的进程就是上面的oom_kill_process中kill掉的进程。再看看badnesspoints是怎么计算的:函数主体的逻辑分为两部分。一方面是,在某些情况下,进程的badness点直接返回给LONG_MIN,即不会被杀死。这些情况包括,oom_score_adj的值是OOM_SCORE_ADJ_MIN,也就是-1000,或者进程已经在被杀进程中,或者进程在vfork进程中。这个函数的另一部分逻辑是计算进程的坏点。大概的计算规则是:points=进程占用的总物理内存+总物理内存*oom_score_adj值的千分之一。oom_score_adj的值是进程唯一的,可以通过写/proc/[pid]/oom_score_adj来调整。取值范围为-1000到1000,值越大,进程总Badness点数越大,进程越容易被杀死。该值越小,进程的总坏点越小,进程被杀死的可能性越小。上面我们也提到了oom_score_adj有一个特殊的值OOM_SCORE_ADJ_MIN,也就是-1000,也就是进程不能kill掉。每个进程的oom_score_adj的值默认为0。综上所述,可以看出linux内核中的oomkiller选择杀死进程的方式取决于每个进程的badnesspoints的大小。默认情况下,由于每个进程的oom_score_adj值为0,进程占用的物理内存越大,badnesspoints越大,越容易被kill。这也就解释了为什么上面的程序第二次执行时,被kill掉的是第一次执行的进程,而不是第二次执行的进程,因为第一次执行的进程时间占用物理内存较大。其实在linux内核中调整oomkiller行为的方法有很多,不仅仅是修改oom_score_adj的值。比如通过修改/proc/sys/vm/panic_on_oom的值,当物理内存不足时,整个系统可以直接panic,而不是选择性的杀掉某个进程。比如修改/proc/sys/vm/overcommit_memory的值,上面第二次执行的测试程序在使用mmap分配虚拟内存时可以直接报错说内存不够。比如修改/proc/[pid]/oom_adj的值,也可以达到修改/proc/[pid]/oom_score_adj的目的,但是2.6.36内核版本之后不推荐这样做。oomkiller行为调整相关的参数,详细解释可以参考proc的man文档:https://man.archlinux.org/man/proc.5说了这么多,明白了oomkiller的机制linux内核对我们来说很重要,对应用有什么帮助?让我们假设以下场景:假设我们有一台机器运行着一个非常重要的服务,比如一个数据库,或者一个应用程序进程。它非常占用内存,但一般情况下,它使用的物理内存肯定不会高于实际的总物理内存大小。有一天我们需要在这台机器上执行一项任务。如果这个任务消耗的内存比较多,很可能是整台机器的物理内存在执行这个任务的时候完全不够用。这时候就会触发linux内核的oomkilling机制。并且由于不调整oom_score_adj的值,linux内核中的oomkiller默认会杀掉占用物理内存最多的进程。一般来说就是我们的数据库进程或者其他应用进程。假设这个进程是一个重要的在线服务,如果它被杀死了,想想这将是一个多么严重的事故。如何避免?此时,我们可以使用上面提到的oom_score_adj参数来调整进程坏点。例如,我们可以使用echo-1000>/proc/[pid]/oom_score_adj命令,将oom_score_adj的值设置为-1000,即进程不能被杀死。再比如,通过上面的echo命令将oom_score_adj的值改小一点,降低被kill的概率。然而,这些方法都不是完美的解决方案。虽然机器上的这个重要服务没有被杀掉,但是操作系统还是会杀掉其他各种进程,以保证整个系统不至于崩溃。如果那些流程不重要,那还好,但如果重要的话,还是会很认??真的。甚至,如果操作系统找不到可以杀死的进程,整个系统就会崩溃,这就更严重了。因此,最好的办法就是人为地避免物理内存不足的情况。在机器上运行各种程序时,需要提前规划和预测整个物理内存的使用情况。最好预留一些内存,以防各种误操作。好了,本文就来聊聊这些内容。如果以后找到你的进程,它会莫名其妙地消失。你可以通过dmesg等方式查看内核日志,判断你的进程是否被oom杀死了。本文转载自微信公众号“猫食猫客”,可通过以下二维码关注。转载本文请联系猫猫猫客公众号。
