LinuxC++项目开发日志——高并发内存池(5-page cache框架开发)
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;
其他类
与前文相同