Linux内存管理:Malloc

对于内核的内存管理,像kmalloc,vmalloc,kmap,ioremap等比较熟悉。而对用户层的管理机制不是很熟悉,下面就从malloc的实现入手.( 这里不探讨linux系统调用的实现机制. ) ,参考了《深入理解计算机系统》和一些网上的资料.
首先从http://ftp.gnu.org/gnu/glibc下载glibc库2.21,

通常我们用的bsp或者sdk里面的工具链都是编译好的,而这个是源码,需要自己编译(常用的有定制交叉编译工具链).有时候我们需要添加自定义库.

Linux中malloc的早期版本是由Doug Lea实现的,它有一个重要问题就是在并行处理时多个线程共享进程的内存空间,各线程可能并发请求内存,在这种情况下应该如何保证分配和回收的正确和有效。Wolfram Gloger在Doug Lea的基础上改进使得glibc的malloc可以支持多线程——ptmalloc,在glibc-2.3.x.中已经集成了ptmalloc2,这就是我们平时使用的malloc.

其做法是,为了支持多线程并行处理时对于内存的并发请求操作,malloc的实现中把全局用户堆(heap)划分成很多子堆(sub-heap)。这些子堆是按照循环单链表的形式组织起来的。每一个子堆利用互斥锁(mutex)使线程对于该子堆的访问互斥。当某一线程需要调用malloc分配内存空间时,该线程搜索循环链表试图获得一个没有加锁的子堆。如果所有的子堆都已经加锁,那么malloc会开辟一块新的子堆,对于新开辟的子堆默认情况下是不加锁的,因此线程不需要阻塞就可以获得一个新的子堆并进行分配操作。在回收free操作中,线程同样试图获得待回收块所在子堆的锁,如果该子堆正在被别的线程使用,则需要等待直到其他线程释放该子堆的互斥锁之后才可以进行回收操作。

申请小块内存时会产生很多内存碎片,ptmalloc在整理时需要对子堆做加锁操作,每个加锁操作大概需要5~10个cpu指令,而且程序线程数很高的情况下,锁等待的时间就会延长,导致malloc性能下降。

因此很多大型的服务端应用会自己实现内存池,以降低向系统malloc的开销。Hoard和TCmalloc是在glibc和应用程序之间实现的内存管理。Hoard的作者是美国麻省的Amherst College的一名老师,理论角度对hoard的研究和优化比较多,相关的文献可以hoard主页下载到到。从我自己项目中的系统使用来看,Hoard确实能够很大程度的提高程序的性能和稳定性。TCMalloc(Thread-Caching Malloc)是google开发的开源工具──“google-perftools”中的成员。这里有它的系统的介绍和安装方法。这个只是对它历史发展的一个简单介绍,具体改动还需去官网查看.

下面我们就看看malloc:

malloc的全称是memory allocation,中文叫动态内存分配,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存。

原型为: extern void *malloc(unsigned int num_bytes)。

具体声明在malloc.h中:

返回值:

如果分配成功则返回指向被分配内存的指针(此存储区中的初始值不确定),否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。函数返回的指针一定要适当对齐,使其可以用于任何数据对象。
注意:
malloc(0) 返回不为空。 Free(p) 后p不为空。

那么malloc到底是从哪里获取的内存呢?
答案是从堆里面获得空间;malloc的应用必然是某一个进程调用,而每一个进程在启动的时候,系统默认给它分配了heap。下面我们就看看进程的内存空间布局:
Anyway, here is the standard segment layout in a Linux process:(这个是x86 虚拟地址空间的默认布局)

在glibc库中找到malloc.c文件:

即malloc别名为__libc_malloc,__malloc.并且在malloc.c中我们不能找到malloc的直接实现,而是有__libc_malloc:

在这个函数的第一行是关于hook的,我们先看一个定义:

它是gcc attribute weak的特性,可以查资料进一步了解.这里说明一下由于是弱属性,所以当有具体的实现的时候,就以外部实现为准.

