首页 理论教育 操作系统实现之路:堆实现详解

操作系统实现之路:堆实现详解

时间:2023-10-21 理论教育 版权反馈
【摘要】:下面对当前版本Hello China的堆的详细实现进行描述。把虚拟区域节点插入堆的虚拟区域链表。这种事务式的处理方式,在Hello China的实现中很常见。其中,销毁当前线程所有堆的实现,也是调用了销毁单个堆的实现算法,因此,在此只简单介绍销毁单个堆的实现代码。最后,释放堆对象本身。在当前的实现中,对于堆内存的申请,最小尺寸是MIN_BLOCK_SIZE,若用户请求的内存小于该数值,则调整为该数值。其中,dwFindSize是待查找的目标空闲块的大小。

操作系统实现之路:堆实现详解

下面对当前版本Hello China的堆的详细实现进行描述。在当前的实现中,对于堆的管理是通过一个堆管理器(Heap Manager)进行的,堆管理器提供五个接口函数:创建堆函数(CreateHeap)、销毁堆函数(DestroyHeap)、销毁所有堆(DestroyAllHeap)、堆内存分配函数(HeapAlloc)和堆内存释放函数(HeapFree)。

1.堆的创建

堆的创建过程比较简单,主要过程如下:

(1)分配一块虚拟区域,虚拟区域的大小,根据用户提供的参数dwInitSize来决定,如果dwInitSize大于预定义的DEFAULT_VIRTUAL_AREA_SIZE(当前定义为16K),则按照dwInitSize申请内存,否则按照DEFAULT_VIRTUAL_AREA_SIZE来申请内存。

(2)创建一个虚拟区域头节点,来管理刚刚申请的虚拟区域对象。

(3)申请核心内存(KMemAlloc),创建一个堆对象。

(4)把虚拟区域节点插入堆的虚拟区域链表

(5)把申请的虚拟区域,作为第一块空闲内存块,插入空闲链表。

(6)然后把上述初始化完成的堆对象,插入当前线程的堆对象链表。

(7)如果上述所有操作成功,则返回堆的起始地址,否则返回NULL,指示失败。

代码如下所示。为了方便,我们分段解释:

上述代码调用VirtualAlloc(GET_VIRTUAL_AREA实际上是VirtualAlloc的宏定义),申请一块虚拟区域,该虚拟区域的大小,为dwInitSize和DEFAULT_VIRTUAL_AREA_SIZE中最大者,然后把申请的虚拟区域看作一块空闲内存块,并初始化其头部。需要注意的是,这块空闲块的大小,是虚拟区域的大小减去空闲块控制头的大小(16B),因为要考虑空闲块控制头所占用的大小。

上述代码创建了一个虚拟区域节点对象,这个节点对象的唯一用途,就是把虚拟区域连接到堆的虚拟区域链表中。

上述代码创建了一个堆对象(__HEAP_OBJECT),并把该堆对象初始化。其中,FreeBlockHeader是空闲链表的头节点,这个头节点仅仅是用来完成空闲块的连接,不代表任何空闲块,因此其尺寸设置为0。完成堆对象的创建,并初始化后,把创建的堆对象插入当前线程的堆链表。需要注意的是,由于堆是基于线程创建的,不存在与其他线程竞争资源的情况,而且当前线程对象的堆链表,也不会被中断处理程序修改,因此上述代码无需保护。

下面的代码,把申请的虚拟区域,加入了当前堆的虚拟区域列表,然后返回创建的堆的基地址。当然,如果上述处理中发生错误,则会导致该函数失败,在这种情况下,函数返回前,需要撤销已经申请的资源。这种事务式的处理方式,在Hello China的实现中很常见。

2.堆的销毁

堆的销毁分两种情况:一种情况是销毁单个堆,另外一种情况是销毁当前线程的所有堆。其中,第二种情况,一般在线程结束的时候,由线程结束收尾函数(KernelThreadWrapper函数)调用,用来释放当前线程尚未释放的所有堆对象。对于第一种情况,一般是由应用程序编写者调用,用来撤销自己创建的堆。

其中,销毁当前线程所有堆(DestroyAllHeap)的实现,也是调用了销毁单个堆的实现算法,因此,在此只简单介绍销毁单个堆的实现代码。对于堆的撤销操作,比较简单,所需要完成的,只是内存资源的回收而已。在DestroyHeap的实现中,完成下列动作:

