【高并發內存池】第四彈---深入理解PageCache:整體設計、核心實現及Span獲取策略詳解

?個人主頁:?熬夜學編程的小林

💗系列專欄:?【C語言詳解】?【數據結構詳解】【C++詳解】【Linux系統編程】【Linux網絡編程】【項目詳解】

目錄

1、pagecache

1.1、整體設計

1.2、核心實現

1.3、獲取Span

1.3.1、獲取一個非空的Span

1.3.2、獲取一個K頁的span

1.3.3、加鎖版獲取非空span?


1、pagecache

1.1、整體設計

page cache 與 central cache結構的相同之處

central cache的結構與thread cache 是相同的,都是哈希桶結構,并且page cache的每個哈希桶中掛的也是一個個的span,這些span也是按照雙鏈表的結構鏈接起來的。

page cache 與 central cache結構的不同之處

1、page cache與 central cache的映射規則不同(包括thread cache)。page cache的哈希桶映射規則采用的是直接定址法,比如1號桶掛的都是1頁的span,2號桶掛的都是2頁的span,一次類推。

2、central cache每個桶中的span被切成一個個對應大小的對象,以供thread cache申請。而page cache當中的span是沒有被進一步切小的,因為page cache服務的是central cache,當central cache沒有span時,向page cache申請的是某一固定頁數的span,而如何切分申請到的這個span由central cache自己來決定。

page cache當中究竟有多少個桶,這就要看你最大想掛多大頁的span了,我們這里就最大掛128頁的span,為了讓桶號與頁號對應起來,我們可以將第0號桶空出來,因此我們需要將哈希桶的個數設置成129

// page cache中哈希桶的個數
static const size_t NPAGES = 129;

為什么這里最大掛128頁的span呢?

因為線程申請單個對象最大是256KB,而128頁可以被切成4個256KB的對象,因此是足夠的。當然,如果你想在page cache中掛更大的span也是可以的,根據具體的需求進行設置即可。

在page cache中獲取一個n頁的span

1、如果central cache要獲取一個n頁的span,那我們就可以在page cache的第n號桶中取出一個span返回給central cache即可

2、如果第n號桶中沒有span,這時我們并不是直接向堆申請一個n頁的span,而是繼續在后面的桶中尋找span

  • 當第n號桶中沒有span時,我們可以找第n+1號桶,因為我們可以將第n+1頁的span分成一個n頁的span和一個1頁的span,這時我們就可以將n頁的span返回,而將切分后的1頁掛接到1號桶中。
  • 當后面的桶當中都沒有span,這時我們就只能向堆申請一個128頁的內存塊,并將其用一個span結構管理起來,然后將128頁的span切分成n頁的span和128-n頁的span,其中n頁的span返回給central cache,而128-n頁的span掛到第128-n號桶中。
  • 注意:當我們直接向堆申請以頁為單位的內存時,我們應該盡量申請大塊一點的內存塊,因為此時申請到的內存是連續的,當線程需要內存時我們可以將其切小后分配給線程,當線程將內存釋放后我們又可以將其合并成大塊連續的內存。?

實際上,我們每次向堆申請的都是128頁大小的內存塊,central cache要的這些span實際都是由128頁的span切分出來的。

1.2、核心實現

1、當每個線程的thread cache沒有內存時都會向central cache申請,此時多個線程的thread cache如果訪問的不是central cache的同一個桶,那么這些線程是可以同時進行訪問的。這時central cache的多個桶可能同時向page cache申請內存,索引page cache也是存在線程安全問題的,因此在訪問page cache時也必須要加鎖

  • 但是page cache這里不能使用桶鎖,因為當central cache向page cache申請內存時,page cache可能會將其他桶當中大頁的span切小再給central cache。此外,當central cache將某個span歸還給page cache時,page cache也會嘗試將該span與其他桶當中的span進行合并。
  • 換句話說,在訪問page cache時,我們可能需要訪問page cache中的多個桶,如果page cache用桶鎖就會出現大量頻繁的加鎖和解鎖,導致程序的效率低下。因此我們在訪問page cache時沒有使用桶鎖,而是用一把大鎖將整個page cache鎖住

