【高并發內存池】六、三種緩存的回收內存過程

文章目錄

  • 前言
  • Ⅰ. `thread cache`的內存回收
  • Ⅱ. `central cache`的內存回收
  • Ⅲ. `page cache`的內存回收

在這里插入圖片描述

前言

? 前面我們將內存的申請流程都走通了,現在就是內存回收的過程,主要是從 thread cache 開始,一層一層往下回收,因為我們調用的申請內存接口也是從 thread cache 開始的!

? 而每一層回收內存的方式其實都不太一樣,因為我們還有一個負載均衡的機制,就是當一個線程緩存中內存塊太多了,我們要將這些內存塊合并成一個大的內存塊對象也就是 span 然后返回給中心緩存,而中心緩存則將那些線程都還回來的 span 對象再組織成一個更大的 span 對象返回給頁緩存保存起來!所以我們需要將它們分開來講解!

Ⅰ. thread cache的內存回收

? 線程緩存的內存回收其實并不難,就是將用戶不需要的小內存塊重新插入到對應哈希桶的空閑鏈表中,此時加入一個負載均衡的判斷:判斷一下此時如果對應空閑鏈表中的內存塊個數大于了當前申請內存塊的數量的時候,我們就認為當前申請內存塊的數量對應的內存塊是不用到的,就將它們從空閑鏈表中彈出,然后返回這些內存塊的起始地址 start 給中心緩存,這樣子做的目的是防止這些不用的內存塊被浪費了!

? 當然 tcmalloc 源碼中實現的會更加復雜,它還會判斷整個哈希桶中的內存塊的個數來進行負載均衡,而我們只是一個簡化版本,不過重要的是學習這個思想!

? 所以我們來完善一下之前 thread cache 中遺留下的 deallocate() 函數,如下所示:

// ThreadCache.cpp文件
// 釋放內存接口
void ThreadCache::deallocate(void* ptr, size_t size)
{assert(size <= THREAD_MAX_SIZE); // 要求申請的空間是小于256KB的assert(ptr != nullptr);// 將ptr指向的空間插入到對應的哈希桶中即可size_t index = AlignClass::get_index(size);_freelists[index].push(ptr);// 當一個桶中的內存塊超過了其當前申請內存塊的數量的時候,// 就將當前申請的內存塊數量的內存塊還給central cache,防止占用太多內存塊if (_freelists[index].get_size() > _freelists[index].get_maxsize())give_back_memory(_freelists[index], size);
}

? 此時需要提供一些細節,比如上面的獲取當前空閑鏈表的內存塊個數 get_size() 函數,以及待會 give_back_memory() 函數中需要用到將多個內存塊從空閑鏈表彈出的操作,所以要提供一個 pop_range() 接口,然后因為我們需要對內存塊進行計數,所以需要在之前的 pushpopappend 函數中處理一下計數,所以我們都在 FreeList 類中完善這些接口,具體看注釋部分即可:

// Common.h文件
class FreeList
{
private:void* _freelist = nullptr; // 指向空閑鏈表頭節點的指針size_t _size = 0;    // 表示當前空閑鏈表中內存塊的個數size_t _maxsize = 1; // 表示當前空閑鏈表申請空間塊時允許獲得的最大個數(會不斷遞增)
public:// 將單個內存對象頭插插入空閑鏈表void push(void* obj){assert(obj != nullptr);get_next(obj) = _freelist;_freelist = obj;_size++; // 記得累加個數}// 將多個內存對象頭插插入空閑鏈表void append(void* start, void* end, size_t size){assert(start != nullptr && end != nullptr);get_next(end) = _freelist;_freelist = start;_size += size; // 記得累加個數}// 頭刪方式取出空閑鏈表中的空間void* pop(){assert(_freelist != nullptr);void* obj = _freelist;_freelist = get_next(obj);_size--; // 記得減去個數return obj;}// 將多個內存塊從空閑鏈表彈出,通過輸出型參數獲取首尾地址void pop_range(void*& start, void*& end, size_t num){// 1. 先定位首尾位置(end要停在第num塊上,所以實際走的是num-1步)start = _freelist;end = start;for (int i = 0; i < num - 1; ++i)end = get_next(end);// 2. 將該區間的內存塊與空閑鏈表斷開,然后將end的next置為空即可_freelist = get_next(end);get_next(end) = nullptr;// 3. 最后別忘了內存塊個數要減少_size -= num;}bool empty() { return _freelist == nullptr; }size_t& get_maxsize() { return _maxsize; }size_t& get_size() { return _size; }
};

