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

  • linux内存管理相关知识
    • 1. 进程的内存申请与分配
    • 2. 当前系统总内存的统计
  • linux内存分析
  • linux内存泄漏相关知识
    • 内存泄露的分类
  • valgrind 内存泄漏分析工具
  • valgrind 将内存泄漏分为 4 类
    • 明确泄漏
    • 间接泄漏
    • 可能泄漏
    • 仍可访达
    • 其他的内存错误使用
    • 1. 非法读/写内存(Illegal read / Illegal write errors)
    • 2. 使用未初始化的变量(Use of uninitialised values)
    • 3.系统调用传递不可访问或未初始化内存(Use of uninitialised or unaddressable values in system calls)
    • 4. 非法释放(Illegal frees)
    • 5. 不对应的内存申请和释放(When a heap block is freed with an inappropriate deallocation function)
    • 6. 源地址和目的地址重叠(Overlapping source and destination blocks)
    • 7. 内存申请可疑大小(Fishy argument values)

这篇文章主要是,收集归纳总结关于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)

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

间接泄漏

间接泄漏就是指针并不直接丢失,但保存指针的内存地址丢失了。比较拗口,咱们看个例子:

// 间接泄露举例:
struct list {struct list *next;
};int main(int argc, char **argv)
{struct list *root;root = (struct list *)malloc(sizeof(struct list));root->next = (struct list *)malloc(sizeof(struct list));printf("root %p roop->next %p\n", root, root->next);root = NULL;return 0;
}

丢失的是 root 指针,导致 root 存储的 next 指针成为了间接泄漏。

valgrind 检查会打印如下日志:

//valgrind 检查会打印如下日志::
# valgrind --tool=memcheck --leak-check=full --show-reachable=yes /data/demo-c
==10435== Memcheck, a memory error detector
==10435== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==10435== Using Valgrind-3.17.0 and LibVEX; rerun with -h for copyright info
==10435== Command: /data/demo-c
==10435==
root 0x4a33040 roop->next 0x4a33090
==10435==
==10435== HEAP SUMMARY:
==10435==     in use at exit: 16 bytes in 2 blocks
==10435==   total heap usage: 3 allocs, 1 frees, 1,040 bytes allocated
==10435==
==10435== 8 bytes in 1 blocks are indirectly lost in loss record 1 of 2
==10435==    at 0x4845084: malloc (vg_replace_malloc.c:380)
==10435==    by 0x4007BF: main (in /data/demo-c)
==10435==
==10435== 16 (8 direct, 8 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
==10435==    at 0x4845084: malloc (vg_replace_malloc.c:380)
==10435==    by 0x4007B3: main (in /data/demo-c)
==10435==
==10435== LEAK SUMMARY:
==10435==    definitely lost: 8 bytes in 1 blocks
==10435==    indirectly lost: 8 bytes in 1 blocks
==10435==      possibly lost: 0 bytes in 0 blocks
==10435==    still reachable: 0 bytes in 0 blocks
==10435==         suppressed: 0 bytes in 0 blocks
==10435==
==10435== For lists of detected and suppressed errors, rerun with: -s
==10435== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

默认情况下,只会打印 明确泄漏 和 可能泄漏,如果需要同时打印 间接泄漏,需要加上选项
–show-reachable=yes.

间接泄漏的内存肯定也要修复的,不过一般会随着 明确泄漏 的修复而修复

可能泄漏

valgrind 之所以会怀疑可能泄漏,是因为指针已经偏移,并没有指向内存头,而是有内存偏移,指向内存内部的位置。

有些时候,这并不是泄漏,因为这些程序就是这么设计的,例如为了实现内存对齐,额外申请内存,返回对齐后的内存地址。但更多时候,是我们不小心 p++ 了。
可能泄漏的情况需要我们根据代码情况自己分析确认。

仍可访达

仍可访达 表示在程序退出时,不管是正常退出还是异常退出,内存申请了没释放,都属于仍可访达的泄漏类型。

如果测试的程序是正常退出的,那么这些 仍可访达 的内存就是泄漏,最好修复了。

如果测试是长期运行的程序,通过信号提前终止,那么这些内存就大概率并不是泄漏。

其他的内存错误使用

即使是 memcheck 一个工具,除了检查内存泄漏之外,还支持其他内存错误使用的检查。

  • 1. 非法读/写内存(Illegal read / Illegal write errors)

例如:

Invalid read of size 4at 0x40F6BBCC: (within /usr/lib/libpng.so.2.1.0.9)by 0x40F6B804: (within /usr/lib/libpng.so.2.1.0.9)by 0x40B07FF4: read_png_image(QImageIO *) (kernel/qpngio.cpp:326)by 0x40AC751B: QImageIO::read() (kernel/qimage.cpp:3621)Address 0xBFFFF0E0 is not stack'd, malloc'd or free'd

在你要操作的内存超出边界或者非法地址时,就会有这个错误提示。常见的错误,例如访问数组边界:

int arr[4];
arr[4] = 10;

例如使用已经释放了的内存:

char *p = malloc(30);
...
free(p);
...
p[1] = '\0';

如果发现这样的错误,最好也修复了。因为这些错误大概率会导致段错误

  • 2. 使用未初始化的变量(Use of uninitialised values)

尤其出现在局部变量未赋值,却直接读取的情况。也包括申请了内存,没有赋值却直接读取,虽然这情况会读出 ‘\0’,不会导致异常,但更多时候是异常逻辑。

例如:

int main()
{int x;printf ("x = %d\n", x);
}

如果要详细列出哪里申请的内存未初始化,需要使用参数 --track-origins=yes,但也会让慢很多。

错误显示是这样的:

Conditional jump or move depends on uninitialised value(s)at 0x402DFA94: _IO_vfprintf (_itoa.h:49)by 0x402E8476: _IO_printf (printf.c:36)by 0x8048472: main (tests/manuel1.c:8)
  • 3.系统调用传递不可访问或未初始化内存(Use of uninitialised or unaddressable values in system calls)

    memcheck 工具会检查所有系统调用的参数:
    ----1.参数是否有初始化
    ----2.如果是系统调用读取程序提供的buffer,会产检整个buffer是否可访问和已经初始化
    ----3.如果是系统调用要往用户的buffer写入数据,会检查buffer是否可访问

    错误显示是这样的:

 Syscall param write(buf) points to uninitialised byte(s)at 0x25A48723: __write_nocancel (in /lib/tls/libc-2.3.3.so)by 0x259AFAD3: __libc_start_main (in /lib/tls/libc-2.3.3.so)by 0x8048348: (within /auto/homes/njn25/grind/head4/a.out)Address 0x25AB8028 is 0 bytes inside a block of size 10 alloc'dat 0x259852B0: malloc (vg_replace_malloc.c:130)by 0x80483F1: main (a.c:5)Syscall param exit(error_code) contains uninitialised byte(s)at 0x25A21B44: __GI__exit (in /lib/tls/libc-2.3.3.so)by 0x8048426: main (a.c:8)
Mismatched free() / delete / delete []at 0x40043249: free (vg_clientfuncs.c:171)by 0x4102BB4E: QGArray::~QGArray(void) (tools/qgarray.cpp:149)by 0x4C261C41: PptDoc::~PptDoc(void) (include/qmemarray.h:60)by 0x4C261F0E: PptXml::~PptXml(void) (pptxml.cc:44)Address 0x4BB292A8 is 0 bytes inside a block of size 64 alloc'dat 0x4004318C: operator new[](unsigned int) (vg_clientfuncs.c:152)by 0x4C21BC15: KLaola::readSBStream(int) const (klaola.cc:314)by 0x4C21C155: KLaola::stream(KLaola::OLENode const *) (klaola.cc:416)by 0x4C21788F: OLEFilter::convert(QCString const &) (olefilter.cc:272)
  • 6. 源地址和目的地址重叠(Overlapping source and destination blocks)

    这里的检查只包括类似 memcpy, strcpy, strncpy, strcat, strncat 这样的有源地址和目的地址操作的C库函数,确保源地址和目的地址指针不会重叠。

    错误显示是这样的:

==27492== Source and destination overlap in memcpy(0xbffff294, 0xbffff280, 21)
==27492==    at 0x40026CDC: memcpy (mc_replace_strmem.c:71)
==27492==    by 0x804865A: main (overlap.c:40)
  • 7. 内存申请可疑大小(Fishy argument values)

    这个问题往往出现在申请的内存大小是负数。因为申请大小往往是非负数和不会大的很夸张,但如果传递了个负数,直接导致申请大小解析为一个非常大的正数。

    错误显示是这样的:

==32233== Argument 'size' of function malloc has a fishy (possibly negative) value: -3
==32233==    at 0x4C2CFA7: malloc (vg_replace_malloc.c:298)
==32233==    by 0x400555: foo (fishy.c:15)
==32233==    by 0x400583: main (fishy.c:23)

memcheck 工具的支持的错误类型可看官方文档:https://www.valgrind.org/docs/manual/mc-manual.html#mc-manual.errormsgs

参考链接:
1.cnblogs.com/gmpy/p/14778243.html
2.https://zhuanlan.zhihu.com/p/149870449
3.https://zhuanlan.zhihu.com/p/368370152
4.https://blog.51cto.com/u_15335146/3501468
5.https://www.cnblogs.com/vinozly/p/5489138.html

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

  1. linux内存管理分析 二,linux内存管理分析【二】

    为建立内存管理系统,在内核初始化过程中调用了下面几个函数: init/main.c asmlinkage void __init start_kernel(void) { ...... 初始化持久映射 ...

  2. Linux内存管理 (透彻分析)

    摘要: 本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法.力求从外到内.水到渠成地引导网友分析Linux的内存管理与使用. ...

  3. Linux内核分析(三)----初识linux内存管理子系统

    原文:Linux内核分析(三)----初识linux内存管理子系统 Linux内核分析(三) 昨天我们对内核模块进行了简单的分析,今天为了让我们今后的分析没有太多障碍,我们今天先简单的分析一下linu ...

  4. Linux内存管理 brk(),mmap()系统调用源码分析2:brk()的内存释放流程

    Linux brk(),mmap()系统调用源码分析 brk()的内存释放流程 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.com/Rt ...

  5. Linux内存管理 brk(),mmap()系统调用源码分析1:基础部分

    Linux内存管理 brk(),mmap(),munmap()系统调用源码分析 基础部分 荣涛 2021年4月30日 内核版本:linux-5.10.13 注释版代码:https://github.c ...

  6. sys_brk分析 linux1.2.0版本,linux内存管理之sys_brk实现分析

    分析完linux内存管理的基本概念与实现之后,就可以接着分析用户空间与内核空间的交互操作了.Brk系统调用属于那种常用但是"可见度"不高的操作,常用于用户空间堆的管理(请参阅本站的 ...

  7. Linux内存管理和分析vmalloc使用的地址范围

    From: http://www.cnblogs.com/dubingsky/archive/2010/04/20/1716158.html Vmalloc可以获得的地址在VMALLOC_START到 ...

  8. 一文掌握 Linux 内存管理

    作者:dengxuanshi,腾讯 IEG 后台开发工程师 以下源代码来自 linux-5.10.3 内核代码,主要以 x86-32 为例. Linux 内存管理是一个很复杂的"工程&quo ...

  9. 浅谈Linux内存管理那些事儿

    linux相关视频解析: 5个方面分析linux内核架构,让你对内核不再陌生 手把手带你实现一个Linux内核文件系统 1 前言 内存管理是Linux内核中非常重要的部分,今天和大家一起学习一下. 当 ...

最新文章

  1. Qt简介、安装及在Ubuntu14.04 32位上简单使用举例
  2. linux lvm 系统快照,利用Linux LVM进行快照备份
  3. 根据mysql数据库日志恢复删除数据
  4. 让你的程序只能运行一个实例
  5. 飞鱼科技游戏开发岗面试经验
  6. mysql的root用户密码_MySQL的root用户密码忘了 , 该怎么办?
  7. hazelcast集群配置_使用HazelCast进行Hibernate缓存:基本配置
  8. redux 思考以及源码解析
  9. 自定义托管宿主WCF解决方案开发配置过程详解
  10. 中国内窥镜仪器固定臂市场趋势报告、技术动态创新及市场预测
  11. git的安装和简单使用
  12. crontab实现定时执行脚本重启服务
  13. 计算机网络知识竞赛没有分数,计算机网络知识竞赛.ppt
  14. Coap协议(1)入门简介
  15. java dto 生成_java – 从多个源DTO映射到一个目标
  16. LeetCode刷题-四因数
  17. Quick MTF,镜头图像质量测试应用程序
  18. Centos下搭建个人网站
  19. 一些关于SLG手游的想法
  20. 一种基于Android、iOS系统的移动端车牌识别方法,实现手机拍照识别车牌

热门文章

  1. python实现矩阵横竖斜的和相等_python打印9宫格,25宫格等奇数格,且横竖斜相加和相等...
  2. IIS发布网站404.0问题
  3. iOS 电量消耗改善:一招套路及相关姿势
  4. 一加9系列怎么样?性价比优选成为大众靠谱选择
  5. 基于OpenCL的图像积分图算法改进
  6. 2007年度最有趣味的网站大全
  7. JVM垃圾回收器介绍和对比
  8. UltraEdit v26(补丁工具)
  9. 集成灶哪个品牌性价比高质量好,最全集成灶测评来啦
  10. 谷歌浏览器背景设置为护眼色