✨个人主页:熬夜学编程的小林
💗系列专栏: 【C语言详解】 【数据结构详解】【C++详解】【Linux系统编程】【Linux网络编程】【项目详解】
目录
1、释放内存过程联调测试
之前我们在测试申请流程时,让单个线程进行了五次内存申请,但是申请五次不能够直接把内存释放完,因此我们此处申请三次内存,看看这其中的释放流程是如何进行的。
void TestConcurrentAlloc3()
{
void* p1 = ConcurrentAlloc(6);
void* p2 = ConcurrentAlloc(8);
void* p3 = ConcurrentAlloc(1);
void* p4 = ConcurrentAlloc(7);
void* p5 = ConcurrentAlloc(8);
cout << p1 << endl;
cout << p2 << endl;
cout << p3 << endl;
cout << p4 << endl;
cout << p5 << endl;
ConcurrentFree(p1, 6);
ConcurrentFree(p2, 8);
ConcurrentFree(p3, 1);
ConcurrentFree(p4, 7);
ConcurrentFree(p5, 8);
}
1、这三次申请和释放的对象大小进行对齐后都是8字节 ,因此对应操作的就是thread cache和central cache的第0号桶,以及page cache的第1号桶。
2、由于第三次对象申请时,刚好将thread cache第0号桶当中仅剩的一个对象拿走了 ,因此在三次对象申请后thread cache的第0号桶当中是没有对象的。
3、通过监视窗口可以看到,此时thread cache第0号桶中自由链表的_maxSize已经慢增长到了3 ,而当我们释放完第一个对象后,该自由链表当中对象的个数只有一个 ,因此不会将该自由链表当中的对象进一步还给central cache。

当第二个对象释放给thread cache的第0号桶后 ,该桶对应自由链表当中对象的个数变成了2 ,也是不会进行ListTooLong操作的。

直到第三个对象释放给thread cache的第0号桶时 ,此时该自由链表的_size的值变为3,与_maxSize的值相等 ,现在thread cache就需要将对象给central cache了。

thread cache先是将第0号桶当中的对象弹出MaxSize个 ,在这里实际上就是全部弹出,此时该自由链表_size的值变为0,然后继续调用central cache当中的ReleaseListToSpans函数,将这三个对象还给central cache当中对应的span。

在进入central cache的第0号桶还对象之前 ,先把第0号桶对应的桶锁加上,然后通过查page cache中的映射表找到其对应的span,最后将这个对象头插到该span的自由链表中,并将该span的_useCount进行--。当第一个对象还给其对应的span时,可以看到该span的**_useCount减到了2**。

由于我们只进行了三次对象申请,并且这些对象大小对齐后大小都是8字节 ,因此我们申请的这三个对象实际都是同一个span切分出来的。当我们将这三个对象都还给这个span时,该span的_useCount就减为了0。

现在central cache就需要将这个span进一步还给page cache ,而在将该span交给page cache之前,会将该span的自由链表以及前后指针都置空。并且在进入page cache之前会先将central cache第0号桶的桶锁解掉,然后再加上page cache的大锁,之后才能进入page cache进行相关操作。

由于这个一页的span是从128页的span的头部切下来的,在向前合并时由于前面的页还未向系统申请,因此在查映射关系时是无法找到的,此时直接停止了向前合并。

同理, 在向后合并时由于后面的页还未向系统申请,因此在查映射关系时是无法找到的,此时直接停止了向后合并。
将这个1页的span插入到第1号桶,然后建立该span与其首尾页的映射 ,便于下次被用于合并,最后再将该span的状态设置为未被使用的状态即可。

当从page cache回来后 ,除了将page cache的大锁解掉,还需要立刻加上central cache中对应的桶锁,然后继续将对象还给central cache中的span,但此时实际上是还完了,因此再将central cache的桶锁解掉就行了。