2、page cache在整個進程中只能存在一個,因此我們也需要將其設置為單例模式

// 單例模式
class PageCache
{
public:// 提供一個全局訪問點static PageCache* GetInstance(){return &_sInst;}std::mutex _pageMtx; // 大鎖,設置公有方便調用
private:SpanList _spanLists[NPAGES];// C++98,構造函數私有PageCache(){}// C++11,禁止拷貝PageCache(const PageCache&) = delete;static PageCache _sInst;
};

當程序運行起來后我們就立馬創建該對象即可。?

// 類外初始化靜態成員
PageCache PageCache::_sInst;

1.3、獲取Span

1.3.1、獲取一個非空的Span

thread cache向central cache申請對象時central cache需要先從對應的哈希桶中獲取到一個非空的Span,然后從這個非空的Span中取出指定大小的對象返回給thread cache。那central cache到底是如何從對應的哈希桶中獲取到一個非空的Span的呢?

1、遍歷central cache對應哈希桶中的雙鏈表,如果該雙鏈表中有非空的Span,那么直接將Span返回即可。為了方便遍歷這個雙鏈表,我們可以模擬迭代器的方式,給SpanList類提供Begin和End成員函數,分別獲取雙鏈表的第一個Span結點和最后一個Span結點的下一個結點,也就是頭結點

// 管理Span的鏈表結構
class SpanList
{
public:Span* Begin(){return _head->_next;}Span* End(){return _head;}
private:Span* _head;     // 雙向鏈表頭結點指針
public: std::mutex _mtx; // 桶鎖,方便類外訪問
};

2、但如果遍歷雙鏈表后發現雙鏈表中沒有span,或該雙鏈表中的span都為空,那么此時central cache就需要向page cache申請內存塊了。

那具體是向page cache申請多大的內存塊呢?

我們可以根據具體所需對象的大小來決定,就像之前我們根據對象的大小計算出,thread cache一次向central cache申請對象的個數上限,現在我們是根據對象的大小計算出,central cache一次應該向page cache申請幾頁的內存塊。

  • 我們可以先根據對象的大小計算出,thread cache一次向central cache申請對象的個數上限,然后將這個上限值乘以單個對象的大小,就算出了具體需要多少字節,最后再將這個算出來的字節數轉換為頁數,如果轉換后不夠一頁,那么我們就申請一頁,否則轉換出來是幾頁就申請幾頁。也就是說,central cache向page cache申請內存時,要求申請到的內存盡量能夠滿足thread cache向central cache申請時的上限。

class SizeClass
{
public:	// central cache一次向page cache獲取多少頁static size_t NumMovePage(size_t size){// 計算出thread cache一次向central cache申請對象的個數上限size_t num = NumMoveSize(size); // num 個size 大小的對象所需的字節數size_t nPage = num * size;nPage >>= PAGE_SHIFT; // 將字節數轉換為頁數if (nPage == 0)nPage = 1; // 至少給一頁return nPage;}
};
  • PAGE_SHIFT代表頁大小轉換偏移,我們這里以頁的大小為8K為例,PAGE_SHIFT的值就是13。
// 頁大小轉換偏移,即一頁定義為2^13,也就是8KB
static const size_t PAGE_SHIFT = 13;
  • 注意:當central cache申請到若干頁的span后,還需要將這個span切成一個個對應大小的對象掛到該span的自由鏈表當中。

如何找到一個span所管理的內存塊呢?

首先需要計算出該span的起始地址,我們可以用這個span的起始頁號乘以一頁的大小即可得到這個span的起始地址,然后用這個span的頁數乘以一頁的大小就可以得到這個span所管理的內存塊的大小,用起始地址加上內存塊的大小即可得到這塊內存塊的結束位置

明確了這塊內存的起始和結束位置后,我們就可以進行切分了。根據所需對象的大小,每次從大塊內存切出一塊固定大小的內存塊尾插到span的自由鏈表中即可。

