linux内存管理、分析、泄露定位与工具整理

这篇文章主要是,收集归纳总结关于linux内存管理与泄露相关的知识,目的是看完相关的内容后,对内存泄露有个初步认识。

linux内存管理相关知识

先从单个进程空间的内存布局与分配,是从全局的视角分析下内核对内存的管理;

1. 进程的内存申请与分配

下图是Linux下32位系统的进程地址空间。

(栈是高地址向低地址增长,堆是向高地址增长,堆栈之间的共享区,主要用来加载动态库)
1、当前执行文件的代码段,该代码段称为text段。
2、执行文件的数据段,主要存储执行文件用到的全局变量,静态变量。(全局和static)
3、存储全局变量和动态产生的数据的堆。(堆)
4、用于保存局部变量和实现函数调用的栈。(栈)
5、采用mmap方式映射到虚拟地址空间中的内存段

2. 当前系统总内存的统计

1、进程占用的总内存可以通过cat maps表计算出来。

2、当系统运行起来以后,会把应用层相关的文件挂载到tmpfs文件系统下,这部分内存是以cache方式统计出来的,但是这部分内存cache无法通过回收策略或者显式的调用释放掉。

3、根文件系统ramdisk占用的内存。

4、当前系统保留内存的大小,可以通过查看/proc/sys/vm/min_free_kbytes来获取或者修改此内存的大小。

5、当然,当系统运行起来后,还应该留有一定的内存用于在硬盘读写时做cache或者网络负荷比较高时分配skb等,一般需要30M以上。

linux内存分析

当我们在终端启动一个程序时,终端进程调用 exec 函数将可执行文件载入内存,此时代码段,数据段,bss 段(未初始化数据段),stack 段都通过 mmap 函数映射到内存空间,堆则要根据是否有在堆上申请内存来决定是否映射。

exec 执行之后,此时并未真正开始执行进程,而是将 cpu 控制权交给了动态链接库装载器,由它来将该进程需要的动态链接库装载进内存。之后才开始进程的执行,这个过程可以通过 strace 命令跟踪进程调用的系统函数来分析。

当第一次调用 malloc 申请内存时,通过系统调用 brk 嵌入到内核,首先会进行一次判断,是否有关于堆的 vma,如果没有,则通过 mmap 匿名映射一块内存给堆,并建立 vma 结构,挂到 mm_struct 描述符上的红黑树和链表上。然后回到用户态,通过内存分配器(ptmalloc,tcmalloc,jemalloc)算法将分配到的内存进行管理,返回给用户所需要的内存。

如果用户态申请大内存时,是直接调用 mmap 分配内存,此时返回给用户态的内存还是虚拟内存,直到第一次访问返回的内存时,才真正进行内存的分配。其实通过 brk 返回的也是虚拟内存,但是经过内存分配器进行切割分配之后(切割就必须访问内存),全都分配到了物理内存.

进程在用户态通过调用 free 释放内存时,如果这块内存是通过 mmap 分配,则调用 munmap 直接返回给系统。否则内存是先返回给内存分配器,然后由内存分配器统一返还给系统,这就是为什么当我们调用 free 回收内存之后,再次访问这块内存时,可能不会报错的原因。

当然,当整个进程退出之后,这个进程占用的内存都会归还给系统。

  1. 内存耗尽之后OOM
    OOM(out of memory)即为系统在内存耗尽时的自我拯救措施,他会选择一个进程,将其杀死,释放出内存。
    OOM 关键文件 oom_kill.c,里面介绍了当内存不够时,系统如何选择最应该被杀死的进程,选择因素有挺多的,除了进程占用的内存外,还有进程运行的时间,进程的优先级,是否为 root 用户进程,子进程个数和占用内存以及用户控制参数 oom_adj 都相关。

当产生 oom 之后,函数 select_bad_process 会遍历所有进程,通过之前提到的那些因素,每个进程都会得到一个 oom_score 分数,分数最高,则被选为杀死的进程。我们可以通过设置 /proc//oom_adj 分数来干预系统选择杀死的进程。

  1. 系统申请的内存都在哪?

对普通进程来说,能看到的其实是内核提供的虚拟内存,这些虚拟内存还需要通过页表,由系统映射为物理内存。物理内存又分为cache和普通物理内存,可以通过 free 命令查看,而且物理内存还有分 DMA,NORMAL,HIGH 三个区,这里主要分析cache和普通内存。
在这里插入图片描述

一个进程的地址空间几乎都是 mmap 函数申请,有文件映射和匿名映射两种。

文件映射:代码段,数据段,动态链接库共享存储段以及用户程序的文件映射段;

代码段动态链接库段是映射到内核cache中,也就是说当执行共享文件映射时,文件是先被读取到 cache 中,然后再映射到用户进程空间中。对于进程空间中的数据段,其必须是私有文件映射,因为如果是共享文件映射,那么同一个可执行文件启动的两个进程,任何一个进程修改数据段,都将影响另一个进程了。

