1. 前言
请先看完页缓存的具体实现(上)
本章重点:
本篇文章着重讲解页缓存是怎样把从中心缓存中还回来的内存挂在桶上,并且进行前后页的内存合并的,合并内存形成更大的一份内存来减少内存碎片的问题
2. 什么是内存碎片问题?
我们拿整个程序地址空间来举例:
可以看见虽然整个程序地址空间还有300多byte的空间,但是要申请300byte却申请不出来,对于我们这个项目来说,假设有两个在地址空间中相邻的span,一个是10页,一个是15页,分别挂在第10号桶和第15号桶中,假设没有合并内存此时外部申请一个20页的span是申请不出来的,即使现在空闲的空间有25页,所以这也就体现出了这个项目中合并内存的重要性!
3. 地址空间上的内存使用情况
在地址空间中,一共是4GB大小的空间.
地址从0000 0000到FFFF FFFF.
第0页的起始地址是0
第一页的起始地址是8*1024KB
...以此类推
当中心缓存还回来一个span后,假设这个span中的大页内存在地址空间中是在第1000页,并且span的大小是50页,那么这个还回来的span就处于空闲状态了,此时我们只需要去检查地址空间中第999页的内存是否空闲,如果空闲就将1000~1050页和999页合并,形成一个51页的大块儿span,并且要不断向前合并直到遇见正在使用的页.同理,需要检查地址空间的1051页是否空闲,如果是空闲的,那么就将它一起合并过去!
4. 页缓存合并内存的代码实现
在pagecache.h文件中:
void PageCache::ReleaseSpanToPageCache(SpanData* span) { if (span->_n > N_PAGES - 1)//大于128页的内存直接还给堆,不需要走pagecache { void* ptr = (void*)(span->_pageid << PAGE_SHIFT); SystemFree(ptr); //delete span; _spanPool.Delete(span); return; } //对span前后的页尝试进行合并,缓解外碎片问题 while (1)//不断往前合并,直到遇见不能合并的情况 { PAGE_ID prevId = span->_pageid - 1; auto prevret = _idSpanMap.find(prevId); if (prevret == _idSpanMap.end())//前面没有页号了 break; SpanData* prevspan = ret; if (ret == nullptr) break; if (prevspan->_isUse == true)//前面的页正在使用 break; if (prevspan->_n + span->_n > N_PAGES - 1)//当前页数加上span的页数大于128了,pagecache挂不下了 break; //开始合并span和span的前面页 span->_pageid = prevspan->_pageid; span->_n += prevspan->_n; _spanList[prevspan->_n].Erase(prevspan);//将被合并的页从pagecache中拿下来 //delete prevspan;//将prevspan中的数据清除,诸如页号,页数等 _spanPool.Delete(prevspan); } while (1)//不断往后合并,直到遇见不能合并的情况 { PAGE_ID nextId = span->_pageid + span->_n; auto nextret = _idSpanMap.find(nextId); if (nextret == _idSpanMap.end())//前面没有页号了 break; //SpanData* nextspan = nextret->second; auto ret = (SpanData*)_idSpanMap.get(nextId); if (ret == nullptr) break; SpanData* nextspan = ret; if (nextspan->_isUse == true)//前面的页正在使用 break; if (nextspan->_n + span->_n > N_PAGES - 1)//当前页数加上span的页数大于128了,pagecache挂不下了 break; //开始合并span和span的前面页 span->_n += nextspan->_n; _spanList[nextspan->_n].Erase(nextspan);//将被合并的页从pagecache中拿下来 //delete nextspan;//将prevspan中的数据清除,诸如页号,页数等 _spanPool.Delete(nextspan); } //合并完后将span挂起来 _spanList[span->_n].PushFront(span); //合并完后,要重新将这个span的首尾两页的id和这个span进行映射,方便别的span来合并我的时候使用 _idSpanMap[span->_pageid] = span; _idSpanMap[span->_pageid + span->_n - 1] = span; span->_isUse = false; }
对于代码的解释都在注释当中,大家可以发现整个合并内存的过程中,我们已经将delete操作符替换为了定长池中的free,这也就是完全脱离了free函数,并且当合并后的页数大于了128,此时整个页缓存哈希桶是挂不下的,所以要特别注意这一种情况
5. 总结以及对代码的拓展
页缓存结构的讲解已经结束,现在回头来看前面设计的这三层缓存结构,可谓是非常之巧妙,第一层线程缓存是无锁的,申请/释放内存非常高效,而第二层中心缓存是用的桶锁,在大多数情况下也没有竞争锁的问题,效率也非常高,所以现在能理解为什么要设计三层而不是两层,甚至是一层,一方面是为了效率的考量,另一方面是为了可以方便合并相邻的空闲页
对代码的拓展:
在使用到了直接向系统返还内存的函数:
inline static void SystemFree(void* ptr) { #ifdef _WIN32 VirtualFree(ptr, 0, MEM_RELEASE); #else // sbrk unmmap等 #endif }
同样,这份代码知道就行了,不需详谈
🔎 下期预告:项目的测试以及优化🔍