openGauss数据库源码解析(三)| 公共组件源码解析(5)

2023年 11月 16日 42.7k 0

3.5 内存管理

数据库在运行过程中涉及许多对象,这些对象具有不同的生命周期,有些处理需要频繁分配内存。如一个SQL语句,在解析时需要对词法单元和语法单元分配内存,在执行过程中需要对执行状态分配内存。在事务结束时,如果不是PREPARE语句,那么SQL语句的执行计划内存和执行过程的状态内存都需要释放。如果是PREPARE语句,那么执行计划需要保存到缓冲池中,执行过程的状态内存释放即可。为了保证内存分配的高效和避免内存泄漏,openGauss设计开发了自己的内存管理,代码实现在“openGauss-serversrccommonbackendutilsmmgr”目录。

openGauss在内存管理上采用了上下文的概念,即具有同样生命周期或者属于同一个上下文语义的内存放到一个MemoryContext管理,MemoryContext的结构代码如下(结构成员参照注释):

typedef struct MemoryContextData {
NodeTag type; /* 上下文类别*/
MemoryContextMethods* methods; /* 虛函数表*/
MemoryContext parent; /* 父上下文。顶级上下文为 NULL*/
MemoryContext firstchild; /* 子上下文的链表头*/
MemoryContext prevchild; /* 前向子上下文 */
MemoryContext nextchild; /* 后向子上下文 */
char* name; /* 上下文名称,方便调试 */
pthread_rwlock_t lock; /*上下文共享时的并发控制锁 */
bool is_shared; /* 上下文是否在多个线程共享 */
bool isReset; /* isReset为true时,表示复位后没有内存空间用于分配*/
int level; /* 上下文层次级别*/
uint64 session_id; /* 上下文属于的会话ID */
ThreadId thread_id; /* 上下文属于的线程ID */
} MemoryContextData;

虛函数表就是具体的内存管理操作函数指针,具体定义代码如下(函数功能参照注释):

/*在上下文中分配内存*/
void* (*alloc)(MemoryContext context, Size align, Size size, const char* file, int line);
/* 释放pointer 内存到上下文中*/
void (*free_p)(MemoryContext context, void* pointer);
/*在上下文中重新分配内存*/
void* (*realloc)(MemoryContext context, void* pointer, Size align, Size size, const char* file, int line);
void (*init)(MemoryContext context); /*上下文初始化*/
void (*reset)(MemoryContext context); /*上下文复位*/
void (*delete_context)(MemoryContext context); /*删除上下文 */
Size (*get_chunk_space)(MemoryContext context, void* pointer); /*获取上下文块大小 */
bool (*is_empty)(MemoryContext context); /*上下文是否为空*/
void (*stats)(MemoryContext context, int level); /*上下文信息统计*/
#ifdef MEMORY_CONTEXT_CHECKING
void (*check)(MemoryContext context); /*上下文异常检查*/
#endif
} MemoryContextMethods;

这些回调函数指针初始化是在AllocSetContextSetMethods函数中调用AllocSetMethodDefinition函数完成的。AllocSetMethodDefinition函数的实现代码如下:

void AlignMemoryAllocator::AllocSetMethodDefinition(MemoryContextMethods* method)
{
method->alloc = &AlignMemoryAllocator::AllocSetAlloc;
method->free_p = &AlignMemoryAllocator::AllocSetFree;
method->realloc = &AlignMemoryAllocator::AllocSetRealloc;
method->init = &AlignMemoryAllocator::AllocSetInit;
method->reset = &AlignMemoryAllocator::AllocSetReset;
method->delete_context = &AlignMemoryAllocator::AllocSetDelete;
method->get_chunk_space = &AlignMemoryAllocator::AllocSetGetChunkSpace;
method->is_empty = &AlignMemoryAllocator::AllocSetIsEmpty;
method->stats = &AlignMemoryAllocator::AllocSetStats;
#ifdef MEMORY_CONTEXT_CHECKING
method->check = &AlignMemoryAllocator::AllocSetCheck;
#endif
}

