logo资料库

Glibc内存管理-Ptmalloc2源代码分析.pdf

第1页 / 共130页
第2页 / 共130页
第3页 / 共130页
第4页 / 共130页
第5页 / 共130页
第6页 / 共130页
第7页 / 共130页
第8页 / 共130页
资料共130页,剩余部分请下载后查看
淘宝网 Glibc 内存管理 Ptmalloc2 源代码分析 华庭(庄明强) 2011/4/17 由于华庭水平有限,因此也不能完全保证对 ptmalloc 的所有理解都准备无误,所以如果 读者发现其中有什么错误,请勿见怪,如果方便请来信告之,并欢迎来信讨论。 (mqzhuang@gmail.com)
目录 2.1 3.2 2.1.1 2.1.2 2.1.3 2.2.1 2.2.2 1. 问题........................................................................................................................................... 3 2. 基础知识 ................................................................................................................................... 4 X86 平台 Linux 进程内存布局 ..................................................................................... 4 32 位模式下进程内存经典布局 ...................................................................... 4 32 位模式下进程默认内存布局 ...................................................................... 5 64 位模式下进程内存布局 .............................................................................. 5 2.2 操作系统内存分配的相关函数 ................................................................................... 6 Heap 操作相关函数 ......................................................................................... 6 Mmap 映射区域操作相关函数 ....................................................................... 7 3. 概述........................................................................................................................................... 8 3.1 内存管理一般性描述 ................................................................................................... 8 3.1.1 内存管理的方法 ............................................................................................... 8 3.1.2 内存管理器的设计目标 ................................................................................. 10 3.1.3 常见 C 内存管理程序 ..................................................................................... 12 Ptmalloc 内存管理概述 .............................................................................................. 13 3.2.1 简介 ................................................................................................................. 13 3.2.2 内存管理的设计假设 ..................................................................................... 14 3.2.3 内存管理数据结构概述 ................................................................................. 14 3.2.4 内存分配概述 ................................................................................................. 19 3.2.5 内存回收概述 ................................................................................................. 21 3.2.6 配置选项概述 ................................................................................................. 22 3.2.7 使用注意事项 ................................................................................................. 23 4. 问题分析及解决 ..................................................................................................................... 24 5. 源代码分析 ............................................................................................................................. 26 5.1 边界标记法 ................................................................................................................. 26 5.2 分箱式内存管理 ......................................................................................................... 34 Small bins ........................................................................................................ 34 Large bins ........................................................................................................ 35 Unsorted bin .................................................................................................... 40 Fast bins ........................................................................................................... 42 5.3 核心结构体分析 ......................................................................................................... 44 malloc_state .................................................................................................... 44 5.3.1 5.3.2 Malloc_par ...................................................................................................... 47 5.3.3 分配区的初始化 ............................................................................................. 49 5.4 配置选项 ..................................................................................................................... 51 Ptmalloc 的初始化...................................................................................................... 53 5.5 Ptmalloc 未初始化时分配/释放内存 ............................................................ 53 ptmalloc_init()函数 ......................................................................................... 55 ptmalloc_lock_all(),ptmalloc_unlock_all(),ptmalloc_unlock_all2() ................ 60 5.6 多分配区支持 ............................................................................................................. 65 5.6.1 Heap_info ........................................................................................................ 65 5.6.2 获取分配区 ..................................................................................................... 66 5.2.1 5.2.2 5.2.3 5.2.4 5.5.1 5.5.2 5.5.3 1
5.6.3 5.6.4 5.6.5 Arena_get2() ................................................................................................... 68 _int_new_arena() ............................................................................................ 70 New_heap() ..................................................................................................... 72 get_free_list()和 reused_arena() .................................................................... 75 grow_heap(),shrink_heap(),delete_heap(),heap_trim() ................................. 77 5.7 内存分配 malloc ......................................................................................................... 82 public_mALLOc() ............................................................................................. 82 _int_malloc() ................................................................................................... 83 5.8 内存释放 free ........................................................................................................... 116 Public_fREe() ................................................................................................. 116 _int_free() ..................................................................................................... 118 sYSTRIm()和 munmap_chunk() ..................................................................... 126 5.7.1 5.7.2 5.6.6 5.6.7 5.8.1 5.8.2 5.8.3 2
1. 问题 项目组正在研发的一个类似数据库的 NoSql 系统,遇到了 Glibc 的内存暴增问题。现象 如下:在我们的 NoSql 系统中实现了一个简单的内存管理模块,在高压力高并发环境下长时 间运行,当内存管理模块的内存释放给 C 运行时库以后,C 运行时库并没有立即把内存归还 给操作系统,比如内存管理模块占用的内存为 10GB,释放内存以后,通过 TOP 命令或者 /proc/pid/status 查看进程占用的内存有时仍然为 10G,有时为 5G,有时为 3G,etc,内存释 放的行为不确定。 我们的 NoSql 系统中的内存管理方式比较简单,使用全局的定长内存池,内存管理模块 每次分配/释放 2MB 内存,然后分成 64KB 为单位的一个个小内存块用 hash 加链表的方式进 行管理。如果申请的内存小于等于 64KB 时,直接从内存池的空闲链表中获取一个内存块, 内存释放时归还空闲链表;如果申请的内存大于 64KB,直接通过 C 运行时库的 malloc 和 free 获取。某些数据结构涉及到很多小对象的管理,比如 Hash 表,B-Tree,这些数据结构从全 局内存池获取内存后再根据数据结构的特点进行组织。为了提高内存申请/释放的效率,减 少锁冲突,为每一个线程单独保留 8MB 的内存块,每个线程优先从线程专属的 8MB 内存块 获取内存,专属内存不足时才从全局的内存池获取。 系统中使用的网络库有独立的内存管理方式,并不从全局内存池中分配内存,该网络库 在处理网络请求时也是按 2M 内存为单位向 C 运行时库申请内存,一次请求完成以后,释放 分配的内存到 C 运行时库。 在弄清楚了系统的内存分配位置以后,对整个系统进行了内存泄露的排查,在解决了数 个内存泄露的潜在问题以后,发现系统在高压力高并发环境下长时间运行仍然会发生内存暴 增的现象,最终进程因 OOM 被操作系统杀掉。 为了便于跟踪分析问题,在全局的内存池中加入对每个子模块的内存统计功能:每个子 模块申请内存时都将子模块编号传给全局的内存池,全局的内存池进行统计。复现问题后发 现全局的内存池的统计结果符合预期,同样对网络模块也做了类似的内存使用统计,仍然符 合预期。由于内存管理不外乎三个层面,用户管理层,C 运行时库层,操作系统层,在操作 系统层发现进程的内存暴增,同时又确认了用户管理层没有内存泄露,因此怀疑是 C 运行时 库的问题,也就是 Glibc 的内存管理方式导致了进程的内存暴增。 问题范围缩小了,但有如下的问题还没有搞清楚,搞不清楚这些问题,我们系统的中的 问题就无法根本性解决。 1. Glibc 在什么情况下不会将内存归还给操作系统? 2. Glibc 的内存管理方式有哪些约束?适合什么样的内存分配场景? 3. 我们的系统中的内存管理方式是与 Glibc 的内存管理的约束相悖的? 4. Glibc 是如何管理内存的? 带着这些问题,决定对 Glibc 的 ptmalloc2 源代码进行一番研究,希望能找到这些问题的 答案,并解决我们系统中遇到的问题。我研究的对象是当前最新版的 glibc-2.12.1 中的内存 管理的相关代码。 3
2. 基础知识 2.1 X86 平台 Linux 进程内存布局 Linux 系统在装载 elf 格式的程序文件时,会调用 loader 把可执行文件中的各个段依次 载入到从某一地址开始的空间中(载入地址取决 link editor(ld)和机器地址位数,在 32 位机 器上是 0x8048000,即 128M 处)。如下图所示,以 32 位机器为例,首先被载入的是.text 段, 然后是.data 段,最后是.bss 段。这可以看作是程序的开始空间。程序所能访问的最后的地 址是 0xbfffffff,也就是到 3G 地址处,3G 以上的 1G 空间是内核使用的,应用程序不可以直 接访问。应用程序的堆栈从最高地址处开始向下生长,.bss 段与堆栈之间的空间是空闲的, 空闲空间被分成两部分,一部分为 heap,一部分为 mmap 映射区域,mmap 映射区域一般 从 TASK_SIZE/3 的地方开始,但在不同的 Linux 内核和机器上,mmap 区域的开始位置一般是 不同的。Heap 和 mmap 区域都可以供用户自由使用,但是它在刚开始的时候并没有映射到 内存空间内,是不可访问的。在向内核请求分配该空间之前,对这个空间的访问会导致 segmentation fault。用户程序可以直接使用系统调用来管理 heap 和 mmap 映射区域,但更 多的时候程序都是使用 C 语言提供的 malloc()和 free()函数来动态的分配和释放内存。Stack 区域是唯一不需要映射,用户却可以访问的内存区域,这也是利用堆栈溢出进行攻击的基础。 2.1.1 32 位模式下进程内存经典布局 这种布局是 Linux 内核 2.6.7 以前的默认进程内存布局形式,mmap 区域与栈区域相对增 长,这意味着堆只有 1GB 的虚拟地址空间可以使用,继续增长就会进入 mmap 映射区域, 这显然不是我们想要的。这是由于 32 模式地址空间限制造成的,所以内核引入了另一种虚 拟地址空间的布局形式,将在后面介绍。但对于 64 位系统,提供了巨大的虚拟地址空间, 这种布局就相当好。 4
2.1.2 32 位模式下进程默认内存布局 从上图可以看到,栈至顶向下扩展,并且栈是有界的。堆至底向上扩展,mmap 映射区 域至顶向下扩展,mmap 映射区域和堆相对扩展,直至耗尽虚拟地址空间中的剩余区域,这 种结构便于 C 运行时库使用 mmap 映射区域和堆进行内存分配。上图的布局形式是在内核 2.6.7 以后才引入的,这是 32 位模式下进程的默认内存布局形式。 2.1.3 64 位模式下进程内存布局 在 64 位模式下各个区域的起始位置是什么呢?对于 AMD64 系统,内存布局采用经典 内存布局,text 的起始地址为 0x0000000000400000,堆紧接着 BSS 段向上增长,mmap 映射 区域开始位置一般设为 TASK_SIZE/3。 ((1UL << 47) - PAGE_SIZE) (test_thread_flag(TIF_IA32) ? \ #define TASK_SIZE_MAX #define TASK_SIZE IA32_PAGE_OFFSET : TASK_SIZE_MAX) #define STACK_TOP TASK_SIZE #define TASK_UNMAPPED_BASE (PAGE_ALIGN(TASK_SIZE / 3)) 计 算 一 下 可 知 , mmap 的 开 始 区 域 地 址 为 0x00002AAAAAAAA000 , 栈 顶 地 址 为 0x00007FFFFFFFF000 5
上图是 X86_64 下 Linux 进程的默认内存布局形式,这只是一个示意图,当前内核默认 配置下,进程的栈和 mmap 映射区域并不是从一个固定地址开始,并且每次启动时的值都 不一样,这是程序在启动时随机改变这些值的设置,使得使用缓冲区溢出进行攻击更加困难。 当然也可以让进程的栈和 mmap 映射区域从一个固定位置开始,只需要设置全局变量 randomize_va_space 值 为 0 , 这 个 变 量 默 认 值 为 1 。 用 户 可 以 通 过 设 置 /proc/sys/kernel/randomize_va_space 来停用该特性,也可以用如下命令: sudo sysctl -w kernel.randomize_va_space=0 2.2 操作系统内存分配的相关函数 上节提到 heap 和 mmap 映射区域是可以提供给用户程序使用的虚拟内存空间,如何获 得该区域的内存呢?操作系统提供了相关的系统调用来完成相关工作。对 heap 的操作,操 作系统提供了 brk()函数,C 运行时库提供了 sbrk()函数;对 mmap 映射区域的操作,操作系 统提供了 mmap()和 munmap()函数。sbrk(),brk() 或者 mmap() 都可以用来向我们的进程添 加额外的虚拟内存。Glibc 同样是使用这些函数向操作系统申请虚拟内存。 这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建 立这个地址的物理映射,这是 Linux 内存管理的基本思想之一。Linux 内核在用户申请内存的 时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当 用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理 内存。内核释放物理页面是通过释放线性区,找到其所对应的物理页面,将其全部释放的过 程。 2.2.1 Heap 操作相关函数 Heap 操作函数主要有两个,brk()为系统调用,sbrk()为 C 库函数。系统调用通常提供一 6
种最小功能,而库函数通常提供比较复杂的功能。Glibc 的 malloc 函数族(realloc,calloc 等) 就调用 sbrk()函数将数据段的下界移动,sbrk()函数在内核的管理下将虚拟地址空间映射到内 存,供 malloc()函数使用。 内核数据结构 mm_struct 中的成员变量 start_code 和 end_code 是进程代码段的起始和 终止地址,start_data 和 end_data 是进程数据段的起始和终止地址,start_stack 是进程堆栈 段起始地址,start_brk 是进程动态内存分配起始地址(堆的起始地址),还有一个 brk(堆 的当前最后地址),就是动态内存分配当前的终止地址。C 语言的动态内存分配基本函数是 malloc(),在 Linux 上的实现是通过内核的 brk 系统调用。brk()是一个非常简单的系统调用, 只是简单地改变 mm_struct 结构的成员变量 brk 的值。 这两个函数的定义如下: #include int brk(void *addr); void *sbrk(intptr_t increment); 需要说明的是,但 sbrk()的参数 increment 为 0 时,sbrk()返回的是进程的当前 brk 值, increment 为正数时扩展 brk 值,当 increment 为负值时收缩 brk 值。 2.2.2 Mmap 映射区域操作相关函数 mmap()函数将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的 大小不是所有页的大小之和,最后一个页不被使用的空间将会清零。munmap 执行相反的操 作,删除特定地址区域的对象映射。函数的定义如下: #include void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length); 在这里不准备对这两个函数做详细介绍,只是对 ptmalloc 中用到的功能做一下介绍,其 他的用法请参看相关资料。 参数: start:映射区的开始地址。 length:映射区的长度。 prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过 or 运算合理地组合在一起。Ptmalloc 中主要使用了如下的几个标志: PROT_EXEC //页内容可以被执行,ptmalloc 中没有使用 PROT_READ //页内容可以被读取,ptmalloc 直接用 mmap 分配内存并立即返回给用户时 设置该标志 PROT_WRITE //页可以被写入,ptmalloc 直接用 mmap 分配内存并立即返回给用户时设 置该标志 PROT_NONE //页不可访问,ptmalloc 用 mmap 向系统“批发”一块内存进行管理时设置 该标志 flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者 多个以下位的组合体 MAP_FIXED //使用指定的映射起始地址,如果由 start 和 len 参数指定的内存区重叠于现 存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起 7
分享到:
收藏