6.2 堆的功能需求定义
在实现具体的堆功能前,需要详细考虑如何定义堆的功能,即在软件工程中所谓的“需求分析”。这个过程是十分关键的,在这个过程中,需要把待实现的系统(或一个简单的功能模块)的具体功能进行完整、详尽的定义和描述,且一旦固定(比如通过了技术评审),将不被改变。这样在该系统的实现过程中,可避免频繁修改功能需求导致的大量反复工作。在堆的实现中,我们充分考虑用户编程的方便,并向标准的C运行库靠拢,我们这样定义堆功能。
(1)堆是基于线程实现的,即一个堆只属于一个线程,但一个线程可以具有多个堆;
(2)为了管理和实现上的方便,采用一个统一的接口——堆管理器(HeapManager)来管理系统中所有的堆;
(3)堆是一个内存池,根据用户需要,从系统中“批发”申请内存,并“零售”给用户;
(4)除了堆的创建、销毁接口之外,堆管理器还应该提供“内存分配”和“内存释放”两个接口,供应用程序调用;
(5)应用程序调用“内存分配”和“内存释放”函数的时候,所操作的堆对象只能是当前线程的堆对象;
(6)其中,“内存分配”和“内存释放”接口函数所需要的参数,能够与标准C运行库函数malloc和free的参数相互映射,这样可通过函数封装或宏定义的方式,通过堆的“内存释放”和“内存分配”函数,来实现free和malloc函数;
(7)在存在多个堆的情况下,应该有一个缺省堆,来对应malloc和free函数,这样这两个函数可以从缺省堆中分配内存;
(8)除非出现系统内存不足的情况,否则堆功能函数“内存申请”和“内存释放”函数不能失败。
其中,上述第(1)条的含义在于,一个线程可能有多个堆对象,而一个堆对象只能属于一个线程。这样的实现,可以具有更大的灵活性,一个线程(或用户应用程序)可能是由若干功能模块组成的,这些功能模块可能互不交叉,比如一个文字处理系统的编辑模块和打印模块等。这样为了实现上的一致性和清晰性,每个功能模块可以单独创建一个自己的内存堆,在申请内存的时候,可从自己的内存堆中申请。当然,这仅仅是一种可选的实现,一个线程的不同模块完全可以公用一个堆,完全可以调用malloc和free函数(这些函数都是作用在线程的缺省堆上的)来实现内存管理。
在上述第(3)条的功能定义中,堆作为一个内存池,在初始化(创建过程中)的时候,就需要事先从系统中申请一部分内存(比如,16KB)作为一个内存池,一旦用户有内存分配需求,就从该内存池中进行分配。若内存池中的内存分配完毕,则需要通过调用VirtualAlloc函数从系统中再次申请内存,并加入内存池中。若用户释放内存,则释放的内存会被重新加入内存池,在积累到一定程度的时候,堆对象会对内存池进行清理,把暂时用不到的大块内存返回系统。这样做的一个好处是可以实现系统内存的按需分配,不至于出现大规模的内存浪费现象。
在上述第(5)条定义中,应用程序只能操作自己的堆,而不能从其他应用程序(线程)的堆中申请内存。这样的实现是符合逻辑的,且实现起来相对简便,无须考虑多线程之间的同步。另外,在中断处理程序中,也不能调用堆功能函数来从堆中分配内存,而应该调用KMemAlloc或VirtualAlloc函数来分配内存。
malloc和free函数是标准C运行库提供的接口函数,实现这两个函数对于代码的移植(把其他操作系统上的应用程序代码,移植到Hello China上)非常有帮助。而且一般的程序员都十分熟悉这两个接口函数,鉴于此,在Hello China当前的堆实现中,通过引入一个默认堆(缺省堆)的对象,通过标准的堆操作接口实现这两个C运行库函数。
在上述第(8)条功能描述中,实际上是提出了一种“按需分配”的实现方式,即开始的时候,堆对象先从系统中申请少量内存作为内存池,等该内存池分配完毕,或用户提出申请的内存尺寸大于这个内存池的时候,堆对象再调用VirtualAlloc函数,从系统空间中申请额外内存池。在这种实现方式下,堆管理器可以根据需要来申请系统内存池,从而做到尽可能的节约系统内存。堆对象也可以被认为是一个内存申请代理机构和缓冲机构,对于数量小的内存块申请,堆直接从本地内存池中分配,而对于一些大块内存的申请,堆管理器也可以作为一个中转代理,从系统中申请。当然,建议应用程序开发者在需要数量比较大的内存的时候(比如,超过页面尺寸4KB大小),直接调用VirtualAlloc函数申请,因为经过堆申请,堆也可能调用VirtualAlloc函数,这样经过堆的中转,会导致性能的少量下降。当然,对于小块的内存申请,若堆的内存池可以满足要求,则不会存在这个问题。