当进行私有文件映射时,首先是将文件映射到 cache 中,然后如果某个文件对这个文件进行修改,则会从其他内存中分配一块内存先将文件数据拷贝至新分配的内存,然后再在新分配的内存上进行修改,这也就是写时复制。这也很好理解,因为如果同一个可执行文件开启多个实例,那么内核先将这个可执行的数据段映射到 cache,然后每个实例如果有修改数据段,则都将分配一个一块内存存储数据段,毕竟数据段也是一个进程私有的。
如果是文件映射,则都是将文件映射到 cache 中,然后根据共享还是私有进行不同的操作。

匿名映射:bss段,堆,以及当 malloc 用 mmap 分配的内存,还有mmap共享内存段

bss 段,堆,栈这些都是匿名映射,因为可执行文件中没有相应的段,而且必须是私有映射,否则如果当前进程 fork 出一个子进程,那么父子进程将会共享这些段,一个修改都会影响到彼此。在进行匿名私有映射时,并没有占用 cache,因为就只有当前进程在使用这块这块内存,没有必要占用宝贵的 cache。
当我们需要在父子进程共享内存时,就可以用到 mmap 共享匿名映射。当进行共享匿名映射时,这时是从 cache 中申请内存,因为父子进程共享这块内存,共享匿名映射存在于 cache,然后每个进程再映射到彼此的虚存空间,这样即可操作的是同一块内存。

linux内存泄漏相关知识

由第二节相关的知识可知,当进程通过 malloc() 申请虚拟内存后,系统并不会立即为其分配物理内存,而是在首次访问时,才通过缺页异常陷入内核中分配内存。为了协调 CPU 与磁盘间的性能差异,Linux 还会使用 Cache 和 Buffer ,分别把文件和磁盘读写的数据缓存到内存中。

对应用程序来说,动态内存的分配和回收,是既核心又复杂的一个逻辑功能模块。管理内存的过程中,也很容易发生各种各样的“事故”。比如,

	没正确回收分配后的内存,导致了泄漏。

	访问的是已分配内存边界外的地址,导致程序异常退出,等等。

那么,内存泄漏到底是怎么发生的,以及发生内存泄漏之后该如何排查和定位?

举个例子,你在程序中定义了一个局部变量,比如一个整数数组 int data[64] ,就定义了一个可以存储 64 个整数的内存段。由于这是一个局部变量,它会从内存空间的栈中分配内存。栈内存由系统自动分配和管理。一旦程序运行超出了这个局部变量的作用域,栈内存就会被系统自动回收,所以不会产生内存泄漏的问题。

再比如,很多时候,我们事先并不知道数据大小,所以你就要用到标准库函数 malloc() 在程序中动态分配内存。这时候,系统就会从内存空间的堆中分配内存。堆内存由应用程序自己来分配和管理。除非程序退出,这些堆内存并不会被系统自动释放,而是需要应用程序明确调用库函数 free() 来释放它们。如果应用程序没有正确释放堆内存,就会造成内存泄漏。

这是两个栈和堆的例子,那么,其他内存段是否也会导致内存泄漏呢?经过我们前面的学习,这个问题并不难回答。

只读段,包括程序的代码和常量,由于是只读的,不会再去分配新的内存,所以也不会产生内存泄漏。
数据段,包括全局变量和静态变量,这些变量在定义时就已经确定了大小,所以也不会产生内存泄漏。
最后一个内存映射段,包括动态链接库和共享内存,其中共享内存由程序动态分配和管理。
所以,如果程序在分配后忘了回收,就会导致跟堆内存类似的泄漏问题。

内存泄漏的危害非常大,这些忘记释放的内存,不仅应用程序自己不能访问,系统也不能把它们再次分配给其他应用。内存泄漏不断累积,甚至会耗尽系统内存。

虽然,系统最终可以通过 OOM (Out of Memory)机制杀死进程,但进程在 OOM 前,可能已经引发了一连串的反应,导致严重的性能问题。比如,其他需要内存的进程,可能无法分配新的内存;内存不足,又会触发系统的缓存回收以及 SWAP 机制,从而进一步导致 I/O 的性能问题等等。

内存泄露的分类

valgrind 内存泄漏分析工具

valgrind是 Linux 业界主流且非常强大的内存泄漏检查工具。在其官网介绍中,内存检查(memcheck)只是其中一个功能。valgrind 这个工具不能用于调试正在运行的程序,因为待分析的程序必须在它特定的环境中运行,它才能分析内存。

valgrind 官网https://www.valgrind.org/

valgrind 将内存泄漏分为 4 类

  • 明确泄漏(definitely lost):内存还没释放,但已经没有指针指向内存,内存已经不可访问
  • 间接泄漏(indirectly lost):泄漏的内存指针保存在明确泄漏的内存中,随着明确泄漏的内存不可访问,导致间接泄漏的内存也不可访问
  • 可能泄漏(possibly lost):指针并不指向内存头地址,而是指向内存内部的位置
  • 仍可访达(still reachable):指针一直存在且指向内存头部,直至程序退出时内存还没释放。

明确泄漏

其实简单来说,就是 内存没释放,但已经没有任何指针指向这片内存,内存地址已经丢失 。

// valgrind 检查到明确泄漏时,会打印类似下面这样的日志:
 ==19182== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
 ==19182== at 0x1B8FF5CD: malloc (vg_replace_malloc.c:130)
 ==19182== by 0x8048385: f (a.c:5)
 ==19182== by 0x80483AB: main (a.c:11)

明确泄漏的内存是强烈建议修复的

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值