? 最后就是來實現另一個輔助接口 give_back_memory(),因為我們上面 deallocate() 接口只是負責判斷是否需要啟動負載均衡操作,而具體操作要交給該函數來實現!
? 其實并不難,其內部就是調用中心緩存的一個接口 merge_memory_from_ThreadCache() 來合并到中心緩存中,具體的實現我們到中心緩存部分再講,下面是 give_back_memory() 函數的實現:

// ThreadCache.cpp
// 釋放對象時,鏈表過長時,歸還對應數量的內存塊給central cache
void ThreadCache::give_back_memory(FreeList& list, size_t size)
{// 1. 先從當前空閑鏈表中取出當前申請內存塊的數量個內存塊void* start = nullptr;void* end = nullptr;list.pop_range(start, end, list.get_maxsize());// 2. 然后調用central cache的接口將這些內存塊合并到central cache中CentralCache::get_instance()->merge_memory_from_ThreadCache(start, size);
}

Ⅱ. central cache的內存回收

? 現在中心緩存要完成的無非就是上面遺留下的 merge_memory_from_ThreadCache() 接口,但是現在有個問題就是,我們拿到了要回收到中心緩存的一串小內存塊鏈表的起始地址 start,并且知道它們的總大小為 size,此時我們需要找到這些小內存塊對應的是中心緩存中的哪個 span 對象呀,這怎么辦??

? 其實可以 用得到的每個小內存塊的地址 start 來除以一頁的大小(我們規定是 8K),就能得到該小內存塊對應的起始頁號,然后我們可以暴力一點,去中心緩存或者頁緩存中遍歷所有的 span 對象的頁號進行比對,就能確認每個小內存塊對應的是哪個 span 對象了(因為小內存塊的排序也是混亂的,所以需要每個小內存塊都去查找),但問題是這種查找方式的時間復雜度比較高,是 O(n^2),這是我們無法接受的!

? 既然說到了比對,我們就想到可以用哈希表來快速索引,所以我們可以 PageCache 類中添加一個哈希表,建立一下每個頁號與對應 span 對象的映射關系,這樣子我們得到了頁號就能快速找到其對應的 span 對象了,時間是非常快的!(至于為什么不是在 CentralCache 類中添加哈希表,是因為后面 PageCache 也會用到頁號來查找對應內存塊的對象,那我們干脆就存放在 PageCache 類中就行了,就不用再多存一份了!)

? 下面只需要關心注釋部分:

// PageCache.h
#pragma once
#include "Common.h"class PageCache
{
private:SpanList _spanlists[PAGELIST_NUMS];std::mutex _mtx; static PageCache _page_instance; std::unordered_map<page_t, Span*> _tables; // 存放頁號與對應span對象的映射關系
public:static PageCache* get_instance() { return &_page_instance; }Span* new_span(size_t k);std::mutex& get_mutex() { return _mtx; }
private:PageCache() {}PageCache(const PageCache&) = delete;PageCache& operator=(const PageCache&) = delete;
};

? 然后我們需要 new_span() 函數中添加上建立頁號和對應 span 對象關系的操作,也就兩行代碼,如下所示:(也是只需要關心注釋部分)

