C指针原理(42)-内存管理与控制

C语言的stdlib库提供了内存分配与管理的函数:

1、通过调用calloc、malloc和realloc所分配的空间,如果连续调用它们,不能保证空间是顺利或连续的。当分配成功后,函数将返回一个指针,这个指针指向分配成功的空间的开始位置, 它可以被指向任意类型的对象,当分配空间失败后,返回NULL指针。

2、通过free函数释放空间。

3、函数说明

(1)calloc函数

函数的原型为:

void *calloc(size_t nmemb,size_t size);

为nmemb个对象的数组分配空间,每个元素的大小为size,分配的空间所有位被初始为0。

(2)free函数

函数的原型为:

原型:extern void free(void *ptr);

释放指针ptr所指向的的内存空间。ptr所指向的内存空间必须是用calloc,malloc,realloc所分配的内存。如果ptr为NULL或指向不存在的内存块则不做任何操作。

释放的空间还可以被重新分配,

(3)malloc函数

void *malloc(size_t  size);

分配长度为size字节的内存块。如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。

(4)realloc函数

void realloc(void ptr, size_t size);

改变ptr所指内存区域的大小为size长度。如果重新分配成功则返回指向被分配内存的指针,否则返回空指针NULL。当内存不再使用时,应使用free()函数将内存块释放。新分配空间比空间大,并包括原空间的的内容,但因为分配新空间,没有初始化0的操作,所以新空间中除去旧空间的部分的内容不能保证清空为零。

4、C语言中数据对象空间分配原理
C语言中的数据对象存储在以下3种空间中:

(1) 程序在开始执行前,分配 静态存储空间,并进行初始化,如果没有指定数据对象的初始值,则每个标题 的值 初始化为零。这样的数据对象在程序结束前一直存在。

(2) 程序在每一个程序块的入口分配动态存储空间。如果没有指定数据初始值,它的初始内容不确定,可能上次程序使用释放过的内存内容(释放内存本身并不清空内存中的数据,只是标记这块内存操作系统可重新使用)。 这些数据对象在程序块执行完毕前一直存在。

(3) 调用calloc、malloc、realloc时,程序才分配可被程序员人为操纵的存储空间,仅当调用calloc时,空间才被初始化。这样分配的数据对象要用free函数释放。否则将生存到程序结束。

5、堆的原理 

静态存储空间存在在于程序的整个执行过程 中,动态 存储空间是后进先出,可用栈实现。

动态存储空间经常与函数调用与返回数据一起使用堆栈。可被人为操纵的存储空间不遵守这个规定。C语言的标准库维持着叫“堆”的空间池来控制被calloc、malloc、realloc函数分配的存储空间。

堆中分配的每块内存都应被free函数释放,free函数要释放内存块,就意味着它必须知道释放多大的内存块,然后调用该函数,并没有将需要释放的内存大小告诉它,因此,必须有一种数据结构记录每个已经分配的内存块的信息,同时在堆内存的多次释放与申请过程中,必须会形成很多内存碎片,形成很多数据对象间的小空间,减少了堆的实际可用空间。

多个内存块(Chunks)通常具有相同的尺寸,从内存块边界地址开始,多个领域( Arena)将众多内存块分割成较小的空间进行分配,超大的内存分配(huge arena)要同时占据多个连续内存块(Chunks)才够用。

分配大小分为3个部分:小的、大的和巨大的,所有分配请求被安排到最接近的大小边界,超大的内存分配大于内存块的一半,这种分配存储在单独的红黑树中。对小型和大型的分配,块分割成页,使用二伙伴(Binary-Buddy)算法作为分割算法,Binary-Buddy在分配内存的时候,首先找到一个空闲内存块,接着把内存块不断的进行对半切分(切分得到的2个同样大小的内存块互为伙伴),直到切出来的内存块刚好满足分配需求为止,合并的时候,只有伙伴才能合并为一个新的内存块。

小型和大型分配通过反复将分配大小折半,最后走到一个内存页,但只能合并的方式是分裂过程的相反操作,运行的状态信息作为页面映射存储为每个内存块(Chunks)的开始处,通过分开存储这些信息,页面只接触它们使用的部分,同时对于超过一半的页,但没有超过一半的内存块的大型分配,这种做法同样高效和安全。

小型分配分为三个类:小、量子尺寸(quantum-spaced)和子页,根据数据类型的不同,现代架构要求内存对齐,malloc(3)要求返回适合边界的内存时,在糟糕的情况下,对齐要求被称为量子尺寸(通常是16字节)。如下图所示:

jemalloc分配机制包括领域(arena)、块(chunk)、执行(bin)、运行(run)、线程缓存等部分,jemalloc使用多个分配领域(Arena)将内存分而治之,以块(chunk)作为具体进行内存分配的区域,块(chunk)以页(page)为单位进行管理,每个块(chunk)的前几个 页(page)用于存储后面所有页(page)的状态,后面的所有页(page)则用于进行实际的分配。执行(bin)用来管理各个不同大小单元的分配,每个执行(bin)通过对它对应的运行(run)操作来进行分配的,一个运行(run)实际上就是块(chunk)里的一块区域 ,在分配内存时首先从线程对应的私有缓存空间(tcache)中找。 

     但现代的处理器都是多处理器,并行计算已经慢慢成为一种趋势,多线程运算不可避免,为提高malloc在多线程环境的效率和安全性,增强多线程的伸缩性,Jason Evans 在他的《A Scalable Concurrent malloc(3) Implementation for FreeBSD》一文中提出了并发状态的malloc机制,jemalloc使用多个分配领域(Arena)减少在多处理器系统中的线程之间的锁竞争,虽然增加一些成本,但更有利于提供多线程的伸缩性。现代的多处理器在每个高速缓存线(per-cache-line )的基础上提供了内存的一致性视图,如果两个线程同时运行在不同的处理器上,但在同一缓存线(cache-line)操纵不同的对象,那么处理器必须仲裁缓存线(cache-line)的所有权。

6、分配机制

malloc函数是内存分配机制的核心,realloc函数内部通过调用malloc函数申请更大的内存空间,calloc函数也是如此,先通过调用malloc函数申请空间,然后将空间初始化为0。

通常来说,在大多数操作系统(流行的UNIX/LINUX系统、MAC OS以及WINDOWS)中,malloc函数的运作原理为:它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函 数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的内存碎片,当用户申请一个大的内存片段,那么空闲链上没有供分配的内存了,malloc函数在空闲链上整理各内存片段,将相邻的小空闲块合并成较大的内存块等等,最大可能得在内存中腾出需要的空闲空间返回,如果努力失败将返回NULL指针。

在多处理器中,内存分配如何减少程序的多个线程的锁竞争?可采用在每个分配器中放置一个锁,为分配器准备多个领域,通过对线程标识的HASH计算将各个线程分配到这些领域中,如下图所示:

jemalloc使用的是比HASH更具弹性的算法将线程分派到领域中,在FREEBSD中,默认情况下,单处理器使用一个领域,而多处理器中使用相当于处理器4倍数量的领域。

当线程分配器第一次分配或释放内存时,被分派一个领域,但不是通过线程标识的HASH,而是循环的方式,每个区域尽量保证被分派的线程数相等,没用的HASH的原因在于,做到线程标识符(就是线程指针)的可靠的伪随机HASH非常困难。线程本地存储(TLS)对高效实现循环领域非常重要,每个线程的领域需要一个存储位置,在一些不支持TLS的架构上,仍需要使用线程标识HASH,因此使用pthreads库的TSD替代TLS解决这一问题。

mmap函数和sbrk函数担负malloc等分配内存的函数向内核申请内存的任务,mmap将一个文件或者其它对象映射进内存。文件被映射到多个页上,如果文件的大小不是所有页的大小之和,最后一个页不被使用的空间将会清零,而sbrk增加程序可用数据段空间。

内存块(chunk)的尺寸默认为2M,分配内存块时,基址是尺寸的常数倍,这就是边界对齐,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。各个硬件平台对存储空间的处理上有很大的不同,一些平台对某些特定类型的数据只能从某些特定地址开始存取,边界不对齐将导致读取效率下降很多。

以freebsd10.0系统为例,freebsd采用的是jemalloc分配机制,它能尽量减少内存碎片,提供可伸缩的并发支持,jemalloc在2005首次被引入到freebsd的libc库的分配器。在下图的分析比较中,可以看到,jemalloc分配机制在众多内存分配机制中(最右边的直方框),性能是最好的,

jemalloc使用多个分配领域(Arena)将内存分而治之,以减少在多处理器系统中的线程之间的锁竞争,虽然增加一些成本,但更有利于提供多线程的伸缩性。现代的多处理器在每个高速缓存线(per-cache-line )的基础上提供了内存的一致性视图,如果两个线程同时运行在不同的处理器上,但在同一缓存线(cache-line)操纵不同的对象,那么处理器必须仲裁缓存线(cache-line)的所有权。

如下图所示,被不同线程使用的2个分配器在物理高速缓存上共享同一根缓存线,如果几个线程同时修改2个分配器,处理器得决定这根缓存线归哪个线程使用。 

 ​

这种看似合理但实质有可能造成低效的高速缓存线路共享会导致严重的性能下降。解决这个问题的方法之一是填充分配(pad allocation),但填充分配与让数据对象尽可能紧密的目标背道而驰,它会引起严重的内存碎片,jemalloc使用一个替代方案,依赖于多个分配领域(Arena)来减少问题,它让应用程序编写者自己进行填充分配(pad allocation),从而在性能的关键代码、线程分配对象的代码以及将对象传递给多个其他线程中,有意避免缓存线程共享机制带来的影响。

猜你喜欢

转载自blog.51cto.com/13959448/2341835