為什么是尾插呢?

因為我們如果是將切好的對象尾插到自由鏈表,這些對象看起來是按照鏈式結構鏈接起來的,而實際它們在物理上是連續的,這時當我們把這些連續內存分配給某個線程使用時,可以提高該線程的CPU緩存利用率

// 獲取一個非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{// 1.先在list中尋找非空的SpanSpan* it = list.Begin();while (it != list.End()){if (it->_freeList != nullptr){return it;}else{it = it->_next;}}// 2.list中沒有非空的Span,只能向page cache申請Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));// 3.計算span大塊內存的其實地址和大塊內存的大小(字節數)char* start = (char*)(span->_pageID << PAGE_SHIFT); // 起始地址size_t bytes = span->_n << PAGE_SHIFT; // 內存大小char* end = start + bytes; // 結束地址// 4.把大塊內存切成自由鏈表鏈接起來// a.先切一小塊做頭,方便尾插(定義一個尾結點指針)span->_freeList = start;start += size;void* tail = span->_freeList;int i = 1; // 用于調試while (start < end){++i;Next(tail) = start;tail = Next(tail); // tail = startstart += size;}Next(tail) = nullptr; // 將尾結點指向空// b.切好Span以后,將切好的Span掛到桶里面(頭插)list.PushFront(span);return span;//return nullptr;
}

1.在list中尋找非空的Span?

2.申請內存和計算起始地址

3.把大塊內存切成自由鏈表鏈接起來?

注意:當我們把span切分好之后,需要將這個切分好的span掛接到central cache對應的哈希桶中。因此SpanList類需要提供一個將span插入到雙鏈表的接口。此處我們選擇頭插,這樣當central cache下一次從該雙鏈表獲取非空span時,一來就能找到。

由于SpanList類已經實現了Insert和Begin函數,這里實現雙鏈表的頭插直接在雙鏈表的Begin位置進行Insert(在頭結點插入數據)即可。

// 管理Span的鏈表結構
class SpanList
{
public:// 頭插Spanvoid PushFront(Span* span){Insert(Begin(), span); // Begin之前插入span}
private:Span* _head;     // 雙向鏈表頭結點指針
public: std::mutex _mtx; // 桶鎖,方便類外訪問
};

1.3.2、獲取一個K頁的span

當我們調用上述的GetOneSpan從central cache的某個哈希桶獲取一個非空的span時,如果遍歷哈希桶中的雙鏈表后發現雙鏈表中沒有span,或該雙鏈表中的span都為空,那么此時central cache就需要向page cache申請若干頁的span了,下面我們來分析如何從page cache獲取一個k頁的span

1、因為page cache是直接按照頁數進行映射的,因此我們要從page cache獲取一個k頁的span,就應該 直接先去找page cache的第k號桶如果第k號桶中有span,那我們直接頭刪一個span返回給central cache就行了。所以我們這里需要再給 SpanList類添加對應的Empty和PopFront函數
// 管理Span的鏈表結構
class SpanList
{
public:// 判斷是否為空bool Empty(){return _head == _head->_next;}// 頭刪SpanSpan* PopFront(){Span* front = _head->_next;Erase(front);return front;}
private:Span* _head;     // 雙向鏈表頭結點指針
public: std::mutex _mtx; // 桶鎖,方便類外訪問
};

?2、如果page cache的第k號桶中沒有span,我們就應該繼續找后面的桶只要后面任意一個桶中有一個n頁的span,我們就可以將其切分成一個k頁的span和一個n-k頁的span,然后將切出來k頁span返回給central cache,再將n-k頁的span掛接到page cache的第n-k號桶中。

3、如果后面的桶中也都沒有span,此時我們需要向堆申請一個128頁的span了,在向堆申請內存時,直接調用我們封裝的SystemAlloc函數即可。

注意:向堆申請內存后得到的是這塊內存的起始地址,此時我們需要將該地址轉換為頁號。由于我們向堆申請內存時都是按照頁進行申請的,因此我們直接將該地址除以一頁的大小即可得到對應的頁號。

