从上图可以看出,进程在退出的时候,会把它建立的映射都给解除掉。换句话说,进程退出时,会把它申请的内存都给释放掉,这个内存泄漏就是没危害的。不过话说回来,虽然这样没有什么危害,但是我们最好还是要在程序里加上free §,这才是符合编程规范的。我们修改一下这个程序,加上free§,再次编译后通过valgrind来检查,就会发现不存在任何内存泄漏了:
```
$ valgrind --leak-check=full ./a.out
==20123== HEAP SUMMARY:
==20123== in use at exit: 0 bytes in 0 blocks
==20123== total heap usage: 1 allocs, 1 frees, 1,073,741,824 bytes allocated
==20123==
==20123== All heap blocks were freed -- no leaks are possible
```
总之,如果进程不是长时间运行,那么即使存在内存泄漏(比如这个例子中的只有malloc没有free),它的危害也不大,因为进程退出时,内核会把进程申请的内存都给释放掉。
我们前面举的这个例子是对应用程序无害的内存泄漏,我们继续来看下哪些内存泄漏会给应用程序产生危害 。我们同样以malloc为例,看一个简单的示例程序:
```
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SIZE (1024 * 1024 * 1024) /* 1G */
void process_memory()
{
char *p;
p = malloc(SIZE);
if (!p)
return;
memset(p, 1, SIZE);
/* Forget to free this memory */
}
/* 处理其他事务,为了简便起见,我们就以sleep为例 */
void process_others()
{
sleep(1);
}
int main()
{
/* 这部分内存只处理一次,以后再也不会用到 */
process_memory();
/* 进程会长时间运行 */
while (1) {
process_others();
}
return 0;
```
这是一个长时间运行的程序,process_memory()中我们申请了1G的内存去使用,然后就再也不用它了,由于这部分内存不会再被利用,这就造成了内存的浪费,如果这样的程序多了,被泄漏出去的内存就会越来越多,然后系统中的可用内存就会越来越少。
对于后台服务型的业务而言,基本上都是需要长时间运行的程序,所以后台服务的内存泄漏会给系统造成实际的危害。那么,究竟会带来什么样的危害,我们又该如何去应对呢?
## 如何预防内存泄漏导致的危害?
我们还是以上面这个malloc()程序为例,在这个例子中,它只是申请了1G的内存,如果说持续不断地申请内存而不释放,你会发现,很快系统内存就会被耗尽,进而触发OOM killer去杀进程。这个信息可以通过dmesg(该命令是用来查看内核日志的)这个命令来查看:
```
$ dmesg
[944835.029319] a.out invoked oom-killer: gfp_mask=0x100dca(GFP_HIGHUSER_MOVABLE|__GFP_ZERO), order=0, oom_score_adj=0
[...]
[944835.052448] Out of memory: Killed process 1426 (a.out) total-vm:8392864kB, anon-rss:7551936kB, file-rss:4kB, shmem-rss:0kB, UID:0 pgtables:14832kB oom_score_adj:0
```
系统内存不足时会唤醒OOM killer来选择一个进程给杀掉,在我们这个例子中它杀掉了这个正在内存泄漏的程序,该进程被杀掉后,整个系统也就变得安全了。但是你要注意,**OOM killer选择进程是有策略的,它未必一定会杀掉正在内存泄漏的进程,很有可能是一个无辜的进程被杀掉。**而且,OOM本身也会带来一些副作用。
我来说一个发生在生产环境中的实际案例,这个案例我也曾经[反馈给Linux内核社区来做改进](https://lore.kernel.org/linux-mm/1586597774-6831-1-git-send-email-laoar.shao@gmail.com/),接下来我们详细说一下它。
这个案例跟OOM日志有关,OOM日志可以理解为是一个单生产者多消费者的模型,如下图所示:
这个单生产者多消费者模型,其实是由OOM killer打印日志(OOM info)时所使用的printk(类似于userspace的printf)机制来决定的。printk会检查这些日志需要输出给哪些消费者,比如写入到内核缓冲区(kernel buffer),然后通过dmesg命令来查看;我们通常也都会配置rsyslog,然后rsyslogd会将内核缓冲区的内容给转储到日志文件(/var/log/messages)中;服务器也可能会连着一些控制台(console ),比如串口,这些日志也会输出到这些console。
问题就出在console这里,如果console的速率很慢,输出太多日志会非常消耗时间,而当时我们配置了“console=ttyS1,19200”,即波特率为19200的串口,这是个很低速率的串口。一个完整的OOM info需要约10s才能打印完,这在系统内存紧张时就会成为一个瓶颈点,为什么会是瓶颈点呢?答案如下图所示:
进程A在申请内存失败后会触发OOM,在发生OOM的时候会打印很多很多日志(这些日志是为了方便分析为什么OOM会发生),然后会选择一个合适的进程来杀掉,从而释放出来空闲的内存,这些空闲的内存就可以满足后续内存申请了。
如果这个OOM的过程耗时很长(即打印到slow console所需的时间太长,如上图红色部分所示),其他进程(进程B)也在此时申请内存,也会申请失败,于是进程B同样也会触发OOM来尝试释放内存,而OOM这里又有一个全局锁(oom_lock)来进行保护,进程B尝试获取(trylock)这个锁的时候会失败,就只能再次重试。
如果此时系统中有很多进程都在申请内存,那么这些申请内存的进程都会被阻塞在这里,这就形成了一个恶性循环,甚至会引发系统长时间无响应(假死)。
针对这个问题,我与Linux内核内存子系统的维护者Michal Hocko以及OOM子模块的活跃开发者Tetsuo Handa进行了[一些讨论](https://lore.kernel.org/linux-mm/1586597774-6831-1-git-send-email-laoar.shao@gmail.com/),不过我们并没有讨论出一个完美的解决方案,目前仍然是只有一些规避措施,如下:
**在发生OOM时尽可能少地打印信息**
通过将[vm.oom_dump_tasks](https://www.kernel.org/doc/Documentation/sysctl/vm.txt)调整为0,可以不去备份(dump)当前系统中所有可被kill的进程信息,如果系统中有很多进程,这些信息的打印可能会非常消耗时间。在我们这个案例里,这部分耗时约为6s多,占OOM整体耗时10s的一多半,所以减少这部分的打印能够缓解这个问题。
**调整串口打印级别,不将OOM信息打印到串口**
通过调整[/proc/sys/kernel/printk](https://www.kernel.org/doc/Documentation/sysctl/kernel.txt)可以做到避免将OOM信息输出到串口,我们通过设置console_loglevel来将它的级别设置的比OOM日志级别(为4)小,就可以避免OOM的信息打印到console,比如将它设置为3: