两种常见的内存管理方法:堆和内存池

在程序运行过程中,可能产生一些数据,例如,串口接收的数据,ADC采集的数据。若需将数据存储在内存中,以便进一步运算、处理,则应为其分配合适的内存空间,数据处理完毕后,再释放相应的内存空间。为了便于内存的分配和释放,AWorks提供了两种内存管理工具:堆和内存池。

本文为《面向AWorks框架和接口的编程(上)》第三部分软件篇——第9章内存管理——第1~2小节:堆管理器和内存池。

本章导读

在计算机系统中,数据一般存放在内存中,只有当数据需要参与运算时,才从内存中取出,交由CPU运算,运算结束再将结果存回内存中。这就需要系统为各类数据分配合适的内存空间。

一些数据需要的内存大小在编译前可以确定。主要有两类:一类是全局变量或静态变量,这部分数据在程序的整个生命周期均有效,在编译时就为这些数据分配了固定的内存空间,后续直接使用即可,无需额外的管理;一类是局部变量,这部分数据仅在当前作用域中有效(如函数中),它们需要的内存自动从栈中分配,也无需额外的管理,但需要注意的是,由于这一部分数据的内存从栈中分配,因此,需要确保应用程序有足够的栈空间,尽量避免定义内存占用较大的局部变量(比如:一个占用数K内存的数组),以避免栈溢出,栈溢出可能破坏系统关键数据,极有可能造成系统崩溃。

一些数据需要的内存大小需要在程序运行过程中根据实际情况确定,并不能在编译前确定。例如,可能临时需要1K内存空间用于存储远端通过串口发过来的数据。这就要求系统具有对内存空间进行动态管理的能力,在用户需要一段内存空间时,向系统申请,系统选择一段合适的内存空间分配给用户,用户使用完毕后,再释放回系统,以便系统将该段内存空间回收再利用。在AWorks中,提供了两种常见的内存管理方法:堆和内存池。

9.1  堆管理器

堆管理器用于管理一段连续的内存空间,可以在满足系统资源的情况下,根据用户的需求分配任意大小的内存块,完全“按需分配”,当用户不再使用分配的内存块时,又可以将内存块释放回堆中供其它应用分配使用。类似于C标准库中的malloc()/free()函数实现的功能。

9.1.1  堆管理器的原理概述

在使用堆管理器前,首先通过一个示例对其原理作简要的介绍,以便用户更加有效的使用堆管理器。例如,使用堆管理器对1024字节的内存空间进行管理。初始时,所有内存均处于空闲状态,可以将整个内存空间看作一个大的空闲内存块。示意图详见图9.1。

两种常见的内存管理方法:堆和内存池

图9.2 分配100字节——分割为两个内存块

注:填充为阴影表示该块已被分配,否则,表示该块未被分配,处于空闲状态,数字表示该块的容量。

同理,若用户再向该堆管理器连续请求三次内存空间,每次请求的内存空间容量分别为:150、250、200字节,则分配后的内存示意图详见图9.3。

两种常见的内存管理方法:堆和内存池

图9.4 释放一个内存块——相邻块不为空闲块

由于该内存块前后均不为空闲块,因此,无需作合并操作。此时,释放了250字节的空闲空间,堆中共存在574字节的内存空间。但是,若用户此时向该堆管理器申请400字节的内存空间,由于第一个空闲块和第二个空闲块均不满足需求,因此,内存分配还是会失败。

这里暴露出了一个堆管理器的缺点。频繁的分配和释放大小不一的内存块,将产生诸多内存碎片,即图9.4中被已分配内存块打断的空闲内存块,图中容量为250字节和容量为324字节的空闲内存块被一个已分配的容量为200字节的内存块打断,使得各个空闲块在物理上不连续,无法形成一个大的空闲块。这种情况下,即使请求的内存空间大小小于当前堆的空闲空间总大小,也可能会由于没有一个大的空闲块而分配失败。

在图9.4的基础上,即使再释放掉第一次分配的100字节内存空间,使总空闲空间达到674字节,详见图9.5,同样无法满足一个400字节内存空间的分配请求。

两种常见的内存管理方法:堆和内存池

图9.6 再分配280字节内存空间

在图9.6的基础上,若用户再释放200字节的内存块,首先,将其变为空闲块,示意图详见图9.7。

两种常见的内存管理方法:堆和内存池

图9.8 释放200字节的内存空间(2)——与相邻空闲块合并

此时,由于存在一个大小为450的空闲块,因此,若此时用户申请400字节的内存空间,则可以申请成功。与图9.5对比可知,虽然图9.5共计有674字节的空闲空间,而图9.8只有594字节的空闲空间,但图9.8却可以满足一个大小为400字节的内存空间请求。由此可见,受内存碎片的影响,总的空闲空间大小并不能决定一个内存请求的成功与否。

申请400字节成功后的示意图详见图9.9。

两种常见的内存管理方法:堆和内存池

图9.10 释放280字节的内存空间(1)——标记为空闲

由于其左右两侧均为空闲块,因此,需要将它们合并为一个大的内存块,即合并为一个大小为374的内存块,示意图详见图9.11。

两种常见的内存管理方法:堆和内存池

图9.12 内存块(含空闲内存块和已分配内存块)链表

图中展示了4个非常重要的信息:magic、used、p_next、p_prev。

magic被称为魔数,会被赋值为一个特殊的固定值,它表示了该内存块是堆管理器管理的内存块,可以在一定程度上检查错误的内存操作。例如,若这个区域被改写,magic的值被修改为了其它值,表明存在非法的内存操作,可能是用户内存操作越界等,应及时处理;在释放一个内存块时,堆管理器会检查magic的值,若其值不为特殊的固定值,表明这并不是堆管理器分配的内存块,该释放操作是非法的。

used用于表示该内存块是否已经被使用。若其值为0,则表示该内存块是空闲块;若其值为1,则表示该内存块已经被分配,不是空闲块。

p_next和p_prev用于将各个内存块使用双向链表的形式组织起来,以便可以方便的找到当前内存块的下一个或上一个内存块,例如,在释放一个内存块时,需要查看相邻的内存块(上一个内存块和下一个内存块)是否为空闲块,以决定是否将它们合并为一个空闲块。

此外,为了在分配内存块时,加快搜索空闲块的效率,信息中还会额外另外两个指针,用于将所有空闲块单独组织为一个双向链表。这样,在分配一个内存块时,只需要在空闲链表中查找满足需求的空闲块即可,无需依次遍历所有的内存块。示意图详见图9.13。

两种常见的内存管理方法:堆和内存池

1.  定义堆管理器实例

在使用堆管理器前,必须先使用aw_memheap_t类型定义堆管理器实例,该类型在aw_memheap.h中定义,具体类型的定义用户无需关心,仅需使用该类型定义堆管理器实例即可,即:

两种常见的内存管理方法:堆和内存池

如此一来,各个应用可以定义自己的堆管理器,以管理该应用中的内存分配与释放,使得各个应用之间的内存使用互不影响。

如果所有应用使用一个堆,那么各个应用之间的内存分配将相互影响,某一应用出现内存泄漏,随着时间的推移,极有可能造成一个堆的空间被完全泄漏,这将影响所有应用程序。同时,所有应用共用一个堆,也造成在一个堆中的分配与释放更加频繁,分配的内存块大小更加不一致,更容易产生内存碎片。

2.  初始化堆管理器

定义堆管理器实例后,必须使用该接口初始化后才能使用,以指定其管理的内存空间,其函数原型为:

两种常见的内存管理方法:堆和内存池

程序中,定义了一个大小为1024字节的数组,用于分配一个1KB的内存空间,以便使用堆管理器进行管理。

3.  分配内存

堆管理器初始化完毕后,可以使用该接口为用户分配指定大小的内存块,内存块的大小可以是满足资源需求的任意大小。分配内存块的函数原型为:

两种常见的内存管理方法:堆和内存池