至此我们便完成了这三个对象的申请和释放流程。
2、大于256KB的大块内存申请问题
2.1、申请内存过程
前面说到,每个线程的thread cache是用于申请小于等于256KB的内存的,而对于大于256KB的内存 ,我们可以考虑直接向page cache申请,但page cache中最大的页也就只有128页,因此如果是大于128页的内存申请 ,就只能直接向堆申请了。
申请内存的大小 | 申请方式 |
---|---|
X<=256KB(32页) | 向thread cache申请 |
32页 < X <= 128页 | 向page cache申请 |
x >= 128页 | 向堆申请 |
注意:当申请的内存大于256KB时,虽然不是从thread cache进行获取,但在分配内存时也是需要进行向上对齐的,对于大于256KB的内存我们可以直接按页进行对齐。
我们之前实现RoundUp函数时,对传入字节数大于256KB的情况直接做了断言处理,因此这里需要对RoundUp函数稍作修改。
static inline size_t RoundUp(size_t size)
{
if (size <= 128) // 1-128 按照8字节对齐
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size, 16);
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else
{
// 大于256KB按页对齐
return _RoundUp(size, 1 << PAGE_SHIFT);
}
}
对于之前的申请对象的逻辑就需要进行修改 了,当申请对象的大小大于256KB时 ,就不用向thread cache申请了,这时先计算出按页对齐后实际需要申请的页数,然后通过调用NewSpan申请指定页数的span即可。
// 申请内存
static void* ConcurrentAlloc(size_t size)
{
if (size > MAX_BYTES)
{
// 计算出对齐后需要申请的页数
size_t alignSize = SizeClass::RoundUp(size);
size_t kPage = alignSize >> PAGE_SHIFT;
// 向page cache申请kPage页的span
PageCache::GetInstance()->_pageMtx.lock(); // 加大锁
Span* span = PageCache::GetInstance()->NewSpan(kPage);
PageCache::GetInstance()->_pageMtx.unlock(); // 解除大锁
void* ptr = (void*)(span->_pageID << PAGE_SHIFT);
return ptr;
}
else
{
// 通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
pTLSThreadCache = new ThreadCache;
}
cout << std::this_thread::get_id() << ":" << pTLSThreadCache << endl;
return pTLSThreadCache->Allocate(size);
}
}
也就是说,申请大于256KB的内存时 ,会直接调用page cache当中的NewSpan函数进行申请,因此这里我们需要再对NewSpan函数进行改造,当需要申请的内存页数大于128页时 ,就直接向堆申请对应页数的内存块。而如果申请的内存页数是小于128页的 ,那就在page cache中进行申请,因此当申请大于256KB的内存调用NewSpan函数时也是需要加锁的,因为我们可能是在page cache中进行申请的。
// 获取一个K页的span(加映射版本)
Span* PageCache::NewSpan(size_t k)
{
// assert(k > 0 && k < NPAGES); // 保证在桶的范围内
assert(k > 0);
// 大于128页直接找堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);
Span* span = new Span;
span->_pageID = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
// 建立页号与span之间的映射
_idSpanMap[span->_pageID] = span;
return span;
}
// 1.检查第k个桶里面有没有Span,有则返回
if (!_spanLists[k].Empty())
{
Span* kSpan = _spanLists[k].PopFront();
// 建立页号与span的映射,方便central cache回收小块内存查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageID + i] = kSpan;
}
return kSpan;
}
// 2.检查一下后面的桶里面有没有span,有则将其切分
for (size_t i = k + 1; i < NPAGES; i++)
{
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
// 在nSpan的头部切一个k页下来
kSpan->_pageID = nSpan->_pageID;
kSpan->_n = k;
// 更新nSpan位置
nSpan->_pageID += k;
nSpan->_n -= k;
// 将剩下的挂到对应映射的位置
_spanLists[nSpan->_n].PushFront(nSpan);
for (PAGE_ID i = 0; i < kSpan->_n; i++)
{
_idSpanMap[kSpan->_pageID + i] = kSpan;
}
return kSpan;
}
}
// 3.没有大页的span,找堆申请128页的span
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1); // 申请128页内存
bigSpan->_pageID = (PAGE_ID)ptr >> PAGE_SHIFT;
bigSpan->_n = NPAGES - 1; // 页数
// 将128页span挂接到128号桶上
_spanLists[bigSpan->_n].PushFront(bigSpan);
// 递归调用自己(避免代码重复)
return NewSpan(k);
}
2.2、释放内存过程
当释放对象时,我们需要判断释放对象的大小:
因此当释放对象时,我们需要先找到该对象对应的span ,但是在释放对象时我们只知道该对象的起始地址 。这也就是我们在申请大于256KB的内存时,也要给申请到的内存建立span结构,并建立起始页号与该span之间的映射关系的原因。此时我们就可以通过释放对象的起始地址计算出起始页号,进而通过页号找到该对象对应的span。
// 释放内存
static void ConcurrentFree(void* ptr, size_t size/*暂时*/)
{
// 大于256KB的内存释放
if (size > MAX_BYTES)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
因此page cache在回收span时也需要进行判断,如果该span的大小是小于等于128页的 ,那么直接还给page cache就行了,page cache会尝试对其进行合并。而如果该span的大小是大于128页的 ,那么说明该span是直接向堆申请的,我们直接将这块内存释放给堆,然后将这个span结构进行delete就行了。
// 释放空闲的span回到PageCache,并合并相邻的span
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 大于128页直接释放给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageID << PAGE_SHIFT);
SystemFree(ptr);
delete span;
return;
}
// 对span的前后页,尝试进行合并,缓解内存碎片问题
// 1、向前合并
while (true)
{
PAGE_ID prevID = span->_pageID - 1;
auto ret = _idSpanMap.find(prevID);
// 前面的页号还没有(还未向系统申请),停止向前合并
if (ret == _idSpanMap.end())
{
break;
}
// 前面的页号对应的span正在被使用,停止向前合并
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{
break;
}
// 合并出超过128页的Span无法管理,停止向前合并
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
// 进行后前合并
span->_pageID = prevSpan->_pageID;
span->_n += prevSpan->_n;
// 将prevSpan从双向链表中移除
_spanLists[prevSpan->_n].Erase(prevSpan);
delete prevSpan;
}
// 向后合并
while (true)
{
PAGE_ID nextID = span->_pageID + span->_n;
auto ret = _idSpanMap.find(nextID);
// 后面的页号没有,停止向后合并
if (ret == _idSpanMap.end())
{
break;
}
// 后面的页号对应的span正在被使用,停止向后合并
Span* nextSpan = ret->second;
if (nextSpan->_isUse == true)
{
break;
}
// 合并出超过128页的span无法进行管理,停止向后合并
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
// 进行向后合并
span->_n += nextSpan->_n;
// 将nextSpan从双链表中移除
_spanLists[nextSpan->_n].Erase(nextSpan);
delete nextSpan;
}
// 将合并后的span挂到对应的双链表中
_spanLists[span->_n].PushFront(span);
// 建议该span与其首位页的映射
_idSpanMap[span->_pageID] = span;
_idSpanMap[span->_pageID + span->_n - 1] = span;
// 将该span设置为未使用状态
span->_isUse = false;
}
向前合并

