tcmalloc的源码考虑了内存的大小和链表的长度。这里实现的是简单的版本之考虑了链表的长度? 。当释放内存块后会挂到线程缓冲区对应的自由链表上,当检测到当前自由链表的长度大于我们一次批量申请的大小之后就会将当前自由链表的一部分(一个批量)还给centralcacahe。不是全拿走,有可能有剩余个其他先线程使用。centralcache拿到以后要继续向pagecache归还
计算出来那个桶节点的自由链表后,先把当前块的内存的还给自由链表。在判断当前链表的长度是否需要归还给centralcache。
//线程缓冲区归还内存
void* ThreadCache::Deallocate(void* obj, size_t size) //需要知道内存大小好计算在哪个桶位置的自由链表上插入
{
assert(obj);
assert(size <= MAX_BYTES);
size_t index = SizeRules::Index(size); //寻找还回来的内存块可以插入桶节点的位置
_freelists[index].Push(obj); //插入得到的内存块
//当换回来的内存块超过一定值的时候,统一还给span内存块(central catche)
//这里用一次批量申请的上限作为阀值(慢启动算法决定的那个值)
if (_freelists[index].MemSize() >= _freelists[index].MaxSize()) //不一定memsize从0开始,_freelist挂的是没使用的内存若申请多了没使用memsize就不是0
{
//合并自由链表的小内存块并统一返还给centralcache
ListTooLong(_freelists[index], size);
}
return obj;
}
释放给centralcache的中间逻辑。需要归还的话再把自由链表切下来一块还给span
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
//先找到自由链表中的要返还的内存块
void* start = nullptr;
void* end = nullptr;
list.PopRange(start, end, list.MaxSize()); //每次直接返回每批次的最大值,有可能会留下一部分
CentralCache::GetCent()->ReleaseListToSpans(start, size);
}
首先根据线程缓冲区要释放的每个节点大小计算出这些节点应该来自于多大的span,从而确定桶下标。同时因为我们可以通过物理地址计算出页号,只要在当前自由链表下找到对应的页号,这样就能找到要归还的span块所在的自由链表的位置。(注意,要归还的自由链表节点有可能不连续且来自多个span。)
但每次遍历整个链表效率过于低下n^2的算法,可以采用哈希表直接映射。构建页号和页块的映射关系。这样我们通过物理地址计算出页号后能直接找到页块位置。这套逻辑由于pagecache也要使用所以写在pagecache模块里
在通过获取到span块后,将当前小内存块还给span然后继续归还,指导整段自由链表全都被归还给span。
这样挂回span块的自由链表其实是乱序的,但并不影响使用,因为所有span块里已经记录了当前区间下自由链表的起始页号。所以链表可以直接置空。
通过对span块内的usecount(使用自由链表节点数)的监控在当前span块全部被归还的时候继续将span块归还给pagecache,同时注意锁切换的问题
void CentralCache::ReleaseListToSpans(void* start, size_t size)//注意点由于释放内存自由链表的顺序不固定,所以可能来自于不同的span
{
//先计算出释放的内存大小对应的桶节点
size_t index = SizeRules::Index(size);
//由于访问中央内存区,需要加锁
_spanList[index].Getmtx().lock();
//将所有节点归还给对应的span
while (start)
{
//找到当前内存对应的span
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);
//自由链表的内存块依次头插进span
void* next = NextObj(start); //保存
NextObj(start) = span->_freeList;//头插
span->_freeList = start;
span->_useCount--;
if (span->_useCount == 0) // 说明span的切分出去的所有小块内存都回来了
{ // 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
_spanList[index].Erase(span); //在当前桶节点里删掉这个span块还给page
span->_freeList = nullptr;
span->_prev = nullptr;
span->_next = nullptr;
//继续往下走pagecache的合并逻辑
//和申请一样这里不用再用桶锁了,转为使用page的整体锁
_spanList[index].Getmtx().unlock();
PageCache::GetInstance()->Getmtx().lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->Getmtx().unlock();
//出page后再把central的锁加回来
_spanList[index].Getmtx().lock();
}
start = next;
}
_spanList[index].Getmtx().unlock();
}
上文说过,因为我们需要页号和页块的映射关系。所以在申请的时候也需要在申请k页span的时候增加上映射关系。
通过物理地址获得span块(哈希表结构)
//通过释放内存块的物理地址计算出来自哪个内存块
Span* PageCache::MapObjectToSpan(void* obj) //传入当前内存块的起始位置
{
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT); //先通过物理地址和页号之间的关系计算出页号。一个页号可以对应多块物理地址。
std::unique_lock<std::mutex> lock(_pagemtx);
auto ret = _idSpanMap.find(id); //哈希映射通过id找到span
if (ret != _idSpanMap.end()) //找到了
{
return ret->second;
}
else //逻辑正确的情况下不应该找不到
{
assert(false);
return nullptr;
}
}
pagecache拿到还回来的span内存块的时候要进行内存合并,可以通过上面的哈希映射关系判断前后页号的span块是否被使用,在pagecache的都可以合并,在centralcache的不能被合并。重点,不能用span下的 usecount来判断当前span块能否被合并,有一种间隙情况, k页的span刚被申请出来还没有被切分,此时usecout仍让是0,若此时被后一个span合并就会产生冲突,需要新增一个变量记录是否被使用。
所有切分出去和没切分出去的span块都需要通过页号建立映射关系,但注意,没切分出去的span块只需要在span块的开头和结尾记录映射关系即可,因为合并的时候只能从前面或者后面搜索映射关系。当他要被使用的时候才会被构建更为详细的映射关系。
拿到span块后不断合并,这是形成一个更大的span块,直到无法合并的时候。最后只有当合并的内存块等于128的时候才会真正的进入内存释放阶段。同时span块的合并就是页号的改变,同时删掉被合并页块的属性结构体,此时不再关注底层的自由链表,自由链表在centralcache阶段就已经被释放了。
//将释放回来的span快整合并划给pagecache
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于128页的直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageid << PAGE_SHIFT);
Memreq::SystemFree(ptr);
_spanPool.Delete(span);
return;
}
else
{
//不断让页前后合并以解决内存外碎片问题
while (1)
{
//向前合并
PAGE_ID prev_id = span->_pageid - 1; //因为要合并连续页号我们首先要判断当前位置的前一个页号是否存在(有可能并未申请过)
auto ret = (Span*)_idSpanMap.get(prev_id);
//前一个页号不存在无法合并
if (ret == nullptr)
{
break;
}
//走到这里说明前一个页号存在继续判断是否正在使用
Span* prevSpan = ret; //拿到这个页号对应的span
if (prevSpan->_isUse == true)
{
break;
}
//合并后span的大小要小于最大的span值
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_pageid = prevSpan->_pageid;
span->_n += prevSpan->_n;
//合并后要把spanlist上的被合并的span删除掉
_spanlists[prevSpan->_n].Erase(prevSpan);
_spanPool.Delete(prevSpan);
}
while (1)
{
//向后合并
PAGE_ID next_id = span->_pageid + span->_n; //因为要合并连续页号我们首先要判断当前位置的前一个页号是否存在(有可能并未申请过)
auto ret = (Span*)_idSpanMap.get(next_id);
//前一个页号不存在无法合并
if (ret == nullptr)
{
break;
}
//走到这里说明前一个页号存在继续判断是否正在使用
Span* nextSpan = ret; //拿到这个页号对应的span
if (nextSpan->_isUse == true)
{
break;
}
//合并后span的大小要小于最大的span值
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_n += nextSpan->_n;
//合并后要把spanlist上的被合并的span删除掉
_spanlists[nextSpan->_n].Erase(nextSpan);
_spanPool.Delete(nextSpan);
}
//span已经合并完成重新添加进spanlist
_spanlists[span->_n].PushFront(span);
//并在pageid和span之间的映射关系上加上自己,等同于申请span时没使用的span
_idSpanMap.set(span->_pageid,span);
_idSpanMap.set(span->_pageid + span->_n - 1, span);
//当前span合并完毕可以被其他块合并
span->_isUse = false;
}
}