程序中,将aw_memheap_alloc()的返回值强制转换为了指向uint8_t数据类型的指针。注意,使用aw_memheap_alloc()分配的内存中,数据的初始值是随机的,不一定为0。因此,若不对ptr指向的内存赋值,则其值可能是任意的。

4.  重新调整内存大小

有时候,需要动态调整之前分配的内存块大小,如果一开始分配了一个较小的内存块,但随着数据的增加,内存不够,此时,就可以使用该函数重新调整之前分配的内存块大小。其函数原型为:

两种常见的内存管理方法:堆和内存池

值得注意的是,重新调整内存空间的大小失败后,原内存空间还是有效的。因此,若采用程序清单9.3第9行的调用形式,若内存扩大失败,则返回值为NULL,这将会导致ptr的值被设置为NULL。此时,虽然原内存空间仍然有效,但由于指向该内存空间的指针信息丢失了,用户无法再访问到对应的内存空间,也无法释放对应的内存空间,造成内存泄漏。

在实际使用时,为了避免调整内存大小失败时将原ptr的值覆盖为NULL,应该使用一个新的指针保存函数的返回值,详见程序清单9.4。

程序清单9.4 重新调整内存大小(使用新的指针保存函数返回值)

两种常见的内存管理方法:堆和内存池

其中,ptr为使用aw_memheap_alloc()或aw_memheap_realloc()函数分配的内存块首地址,即调用这些函数的返回值。注意,ptr只能是上述几个函数的返回值,不能是其它地址值,例如,不能将数组首地址作为函数参数,以释放静态数组占用的内存空间。传入错误的地址值,极有可能导致系统崩溃。

当使用aw_memheap_free()将内存块释放后,相应的内存块将变为无效,用户不能再继续使用。释放内存块的范例程序详见程序清单9.5。

程序清单9.5 释放内存块的范例程序

两种常见的内存管理方法:堆和内存池

这也属于一种静态变量,其占用的内存大小是已知的。

对于栈空间,在AWorks中,栈空间是静态分配的,类似于一个静态数组,其占用的内存空间大小由用户决定,同样是已知的。

通常情况下,程序代码和常量都存储在ROM或FLASH等只读存储器中,不会放在内存中。但在部分平台中,出于效率或芯片架构的考虑,也可能将程序代码和常量存储在内存中。例如,在i.MX28x平台中,程序代码和常量也存储在DDR内存中。程序代码和常量占用的内存空间大小在编译后即可确定,占用的内存空间大小也是已知的。

在满足这些数据的存储后,剩下的所有内存空间即作为系统堆空间,便于用户在程序运行过程中动态使用。

为了便于用户使用,需要使用某种合适的方法管理系统堆空间。在AWorks中,默认使用前文介绍的堆管理器对其进行管理。出于系统的可扩展性考虑,AWorks并没有限制必须基于AWorks提供的堆管理器管理系统堆空间,如果用户有更加适合特殊应用场合的管理方法,也可以在特定环境下使用自有方法管理系统堆空间。为了保持应用程序的统一,AWorks定义了一套动态内存管理通用接口,便于用户使用系统堆空间,而无需关心具体的管理方法。相关函数原型详见表9.2。

表9.2 动态内存管理接口(aw_mem.h)

两种常见的内存管理方法:堆和内存池

其中,__g_system_heap为系统定义的堆管理器实例,已在系统启动时完成了初始化。程序清单9.6只是aw_mem_alloc()函数实现的一个简单范例,实际中,用户可以根据具体情况,使用最为合适的方法管理系统堆空间,实现AWorks定义的动态内存管理通用接口。

下面,详细介绍各个接口的含义及使用方法。

1.  分配内存

aw_mem_alloc()用于从系统堆中分配指定大小的内存块,用法和C标准库中的malloc()相同,其函数原型为:

两种常见的内存管理方法:堆和内存池

