我认为您可能会感到困惑的一部分是,覆盖存储现有标头的内存并没有错。实际上,除了在拆分时动态插入新标头之外,您还希望通过合并合并块时有效地发生这种情况。
因此,如果我们设想一个简化示例,其中您的 0 阶块为 64 字节,而最大的 3 阶块为 256 字节,那么我们从 256 字节的 2 阶块开始,如下所示:
[nil<-header1->nil][data1 ...............................]
data1 是提供给请求内存以随意覆盖的客户端的内存。作为一个基本示例,假设您使用 8 字节标头进行 64 位对齐,它存储到相邻的前一个/下一个块的整体偏移量以及一个指示使用哪些块及其大小的标志(一个块是伙伴如果它有一个下一个/上一个偏移量,使其相邻并匹配相同的大小)。最初,这些值可以是 -1,表示没有可用的相邻块。
查看初始 256 字节 2 序块的另一种方法是需要有足够空间容纳 4 个 0 序块。所以block的总内存大小应该是256+4*8字节,也就是288字节。
现在假设客户端请求分配 128 个字节。在这种情况下,我们需要将 256 字节的 order-2 块拆分为两个 order-1 块,将它们的大小减半。在这个过程中,我们可以在中间插入一个新的header,在data1开始的地址后128字节。
[nil<-header1->header2][data1........[header1<-header2->nil][data2]........]
这个新的header2 应该有一个-1 的next 偏移量,以表明它位于列表的末尾,而之前的偏移量指向它的伙伴的header1。 header1 还应该更新它的下一个偏移量以指向 header2 并且我们应该使两者的大小为 128 字节(在块被分割之前,header1 最初存储的大小的一半)。
然后我们可以返回指向data1 的指针,供客户随意写入,并将header1 标记为正在使用中。
现在假设客户端请求释放该块。在这种情况下,我们可以将他传入的内存地址减去8个字节,得到从data1到header1。从header1也可以看出,它的邻居是header2,它目前没有使用,但与header1大小相同,适合合并。这样我们就可以合并了。
这个过程非常简单。 header1's 下一个偏移量被简单地覆盖为header2's 下一个偏移量,即 -1,并且它的大小加倍。 header2 数据仍然会在内存中徘徊,直到用户覆盖它(除非它是 calloc 样式操作,我们自己写 0 来覆盖它)。然而,它只是漂浮在内存中的一个挥之不去的人工制品,现在打算被覆盖。
[nil<-header1->nil][data1 ...............................]
这描述了一个相当基本的实现,我只介绍了如何处理拆分和合并的一种可能性。您还可以在单独的内存空间中获取头文件并将其存储,但这通常会通过某种搜索来从指针中获取适当的头信息,从而带来更多的处理开销。它对于使用最小的 0 阶块(主要是通过避免对齐要求)减少内存开销很有用,但要快速实现有点困难。当 header 存储在同一个地方时,我们可以做一些基本的事情,比如简单的减法来获取 header 内容。