可以看到,这些实际操作内存管理的函数为AlignMemoryAllocator类中的AllocSetAlloc函数、AllocSetFree函数、AllocSetRealloc函数、AllocSetInit函数、AllocSetReset函数、AllocSetDelete函数、AllocSetGetChunkSpace函数、AllocSetIsEmpty函数、AllocSetStats函数和AllocSetCheck函数。在这些处理函数中,涉及的结构体代码如下:

typedef struct AllocSetContext {
MemoryContextData header; /*内存上下文,存储空间是在这个内存上下文中分配的 */
AllocBlock blocks; /* AllocSetContext所管理内存块的块链表头 */
AllocChunk freelist[ALLOCSET_NUM_FREELISTS]; /* 空闲块链表*/
/*这个上下文的分配参数 */
Size initBlockSize; /* 初始块大小*/
Size maxBlockSize; /* 最大块大小 */
Size nextBlockSize; /* 下一个分配的块大小 */
Size allocChunkLimit; /* 块大小上限*/
AllocBlock keeper; /* 在复位时,保存的块 */
Size totalSpace; /* 这个上下文分配的总空间 */
Size freeSpace; /* 这个上下文总的空闲空间 */
Size maxSpaceSize; /* 最大内存空间 */
MemoryTrack track; /* 跟踪内存分配信息 */
} AllocSetContext;
AllocBlock定义如下:
typedef struct AllocBlockData* AllocBlock;
typedef struct AllocBlockData {
AllocSet aset; /* 哪个AllocSetContext 拥有此块,AllocBlockData 归属AllocSetContext管理*/
AllocBlock prev; /* 在块链表中的前向指针 */
AllocBlock next; /* 在块链表中的后向指针 */
char* freeptr; /* 这个块空闲空间的起始地址 */
char* endptr; /* 这个块空间的结束地址*/
Size allocSize; /* 分配的大小*/
#ifdef MEMORY_CONTEXT_CHECKING
uint64 magicNum; /* 魔鬼数字值,用于内存校验。当前代码固定填写为DADA */
#endif
} AllocBlockData;
typedef struct AllocChunkData* AllocChunk; /* AllocChunk 内存前面部分是一个AllocBlock结构*/
typedef struct AllocChunkData {
void* aset; /* 拥有这个chunk的AllocSetContext,如果空闲,则为空闲列表链接*/
Size size; /* chunk中的使用空间 */
#ifdef MEMORY_CONTEXT_CHECKING
Size requested_size; /* 实际请求大小,在空闲块中时为0 */
const char* file; /* palloc/palloc0调用时的文件名称 */
int line; /* palloc/palloc0 调用时的行号*/
uint32 prenum; /* 前向魔鬼数字*/
#endif
} AllocChunkData;

从前面的数据结构可以看出,核心数据结构为AllocSetContext,这个数据结构有3个成员“MemoryContextData header;”、“AllocBlock blocks;”和“AllocChunk freelist[ALLOCSET_NUM_FREELISTS];”。这3个成员把内存管理分为3个层次。
(1) MemoryContext管理上下文之间的父子关系,设置MemoryContext的内存管理函数。
(2) AllocBlock blocks把所有内存块通过双链表链接起来。
(3) 具体的内存单元chunk。内存单元chunk是从内存块AllocBlock内部分配的,内存块和内存单元chunk的转换关系为:“AllocChunk chunk = (AllocChunk)(((char*)block) + ALLOC_BLOCKHDRSZ);”和“AllocBlock block = (AllocBlock)(((char*)chunk) - ALLOC_BLOCKHDRSZ);”。
内存单元chunk经过转换得到最终的用户指针,内存单元chunk和用户指针的转换关系为:((AllocPointer)(((char*)(chk)) + ALLOC_CHUNKHDRSZ))和((AllocChunk)(((char*)(ptr)) - ALLOC_CHUNKHDRSZ))。数据结构的基本关系如图3-3所示。

图3-3 数据结构的基本关系
下面先看第1层MemoryContext(内存上下文)的实现,主要实现在mcxt.cpp文件中,如表3-4所示。

再看第2层AllocSet的实现,主要实现在aset.cpp文件中,如表3-5所示。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论