// PageCache.cpp
// 獲取一個k頁大小的span
Span* PageCache::new_span(size_t k)
{// 看看當前k頁大小的哈希桶中是否有可用的span對象,找到了直接將其取出然后進行返回即可if (!_spanlists[k].empty()){Span* span = _spanlists[k].pop_front();// 建立back中每個頁號和span的映射,方便central cache回收小塊內存時,查找對應的spanfor (page_t i = 0; i < span->_num; ++i)_tables[span->_pid + i] = span;return span;}for (int i = k + 1; i < PAGELIST_NUMS; ++i){if (!_spanlists[i].empty()){Span* front = _spanlists[i].pop_front();Span* back = new Span;back->_pid = front->_pid;back->_num = k;front->_pid += k;front->_num -= k;_spanlists[front->_num].push_front(front);/*對于front中每個頁號與span的映射,我們只需要記錄首尾頁號即可,因為front是沒有分配中心緩存使用的,所以后面我們再合并已經分配的那些內存塊的時候,是中心發散的去查找內存塊左右臨近的內存塊進行合并的,此時我們對于front的頁號與span的映射只需要記錄下首尾即可,而不需要向back一頁每個小內存塊的頁號都要去進行映射,因為back是被分配去使用的,所以需要進行每個頁號的映射*/_tables[front->_pid] = front;_tables[front->_pid + front->_num - 1] = front;// 建立頁號和span的映射,方便central cache回收小塊內存時,查找對應的spanfor (page_t i = 0; i < back->_num - 1; ++i)_tables[back->_pid + i] = back;return back;}}void* ptr = SystemAlloc(PAGELIST_NUMS - 1);Span* newspan = new Span;newspan->_num = PAGELIST_NUMS - 1;newspan->_pid = (page_t)ptr >> PAGE_SHIFT;_spanlists[PAGELIST_NUMS - 1].push_front(newspan);return new_span(k);
}

? 此時我們可以提供一個功能函數,用于將傳入的內存塊地址獲取對應的 span 對象指針,如下所示:

// PageCache.cpp
// 根據傳入的內存塊地址返回對應的span對象的指針
Span* PageCache::get_span_from_pageID(void* ptr)
{// 1. 先根據地址求出其所屬的頁號page_t pid = ((page_t)ptr >> PAGE_SHIFT);// 2. 找到對應的頁號對應的span指針進行返回auto it = _tables.find(pid);if (it != _tables.end())return it->second;elsereturn nullptr;
}

? 有了上面的鋪墊,我們就能實現 merge_memory_from_ThreadCache() 函數了,但是需要借助到頁緩存中實現的 merge_memory_from_CentralCache() 接口,這個接口等下面我們將頁緩存的回收內存再來講!剩下的具體步驟參考下面代碼以及注釋:

// CentralCache.cpp
// 將size大小的內存塊合并到對應的span哈希桶中
void CentralCache::merge_memory_from_ThreadCache(void* start, size_t size)
{// 1. 獲取對應哈希桶下標并且加上桶鎖size_t index = AlignClass::get_index(size);_spanlists[index].get_mutex().lock();// 2. 遍歷每一個小內存塊while (start != nullptr){// 3. 找到小內存塊對應的span對象,并將該小內存塊鏈接到該span上面(先記錄下start后面的指針next,防止start修改連接后丟失)void* next = get_next(start);Span* span = PageCache::get_instance()->get_span_from_pageID(start);get_next(start) = span->_freelist;span->_freelist = start;// 4. 減少span的使用個數,然后判斷是否可以回收給PageCachespan->_use_count--;if (span->_use_count == 0){// 可以的話調用PageCache對應的接口回收該span對象// 再次之前要先將該span對象從central cache中彈出,并且修改span的鏈接屬性_spanlists[index].erase(span);span->_freelist = nullptr;span->_next = span->_prev = nullptr;// 因為涉及到頁緩存操作,所以需要加鎖,并且此時可以順便先將中心緩存的鎖給釋放,讓其它線程可以執行,提高效率_spanlists[index].get_mutex().unlock();PageCache::get_instance()->get_mutex().lock();PageCache::get_instance()->merge_memory_from_CentralCache(span); // 這步就是PageCache回收span對象的操作PageCache::get_instance()->get_mutex().unlock();_spanlists[index].get_mutex().lock();}// 5. 別忘了讓start迭代start = next;}// 6. 最后別忘了釋放桶鎖_spanlists[index].get_mutex().unlock();
}

Ⅲ. page cache的內存回收