程序中,将aw_mem_alloc()的返回值强制转换为了指向int数据类型的指针。注意,使用aw_mem_alloc()分配的内存中,数据的初始值是随机的,不一定为0。因此,若不对ptr指向的内存赋值,则其值将是任意的。

2.  分配多个指定大小的内存块

除使用aw_mem_alloc()直接分配一个指定大小的内存块外,还可以使用aw_mem_calloc()分配多个连续的内存块,用法与C标准库中的calloc()相同,其函数原型为:

两种常见的内存管理方法:堆和内存池

由于分配的内存空间会被初始化为0,因此,即使不对ptr指向的内存赋值,其值也是确定的0。

3.  分配具有一定对齐要求的内存块

有时候,用户申请的内存块可能用来存储具有特殊对齐要求的数据,要求分配内存块的首地址必须按照指定的字节数对齐。此时,可以使用aw_mem_align()分配一个满足指定对齐要求的内存块。其函数原型为:

两种常见的内存管理方法:堆和内存池

程序中,将分配的地址通过aw_kprintf()打印输出,以查看地址的具体值,实际运行可以发现,地址值是满足16字节对齐的。注意,该函数与aw_mem_alloc()分配的内存块一样,其中的数据初始值是随机的,不一定为0。

在堆管理器中,并没有类似的分配满足一定对齐要求的内存块接口,只有普通的分配内存块接口:aw_memheap_alloc()。其分配的内存块,可能是对齐的,也可能是未对齐的。为了使返回给用户的内存块能够满足对齐要求,在使用aw_memheap_alloc()分配内存块时,可以多分配align – 1字节的空间,此时,即使获得的内存块首地址不满足对齐要求,也可以返回从内存块首地址开始,顺序第一个对齐的地址给用户,以满足用户的对齐需求。

例如,要分配200字节的内存块,并要求满足8字节对齐,则首先使用aw_memheap_alloc()分配207字节(200 + 8 – 1)的内存块,假定得到的内存块地址范围为:3 ~ 209,示意图详见图9.14(a)。由于首地址3不是8的整数倍,因此其不是按8字节对齐的,此时,直接返回顺序第一个对齐的地址给用户,即:8。由于用户需要的是200字节的内存块,因此,对于用户来讲,其使用的内存块地址范围为 :8 ~ 207,显然,其在实际使用aw_memheap_alloc()获得的内存块地址范围内,用户获得的内存块是完全有效的,示意图详见图9.14(b)。

两种常见的内存管理方法:堆和内存池

图9.15 内存对齐的处理——未多分配align-1字节的空间

实际中,由于align – 1的值往往是一些比较特殊的奇数值,例如:3、7、15、31等,经常如此分配容易把内存块的首地址打乱,出现很多非对齐的地址。因此,往往会直接多分配align字节的内存空间。

同时,出于效率考虑,在AWorks中,每次分配的内存往往都按照默认的CPU自然对齐字节数对齐,例如,在32位系统中,默认分配的所有内存都按照4字节对齐,基于此,aw_mem_alloc()函数的实现可以更新为如程序清单9.10所示的程序。 

程序清单9.10 aw_mem_alloc()函数分配的内存按照4字节对齐

两种常见的内存管理方法:堆和内存池

其中,ptr为使用aw_mem_alloc()、aw_mem_calloc()或aw_mem_align()函数分配的内存块首地址,即调用这些函数的返回值。new_size为调整后的大小。返回值为调整大小后的内存块首地址,特别地,若调整大小失败,则返回值为NULL。

例如,首先使用aw_mem_alloc()分配了存储1个int类型数据的内存块,然后重新调整内存块的大小,使其可以存储2个int类型的数据。范例程序详见程序清单9.11。

程序清单9.11 内存分配范例程序(重新调整内存大小)

两种常见的内存管理方法:堆和内存池

其中,ptr为使用aw_mem_alloc()、aw_mem_calloc()、aw_mem_align()或aw_mem_realloc()函数分配的内存块首地址,即调用这些函数的返回值。

当使用aw_mem_free()将内存块释放后,相应的地址空间将变为无效,用户不能再继续使用。释放内存块的范例程序详见程序清单9.12。

