2万字|30张图带你领略glibc内存管理精髓(因为OOM导致了上千万损失)

本文详细解读了glibc内存管理的精髓,包括内存布局、glibc内存管理策略、malloc和free的工作机制,以及针对项目中的OOM问题的解决方案,适合C/C++开发者提升内存管理技能。

由于此文涉及内容较多,且malloc和free的流程图太大,导致被压缩,需要本文pdf或者高清 原图的,请关注公众号【高性能架构探索】,也可以后台回复【pdf】,获取计算机必备经典书籍

  

前言

大家好,我是雨乐。

5年前,在上家公司的时候,因为进程OOM造成了上千万的损失,当时用了一个月的时间来分析glibc源码,最终将问题彻底解决。

最近在逛知乎的时候,发现不少人有对malloc/free有类似的疑惑,恰好自己有阅读过这方面的源码,所以将之前的源码阅读笔记整理了下,用了大概3周的时间写了这篇文章,分析glibc的内存管理精髓,相信对c/c++从业者都会有用。

提纲

主要内容

1 写在前面

源码分析本身就很枯燥乏味,尤其是要将其写成通俗易懂的文章,更是难上加难。

本文尽可能的从读者角度去进行分析,重点写大家关心的点,必要的时候,会贴出部分源码,以加深大家的理解,尽可能的通过本文,让大家理解内存分配释放的本质原理。

接下来的内容,干货满满,对于你我都是一次收获的过程。主要从内存布局、glibc内存管理、malloc实现以及free实现几个点来带你遨游glibc内存管理的实质。最后,针对项目中的问题,指出了解决方案。

2 背景

几年前,在上家公司做了一个项目,暂且称之为SeedService吧。SeedService从kafka获取feed流的转、评、赞信息,加载到内存。因为每天数据不一样,所以kafka的topic也是按天来切分,整个server内部,类似于双指针实现,当天0点释放头一天的数据。

项目上线,一切运行正常。

但是几天之后,进程开始无缘无故的消失。开始定位问题,最终发现是因为内存暴增导致OOM,最终被操作系统kill掉。

弄清楚了进程消失的原因之后,开始着手分析内存泄漏。在解决了几个内存泄露的潜在问题以后,发现系统在高压力高并发环境下长时间运行仍然会发生内存暴增的现象,最终进程因OOM被操作系统杀掉。

由于内存管理不外乎三个层面,用户管理层,C 运行时库层,操作系统层,在操作系统层发现进程的内存暴增,同时又确认了用户管理层没有内存泄露,因此怀疑是 C 运行时库的问题,也就是Glibc 的内存管理方式导致了进程的内存暴增。

问题缩小到glibc的内存管理方面,把下面几个问题弄清楚,才能解决SeedService进程消失的问题:

  • glibc 在什么情况下不会将内存归还给操作系统?

  • glibc 的内存管理方式有哪些约束?适合什么样的内存分配场景?

  • 我们的系统中的内存管理方式是与glibc 的内存管理的约束相悖的?

  • glibc 是如何管理内存的?

带着上面这些问题,大概用了将近一个月的时间分析了glibc运行时库的内存管理代码,今天将当时的笔记整理了出来,希望能够对大家有用。

3 基础

Linux 系统在装载 elf 格式的程序文件时,会调用 loader 把可执行文件中的各个段依次载入到从某一地址开始的空间中。

用户程序可以直接使用系统调用来管理 heap 和mmap 映射区域,但更多的时候程序都是使用 C 语言提供的 malloc()和 free()函数来动态的分配和释放内存。stack区域是唯一不需要映射,用户却可以访问的内存区域,这也是利用堆栈溢出进行攻击的基础。

3.1 进程内存布局

计算机系统分为32位和64位,而32位和64位的进程布局是不一样的,即使是同为32位系统,其布局依赖于内核版本,也是不同的。

在介绍详细的内存布局之前,我们先描述几个概念:

  • 栈区(Stack)— 存储程序执行期间的本地变量和函数的参数,从高地址向低地址生长

  • 堆区(Heap)动态内存分配区域,通过 malloc、new、free 和 delete 等函数管理

  • 未初始化变量区(BSS)— 存储未被初始化的全局变量和静态变量

  • 数据区(Data)— 存储在源代码中有预定义值的全局变量和静态变量

  • 代码区(Text)— 存储只读的程序执行代码,即机器指令