// 獲取一個K頁的span
Span* PageCache::NewSpan(size_t k)
{assert(k > 0 && k < NPAGES); // 保證在桶的范圍內// 1.檢查第k個桶里面有沒有Span,有則返回if (!_spanLists[k].Empty()){return _spanLists[k].PopFront();}// 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);return kSpan;}}// 3.沒有大頁的span,找堆申請128頁的spanSpan* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1); // 申請128頁內存bigSpan->_pageID = (PAGE_ID)ptr >> PAGE_SHIFT;bigSpan->_n = NPAGES - 1; // 頁數_spanLists[bigSpan->_n].PushFront(bigSpan);// 遞歸調用自己(避免代碼重復)return NewSpan(k);
}

1.第k個桶中有Span

2.第k個桶后面有Span?

3.沒有大頁的span,找堆申請128頁的span?

說明:當我們向堆申請到128頁的span后,需要將其切分成k頁的span和128-k頁的span,但是為了盡量避免出現重復的代碼,此處最好不要再編寫對應的切分代碼。我們可以先將申請到的128頁的span掛接到page cache對應的哈希桶中,然后再遞歸調用該函數即可,此時在往后找span時就一定會在第128號桶找到該span,然后進行切分。

有一個問題:當central cache向page cache申請內存時,central cache對應的哈希桶是處于加鎖的狀態的,那在訪問page cache之前我們應不應該把central cache對應的桶鎖解掉呢?

這里建議在訪問page cache前,先把central cache對應的桶鎖解掉。雖然此時central cache的這個桶當中是沒有內存供其他thread cache申請的,但thread cache除了申請內存還會釋放內存,如果在訪問page cache前將central cache對應的桶鎖解掉,那么此時當其他thread cache想要歸還內存到central cache的這個桶時就不會被阻塞

因此在調用NewSpan函數之前,我們需要先將central cache對應的桶鎖解掉,然后再將page cache的大鎖加上,當申請到k頁的span后,我們需要將page cache的大鎖解掉,但此時我們不需要立刻獲取到central cache中對應的桶鎖。因為central cache拿到k頁的span后還會對其進行切分操作,因此我們可以在span切好后需要將其掛到central cache對應的桶上時,再獲取對應的桶鎖。

1.3.3、加鎖版獲取非空span?

// 獲取一個非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{// 1.先在list中尋找非空的SpanSpan* it = list.Begin();while (it != list.End()){if (it->_freeList != nullptr){return it;}else{it = it->_next;}}// 先把central cache的桶鎖解掉,這樣如果其他線程釋放內存對象回來,不會阻塞list._mtx.unlock();// 2.list中沒有非空的Span,只能向page cache申請PageCache::GetInstance()->_pageMtx.lock();Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));PageCache::GetInstance()->_pageMtx.unlock();// 3.計算span大塊內存的其實地址和大塊內存的大小(字節數)char* start = (char*)(span->_pageID << PAGE_SHIFT); // 起始地址size_t bytes = span->_n << PAGE_SHIFT; // 內存大小char* end = start + bytes; // 結束地址// 4.把大塊內存切成自由鏈表鏈接起來// a.先切一小塊做頭,方便尾插(定義一個尾結點指針)span->_freeList = start;start += size;void* tail = span->_freeList;int i = 1; // 用于調試while (start < end){++i;Next(tail) = start;tail = Next(tail); // tail = startstart += size;}Next(tail) = nullptr; // 將尾結點指向空// b.切好Span以后,將切好的Span掛到桶里面(頭插),再加鎖list._mtx.lock();list.PushFront(span);return span;//return nullptr;
}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/pingmian/73290.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/73290.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/73290.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

深入理解C語言數據結構之快速排序三路劃分

在數據結構和算法的世界里&#xff0c;排序算法是基石一般的存在。快速排序作為一種高效的排序算法&#xff0c;以其平均情況下的優秀時間復雜度而被廣泛應用。今天&#xff0c;讓我們深入探討快速排序的一種變體——三路劃分的快速排序&#xff0c;看看它是如何在C語言中施展魔…

