PageCache
- page cache 設計邏輯
- 一、PageCache 的核心定位:理解它與 CentralCache 的本質區別
- 二、PageCache 的內存分配流程:從 “精確匹配” 到 “拆分適配”
- 三、PageCache 的內存釋放流程:合并小 Span,解決內存碎片問題
- page cache 代碼實現
- common頭文件
- thread cache類
- central cache 類
- sizeclass類
- page cache類
- 其他類
page cache 設計邏輯
在高并發內存池的三級緩存架構(ThreadCache -> CentralCache -> PageCache)中,PageCache 作為最頂層的緩存組件,直接與操作系統交互,承擔著 “大內存管理” 和 “內存碎片整理” 的核心職責。它通過管理以 “頁” 為單位的內存塊(Span),為下層的CentralCache 提供高效的內存分配支持,同時通過內存合并機制減少碎片,保障整個內存池的長期運行效率。本文將從內存分配和內存釋放兩大核心流程入手,拆解 PageCache 的設計邏輯與實現細節。
一、PageCache 的核心定位:理解它與 CentralCache 的本質區別
在分析具體流程前,必須先明確 PageCache 與下層 CentralCache 的核心差異 —— 二者雖均基于 “SpanList 哈希桶” 作為核心數據結構,但哈希桶的映射規則、管理的內存形態完全不同,這直接決定了它們的功能分工:
對比維度 | PageCache | CentralCache |
---|---|---|
哈希桶映射規則 | 按下標(桶號)直接映射,第 i 號桶僅存儲 “包含 i 頁內存” 的 Span。例如:第 4 號桶掛 4 頁 Span,第 10 號桶掛 10 頁 Span | 按 “小內存塊大小” 映射,與最下層 ThreadCache 的大小對齊規則一致(如 8 字節、16 字節、32 字節等)。例如:管理 “8 字節小塊內存” 的 Span,會掛載到對應 8 字節映射的桶中 |
管理的內存形態 | Span 內的內存為 “連續的未切割頁塊”,未拆分為小內存塊,保持原始頁級粒度 | Span 內的內存已按對應 “小塊大小” 切割成鏈表,直接供 ThreadCache 申請時取用 |
核心職責 | 向操作系統申請大頁內存、拆分大 Span 為小 Span、合并小 Span 為大 Span(碎片整理) | 作為 ThreadCache 與 PageCache 的中間層,緩存切割后的小內存塊,平衡線程間內存分配 |
這種設計的本質目的是 “分層解耦”:PageCache 專注于 “頁級粗粒度管理”,避免頻繁與操作系統交互;CentralCache 專注于 “小塊內存的切割與分發”,減少 ThreadCache 直接操作大內存的開銷。
二、PageCache 的內存分配流程:從 “精確匹配” 到 “拆分適配”
當 CentralCache 向 PageCache 申請內存時(通常是 CentralCache 某類小塊內存耗盡,需要補充新的 Span),PageCache 會遵循 “先找適配 Span,再拆大 Span,最后向系統申請” 的優先級邏輯,確保分配效率最大化。具體流程可拆解為三個關鍵步驟:
1. 第一步:優先查找 “精確匹配” 的 Span
PageCache 分配內存的核心原則是 “最小適配”—— 優先使用與申請頁數完全匹配的 Span,避免不必要的拆分。
假設 CentralCache 需要申請4 頁內存:
PageCache 會直接檢查 “第 4 號 SpanList 哈希桶”(對應存儲 4 頁 Span 的桶)。
若該桶非空(存在空閑的 4 頁 Span),則直接從桶中取出一個 Span,返回給CentralCache。
這種 “精確匹配” 的方式無需拆分操作,是最高效的分配路徑。
2. 第二步:無精確匹配時,拆分更大的 Span
若第一步中 “精確匹配的桶為空”(如 4 號桶沒有空閑 Span),PageCache 會進入 “拆分大 Span” 的流程,利用已有的更大頁 Span 來適配需求。
仍以 “申請 4 頁內存” 為例:
PageCache 會從 “比 4 大的桶號” 開始遍歷(從第 5 號桶到最大桶號,通常是 128 號),尋找第一個 “非空的 SpanList 桶”。
假設遍歷到第 10 號桶時發現一個空閑的 10 頁 Span,此時會觸發 “Span 拆分”:
將 10 頁 Span 拆分為兩個獨立的 Span:一個是 “4 頁 Span”(滿足當前申請需求),另一個是 “6 頁 Span”(剩余的未使用部分)。
拆分后,4 頁 Span 直接返回給 CentralCache;6 頁 Span 則被掛載到 “第 6 號 SpanList 桶” 中,等待后續被申請(避免內存浪費)。
這種 “拆大補小” 的邏輯,確保了 PageCache 中已有的大 Span 能被充分利用,減少向操作系統申請新內存的頻率。
3. 第三步:向操作系統申請大頁內存,再重復分配流程
若遍歷完所有桶(直到最大桶號,通常是 128 號)后,仍未找到任何空閑的 Span(即 PageCache 已無可用內存),PageCache 會直接與操作系統交互,申請一塊 “最大粒度” 的內存(通常是 128 頁,對應最大桶號的 Span),再基于這塊新內存完成分配。
具體操作如下:
調用操作系統底層接口(如 Linux 的mmap/brk、Windows 的VirtualAlloc),申請一塊包含 128 頁的連續物理內存。
將這塊 128 頁內存封裝成一個 “128 頁 Span”,掛載到 “第 128 號 SpanList 桶” 中。
此時 PageCache 中已有了可用的大 Span,重新回到第一步流程(從 “精確匹配” 開始),拆分 128 頁 Span 以滿足當前申請需求(如拆出 4 頁 Span 返回,剩余 124 頁 Span 掛載到 124 號桶)。
向操作系統申請 128 頁大內存,而非按需申請小內存,核心目的是 “減少系統調用次數”—— 操作系統層面的內存申請 / 釋放是高開銷操作,一次性申請大內存后,通過 PageCache 內部拆分復用,能顯著降低系統調用頻率,提升整體性能。
三、PageCache 的內存釋放流程:合并小 Span,解決內存碎片問題
內存分配容易產生 “碎片”(尤其是頻繁分配 / 釋放不同大小的內存后,會出現大量無法利用的小內存塊),而 PageCache 的釋放流程核心目標就是 “合并碎片”—— 將 CentralCache 釋放回的小 Span,與前后相鄰的空閑 Span 合并成大 Span,恢復頁級內存的連續性,以便后續能被重新分配給更大的內存需求。
具體釋放邏輯可概括為 “鏈式合并,向前追溯”:
當 CentralCache 釋放一個 Span(如釋放一個 4 頁 Span)時,PageCache 首先會找到該 Span 對應的 “頁 ID 范圍”(每個 Span 都記錄了起始頁 ID 和頁數,可計算出結束頁 ID)。
檢查該 Span 的 “前一個相鄰頁 ID” 是否對應一個 “空閑的 Span”(即前一頁是否屬于某個未被使用的 Span): 若存在前向空閑 Span,則將當前 Span 與前向 Span 合并,形成一個更大的 Span。
合并后,不再停留于當前位置,繼續向前檢查 “新合并 Span 的前一個相鄰頁 ID”,判斷是否還有可合并的空閑 Span。
重復第二步的 “向前追溯” 邏輯,直到找不到任何可合并的前向空閑 Span 為止。
同理,檢查該 Span 的 “后一個相鄰頁 ID” 是否對應空閑 Span,若存在則合并;但通常以 “向前合并” 為主,確保合并后的 Span 能復用已有的空閑塊管理結構,減少數據結構調整開銷。
舉個例子:假設 PageCache 中存在三個相鄰的空閑 Span——2 頁 Span(頁 ID 0-1)、4 頁 Span(頁 ID 2-5)、6 頁 Span(頁 ID 6-11)。當這三個 Span 陸續被釋放回 PageCache 時:
釋放 4 頁 Span(2-5)時,會先檢查前向頁 ID 1(屬于 2 頁 Span),合并為 “6 頁 Span(0-5)”;再檢查前向頁 ID -1(無),停止向前追溯。
釋放 6 頁 Span(6-11)時,檢查前向頁 ID 5(屬于合并后的 6 頁 Span),合并為 “12 頁 Span(0-11)”,最終形成一個連續的大 Span,可被用于后續 12 頁內存的申請。
這種 “合并到底” 的釋放邏輯,能最大限度地將分散的小 Span 整合為大 Span,從根本上緩解內存碎片問題,保障 PageCache 長期運行的內存利用率。
page cache 代碼實現
common頭文件
#pragma once // 防止頭文件被重復包含#include <cstddef>
#include <iostream>
#include <string>
#include <vector>
#include <cstdlib>
#include "Log.hpp" // 日志模塊頭文件
#include "Lockguard.hpp" // 鎖保護模塊頭文件
#ifdef __linux__#include <sys/mman.h>
#else#include<windows.h>
#endifusing std::cout;
using std::endl;
using namespace LogModule; // 使用日志模塊的命名空間
using namespace LockGuardModule; // 使用鎖保護模塊的命名空間// 自由鏈表的總數量:208個桶,對應不同大小的內存塊
static constexpr size_t NFREELISTS = 208;// 線程緩存能處理的最大內存大小:256KB
// 超過這個大小的內存申請直接走中央緩存或頁堆
static constexpr size_t MAX_BYTES = 256 * 1024;//頁緩存的數組數量,下標數字表示存貯的多少頁的span
static constexpr size_t NPAGES = 129;//頁大小 4kb = 1024 * 4
static constexpr size_t PAGESIZE = 1 << 12;
//頁偏移
static constexpr size_t PAGE_SHIFT = 12;// 如果是Windows 32位或64位系統(_WIN32是Windows平臺的預定義宏)
#ifdef _WIN32// 在Windows系統上,將PageID定義為size_t類型// size_t在Windows中通常與指針位數一致(32位系統為4字節,64位系統為8字節)typedef size_t PageID;
#else// 對于非Windows系統(如Linux、macOS等類Unix系統)// 將PageID定義為unsigned long long類型(固定8字節,可表示更大范圍的頁編號)typedef unsigned long long PageID;
#endif/*** @brief 獲取內存塊中的下一個對象指針* @param ptr 當前內存塊的指針引用* @return void*& 下一個對象的指針引用* * 這個函數用于實現自由鏈表的連接功能。它將內存塊的前8字節(在64位系統)* 用作存儲下一個內存塊的地址,實現鏈式結構。* * 工作原理:* 1. 將ptr強制轉換為void**(指向指針的指針)* 2. 解引用得到void*&(指針的引用)* 3. 這樣就可以直接修改下一個節點的地址*/
inline void*& NextObj(void*& ptr)
{return *static_cast<void**>(ptr);
}inline static void* SystemAlloc(size_t kpage)
{void* ptr = nullptr;
#ifdef _WIN32ptr = VirtualAlloc(nullptr, PAGESIZE*kpage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);if(ptr == NULL)throw std::bad_alloc();
#elseptr = mmap(nullptr, PAGESIZE*kpage, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);if(ptr == MAP_FAILED)throw std::bad_alloc();
#endifreturn ptr;
}inline static void SystemFree(void* ptr, size_t kpage)
{if(ptr == nullptr)return;
#ifdef _WIN32VirtualFree(ptr, 0, MEM_RELEASE);
#elsemunmap(ptr, kpage*PAGESIZE);
#endif
}/*** @brief 不可拷貝的基類* * 這個類提供了禁止拷貝語義的基礎功能,同時允許移動語義。* 常用于需要防止意外拷貝的資源管理類(如單例、資源句柄等)。* * 設計特點:* 1. 保護構造函數:只能被派生類訪問* 2. 刪除拷貝構造函數和拷貝賦值運算符:防止拷貝* 3. 默認移動構造函數和移動賦值運算符:允許移動* 4. 虛析構函數:確保正確的多態銷毀*/
class NonCopyable
{
protected:// 保護構造函數,只能被派生類實例化NonCopyable() = default;// 虛析構函數,確保派生類正確析構virtual ~NonCopyable() = default;// 刪除拷貝操作,防止對象拷貝NonCopyable(const NonCopyable&) = delete;NonCopyable& operator=(const NonCopyable&) = delete;// 允許移動操作,支持資源轉移NonCopyable(NonCopyable&&) = default;NonCopyable& operator=(NonCopyable&&) = default;
};
thread cache類
#pragma once
// 引入依賴頭文件:日志、通用工具、自由鏈表、內存大小計算、中心緩存
#include "Log.hpp"
#include "common.h"
#include "FreeList.hpp"
#include "SizeClass.hpp"
#include "Centralcache.hpp"
#include <algorithm> // 用于 std::min 函數(計算批量申請數量)
#include <cstddef> // 用于 size_t 等基礎類型// ThreadCache 類:繼承 NonCopyable 禁止拷貝構造和賦值(確保每個線程唯一實例)
class ThreadCache : public NonCopyable
{
public:// 構造函數:默認空實現(成員 _freelists 會自動調用 FreeList 默認構造)ThreadCache(){}// 內存分配接口:對外提供的核心分配函數,輸入需求內存大小,返回分配的內存指針void* Allocate(size_t size){void* obj = nullptr; // 存儲最終分配的內存塊指針,初始化為空// 1. 合法性檢查:排除無效大小(≤0 或超過 ThreadCache 處理上限 MAX_BYTES)if(size <= 0 || size > MAX_BYTES){Log(WARNING) << "size invalid!"; // 日志輸出警告return obj; // 返回空指針(分配失敗)}// 2. 內存大小對齊:將需求大小向上對齊到預設的標準大小(避免內存碎片)int alignsize = SizeClass::RoundUp(size);// 3. 計算哈希索引:根據對齊后的大小,找到對應的自由鏈表在 _freelists 中的位置int index = SizeClass::Index(size);// 4. 優先從本地自由鏈表分配:若對應鏈表非空,直接彈出一個內存塊if(!_freelists[index].Empty()){obj = _freelists[index].Pop();}// 5. 本地緩存不足:向 CentralCache 批量申請內存塊else{obj = FetchFromCentralCache(index, alignsize);}return obj; // 返回分配的內存塊(成功非空,失敗為空)}// 內存釋放接口:對外提供的核心釋放函數,輸入待釋放內存指針和其原始大小void Deallocate(void* ptr, size_t size){// 1. 合法性檢查:排除無效大小、空指針if(size <= 0 || size > MAX_BYTES || ptr == nullptr){Log(WARNING) << "size invalid or ptr is null!"; // 日志輸出警告return; // 直接返回(釋放失敗)}// 2. 計算哈希索引:根據原始大小找到對應的自由鏈表位置(無需對齊,因分配時已對齊)int index = SizeClass::Index(size);// 3. 將內存塊歸還給本地自由鏈表(線程內操作,無鎖,效率高)_freelists[index].Push(ptr);// TODO:待實現邏輯——當自由鏈表長度超過閾值時,將多余內存塊歸還給 CentralCache(避免本地緩存囤積)}private:// 核心輔助函數:向 CentralCache 批量申請內存塊,補充本地緩存void* FetchFromCentralCache(size_t index, size_t size){// 慢開始反饋調節算法:動態控制向 CentralCache 申請的批量大小,平衡效率與內存利用率// 核心邏輯:// 1. 初始申請量小,避免內存浪費;// 2. 若該大小內存持續需求,申請量逐步增長(直至上限);// 3. 內存塊越大,單次申請量越小(大內存浪費風險高);// 4. 內存塊越小,單次申請量越大(小內存分配頻繁,減少申請次數)// 計算本次申請量:取「本地鏈表最大限制」與「SizeClass 推薦量」的較小值size_t batchNum = std::min(_freelists[index].Maxsize(), SizeClass::NumMoveSize(size));// 若本次申請量達到本地鏈表的當前最大限制,下次允許申請更多(動態增長)if (batchNum == _freelists[index].Maxsize()) {_freelists[index].AddMaxsize(1); // 最大限制 +1(后續申請量可更大)}// 1. 獲取 CentralCache 單例實例(全局唯一)CentralCache* ccins = CentralCache::GetInstance();void* start = nullptr; // 存儲從 CentralCache 獲取的內存塊鏈表頭void* end = nullptr; // 存儲從 CentralCache 獲取的內存塊鏈表尾// 2. 向 CentralCache 批量申請內存塊:傳入申請量、內存大小,返回實際獲取的數量size_t actualNum = ccins->FetchRangeObj(start, end, batchNum, size);// 3. 處理申請結果:實際獲取數量無效(<1),日志警告并返回空if(actualNum < 1){Log(LogModule::DEBUG) << "actualnum invalid";return nullptr;}// 4. 僅獲取到 1 個內存塊:直接返回給調用者(無需加入本地鏈表,減少操作)else if(actualNum == 1){return start;}// 5. 獲取到多個內存塊:將「第一個塊之外的所有塊」加入本地自由鏈表(第一個塊返回給調用者)// NextObj(start):獲取 start 內存塊的下一個塊(依賴內存塊頭部的 next 指針)_freelists[index].PushRange(NextObj(start), end);return start; // 返回第一個內存塊給調用者}// 待實現函數:處理「本地自由鏈表過長」的邏輯(將多余塊歸還給 CentralCache)void ListTooLong(FreeList& list, size_t size){// TODO:后續需實現——例如取出鏈表中一半的塊,調用 CentralCache 接口歸還給中心緩存}public:// 析構函數:默認空實現(若后續需在線程退出時釋放本地緩存,需補充邏輯)~ThreadCache(){}private:// 核心成員:哈希桶結構的自由鏈表數組,每個桶對應一類對齊后的內存大小// NFREELISTS:自由鏈表數量(與 SizeClass 中對齊大小的類別數量一致)FreeList _freelists[NFREELISTS];
};/*** 線程局部的 ThreadCache 單例指針:高性能內存分配器的核心設計* 關鍵修飾符說明:* 1. static:靜態存儲期,指針本身在程序整個生命周期內存在(不會棧銷毀)* 2. thread_local:線程局部存儲,每個線程擁有該指針的獨立副本(線程間互不干擾)* 3. ThreadCache*:指針類型,指向當前線程的 ThreadCache 實例* 4. = nullptr:初始化為空指針(需在首次使用時創建 ThreadCache 實例)* 作用:確保每個線程操作自己的本地緩存,完全避免線程間鎖競爭,最大化分配效率
*/
static thread_local ThreadCache* pTLSThreadCache = nullptr;
central cache 類
#pragma once
// 引入依賴頭文件:日志工具、內存大小管理、通用配置、Span鏈表管理、標準互斥鎖
#include "Log.hpp"
#include "SizeClass.hpp"
#include "Span.hpp"
#include "common.h"
#include "Spanlist.hpp"
#include "PageCache.hpp"
#include <cstddef>
#include <cstdint>
#include <mutex>// CentralCache 類:中心緩存,繼承 NonCopyable 禁止拷貝(確保全局唯一實例)
class CentralCache : NonCopyable
{
private:// 私有構造函數:禁止外部直接實例化(單例模式核心,確保僅通過 GetInstance 創建)CentralCache(){}public:// 單例模式接口:獲取 CentralCache 全局唯一實例(雙檢鎖 DCLP,保證線程安全)static CentralCache* GetInstance(){static CentralCache* instance = nullptr; // 靜態指針存儲單例實例// 第一次檢查:避免每次調用都加鎖(提升效率)if(instance == nullptr){// 加鎖:確保多線程下僅初始化一次std::lock_guard<std::mutex> lock(_mutex);// 第二次檢查:防止多線程并發時重復初始化if(instance == nullptr){instance = new CentralCache; // 初始化單例實例}}return instance; // 返回單例實例}// 核心輔助方法:從指定的 SpanList 中獲取一個有空閑內存塊的 Span// 參數:list - 待查找的 Span 鏈表;byte_size - 需要的內存塊大小(對齊后)// 返回值:有空閑塊的 Span(未實現,當前返回 nullptr)Span* GetOneSpan(SpanList& list, size_t byte_size){// 1. 遍歷 list,查找 _freelist 非空的 Span(有空閑內存塊)Span* start = list.begin();while(start != list.end()){if(start->_freelist){return start;}start = start->_next;}// 2. 若未找到,向 PageCache 申請新的 Span(按 byte_size 計算所需頁數)PageCache* pc = PageCache::GetInstance();size_t pages = SizeClass::NumMovePage(byte_size);pc->lock();Span* newspan = pc->NewSpan(pages);pc->unlock();// 3. 將新 Span 切割為 byte_size 大小的小內存塊,串聯成 _freelistchar* begin = reinterpret_cast<char*>(static_cast<uint64_t>(newspan->_pageid) << PAGE_SHIFT);char* end = begin + newspan->_n * PAGESIZE;while(begin < end){if(end - begin < byte_size)break;void* tail = static_cast<void*>(begin);NextObj(tail) = newspan->_freelist;newspan->_freelist = tail;begin += byte_size;}newspan->_size = byte_size;// 4. 將新 Span 加入 list,返回該 Spanlist.PushFront(newspan);return newspan;}// 核心對外接口:向 ThreadCache 批量提供內存塊// 參數:// start/end - 輸出參數,存儲獲取到的內存塊鏈表的頭/尾指針// batchNum - ThreadCache 期望申請的批量數量// size - 單個內存塊的大小(對齊后)// 返回值:實際獲取到的內存塊數量(0 表示失敗)size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size){// 1. 計算 size 對應的哈希桶索引(與 ThreadCache/SizeClass 規則一致)size_t index = SizeClass::Index(size);// 2. 加桶鎖:僅鎖定當前哈希桶的 SpanList,減少多線程競爭(提升并發效率)std::lock_guard<std::mutex> lock(_spanlist[index]._mutex);// 3. 獲取一個有空閑塊的 Span(調用 GetOneSpan,未實現時返回 nullptr)Span* span = GetOneSpan(_spanlist[index], size);// 4. 檢查 Span 有效性:Span 為空或其自由鏈表為空,日志警告并返回 0if(span == nullptr || span->_freelist == nullptr){Log(LogModule::DEBUG) << "GetOneSpan fail!";return 0;}// 5. 初始化內存塊鏈表:從 Span 的 _freelist 開始取塊size_t actualNum = 1; // 實際獲取的數量(至少 1 個,初始化為 1)start = span->_freelist; // 鏈表頭指向 Span 自由鏈表的第一個塊end = start; // 鏈表尾初始與頭重合(僅 1 個塊時)size_t i = 0;// 6. 批量取塊:最多取 batchNum 個,或取完 Span 中所有空閑塊(有多少拿多少)// 循環條件:未取滿 batchNum-1 個(因已初始化 1 個)、當前塊非空、下一個塊非空while (i < batchNum - 1 && end != nullptr && NextObj(end) != nullptr) {end = NextObj(end); // 尾指針后移到下一個塊i++; // 計數+1actualNum++; // 實際數量+1span->_usecount++; // Span 的已使用塊計數+1(跟蹤使用狀態)}// 7. 更新 Span 的自由鏈表:指向剩余未取走的第一個塊(若有)span->_freelist = NextObj(end);// 8. 切斷內存塊鏈表:將取出的最后一個塊的 next 置空(避免與 Span 剩余塊關聯)NextObj(end) = nullptr;// 9. 返回實際獲取的內存塊數量return actualNum;}// 析構函數:默認空實現(單例實例通常隨程序生命周期結束,無需手動釋放)~CentralCache(){}private:// 核心成員:哈希桶結構的 SpanList 數組,每個桶對應一類對齊后的內存塊大小// NFREELISTS:桶數量(與 SizeClass 中內存塊類別數量一致)SpanList _spanlist[NFREELISTS];// 靜態互斥鎖:保護單例實例的初始化過程(雙檢鎖中的鎖)static std::mutex _mutex;
};inline std::mutex CentralCache::_mutex;
sizeclass類
#pragma once
// 引入依賴頭文件:日志工具、通用配置、基礎類型定義
#include "Log.hpp"
#include "common.h"
#include <cstddef>/*** 內存大小對齊規則說明(核心目標:控制內碎片浪費≤10%)* 按需求內存大小劃分 5 個區間,每個區間采用不同的對齊粒度,對應不同的自由鏈表組:* 1. 區間 [1, 128] 字節:8 字節對齊 → 對應 freelist 索引 [0, 16) → 共 16 個鏈表* 2. 區間 [129, 1024] 字節:16 字節對齊 → 對應 freelist 索引 [16, 72) → 共 56 個鏈表* 3. 區間 [1025, 8192] 字節(8*1024):128 字節對齊 → 對應 freelist 索引 [72, 128) → 共 56 個鏈表* 4. 區間 [8193, 65536] 字節(64*1024):1024 字節對齊 → 對應 freelist 索引 [128, 184) → 共 56 個鏈表* 5. 區間 [65537, 262144] 字節(256*1024):8192 字節對齊(8*1024)→ 對應 freelist 索引 [184, 208) → 共 24 個鏈表
*/// SizeClass 類:封裝內存大小對齊、索引計算、批量申請數量計算的靜態方法(無需實例化)
class SizeClass
{
public:// 核心方法1:將輸入的內存大小 bytes 向上對齊到對應區間的標準大小// 返回值:對齊后的標準大小(失敗返回 -1)static inline int RoundUp(size_t bytes){// 區間1:[1, 128] 字節 → 8字節對齊if(betweenNum(bytes, 1, 128)){return _RoundUp(bytes, 8);}// 區間2:[129, 1024] 字節 → 16字節對齊(注意:betweenNum 是左閉右開,故上限用 1024)else if(betweenNum(bytes, 128+1, 1024)){return _RoundUp(bytes, 16);}// 區間3:[1025, 8192] 字節 → 128字節對齊(8*1024=8192)else if(betweenNum(bytes, 1024+1, 8*1024)){return _RoundUp(bytes, 128);}// 區間4:[8193, 65536] 字節 → 1024字節對齊(64*1024=65536)else if(betweenNum(bytes, 8*1024+1, 64*1024)){return _RoundUp(bytes, 1024);}// 區間5:[65537, 262144] 字節 → 8192字節對齊(256*1024=262144)else if(betweenNum(bytes, 64*1024+1, 256*1024)){return _RoundUp(bytes, 1024*8);}// 超出支持的最大區間(>256KB)→ 輸出警告并返回 -1else {Log(WARNING) << "size invalid!";return -1;}}// 核心方法2:根據輸入的內存大小 bytes,計算其對齊后大小對應的自由鏈表索引// 返回值:自由鏈表的索引(失敗返回 -1)static inline int Index(size_t bytes){// 各區間對應的自由鏈表數量(與注釋中的鏈表數量一一對應)// groupnum[0]:區間1的鏈表數(16);groupnum[1]:區間2的鏈表數(56);以此類推std::vector<int> groupnum = {16, 56, 56, 56, 14};// 區間1:[1, 128] 字節 → 計算該區間內的子索引(align_shift=3 對應 2^3=8 字節對齊)if(betweenNum(bytes, 1, 128)){return _Index(bytes, 3);}// 區間2:[129, 1024] 字節 → 先減去區間1的最大值(128),再算子索引,最后加上前一區間的總鏈表數(16)else if(betweenNum(bytes, 128+1, 1024)){return _Index(bytes-128, 4) + groupnum[0]; // align_shift=4 對應 2^4=16 字節對齊}// 區間3:[1025, 8192] 字節 → 減去區間2的最大值(1024),算子索引,加上前兩區間總鏈表數(16+56=72)else if(betweenNum(bytes, 1024+1, 8*1024)){return _Index(bytes-1024, 7) + groupnum[0] + groupnum[1]; // align_shift=7 對應 2^7=128 字節對齊}// 區間4:[8193, 65536] 字節 → 減去區間3的最大值(8192),算子索引,加上前三區間總鏈表數(16+56+56=128)else if(betweenNum(bytes, 8*1024+1, 64*1024)){return _Index(bytes-8*1024, 10) + groupnum[0] + groupnum[1] + groupnum[2]; // align_shift=10 對應 2^10=1024 字節對齊}// 區間5:[65537, 262144] 字節 → 減去區間4的最大值(65536),算子索引,加上前四區間總鏈表數(16+56+56+56=184)else if(betweenNum(bytes, 64*1024+1, 256*1024)){return _Index(bytes-64*1024, 13) + groupnum[0] + groupnum[1] + groupnum[2] + groupnum[3]; // align_shift=13 對應 2^13=8192 字節對齊}// 超出支持的最大區間 → 輸出警告并返回 -1else {Log(WARNING) << "size invalid!: " << bytes;return -1;}}// 核心方法3:計算 ThreadCache 向 CentralCache 批量申請內存塊的數量(慢開始算法的基礎)// 邏輯:內存塊越小,單次申請數量越多;內存塊越大,單次申請數量越少,同時限制上下限(2~512)static size_t NumMoveSize(size_t bytes){// 合法性檢查:字節數為0 或 超過 ThreadCache 支持的最大字節數(MAX_BYTES,通常為256KB)if(bytes == 0 || bytes > MAX_BYTES){Log(LogModule::WARNING) << "bytes invalid!";return 0;}// 基礎計算:用最大支持字節數(MAX_BYTES)除以當前字節數,得到理論申請數量(確保總申請內存不超標)size_t ret = MAX_BYTES / bytes;// 下限:單次申請數量至少2個(避免頻繁申請)if(ret < 2){ret = 2;}// 上限:單次申請數量最多512個(避免一次性申請過多內存導致浪費)if(ret > 512){ret = 512;}return ret;}static size_t NumMovePage(size_t size){size_t num = NumMoveSize(size);size_t allsize = size * num;size_t pages = allsize / PAGESIZE;if(pages == 0){pages = 1;}return pages;}private:// 輔助方法1:通用的向上對齊實現(僅支持 align 為 2 的冪次方的情況)// 公式邏輯:(bytes + align - 1) 先將 bytes 補到下一個 align 的倍數,再通過 & (~(align-1)) 清除低位冗余static size_t _RoundUp(size_t bytes, size_t align){return (bytes + align - 1) & (~(align - 1));}// 輔助方法2:計算某區間內的子索引(基于對齊粒度的移位運算,align_shift 是對齊粒度的2的冪次)// 邏輯:等價于 (向上取整(bytes / 對齊粒度) ) - 1(索引從0開始)// 示例:bytes=9,align_shift=3(8字節對齊)→ (9+8-1)>>3 = 16>>3=2 → 2-1=1 → 子索引1static size_t _Index(size_t bytes, size_t align_shift){return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;}// 輔助方法3:判斷數值 num 是否在 [low, high) 區間內(左閉右開,適配區間劃分邏輯)static bool betweenNum(int num, int low, int high){return (num >= low && num <= high) ? true : false;}
};
page cache類
#pragma once
// 引入依賴頭文件:日志工具、Span鏈表管理、通用配置、標準類型和互斥鎖
#include "Log.hpp"
#include "Spanlist.hpp"
#include "common.h"
#include <cstddef>
#include <mutex>// PageCache 類:頁緩存,繼承 NonCopyable 禁止拷貝(確保全局唯一實例)
class PageCache : public NonCopyable
{
public:// 靜態加鎖接口:供外部手動鎖定頁緩存(如合并Span時需要跨桶操作)static void lock(){_mutex.lock();}// 靜態解鎖接口:與 lock() 配對使用static void unlock(){_mutex.unlock();}// 單例模式接口:獲取 PageCache 全局唯一實例(雙檢鎖 DCLP,保證線程安全)static PageCache* GetInstance(){static PageCache* instance = nullptr; // 靜態指針存儲單例實例// 第一次檢查:避免每次調用都加鎖(提升效率)if(instance == nullptr){// 加鎖:確保多線程下僅初始化一次std::lock_guard<std::mutex> lock(_mutex);// 第二次檢查:防止多線程并發時重復初始化if(instance == nullptr){instance = new PageCache; // 初始化單例實例}}return instance; // 返回單例實例}// 核心方法:分配指定頁數(pages)的連續物理內存塊,返回對應的 Span// 參數:pages - 需要分配的連續頁數(必須 ≥1 且 ≤ NPAGES-1)// 返回值:成功返回包含連續 pages 頁的 Span,失敗返回 nullptrSpan* NewSpan(size_t pages){// 1. 合法性檢查:頁數無效(<1 或 > 最大支持頁數 NPAGES-1)if(pages < 1 || pages > NPAGES - 1){Log(LogModule::WARNING) << "pages invalid!";return nullptr;}// 2. 優先從對應頁數的 SpanList 中分配(精確匹配)Span* obj = nullptr;if(!_spanlist[pages].empty()){return _spanlist[pages].PopFront(); // 直接返回鏈表中的第一個 Span}// 3. 若沒有精確匹配的 Span,則從更大頁數的 SpanList 中查找(拆分大 Span)// 遍歷比 pages 大的所有桶,找到第一個非空的 SpanListfor(int i = pages + 1; i < NPAGES; i++){if(!_spanlist[i].empty()){// 3.1 取出大 Span(頁數為 i)Span* ispan = _spanlist[i].PopFront();// 3.2 創建新 Span(頁數為 pages,用于返回)obj = new Span;// 3.3 拆分大 Span:新 Span 占用前 pages 頁obj->_pageid = ispan->_pageid; // 新 Span 的起始頁ID與大 Span 相同ispan->_pageid += pages; // 剩余 Span 的起始頁ID后移 pages 頁ispan->_n -= pages; // 剩余 Span 的頁數減少 pages 頁obj->_n = pages; // 新 Span 的頁數為 pages// 3.4 將剩余的 Span 放回對應頁數的 SpanList 中_spanlist[ispan->_n].PushFront(ispan);return obj; // 返回新拆分出的 Span}}// 4. 若所有桶都沒有足夠大的 Span,則向系統申請一大塊內存(NPAGES-1 頁)void* ptr = SystemAlloc(NPAGES - 1); // 調用底層系統接口分配內存PageID id = (PageID)ptr >> PAGE_SHIFT; // 將內存地址轉換為起始頁ID// 4.1 創建管理這塊大內存的 SpanSpan* bigspan = new Span;bigspan->_pageid = id; // 起始頁IDbigspan->_n = NPAGES - 1; // 總頁數_spanlist[NPAGES - 1].PushFront(bigspan); // 放入對應桶中// 4.2 遞歸調用自身,從剛分配的大 Span 中拆分出需要的 pages 頁return NewSpan(pages);}// 析構函數:默認空實現(單例實例通常隨程序生命周期結束,無需手動釋放)~PageCache(){}private:// 私有構造函數:禁止外部直接實例化(單例模式核心)PageCache(){}private:static std::mutex _mutex; // 靜態互斥鎖:保護單例初始化和頁緩存操作// 核心成員:哈希桶結構的 SpanList 數組,索引表示 Span 包含的頁數SpanList _spanlist[NPAGES];
};inline std::mutex PageCache::_mutex;
其他類
與前文相同