高并发内存池(13)-CentralCache回收内存 - 教程
高并发内存池(13)-CentralCache回收内存
这段测试代码的目的是验证页号与物理地址之间的转换是否正确,但代码中存在一个逻辑问题导致最终结果总是2000。
代码功能分析
1. 页号到地址的转换
PAGE_ID id1 = 2000;
PAGE_ID id2 = 2001;
char* p1 = (char*)(id1 << PAGE_SHIFT); // 页号转物理地址
char* p2 = (char*)(id2 << PAGE_SHIFT);
假设 PAGE_SHIFT = 13(8KB页大小):
p1 = 2000 × 8192 = 16384000p2 = 2001 × 8192 = 16384000 + 8192 = 16392192
2. 循环遍历地址
while (p1 > PAGE_SHIFT) << endl;
p1 += 8; // 每次增加8字节
}
为什么结果总是2000?
数学原理
关键在于这行代码:
((PAGE_ID)p1 >> PAGE_SHIFT)
让我们计算一下:
- 初始状态:
p1 = 2000 × 8192 = 16384000(PAGE_ID)p1 >> 13 = 16384000 ÷ 8192 = 2000
- 第一次迭代后:
p1 = 16384000 + 8 = 16384008(PAGE_ID)p1 >> 13 = 16384008 ÷ 8192 = 2000.0009765625- 整数除法截断小数部分 → 2000
- 直到接近边界:
- 只有当
p1增加到16392184时: 16392184 ÷ 8192 = 2000.999755859375→ 截断为2000- 下一个地址
16392192 ÷ 8192 = 2001.0→ 刚好等于2001
- 只有当
- 循环条件:
while (p1 < p2),当p1 = 16392192时不再小于p2- 所以永远不会打印出页号2001
可视化过程
地址范围: [16384000, 16392192) // 2000页对应的地址范围
p1从16384000开始,每次+8:
16384000 ÷ 8192 = 2000.000000 → 2000
16384008 ÷ 8192 = 2000.000976 → 2000
16384016 ÷ 8192 = 2000.001953 → 2000
...
16392184 ÷ 8192 = 2000.999755 → 2000
16392192 ÷ 8192 = 2001.000000 → 2001(但循环已结束)
正确的测试方法
如果要看到页号从2000变为2001,应该:
void TestAddressShift()
{
PAGE_ID id1 = 2000;
PAGE_ID id2 = 2001;
char* p1 = (char*)(id1 > PAGE_SHIFT) << endl;
p1 += 8192; // 按页大小递增,而不是8字节
}
}
代码的实际意义
这个测试验证了内存池中的关键算法:
- 页号 → 物理地址:
地址 = 页号 × 页大小 - 物理地址 → 页号:
页号 = 地址 ÷ 页大小
在内存回收时,CentralCache需要通过对象的地址找到对应的Span:
// 通过地址找到页号,再找到Span
PAGE_ID id = (PAGE_ID)obj >> PAGE_SHIFT;
Span* span = _idSpanMap.get(id);
这个测试确保了地址转换的准确性,这是内存池正确工作的基础!
这段代码是中心缓存(CentralCache) 中用于将内存块释放回对应Span的关键函数。我来详细解析它的工作原理:
函数功能
ReleaseListToSpans将一个内存块链表释放回对应的Span中,并管理Span的使用计数。当Span的所有内存块都归还时,将其返回给页缓存(PageCache)。
代码逐段解析
1. 初始化并加锁
size_t index = SizeClass::Index(size); // 根据对象大小找到对应的桶号
_spanLists[index]._mtx.lock(); // 加桶锁保证线程安全
2. 遍历内存块链表
while (start) // 遍历整个内存块链表
{
void* next = NextObj(start); // 保存下一个内存块的地址
// 找到该内存块所属的Span
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
这里使用MapObjectToSpan通过内存块地址找到对应的Span。
3. 将内存块插入Span的自由链表
NextObj(start) = span->_freeList; // 新内存块的next指向Span当前链表头
span->_freeList = start; // Span链表头指向新内存块(头插法)
span->_useCount--; // Span使用计数减1
头插法可视化:
Span原链表: span->_freeList → A → B → nullptr
插入start: span->_freeList → start → A → B → nullptr
4. 检查Span是否完全空闲
if (span->_useCount == 0) // 所有内存块都已归还
{
_spanLists[index].Erase(span); // 从中心缓存移除
span->_freeList = nullptr; // 清空自由链表
span->_next = nullptr; // 断开链表连接
span->_prev = nullptr;
// 这里应该将span还给PageCache,但代码中缺少这一步!
}
5. 继续处理下一个内存块
start = next; // 移动到链表中的下一个内存块
6. 释放锁
_spanLists[index]._mtx.unlock(); // 释放桶锁
在内存回收流程中的角色
graph TD
A[ThreadCache释放内存块] --> B[调用CentralCache.ReleaseListToSpans]
B --> C[遍历内存块链表]
C --> D[找到每个内存块对应的Span]
D --> E[将内存块插入Span的自由链表]
E --> F{Span使用计数是否为0?}
F -->|是| G[从CentralCache移除]
F -->|否| H[继续处理下一个内存块]
G --> I[应该将Span还给PageCache]
关键设计思想
1. 批量处理优化
- 一次处理整个内存块链表,而不是单个内存块
- 减少锁竞争和函数调用开销
2. 使用计数管理
_useCount跟踪Span中已分配的内存块数量- 当计数归零时,说明Span完全空闲,可以回收
3. 地址到Span的映射
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
这是通过_idSpanMap实现的,存储了页号到Span的映射,可以快速查找。
代码中的问题
缺少重要步骤:当span->_useCount == 0时,应该将Span返回给PageCache:
if (span->_useCount == 0)
{
_spanLists[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
// 应该添加:将span还给PageCache
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
实际应用场景
当ThreadCache中的内存块过多时,会批量归还给CentralCache:
// ThreadCache中的回收逻辑
void ThreadCache::Deallocate(void* ptr, size_t size)
{
// ... 将内存块插入thread cache的自由链表
// 如果自由链表过大,归还给central cache
if (freeList.Size() >= freeList.MaxSize())
{
freeList.PopRange(start, end, batchNum);
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
}
这个函数是内存池回收机制的核心,确保了内存块能够正确回到对应的Span中,从而实现内存的重用和合并。
这段代码是页缓存(PageCache) 中的关键函数,它的作用是通过内存块地址找到对应的Span对象。这是内存回收机制的核心组件。
函数功能
MapObjectToSpan(void* obj)接收一个内存块的地址,返回管理这个内存块的Span对象的指针。
代码逐行解析
1. 计算页号
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
作用:将内存地址转换为对应的页号。
原理:
PAGE_SHIFT通常是13(表示页大小8KB = 2¹³字节)- 右移13位相当于除以8192
- 例如:地址
16384000 >> 13 = 2000(第2000页)
2. 查找Span对象
auto ret = _idSpanMap.find(id);
作用:在映射表中查找该页号对应的Span。
3. 返回结果
if (ret != _idSpanMap.end())
{
return ret->second; // 找到,返回Span指针
}
else
{
assert(false); // 找不到,触发断言(不应该发生)
return nullptr;
}
关键数据结构
_idSpanMap映射表
这是一个关键的数据结构,存储了:
- 键(Key):页号(
PAGE_ID) - 值(Value):管理该页的Span对象指针
映射关系:
页号2000 → 管理2000页的Span对象
页号2001 → 管理2001页的Span对象
...
为什么需要这个函数?
内存回收的场景
当CentralCache要回收一个内存块时:
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{
// 需要知道这个内存块属于哪个Span
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
// 才能将内存块插入正确的Span的自由链表
}
地址到Span的映射原理
- Span管理连续内存页:一个Span可能管理多个连续的页
- 每个页对应同一个Span:Span中的所有页都映射到同一个Span对象
- 快速查找:通过页号可以快速找到管理该内存的Span
可视化示例
假设:
- 页大小:8KB
- Span管理3个页:2000、2001、2002
- 内存块地址:16384000(第2000页)
映射关系建立
// 当创建Span时,建立所有页的映射
for (PAGE_ID i = 0; i _n; ++i)
{
_idSpanMap[span->_pageId + i] = span;
}
结果:
_idSpanMap[2000] = span_idSpanMap[2001] = span_idSpanMap[2002] = span
查找过程
void* obj = (void*)16384008; // 某个内存块地址
PAGE_ID id = 16384008 >> 13; // = 2000
Span* span = _idSpanMap.find(2000)->second; // 找到对应的Span
设计优势
- O(1)时间复杂度:哈希表查找非常快速
- 精确映射:每个内存块都能找到正确的Span
- 内存高效:只需要存储页号到指针的映射,开销小
- 线程安全:通常由外层锁保护
在内存池架构中的位置
graph TD
A[内存块地址] --> B[计算页号]
B --> C[查询_idSpanMap]
C --> D[找到对应的Span]
D --> E[内存回收操作]
这个函数是内存池能够正确回收和合并内存的基础设施,确保了内存管理的精确性和高效性。
浙公网安备 33010602011771号