一、总体结构
页缓存资源层的结构与前两层不同了:
- 线程资源层ThreadCache的结构是一个数组里放很多个桶,每个桶里放大小依次递增的内存块
- 中心资源曾CentreCache的结构是一个数组里放很多个桶,每个桶里放着很多个Span包,每个Span包里放着很多个内存块,内存块的大小取决于Span包在哪个桶里,依次递增
- 页缓存资源层PageCache的结构也是一个数组里放很多个桶,每个桶里也放着很多个Span包,但是每个Span包里存放的不是一个个小内存块了,而是一个完整的,占着X页的大号内存块(X为桶的编号),每页的大小是由程序里的PAGE_SHIFT常量指定的。
如图所示:
设定最大页桶为128页
二、分配机理
(1) 当中心资源层向页缓存资源层申请Span块时,首先应把所需的内存块大小转换为页数,然后向PageCache申请合适的页Span块。
- 例如1页的大小为8KB,中心资源层缺少64KB大小的Span块,经计算决定向PageCache申请4*64KB=256KB大小的空间
- 页缓存资源层计算256KB/8KB=32页,因此将分配一个包含着32页内存的页Span块给中心资源层
- 中心资源层得到包含着32页内存得页Span块后,对该页Span块进行切分,把页Span块内的连续内存,分割成一个个64KB的内存块,这样页Span块就转化成了中心资源层的普通Span块了
- 中心资源层将该Span块放入到对应的桶(SpanList)里去,就获得了一个新的Span块。
如图所示:
(2)如果,页缓存资源层的32页桶中没有页Span块了呢?
这就是页缓存资源层与前面两层最不同的地方了,前两层此时将向下层申请资源,但页缓存资源层已经是最低层了!此时必须加全锁(中心资源层是桶锁,因为桶与桶之间没有交互,桶锁即可),因为页缓存资源层一旦某个桶里没有页Span块了,立刻就会向更大的桶里去寻找页Span块,先是去33页桶去找,还没有就去34页桶去找......一旦找到,那么就从这个桶里取出页Span块,并对其进行切割!
比如从40页桶里找到了页Span块,那么就将它切割为32页Span块和8页Span块,将32页Span块返回给中心资源层,8页Span块插入到8页桶里去。在这个过程中一旦有其它线程干扰,就会产生线程安全问题,因此在页缓存资源层必须加全锁。
(3)如果,直到找到最大的128页页桶还是没有找到页Span块呢?
事实上,这一步正是向整个并发内存池中添加内存资源的一步!
当128号页桶仍然没有页Span时,就该向系统申请资源了。通过调用Windows下的VirtualAlloc函数(Linux下为mmap函数),以页为单位像系统申请空间,一次申请128页。
#include "tool.hpp"
#include <iostream>
#if _WIN32
#include <Windows.h>
#else
#include <unistd.h>
#include <sys/mman.h>
#endif // _WIN32
using std::bad_alloc;
static const size_t SIZEofPAGE = 1 << PAGE_SHIFT;
inline static void* systemalloc(size_t kpage)
{
void* ptr = nullptr;
#ifdef _WIN32
ptr = VirtualAlloc(NULL, kpage * SIZEofPAGE, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
ptr = mmap(NULL, kpage * SIZEofPAGE, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);//Linux下采用mmap接口,由操作系统在空闲堆区中选择一块合适的空间返回
#endif
if (ptr == NULL)
{
throw bad_alloc();
}
else
return ptr;
}
//注意,无论是VirtualAlloc还是mmap函数,接口返回的地址一定是程序中定义的页大小(4K或8K)的倍数
注意:参考这篇文章VirtualAlloc函数与mmap函数所申请空间内存对齐的问题可知,系统返回的地址一定能是被程序定义的页的大小所整除的,无论是4KB还是8KB大小。
重新回顾Span块的构成:
//内存包span,里面包含了连续页的内存,是中心资源层和页缓存资源层的重要结构
struct Span
{
PAGE_ID pageID = 0;//包中起始页在内存所有页中的编号(把内存视为一个个页,页编号计算方法为页起始地址/每页的大小)
size_t counts = 0;//包中页的数量
Span* prev = nullptr;
Span* next = nullptr;
void* freelist = nullptr;//里面存放着span内存包中被切好的内存块的“桶”
size_t usedBlocks = 0;//已经被分配出去的内存块数量
};
那么现在可以明白了,这个pageID就是从堆申请来的内存空间的起始页的页号:
这个从堆中申请来的占128页的超大内存块被与Span结构体绑定后,就插入到了页缓存资源层,这样一来,页缓存资源层就有资源给中心资源层调用了。比如中心资源层的请求是一个10页大小的页Span块,那么过程切割过程如下图所示:
到此,并发内存池三层的申请内存这条路线已经大致介绍完毕,接下来便是回收内存与性能测试和调优了。