__libc_malloc中首先判断hook函数指针是否为空,不为空则调用它,并返回。glibc2.21里默认malloc_hook是初始化为malloc_hook_ini的。
但是我们发现在malloc_hook_ini中把__malloc_hook赋值为NULl,这样就避免了递归调用.
同理在最后部分也有一个__malloc_initialize_hook的:默认为空.

那么ptmalloc_init到底又做了什么工作呢?

而__malloc_initialized在arena.c中默认初始化为:即开始的时候小于0.

函数开始把它赋值为0,最后初始化完成赋值为1. 所以这个函数完成了malloc的初始化工作.只有第一次调用的时候会用到.
接着是处理_environ即传递过来的环境变量,进行内存分配策略控制,你可以定制内存管理函数的行为,通过调整由mallopt()函数的参数。(默认环境变量为空。)

内存分配调整甚至可以不在你的程序中引入mallopt()调用和重新编译它。在你想快速测试一些值或者你没有源代码时,这非常有用。你仅需要做的是在运行程序前,设置合适的环境变量。表1展示mallopt()参数和环境变量的映射关系以及一些额外的信息。例如,如果你希望设置内存消减阈值为64k,你可以运行这个程序:
#MALLOC_TRIM_THRESHOLD=65536 my_prog

内存调试:连续性检查 ,可以设置变量MALLOC_CHECK_=1

#MALLOC_CHECK_=1 my_prog

还有一个mtrace使用的例子:

运行: #MALLOC_TRACE=”1.txt” ./a.out
然后用mtrace查看结果:

一些GNU C库提供的标准调试工具可能并不适合你程序的特殊需求。在这种情况下,你可以借助一个外部的内存调试工具(见 Resource)或者在你的库内部作修改。做这件事中只是简单的写三个函数以及将它们与预先定义的变量相关联:

  • __malloc_hook points to a function to be called when the user calls malloc(). You can do your own checks and accounting here, and then call the real malloc() to get the memory that was requested.
  • __malloc_hook 指向一个函数,当用户调用malloc()时,这个函数将被调用。你可以在这里做你自己的检查和计数,然后调用真实的malloc来得到被请求的内存。
  • __free_hook points to a function called instead of the standard free().
  • __free_hook 指向一个函数,用来替换标准的free()
  • __malloc_initialize_hook points to a function called when the memory management system is initialized. This allows you to perform some operations, say, setting the values of the previous hooks, before any memory-related operation takes place.

__malloc_initialize__hook 指向一个函数,当内存管理系统被初始化的时候,这个函数被调用。这允许你来实施一些操作,例如,在任何内存相关的操作生效前,设置前面的勾子值。

在其它的内存相关的调用中,Hooks()也有效,包括realloc(),calloc()等等。确保在调用malloc()或free()之前,保存先前的勾子的值,把它们存储起来。如果你不这么做,你的程序将陷入无尽的递归。看看libc info page给的一个内存调试的例子来看看相关细节,最后一点,勾子也被mcheck和mtrace系统使用。在使用所有它们的组合的时候,小心是没错的。

而下面的是关于多线程的:

创建线程私有实例 arena_key,该私有实例保存的是分配区( arena )的 malloc_state 实例指针。 arena_key 指向的可能是主分配区的指针,也可能是非主分配区的指针,这里将调用 ptmalloc_init() 的线程的 arena_key 绑定到主分配区上。意味着本线程首选从主分配区分配内存。

然后调用 thread_atfork() 设置当前进程在 fork 子线程( linux 下线程是轻量级进程,使用类似 fork 进程的机制创建)时处理 mutex 的回调函数,在本进程 fork 子线程时,调用 ptmalloc_lock_all() 获得所有分配区的锁,禁止所有分配区分配内存,当子线程创建完毕,父进程调用 ptmalloc_unlock_all() 重新 unlock 每个分配区的锁 mutex ,子线程调用 ptmalloc_unlock_all2() 重新初始化每个分配区的锁 mutex

当有多个线程同时申请访问内存的时候,arena_key的main_arena处于保持互斥锁状态,那么为了提高效率即上面的代码,保证了在获取不到主分区的时候,调用arena_get2自动创建次分区state。见代码:

在继续之前我们补一下关键的数据结构:

还有具体分配的chunk:关于它的注释部分这么就不翻译了,但需要好好看看。