? 頁緩存的內存回收,就是嘗試將中心緩存返回的小 span 對象以及其相鄰的空閑的 span 對象合并成一個頁。比如中心緩存中一個 span 對象中有 3 頁,其左邊有一個空閑的 span 對象為 2 頁大小,那么就將它們兩個合并成一個 5 頁大小的 span 對象交給頁緩存管理!

? 但此時有一個問題,就是我們得知道其相鄰的 span 對象是否為空閑狀態,如果我們用 _use_count == 0 去判斷為空閑的話,是不合適的,因為有可能別的線程正在準備使用該相鄰的內存塊,但是還沒有將其 _use_count 進行改變,此時如果我們就將其判斷為空閑然后回收的話,那么別的線程就出錯了!

? 所以需要 Span 結構體中再添加一個布爾值變量 _is_used 來表示當前對象是否被線程使用著

// 管理以頁為單位的大內存塊
struct Span
{page_t _pid = 0; // 大塊內存起始頁的頁號size_t _num = 0; // 頁的個數Span* _next = nullptr; // 雙向鏈表結構Span* _prev = nullptr;size_t _use_count = 0;	   // 當前分配給ThreadCache對象的小內存塊個數void* _freelist = nullptr; // 當前大內存塊對應的空閑鏈表bool _is_used = false; // 表示當前對象是否被線程使用著
};

? 然后我們只需要用中心擴散法,向前和向后合并空閑的內存塊即可,最后再將合并好的內存塊掛到 page cache 中,如下所示,具體參考代碼注釋:

// PageCache.cpp
// 將CentralCache傳來的span對象合并到頁緩存中管理
void PageCache::merge_memory_from_CentralCache(Span* span)
{// 1. 向前合并空閑的span對象while (true){// 獲取前面一個span對象(如果沒有則直接退出)page_t prev_pid = span->_pid - 1;auto it = _tables.find(prev_pid);if (it == _tables.end())break;// 如果span對象被使用著,或者和當前span的頁數加起來超過了PAGELIST_NUMS-1頁的話,則直接退出Span* prev_span = it->second;if (prev_span->_is_used == true)break;if (prev_span->_num + span->_num > PAGELIST_NUMS - 1)break;// 走到這說明prev_span對象是可以合并的,則將其進行合并操作// 合并操作其實只需要修改span的屬性即可,然后將原本prev_span從哈希表中去除,最后釋放prev_span空間的內容即可span->_pid = prev_span->_pid;span->_num += prev_span->_num;_spanlists[prev_span->_num].erase(prev_span);delete prev_span;}// 2. 向后合并空閑的span對象(和上面是類似的操作,就是位置變了而已)while (true){// 獲取后面一個span對象(如果沒有則直接退出)page_t next_pid = span->_pid + span->_num;auto it = _tables.find(next_pid);if (it == _tables.end())break;// 如果span對象被使用著,或者和當前span的頁數加起來超過了PAGELIST_NUMS-1頁的話,則直接退出Span* next_span = it->second;if (next_span->_is_used == true)break;if (next_span->_num + span->_num > PAGELIST_NUMS - 1)break;// 走到這說明next_span對象是可以合并的,則將其進行合并操作// 合并操作其實只需要修改span的屬性即可,然后將原本next_span從哈希表中去除,最后釋放next_span空間的內容即可span->_num += next_span->_num;_spanlists[next_span->_num].erase(next_span);delete next_span;}// 3. 將合并后的span對象插入到page cache的哈希桶中,然后設置其映射關系_spanlists[span->_num].push_front(span);span->_is_used = false;_tables[span->_pid] = span;_tables[span->_pid + span->_num - 1] = span;
}

在這里插入圖片描述

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

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

相關文章

DeerFlow 實踐:華為IPD流程的評審智能體設計

目錄 一、項目背景與目標 二、IPD 流程關鍵評審點與 TR 點解析 &#xff08;一&#xff09;4 個關鍵評審點 &#xff08;二&#xff09;6 個 TR 點 三、評審智能體詳細設計與協作機制 機制設計核心原則 &#xff08;一&#xff09;概念評審&#xff08;CDCP&#xff09;…

【ubuntu】ubuntu中找不到串口設備問題排查