Java實現后量子密碼(PQC)與國密算法(SM4)混合加密

以下是使用Java實現一種后量子密碼(PQC)與國密算法(SM4)混合加密的示例方案。該方案結合了后量子密碼的抗量子特性與國密算法的國產化合規要求,適合需要雙重安全保障的場景。 一 . 方案驗證 1.代碼截圖 2.運行測試 二 . 方案設計 密鑰交換:使用后量子密碼(如Kyber)生…

【SQL Server數據庫備份詳細教程】

&#x1f3a5;博主&#xff1a;程序員不想YY啊 &#x1f4ab;CSDN優質創作者&#xff0c;CSDN實力新星&#xff0c;CSDN博客專家 &#x1f917;點贊&#x1f388;收藏?再看&#x1f4ab;養成習慣 ?希望本文對您有所裨益&#xff0c;如有不足之處&#xff0c;歡迎在評論區提出…

SpringBoot古典舞在線交流平臺設計與實現

隨著古典舞文化的普及&#xff0c;越來越多的人希望通過線上平臺交流學習。幽絡源作為一站式綜合平臺&#xff0c;致力于為用戶提供免費源碼、技術教程及網絡兼職資源。本文將詳細介紹基于SpringBoot的古典舞在線交流平臺的設計與實現&#xff0c;幫助開發者快速搭建一個功能完…

關于絕對時間、人類時間、本地時間、時區時間的對比分析,結合編程場景(如Java)進行說明

以下是關于絕對時間、人類時間、本地時間、時區時間的對比分析&#xff0c;結合編程場景&#xff08;如Java&#xff09;進行說明&#xff1a; 1. 定義與核心區別 (1) 絕對時間&#xff08;Absolute Time&#xff09; 定義&#xff1a;不受時區影響&#xff0c;以固定時間起點…

go語言中的strings庫

strings庫 func EqualFold func EqualFold(s, t string) bool判斷兩個utf-8編碼字符串&#xff08;將unicode大寫、小寫、標題三種格式字符視為相同&#xff09;是否相同。 func main() {fmt.Println(strings.EqualFold("hello", "hello")) //truefmt.…

Git沖突解決

目錄 一、Git沖突產生的原因二、解決Git沖突的步驟1. 發現沖突2. 查看沖突文件3. 手動解決沖突4. 提交解決后的代碼5. 完成合并 三、預防Git沖突的小技巧四、總結 在團隊協作開發中&#xff0c;Git沖突是常見的問題。當多個開發者同時修改了同一個文件的不同部分&#xff0c;然…

Spring AOP + RocketMQ 實現企業級操作日志異步采集(實戰全流程)

Spring AOP + RocketMQ 實現企業級操作日志異步采集(實戰全流程) ?? 項目背景 在企業級微服務架構中,記錄操作日志是一項剛需。傳統方式常使用數據庫直接寫入或通過 Feign 調用日志微服務,但這樣存在耦合高、主流程阻塞、擴展性差等問題。 為此,我們將使用: Spring …

Git Flow 分支管理策略

優勢 清晰的分支結構&#xff1a;每個分支都有明確的用途&#xff0c;便于團隊協作。 穩定的 master 分支&#xff1a;生產環境代碼始終穩定。 靈活的發布管理&#xff1a;通過發布分支和熱修復分支&#xff0c;可以靈活管理版本發布和緊急修復。 主要分支 master 分支 代表…

Altium Designer數模電學習筆記

模電 電容 **退耦&#xff1a;**利用通交阻直&#xff0c;將看似直流的信號中的交流成分濾除 &#xff08;一般用在給MPU供電&#xff0c;盡量小一些&#xff0c;10nf~100nf~1uf以下&#xff09; **濾波&#xff1a;**也可以理解為給電容充電&#xff0c;讓電容在電平為低時…