3.1.1 32位进程内存布局

基于内核版本的不同,在32位系统中,进程内的布局也不一样。

3.1.1.1 经典布局

在Linux内核2.6.7以前,进程内存布局如下图所示。

32位默认布局

在该内存布局示例图中,mmap 区域与栈区域相对增长,这意味着堆只有 1GB 的虚拟地址空间可以使用,继续增长就会进入 mmap 映射区域, 这显然不是我们想要的。这是由于 32 模式地址空间限制造成的,所以内核引入了另一种虚拟地址空间的布局形式。但对于 64 位系统,因为提供了巨大的虚拟地址空间,所以64位系统就采用的这种布局方式。

3.1.1.2 默认布局

如上所示,由于经典内存布局具有空间局限性,因此在内核2.6.7以后,就引入了下图这种默认进程布局方式。

32位经典布局

从上图可以看到,栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这种结构便于C运行时库使用 mmap 映射区域和堆进行内存分配。

3.1.2 64位进程内存布局

如之前所述,64位进程内存布局方式由于其地址空间足够,且实现方便,所以采用的与32位经典内存布局的方式一致,如下图所示:

64位进程布局

3.2 操作系统内存分配函数

在之前介绍内存布局的时候,有提到过,heap 和mmap 映射区域是可以提供给用户程序使用的虚拟内存空间。那么我们该如何获得该区域的内存呢?

操作系统提供了相关的系统调用来完成内存分配工作。

  • 对于heap的操作,操作系统提供了brk()函数,c运行时库提供了sbrk()函数。

  • 对于mmap映射区域的操作,操作系统提供了mmap()和munmap()函数。

sbrk(),brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。而glibc就是使用这些函数来向操作系统申请虚拟内存,以完成内存分配的。

这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是 Linux 内存管理的基本思想之一。Linux 内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过程。

内存分配

进程的内存结构,在内核中,是用mm_struct来表示的,其定义如下:

 struct mm_struct {
  ...
  unsigned long (*get_unmapped_area) (struct file *filp,
  unsigned long addr, unsigned long len,
  unsigned long pgoff, unsigned long flags);
  ...
  unsigned long mmap_base; /* base of mmap area */
  unsigned long task_size; /* size of task vm space */
  ...
  unsigned long start_code, end_code, start_data, end_data;
  unsigned long start_brk, brk, start_stack;
  unsigned long arg_start, arg_end, env_start, env_end;
  ...
 }

在上述mm_struct结构中:

  • [start_code,end_code)表示代码段的地址空间范围。

  • [start_data,end_start)表示数据段的地址空间范围。

  • [start_brk,brk)分别表示heap段的起始空间和当前的heap指针。

  • [start_stack,end_stack)表示stack段的地址空间范围。

  • mmap_base表示memory mapping段的起始地址。

C语言的动态内存分配基本函数是 malloc(),在 Linux 上的实现是通过内核的 brk 系统调用。brk()是一个非常简单的系统调用, 只是简单地改变mm_struct结构的成员变量 brk 的值。

mm_struct

3.2.1 Heap操作

在前面有提过,有两个函数可以直接从堆(Heap)申请内存,brk()函数为系统调用,sbrk()为c库函数。

系统调用通常提过一种最小的功能,而库函数相比系统调用,则提供了更复杂的功能。在glibc中,malloc就是调用sbrk()函数将数据段的下界移动以来代表内存的分配和释放。sbrk()函数在内核的管理下,将虚拟地址空间映射到内存,供malloc()函数使用。

下面为brk()函数和sbrk()函数的声明。

 #include 
 int brk(void *addr);
 ​
 void *sbrk(intptr_t increment);

需要说明的是,当sbrk()的参数increment为0时候,sbrk()返回的是进程当前brk值。increment 为正数时扩展 brk 值,当 increment 为负值时收缩 brk 值。

3.2.2 MMap操作

在Linux系统中我们可以使用mmap用来在进程虚拟内存地址空间中分配地址空间,创建和物理内存的映射关系。

评论 5
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值