ubuntu中找不到串口問題排查1. 檢查設備識別情況2. 檢查并安裝驅動3. 檢查內核消息4. 禁用brltty服務1. 停止并禁用 brltty 服務2. 完全移除 brltty 包3. 重啟系統或重新插拔設備5.輸出結果問題&#xff1a;虛擬機ubuntu中&#xff0c;已經顯示串口設備連接成功&#xff0c;但是…

Unity 性能優化 之 靜態資源優化 (音頻 | 模型 | 紋理 | 動畫)

Unity 之 性能優化 -- 靜態資源優化參考性能指標靜態資源資源工作流程資源分類原理小結Audio 實戰優化建議模型導入工作流程DCC中模型導出.DCC中Mesh生產規范模型導出檢查流程模型優化建議紋理優化紋理基礎概念紋理類型紋理大小紋理顏色空間紋理壓縮紋理圖集紋理過濾紋理Mipmap…

GitHub 熱榜項目 - 日榜(2025-09-13)

GitHub 熱榜項目 - 日榜(2025-09-13) 生成于&#xff1a;2025-09-13 統計摘要 共發現熱門項目&#xff1a;18 個 榜單類型&#xff1a;日榜 本期熱點趨勢總結 本期GitHub熱榜項目呈現三大技術熱點&#xff1a;AI開發工具化&#xff08;如GenKit、ROMA多智能體框架&#xff…

Pytest 常見問題及其解決方案

常見問題及解決方案 1. 測試通過了,但覆蓋率不達標 現象: 雖然所有測試都通過了,但覆蓋率報告顯示某些代碼沒有被覆蓋。 解決方案: 檢查覆蓋率配置:確保 .coveragerc 或 pytest.ini 中正確設置了要分析的源代碼路徑。 使用標記(markers)排除測試文件本身:避免測試代…

直擊3D內容創作痛點-火山引擎多媒體實驗室首次主持SIGGRAPH Workshop,用前沿技術降低沉浸式內容生成門檻

當3D、VR技術在游戲、教育、醫療、文化領域遍地開花&#xff0c;“內容短缺”卻成了制約行業爆發的關鍵瓶頸——傳統3D/4D創作不僅耗時耗力、依賴專業技能&#xff0c;還難以適配消費級設備&#xff0c;讓許多創作者望而卻步。近日&#xff0c;由火山引擎多媒體實驗室聯合領域頂…

華為基本命令

我們使用的是華為官方的模擬器eNSP 一、華為設備的模式 華為的設備有兩種模式&#xff1a; 用戶視圖和系統視圖 用戶視圖只能讀取&#xff0c;或者進行一些基礎查詢 系統視圖能對設備和接口進行一些配置管理&#xff0c;和一些高級操作 在“用戶視圖”下使用system-view系統可…

2025.9.14英語紅寶書【必背16-20】

單詞組合 中文速記句子 英文句子 confine, misery, necessitate, negotiate, preach, precaution, precision, stretch 病人被 confine(限制) 在床上,感受 misery(痛苦),情況 necessitate(需要) 醫生 negotiate(商討),牧師 preach(布道) 并提醒 precaution(預防)…

HUST-STAR電控組視覺任務

視覺任務 注意&#xff1a;視覺部分建議采用 python 完成&#xff0c;下面教程也大多針對 python。其原因在于 python 配置相應環境更為輕松&#xff0c;且內置庫較為豐富&#xff0c;屬于初學者友好類型。沒接觸過 python 也不必擔心&#xff0c;它的大體邏輯與 C 相近&#…

壓縮和歸檔 文件傳輸

壓縮和歸檔壓縮&#xff1a;4G----1.5Gbzip2-bunzip2 gzip-gunzip xz-unxzgzip 要壓縮的文件原來的文件就會被刪除 (壓縮和解壓縮)會生成一個 aaa.gz 的文件歸檔&#xff1a; 4G----4G 打包tarc 創建歸檔文件 v 看到創建的詳細過程 f 文件類型 t 不展開歸檔文件&…

深入探索 C++ 元組:從基礎到高級應用