实际分配的chunk图,空间里如何布局的:

而空的chunk结构如下图:

因为后边代码就是按照这个结构来操作的.

继续回到__libc_malloc函数,hook处理完之后,Arena_lookup查询arena_key,当然不为空,前面第一次调用hook时已经赋值。(如果多线程下,获取不到main_arena,则分配次分区,这个前面也讨论过)

那么获得互斥锁。

进入内存分配的核心函数_int_malloc,它是malloc分配的核心代码和实现.代码挺多,自行分析.
1. 判断申请的空间是否在fastbin,如果在则申请返回,否则继续(<64B,一般小字节chunk释放后放在这里)
2. 判断申请的空间是否在smallbin(小于512B),如果是申请返回否则在largebin中
3. 前两个都不在,那么肯定在largebin,计算索引,继续
4. 进入for(;;)后续处理.主要是垃圾回收工作。如果垃圾回收也不行,则进入use_top chunk
5. use_top chunk申请,如果没有,则调用sysmalloc扩展heap,如下:

对于第一次调用malloc它直接到sysmalloc来扩展heap,自测的例子是先申请200字节的空间.由于程序一开始fast_max为0,所以肯定在smallbins分类中,但是由于初始化,所以
会调用malloc_consolidate来初始化bins,见malloc_init_state(av);:

我们进入sysmalloc,一开始判断申请的nb是否大于需要map的阀值,如果大于则进入mmap。一般大于128K,它可以动态调整

然后是判断是主分配区或非主分配区,分别不同处理。
这里进入主分配,我们看下部分核心分配代码:

由于申请的是200B,8字节对齐为208B,而mp_.top_pad用的默认值为7个pages(0x20000),MINSIZE为16B.后边还需要page对齐,所以需要申请8个page。
下面我们看关键的代码:

这个是什么?

而__default_morecore是:

这里解释下sbrk:

sbrk不是系统调用,是C库函数。系统调用通常提供一种最小功能,而库函数通常提供比较复杂的功能。sbrk/brk是从堆中分配空间,本质是移动一个位置,向后移就是分配空间,向前移就是释放空间,sbrk用相对的整数值确定位置,如果这个整数是正数,会从当前位置向后移若干字节,如果为负数就向前若干字节。在任何情况下,返回值永远是移动之前的位置。sbrk是brk的封装。
默认mp_.sbrk_base为空。所以需要:

av->system_mem默认也为0

然后需要做一些调整:

后面有这么一句:由于correction为0,所以返回当前的值.

最后来分配空间:

av->top是什么值呢?

也就是top就是一个指向heap开始的指针.并转换为struct malloc_chunk 指针.和我们上面的 图就对应起来了。

然后重新设置top指针,和size的标志位,偏移过pre_size和size,就是实际数据地址即return chunk2mem (p);

如果我们紧接着申请了200B后,马上申请16B,由于fastbins虽然设置了max 为64B但是它里面的chunk是free的时候放置进来的,目前为空。

所以继续进入smallbin。同理由于没有free的small chunk 。所以进入top chunk 分配成功:

当然如果我们释放了16B后,有马上申请16B,那么它会直接进入fastbin并申请返回成功。,这里我们知道当第一次使用的时候不论什么bin都是空的,只有当多次使用多次释放的时候才会体会出来它的优势和效率来.

这里附上自己测试的小程序:

关于不论fastbin还是smallbin的机制,或许我们记得在《深入理解计算机系统》中,讲到垃圾回收的时候,脚注法。书和代码一起看效果会不错.

内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是Linux内存管理的基本思想之一

还有就是:

内核默认配置下,进程的栈和mmap映射区域并不是从一个固定地址开始,并且每次启动时的值都不一样,这是程序在启动时随机改变这些值的设置,使得使用缓冲区溢出进行攻击更加困难。当然也可以让进程的栈和mmap映射区域从一个固定位置开始,只需要设置全局变量randomize_va_space值为0,这个变量默认值为1。用户可以通过设置/proc/sys/kernel/randomize_va_space来停用该特性,也可以用如下命令: sudo sysctl -w kernel.randomize_va_space=0

1 4 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部