光譜儀與光譜相機的核心區別與協同應用

一、核心功能與數據維度 ?光譜儀? ?功能定位?&#xff1a;專注單點或線狀區域的光譜分析&#xff0c;通過色散元件&#xff08;光柵/棱鏡&#xff09;分離波長&#xff0c;生成一維或二維光譜曲線&#xff0c;用于量化光強、吸收率等參數?。 ?數據維度?&#xff1a;輸…

Pytorch中layernorm實現詳解

平時我們在編寫神經網絡時&#xff0c;經常會用到layernorm這個函數來加快網絡的收斂速度。那layernorm到底在哪個維度上進行歸一化的呢&#xff1f; 一、問題描述 首先借用知乎上的一張圖&#xff0c;原文寫的也非常好&#xff0c;大家有空可以去閱讀一下&#xff0c;鏈接放…

linux--時區查看和修改

查看當前時間和時區: 打開終端&#xff0c;輸入以下命令查看當前的日期和時間設置&#xff1a; timedatectl修改時區: 使用 timedatectl 命令來修改時區&#xff1a; sudo timedatectl set-timezone <時區>例如&#xff0c;設置時區為北京時間&#xff08;中國標準時間&a…

在windows下安裝windows+Ubuntu16.04雙系統(上)

這篇文章的內容主要來源于這篇文章&#xff0c;給文章很詳細的介紹了如何從windows下安裝windowsubuntu16.04雙系統。我剛開始裝雙系統都是參照這個方法&#xff0c;該作者前后更新了兩個版本&#xff0c;在這里對其稍微進行整理一下。 一、準備&#xff1a;&#xff08;這里推…

如何獲取thinkphp的所有發行版本

是的&#xff0c;你只需要一行代碼 composer show topthink/think --all 然后做了一個小實驗&#xff0c;神奇的事情發生了。是我眼睛花了嗎&#xff1f; 命令也能模糊查詢了嗎&#xff1f;tp6也太。。。。

算法模型從入門到起飛系列——遞歸(探索自我重復的奇妙之旅)

文章目錄 前言一、遞歸本質1.1 遞歸的要素1.2 遞歸特點 二、遞歸&迭代2.1 遞歸&迭代比較2.2 遞歸&迭代如何實現相同功能2.2.1 遞歸實現2.2.2 迭代實現2.2.3 性能對比 三、優雅的遞歸理解3.1 階乘計算分解3.2 [DFS](https://blog.csdn.net/qq_38315952/article/deta…

Android 系統進程啟動Activity方法說明

前面文章Android Activity的啟動器ActivityStarter入口說到Activity的恢復執行是由 mRootWindowContainer.resumeFocusedTasksTopActivities(mTargetRootTask, mStartActivity, mOptions, mTransientLaunch)來實現的&#xff0c;下面就看下它的實現。 RootWindowContainer類的…

PostgreSQL_安裝

目錄 前置&#xff1a; 安裝過程&#xff1a; 1 下載軟件 2 創建安裝文件夾和放置數據的文件夾 3 雙擊安裝 4 連接服務 前置&#xff1a; PostgreSQL 15 windows 10 專業版 安裝過程&#xff1a; 1 下載軟件 PostgreSQL: Downloads 大小326MB 2 創建安裝文件夾和放…

docker desktop 集成WSL Ubuntu22.04

Windows docker desktop 設置WSL ubuntu 22.04啟用與其他發行版的集成 Windows docker desktop 安裝參考 wsl ubuntu 22.04 查看我宿主機的docker desktop 容器全部的信息 wsl -d Ubuntu-22.04 -u root

從國家能源到浙江交通投資,全息技術在能源交通領域的創新應用

一、3D全息技術行業應用參數及設計制作要求 全息投影 全息投影技術通過激光器、全息片等設備&#xff0c;將物體的三維信息記錄下來&#xff0c;并在特定條件下再現。應用參數包括投影距離、投影面積、投影亮度等。設計制作要求&#xff1a;高清晰度、高亮度、低噪音、穩定性好…