在現代 C 編程中&#xff0c;元組&#xff08;std::tuple&#xff09;是一個強大且靈活的容器&#xff0c;能夠存儲和操作多個不同類型的數據。它在標準庫中扮演著重要角色&#xff0c;并在實際開發中提供了諸多便利。本文將全面探討 C 元組的各個方面&#xff0c;從基礎用法到…

Excel批量處理一列數據---分列功能

0 Preface/Foreword當有多行數據需要處理時&#xff0c;為了減少手動操作&#xff0c;可以EXCEL數據分列功能可以提高效率。1 數據分列1.1 數據分類步驟如下&#xff1a;選中需要處理的一列數據&#xff1b;選擇菜單欄中的“數據”&#xff1b;選擇分列按照需求設置即可1.2 查找…

HTTPS + 域名 + 雙向證書認證(下)

文章目錄1. .p12文件1.1 主要特點1.2 常見用途1.3 常見操作1.4 與其他格式的區別1.5 與公鑰的區別和聯系1.6 安全性注意事項2. Nginx 配置2.1 location指令2.2 alias 與 root 指令的區別3 雙向認證配置3.1 創建根證書3.1.1 生成根CA的私鑰3.1.2 生成請求證書3.1.3 生成自簽署CA…

嵌入式 - ARM3

一、arm啟動C語言1. 配置異常向量表2. 實現了軟件中斷的部分注&#xff1a;ldmfd sp!, {r0-r12, lr} ldmfd sp!, {r0-r12, pc}^ bx lr 左半部分&#xff1a;繁瑣易理解的返回方式&#xff1a;先彈出所有通用寄存器和lr &…

如何通過標簽和分類提升知識復用效率

通過標簽和分類提升知識復用效率&#xff0c;其核心在于構建一個結構化與靈活性兼備的知識組織體系。這需要將分類的“確定性”與標簽的“多維性”進行有效結合&#xff0c;為知識的存儲與檢索建立清晰的“骨架”和豐富的“神經網絡”。具體實踐中&#xff0c;要求我們進行頂層…

ZYNQ PS讀寫PL BRAM

一、實驗室任務 本章的實驗任務是 PS 將數據寫入BRAM&#xff0c;然后從 BRAM 中讀出數據&#xff0c;并通過串口打印出來&#xff1b;與此同時&#xff0c;PL 從通過自定義ip核從BRAM中同樣讀出數據&#xff0c;并通過ILA 來觀察讀出的數據與串口打印的數據是否一致。這里是通…

LinuxC++項目開發日志——高并發內存池(5-page cache框架開發)

PageCachepage cache 設計邏輯一、PageCache 的核心定位&#xff1a;理解它與 CentralCache 的本質區別二、PageCache 的內存分配流程&#xff1a;從 “精確匹配” 到 “拆分適配”三、PageCache 的內存釋放流程&#xff1a;合并小 Span&#xff0c;解決內存碎片問題page cache…

Matplotlib:繪制你的第一張折線圖與散點圖

Matplotlib入門&#xff1a;繪制你的第一張折線圖與散點圖導語 歡迎來到 Matplotlib 的世界&#xff01;對于任何使用 Python 進行數據分析或機器學習的人來說&#xff0c;數據可視化都是一項至關重要的技能。Matplotlib 是 Python 中最流行、最基礎的可視化庫&#xff0c;它功…

MySQL保姆級安裝教程

MySQL 安裝詳細文檔&#xff0c;適用于 Windows、macOS 和 Linux 系統&#xff0c;包含了從下載到驗證安裝的完整步驟&#xff1a; 一、Windows 系統安裝 MySQL 1. 下載 MySQL 安裝包 訪問 MySQL 官方下載頁&#xff1a;https://dev.mysql.com/downloads/installer/選擇 “MySQ…

重塑你的大腦:從理解突觸到掌控人生

重塑你的大腦&#xff1a;從理解突觸到掌控人生你是否曾對自己的某些行為感到無力&#xff1f;明知應該早睡&#xff0c;卻總忍不住刷手機&#xff1b;下定決心要鍛煉&#xff0c;卻常常半途而廢。這些困擾我們的習慣&#xff0c;并非簡單的意志力問題&#xff0c;其根源深深植…