(1)从当前线程的堆链表中,删除要销毁的堆对象(该对象作为函数的参数传递)。

(2)释放该堆对象申请的所有虚拟区域。

(3)然后把虚拟区域链表占用的内存释放,即虚拟区域节点占用内存。

(4)最后,释放堆对象本身。

代码比较简单,摘录如下(为了看起来方便,删除了一些无关紧要的代码,比如注释、参数安全检查等):

3.堆内存申请

HeapAlloc函数完成堆内存的申请,该函数接受两个参数:目标堆对象和待申请内存的大小。若申请成功,则返回成功申请的内存基地址,否则返回NULL。该函数完成如下动作:

(1)首先,做一个堆归属的合法性检查,即判断用户提供的堆对象,是不是属于当前线程。若属于当前线程,则继续下一步的动作,否则直接返回NULL。

(2)检查空闲链表,以寻找一块大于用户请求大小的空闲内存块。

(3)如果能够找到这样的内存块,则根据内存块的大小,确定是否需要进一步拆分,还是直接返回用户。

(4)如果需要拆分,则把找到的内存块拆分成两块,然后把拆分后的两块内存块中的后一块,重新插入空闲链表,把第一块返回用户。若不需要拆分,则把找到的空闲块,直接从空闲链表中删除,然后返回用户。

(5)如果从空闲链表中无法找到满足要求的空闲块,则调用VirtualAlloc函数,重新分配一个虚拟区域,并把该区域当成一块空闲块,进行上述处理。

(6)返回用户分配的内存块地址,或者在失败的情况下,返回NULL。

实现代码如下:

上述代码完成了堆归属检查,并重新计算了用户所申请的内存块的大小。在当前的实现中,对于堆内存的申请,最小尺寸是MIN_BLOCK_SIZE(定义为16B),若用户请求的内存小于该数值,则调整为该数值。其中,dwFindSize是待查找的目标空闲块的大小。之所以在dwSize的基础上,增加MIN_BLOCK_SIZE和控制头的尺寸,是为了确保任何空闲块的可用尺寸,都应该大于MIN_BLOCK_SIZE。在找到一块空闲块之后,若该空闲块的大小大于dwFindSize,则需要对该空闲块进行分割,否则无需分割,直接返回给用户。(www.xing528.com)

上述代码完成空闲块链表的搜索,一旦找到一块满足要求尺寸(dwSize)的空闲块,则进一步判断该空闲块的大小,是否大于dwFindSize。若大于,则可以进行进一步拆分,否则直接返回用户找到的内存块。在拆分的情况下,把拆分后的第二块内存,重新插入空闲链表。

这时候,就可以很清楚地解释为什么只有在大于dwFindSize的时候,才需要拆分了。因为在拆分后,实际上还需要在空闲块的开头,预留16B(空闲控制头的大小),作为空闲块的控制头,这样若找到的空闲块小于dwFindSize,则无法保证拆分后空闲块的大小会大于MIN_BLOCK_SIZE(这是空闲块的最小尺寸)。

如果无法从空闲链表中找到满足的空闲块,则需要扩充堆的内存池了,这时候,需要调用VirtualAlloc函数,从系统空间中重新申请一个虚拟区域,然后把该虚拟区域当作一个空闲块对待,插入堆的空闲链表,这时候,还需要把虚拟区域插入堆的虚拟区域链表。代码如下:

若调用VirtualAlloc也失败,则HeapAlloc只能返回NULL了,否则,在成功调用VirtualAlloc的情况下,堆的空闲链表中会增加一块满足要求的内存空闲块(新申请的虚拟区域),这时候,重新调用HeapAlloc,肯定是成功的,因此,HeapAlloc递归调用自己,然后把计算结果返回给用户。

4.堆内存释放

HeapFree函数完成堆内存的释放,该函数接受两个参数:待释放内存的起始地址,以及所属的堆对象。内存释放函数完成下列操作:

(1)首先,把待释放的内存(实际上是一个空闲块),修改标志并插入空闲链表。

(2)找到该空闲块所属的虚拟区域。

(3)对该虚拟区域,发起一个合并空闲内存块的操作。