说明:直接向堆申请内存时我们调用的接口是VirtualAlloc ,与之对应的将内存释放给堆的接口叫做VirtualFree ,而Linux下的brk和mmap对应的释放接口叫做sbrk和unmmap。此时我们也可以将这些释放接口封装成一个叫做SystemFree的接口,当我们需要将内存释放给堆时直接调用SystemFree即可。
// 将申请的空间释放给堆
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
// sbrk unmmap等
#endif
}
2.3、测试
下面我们对大于256KB的申请释放流程进行简单的测试:
void BigAlloc()
{
void* p1 = ConcurrentAlloc(257 * 1024); // 257KB
ConcurrentFree(p1, 257 * 1024);
void* p2 = ConcurrentAlloc(129 * 8 * 1024); // 129页
ConcurrentFree(p2, 129 * 8 * 1024);
}
当申请257KB的内存时 ,由于257KB的内存按页向上对齐后是33页,并没有大于128页,因此不会直接向堆进行申请,会向page cache申请内存,但此时page cache当中实际是没有内存的 ,最终page cache就会向堆申请一个128页的span,将其切分成33页的span和95页的span,并将33页的span进行返回。

在释放内存时 ,由于该对象的大小大于了256KB,因此不会将其还给thread cache,而是直接调用的page cache当中的释放接口。

由于该对象的大小是33页,不大于128页,因此page cache也不会直接将该对象还给堆 ,而是尝试对其进行合并,最终就会把这个33页的span和之前剩下的95页的span进行合并,最终将合并后的128页的span挂到第128号桶中。

当申请129页的内存时 ,由于是大于256KB的,于是还是调用的page cache对应的申请接口,但此时申请的内存同时也大于128页 ,因此会直接向堆申请。在申请后还会建立该span与其起始页号之间的映射,便于释放时可以通过页号找到该span。

在释放内存时 ,通过对象的地址找到其对应的span,从span结构中得知释放内存的大小大于128页,于是会将该内存直接还给堆。
