文章目录

  • 前言
  • Ⅰ. `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;其根源深深植…