(4)完成块的合并操作后,检查当前虚拟内存区域是不是一个完整的内存块,即不包含尚未释放的内存,若是,则调用VirtualFree函数,把该虚拟区域返回系统,否则函数就直接返回。

之所以增加第四步操作,是为了实现一种“按需分配”的目的,一个策略就是,尽量返回不用的资源给操作系统,这样不会造成资源的浪费。如果不进行上述第四步操作,则可能出现当前线程申请了大量的虚拟区域却闲置不用,而其他线程申请内存失败的情况。

HeapFree函数代码如下:

上述代码根据待释放内存的起始地址,找到该内存块对应的控制头,然后检查控制头的标记,若标记不是空闲,则属于一种异常情况,否则继续执行。

上述代码检查待释放内存属于哪个虚拟区域,找到对应的虚拟区域,目的是为了完成一个合并操作。若待释放内存无法与一个虚拟区域对应,则可能是一个不正常的操作,因此直接返回。

上述代码把待释放的空闲块,插入当前堆的空闲链表中,然后发起一个合并操作,合并的对象,就是待释放内存所属的虚拟区域。对于合并过程,后面会详细解释。

在完成了虚拟区域的合并之后,则检查当前虚拟区域本身是不是一个完整的空闲块。若是,则调用VirtualFree函数释放该虚拟区域,否则函数返回。对于虚拟区域本身是不是一个空闲块的判断方式十分简单,只需要判断虚拟区域的第一个内存块的长度,加上控制头,是否等于整个虚拟区域大小即可。若是,则整个虚拟区域是一个空闲块,否则就不是。

下面的CombineBlock函数,完成了特定虚拟区域的空闲内存块合并工作。该函数操作过程如下:

(1)从第一个空闲块开始,判断是否有连续的两个内存块,其状态都是空闲。这里的连续,是内存块地址的连续(相邻),而不是空闲块在空闲链表中的连续。

(2)如果发现这样的两块内存,则把第二块从空闲链表中删除,合并到第一块中。

(3)继续执行上述操作,直到到达虚拟区域的末端。

代码如下:

其中,lpEndAddr是一个结束标志,一旦待合并内存块的控制头地址跟该地址相同,则说明合并已经结束。

初始化lpFirstBlock和lpSecondBlock变量,使得lpFirstBlock指向当前虚拟区域中的第一个内存块,lpSecondBlock指向第二个内存块,然后进入下面的循环。

上述循环比较简单,只是判断lpFirstBlock的标志字段是否为空闲(FREE),若是,则进一步判断lpSecondBlock是否也空闲,如果是,则具备合并条件,合并lpFirstBlock和lpSecondBlock,然后更新lpSecondBlock,重新进入循环,否则(lpFirstBlock和lpSecondBlock中至少一个是非空闲块)更新lpFirstBlock为lpSecondBlock,更新lpSecondBlock为lpSecondBlock的相邻块,继续下一轮迭代。

5.malloc和free的实现

malloc和free是标准C运行时库函数,实现这两个函数,对于代码的移植十分有意义。很明显,采用HeapAlloc和HeapFree函数可以很容易地模拟这两个函数,但HeapAlloc和HeapFree,都接受一个堆对象指针作为参数,而malloc和free则没有。因此,为了屏蔽这个差异,我们在核心线程对象(__KERNEL_THREAD_OBJECT)中引入一个缺省堆的变量,所有malloc和free函数的内存申请,都从缺省堆中进行。

下面是malloc的实现代码:

首先,该函数判断当前线程的缺省堆对象(lpDefaultHeap)是否存在,若存在(不为NULL),则直接以缺省堆为目标堆,调用HeapAlloc函数,把该函数的结果返回给用户,否则,该函数首先调用CreateHeap,创建一个缺省堆,并初始化当前线程的lpDefaultHeap变量,在此基础上,再调用HeapAlloc函数。

很明显,只有malloc函数第一次调用的时候,可能会发生lpDefaultHeap为NULL的情况,后续调用的时候,不会出现这种情况。

free函数的实现更简单:

首先,该函数确保当前线程的缺省堆是存在的,然后调用HeapFree来释放具体的内存。否则可能是一种不正常操作,直接导致free返回。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