程序清单9.12 释放内存块的范例程序

两种常见的内存管理方法:堆和内存池

图9.16 初始状态——8个空闲块

在AWorks中,为便于管理,将各个空闲内存块使用单向链表的形式组织起来,示意图详见图9.17。

两种常见的内存管理方法:堆和内存池

图9.18 从链表中取出一个内存块

此时,空闲块链表中,将只剩下7个空闲块,示意图详见图9.19。

两种常见的内存管理方法:堆和内存池

图9.20 释放一个内存块

释放后,空闲链表中将新增一个内存块,示意图详见图9.21。

两种常见的内存管理方法:堆和内存池

1.  定义内存池实例

在使用内存池前,必须先使用aw_pool_t类型定义内存池实例,该类型在aw_pool.h中定义,具体类型的定义用户无需关心,仅需使用该类型定义内存池实例即可,即:

两种常见的内存管理方法:堆和内存池

为了满足各种大小的内存块需求,可以定义多个具有不同内存块大小的内存池。例如:定义小、中、大三种尺寸的内存池,它们对应的内存块大小分别为8、64、128。用户根据实际用量选择从合适的内存池中分配内存块,以在一定程度上减少内存的浪费。

2.  初始化内存池

定义内存池实例后,必须使用该接口初始化后才能使用,以指定内存池管理的内存空间,以及内存池中各个内存块的大小。其函数原型为:

两种常见的内存管理方法:堆和内存池

程序中,将1024字节的空间分成了大小为16字节的内存块进行管理。注意,出于效率考虑,块大小并不能是任意值,只能为自然对齐字节数的正整数倍。例如,在32位系统中,块大小应该为4字节的整数倍,若不满足该条件,初始化时,将会自动向上修正为4字节的整数倍,例如,块大小的值设置为5,将被自动修正为8。用户可以通过aw_pool_item_size ()函数获得实际的内存块大小。

3.  获取内存池中实际的块大小

前面提到,初始化时,为了保证内存池的管理效率,可能会对用户传入的块大小进行适当的修正,用户可以通过该函数获取当前内存池中实际的块大小。其函数原型为:

两种常见的内存管理方法:堆和内存池

运行程序可以发现,实际内存块的大小为8。

实际应用中,为了满足不同容量内存申请的需求,可以定义多个内存池,每个内存池定义不同的块大小。如定义3种块大小尺寸的内存池,分别为8字节(小)、64字节(中)、128字节(大)。范例程序详见程序清单9.15。

程序清单9.15 定义多种不同块大小的内存池

两种常见的内存管理方法:堆和内存池

其中,pool_id为初始化函数返回的内存池ID,其用于指定内存池,表示从该内存池中获取内存块。返回值为void *类型的指针,其指向获取内存块的首地址,特别地,若返回值为NULL,则表明获取失败。从内存池中获取一个内存块的范例程序详见程序清单9.16。

程序清单9.16 获取内存块范例程序

两种常见的内存管理方法:堆和内存池

其中,pool_id为初始化函数返回的内存池ID,其用于指定内存池,表示将内存块释放到该内存池中。p_item为使用aw_pool_item_get()函数获取内存块的首地址,即调用aw_pool_item_get()函数的返回值,表示要释放的内存块。

返回值为aw_err_t类型的标准错误号,若值为AW_OK,表示释放成功,否则,表示释放失败,释放失败往往是由于参数错误造成的,例如,释放一个不是由aw_pool_item_get()函数获取的内存块。注意,内存块从哪个内存池中获取,释放时,就必须释放到相应的内存池中,不可将内存块释放到其它不对应的内存池中。

当使用aw_pool_item_return()将内存块释放后,相应的内存空间将变为无效,用户不能再继续使用。释放内存块的范例程序详见程序清单9.17。

程序清单9.17 释放内存块范例程序

来源:Mr.Emiya

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2019年3月23日
下一篇 2019年3月23日

相关推荐