上一篇openGauss数据库源码解析系列文章——公共组件源码解析(上)中,从系统表、数据库初始化、多线程架构及线程池技术四个方面对公共组件的源代码实现进行介绍。本篇接着从内存管理、多维监控、模拟信号机制展开介绍。
五、内存管理
数据库在运行过程中涉及许多对象,这些对象具有不同的生命周期,有些处理需要频繁分配内存。如一个SQL语句,在解析时需要对词法单元和语法单元分配内存,在执行过程中需要对执行状态分配内存。在事务结束时,如果不是prepare语句,那么SQL语句的执行计划内存和执行过程的状态内存都需要释放。如果是prepare语句,那么执行计划需要保存到缓冲池中,执行过程的状态内存释放即可。为了保证内存分配的高效和避免内存泄漏,openGauss设计开发了自己的内存管理,代码实现在“openGauss-server\src\common\backend\utils\mmgr”目录。
openGauss在内存管理上采用了上下文的概念,即具有同样生命周期或者属于同一个上下文语义的内存放到一个MemoryContext管理,MemoryContext的结构代码如下(结构成员参照注释):
typedef struct MemoryContextData* 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;
虛函数表就是具体的内存管理操作函数指针,具体定义代码如下(函数功能参照注释):
typedef struct MemoryContextMethods {
/*在上下文中分配内存*/
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函数的实现代码如下:
template <bool enable_memoryprotect, bool is_shared, bool is_tracked>
void AlignMemoryAllocator::AllocSetMethodDefinition(MemoryContextMethods* method)
{
method->alloc = &AlignMemoryAllocator::AllocSetAlloc<enable_memoryprotect, is_shared, is_tracked>;
method->free_p = &AlignMemoryAllocator::AllocSetFree<enable_memoryprotect, is_shared, is_tracked>;
method->realloc = &AlignMemoryAllocator::AllocSetRealloc<enable_memoryprotect, is_shared, is_tracked>;
method->init = &AlignMemoryAllocator::AllocSetInit;
method->reset = &AlignMemoryAllocator::AllocSetReset<enable_memoryprotect, is_shared, is_tracked>;
method->delete_context = &AlignMemoryAllocator::AllocSetDelete<enable_memoryprotect, is_shared, is_tracked>;
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 AllocSetContext* AllocSet;
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))。数据结构的基本关系如图1所示。
图1 数据结构的基本关系
下面先看第1层MemoryContext的实现,主要实现在mcxt.cpp文件中,如表1所示。
表1 MemoryContext的实现函数
函数 | 功能介绍 |
---|---|
ChooseRootContext | 在线程池机制下,上下文有3个类别,即实例级别、会话级别、线程级别。这个函数根据tag类型和parent返回相应类别的根上下文 |
MemoryContextCreate | 首先根据root是否为空确定是从父MemoryContext分配内存还是从操作系统调用malloc分配内存,然后对分配的MemoryContext进行初始化,如果存在父MemoryContext,则挂到父MemoryContext上 |
MemoryContextDelete | 先删除这个MemoryContext的子节点,把这个MemoryContext的父节点置为空,回调AllocSetDelete方法释放分配的对象,最后释放上下文本身 |
MemoryContextIsEmpty | 先看看当前上下文是否有子节点,然后回调is_empty检查当前上下文是否为空 |
MemoryContextReset | 先看看当前上下文是否有子节点,如果有子节点,则遍历子节点,递归调用MemoryContextReset进行复位。最后回调reset复位当前上下文 |
MemoryContextSetParent | 如果上下文有父节点,则从父上下文解除当前上下文。然后把上下文挂到新的父上下文 |
GetMemoryChunkSpace | 当前指针pointer前移STANDARDCHUNKHEADERSIZE找到标准StandardChunkHeader位置,然后根据块属于上下文的回调AllocSetGetChunkSpace获取块空间大小 |
MemoryContextStats | 先回调AllocSetStats统计当前上下文信息,然后遍历子节点,递归调用MemoryContextStatsInternal统计上下文信息 |
MemoryContextAllocDebug | 检查分配内存大小(size)是否小于MaxAllocSize,回调AllocSetAlloc分配内存 |
pfree | 根据当前指针pointer偏移STANDARDCHUNKHEADERSIZE找到标准头,根据头部StandardChunkHeader找到归属的上下文,回调AllocSetFree释放内存 |
再看第2层AllocSet的实现,主要实现在aset.cpp文件中,如表2所示。
表2 AllocSet的实现函数 |函数 | 功能介绍 | |--|--| |AllocSetFreeIndex | 根据请求的size(内存大小)计算应该在那个空闲块链表freechunk中分配内存 | |set_sentinel | 设置哨兵0x7E,用于踩内存检查 | | sentinel_ok |内存检查是否正常 | | MemoryContextControlSet |根据白名单设置上下文的maxSpaceSize限制大小 | |AllocSetContextCreate | 根据contextType类型,调用不同的AllocSetContextCreate分配器分配MemoryContext| |AllocSetMethodDefinition | 设置MemoryContext回调处理方法 | |AllocSetContextSetMethods | 设置不同上下文类型的分配器 | | AllocSetContextCreate | 创建一个具体的MemoryContext。根据类型,确定内存保护函数。调用MemoryContextCreate函数创建一个AllocSetContext,设置maxSpaceSize大小,设置回调方法,设置初始块大小、下一个块大小、最大块大小。根据分配的最大块大小设置allocChunkLimit。如果上下文最值超过了“ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ”,则分配一个AllocBlock;设置AllocBlock的上下文,空闲起始地址(freeptr)为跳过头部管理占用空间后剩余的空间,末尾地址(endptr)为块结束地址,分配大小(allocSize)为分配的块大小,魔鬼数字(magicNum)为0xDADADADADADADADA,context的总空间(totalSpace)加上这次分配的块大小,上下文的空闲空间(freeSpace)加上这个块的空闲空间,块的前向和后向指针为空,AllocSetContext的第一个块(blocks)指向这个块,保留块(keeper)指向这个块。返回AllocSetContext| |AllocSetInit | 特定AllocSetContext初始化函数,当前为空,没有使用| |AllocSetReset | 根据上下文类型选择保护函数,把空闲链表(freelist)置空,遍历内存块,如果是保留块,则对这块内存重新初始化。如果不是,则释放掉。根据是否有保留块重新初始化上下文| | AllocSetDelete | 如果上下文没有内存块,则直接返回。根据上下文类型选择保护函数,获取内存块首地址,把保留块和内存块头置空。遍历所有内存块,释放掉。把上下文的空闲空间(freeSpace)和总空间(totalSpace)置为0 | |AllocSetAlloc | 根据上下文类型选择保护函数,如果申请内存大小超过了内存块大小上限,则直接调用OS(操作系统)接口分配一个内存块,初始化这个内存块。把内存块转换为一个内存单元(chunk),对这个内存单元chunk进行初始化。把这个内存块挂到上下文上,返回内存单元chunk指针。如果申请内存大小没有超过内存块大小上限,根据内存大小映射的空闲链表(freelist),看看相应空闲链表中是否有相应大小的内存单元(chunk),如果有空闲的内存单元(chunk),则分配一个内存单元(chunk)返回。如果空闲链表(freelist)没有空闲的chunk,看看当前的块(blocks)是否有足够的内存,如果没有足够的内存,则根据块剩余的内存大小放到相应的空闲链表(freelist)中。分配一个新的块,对这个新的块进行初始化,把块挂到上下文上,从这个块上分配一个内存单元(chunk)返回。如果当前的块有足够的内存,则从当前块上分配一个内存单元(chunk)返回 | | AllocSetFree | 根据类型,确定内存保护函数。如果释放内存单元大小超过了内存块大小上限,把这个内存单元转换为AllocBlock,把内存块从上下文中解除,把内存块变量置空,释放内存块、如果内存单元大小小于内存块上限,则根据内存单元大小映射的空闲链表(freelist),把释放的内存单元挂到上下文的空闲链表(freelist)上 | | AllocSetRealloc | 根据类型,确定内存保护函数。如果原来的内存单元大小能够满足请求的大小,则重新赋值新的内存检查信息后直接返回当前的内存单元、如果旧的内存单元大小超过了内存块的上限,则调用OS重分配内存接口重新分配一个内存块,对新的内存块初始化,把新的内存块转换为内存单元AllocChunk返回、如果旧的内存单元大小没有超过内存块的上限,则根据新的大小调用AllocSetAlloc分配一个新的内存单元,把旧的内存单元值复制到新的内存单元,调用AllocSetFree释放原来的内存单元 | | AllocSetGetChunkSpace | 返回内存单元的大小,包括头部占用的空间 | | AllocSetIsEmpty | 检查是否复位(isReset),如果是,则返回true;否则返回false | | AllocSetStats | 显示上下文的内存消耗信息,打印到标准stderr输出上 |
六、多维监控
数据库是企业的关键组件,数据库的性能直接决定了很多业务的吞吐量。为了简化数据库维护人员的调优,openGauss对数据库运行进行了多维度的监控,并且开发了一些维护特性,比如WDR(wordload dignostic report,工作负荷诊断报告)性能诊断报告、慢SQL诊断、session性能诊断、系统KPI(key performance indicator,关键性能指标)辅助诊断等,帮助维护人员对数据库的性能进行诊断。这些监控项都以视图的方式对外呈现,集中在DBE_PERF模式下。WDR Snapshot除了自身快照的元数据,其他数据表来源也是DBE_PERF schema下的视图。WDR Snapshot数据表命名原则:snap_{源数据表},根据这个关系可以找到snap表所对应的原表。对这些视图的解释参照openGauss的官网(https://opengauss.org)中《开发者指南》手册的“DBE_PERF schema”章节。 性能视图的源代码在“openGauss-server\src\common\backend\catalog\performance_views.sql”文件中(网址为:https://gitee.com/opengauss/openGauss-server/blob/master/src/common/backend/catalog/performance_views.sql,安装后会复制到安装路径的“/share/postgresql/performance_views.sql”下)。在数据库初始化阶段由initdb读取这个文件在数据库系统中创建相应的视图。这些视图遵循了openGauss通用视图的实现逻辑,即视图来自函数的封装,这些函数可能是内置函数,也可能是存储函数。OS运行的性能视图“dbe_perf.get_global_os_runtime”的相关代码如下:
CREATE OR REPLACE FUNCTION dbe_perf.get_global_os_runtime
(OUT node_name name, OUT id integer, OUT name text, OUT value numeric, OUT comments text, OUT cumulative boolean)
RETURNS setof record
AS $$
DECLARE
row_data dbe_perf.os_runtime%rowtype;
query_str := 'SELECT * FROM dbe_perf.os_runtime';
FOR row_data IN EXECUTE(query_str) LOOP
......
END LOOP;
return;
END; $$
LANGUAGE 'plpgsql' NOT FENCED;
CREATE VIEW dbe_perf.global_os_runtime AS
SELECT DISTINCT * FROM dbe_perf.get_global_os_runtime();
global_os_runtime视图来自存储函数get_global_os_runtime的封装,在存储函数内访问“dbe_perf.os_runtime”视图、os_runtime视图的SQL语句为“CREATE VIEW dbe_perf.os_runtime AS SELECT * FROM pv_os_run_info();”。pv_os_run_info是内置函数,而内置函数负责读取数据库系统的监控指标,pv_os_run_info函数的相关代码如下:
Datum pv_os_run_info(PG_FUNCTION_ARGS)
{
FuncCallContext* func_ctx = NULL;
/* 判断是不是第一次调用 */
if (SRF_IS_FIRSTCALL()) {
MemoryContext old_context;
TupleDesc tup_desc;
/* 创建函数上下文 */
func_ctx = SRF_FIRSTCALL_INIT();
/*
* 切换内存上下文到多次调用上下文
*/
old_context = MemoryContextSwitchTo(func_ctx->multi_call_memory_ctx);
/* 创建一个包含5列的元组描述模板*/
tup_desc = CreateTemplateTupleDesc(5, false);
TupleDescInitEntry(tup_desc, (AttrNumber)1, "id", INT4OID, -1, 0);
TupleDescInitEntry(tup_desc, (AttrNumber)2, "name", TEXTOID, -1, 0);
TupleDescInitEntry(tup_desc, (AttrNumber)3, "value", NUMERICOID, -1, 0);
TupleDescInitEntry(tup_desc, (AttrNumber)4, "comments", TEXTOID, -1, 0);
TupleDescInitEntry(tup_desc, (AttrNumber)5, "cumulative", BOOLOID, -1, 0);
/* 填充元组描述模板 */
func_ctx->tuple_desc = BlessTupleDesc(tup_desc);
/* 收集系统信息 */
getCpuNums();
getCpuTimes();
getVmStat();
getTotalMem();
getOSRunLoad();
(void)MemoryContextSwitchTo(old_context);
}
/*设置函数的上下文,每次函数调用都需要*/
func_ctx = SRF_PERCALL_SETUP();
while (func_ctx->call_cntr < TOTAL_OS_RUN_INFO_TYPES) {
/* 填充所有元组每个字段的值 */
Datum values[5];
bool nulls[5] = {false};
HeapTuple tuple = NULL;
errno_t rc = 0;
rc = memset_s(values, sizeof(values), 0, sizeof(values));
securec_check(rc, "\0", "\0");
rc = memset_s(nulls, sizeof(nulls), 0, sizeof(nulls));
securec_check(rc, "\0", "\0");
if (!u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].got) {
ereport(DEBUG3,
(errmsg("the %s stat has not got on this plate.",
u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].name)));
func_ctx->call_cntr++;
continue;
}
values[0] = Int32GetDatum(func_ctx->call_cntr);
values[1] = CStringGetTextDatum(u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].name);
values[2] = u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].getDatum(
u_sess->stat_cxt.osStatDataArray[func_ctx->call_cntr]);
values[3] = CStringGetTextDatum(u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].comments);
values[4] = BoolGetDatum(u_sess->stat_cxt.osStatDescArray[func_ctx->call_cntr].cumulative);
tuple = heap_form_tuple(func_ctx->tuple_desc, values, nulls);
SRF_RETURN_NEXT(func_ctx, HeapTupleGetDatum(tuple));
}
/* 填充结束,返回结果 */
SRF_RETURN_DONE(func_ctx);
}
pv_os_run_info函数可以分为3段:
(1)调用CreateTemplateTupleDesc函数和TupleDescInitEntry函数定义元组描述信息。 (2)调用getCpuNums函数、getCpuTimes函数、getVmStat函数、getTotalMem函数、getOSRunLoad函数收集系统信息。 (3)把收集的u_sess信息填充到元组数据中,最后返回给调用者。openGauss提供了实现返回结果的通用SQL函数的实现步骤和方法,它们是SRF_IS_FIRSTCALL、SRF_PERCALL_SETUP、SRF_RETURN_NEXT和SRF_RETURN_DONE。从代码可以看出,pv_os_run_info的实现流程也是遵循openGauss通用的SQL函数实现方法。 系统指标的收集来自读取系统信息,对数据库系统中一些模块进行打点(打点就是按照规格采集指定数据,用以记录系统运行的一些关键点)。很多打点集中在两个方面: 事务执行次数和执行时间。从而推断最大时间、最小时间、平均时间。这些比较分散,代码逻辑相对简单,这里不再进行介绍。只需要根据内置函数读取的变量查看这些变量赋值的地方就可以追踪具体的实现位置。
openGauss数据库主要维护特性的实现代码在“openGauss-server\src\gausskernel\cbb\instruments”目录中,比如WDR、SQL百分位计算,这里不再进行介绍。
性能统计对openGauss的正常运行也会带来一定的性能损耗,所以这些特性都有开关控制。具体说明如下。
(1)等待事件信息实时收集功能的开关为enable_instr_track_wait。 (2)Unique SQL信息实时收集功能的开关为enable_instr_unique_sql、enable_instr_rt_percentile。 (3)数据库监控快照功能的开关为enable_wdr_snapshot。 其他功能也都有相应的GUC参数进行调节,根据平常使用的需要,可以打开具体维护项查看系统的运行情况。
七、模拟信号机制
信号是Linux进程/线程之间的一种通信机制,向一个进程发送信号的系统函数是kill,向一个线程发送信号的系统函数是pthread_kill。在openGauss中既有gs_ctl向openGauss进程发送的进程间信号,也有openGauss进程中线程间的信号。
信号是一种有限的资源,OS提供的信号有SIGINT、SIGQUIT、SIGTERM、SIGALRM、SIGPIPE、SIGFPE、SIGUSR1、SIGUSR2、SIGCHLD、SIGTTIN、SIGTTOU、SIGXFSZ等。这些信号一般都是系统专用的,每个信号都有专门的用途,比如SIGALRM是系统定时器的通知信号。留给应用的信号主要是SIGUSR1、SIGUSR2。
在系统信号有限的情况下,为了在openGauss中表达不同的丰富的通信语义,openGauss额外增加了新的变量表示具体的语义。openGauss是多线程架构,在同一个进程内如果不同的线程注册了不同的处理函数,则后者会覆盖前者的信号处理。为了不同线程能够注册不同的处理函数,需要自己管理信号对应的注册函数。为了解决这些问题,openGauss实现了信号的模拟机制。信号模拟的基本原理是每个线程注册管理自己的信号处理函数,信号枚举值仍然使用系统的信号值,线程使用自己的变量记录信号和回调函数对应关系。线程之间发送信号时,先设置变量为具体的信号值,然后使用系统调用pthread_kill发送信号,线程收到通知后再根据额外的变量表示的具体信号值,回调对应的信号处理函数。
信号处理涉及的数据结构代码如下。每个线程有一个GsSignalSlot结构,保存了线程ID、线程名称和GsSignal结构,而GsSignal结构保存了每个信号对应的处理函数数组和每个线程相关的信号池。而信号池struct SignalPool包括了使用的信号列表和空闲的信号列表,当一个模拟信号到达时,找一个空闲信号GsNode,然后放到使用的列表中。GsNode中存放了信号值结构GsSndSignal sig_data。在GsSndSignal结构中保存了发送的信号具体值和发送的线程ID。当需要设置一些额外检查信息时,设置GsSignalCheck内容。相关代码如下。
typedef struct GsSignalSlot {
ThreadId thread_id;
char* thread_name;
GsSignal* gssignal;
} GsSignalSlot;
typedef struct GsSignal {
gs_sigfunc handlerList[GS_SIGNAL_COUNT];
sigset_t masksignal;
SignalPool sig_pool;
volatile unsigned int bitmapSigProtectFun;
} GsSignal;
typedef struct SignalPool {
GsNode* free_head; /* 空闲信号列表头部 */
GsNode* free_tail; /* 空闲信号列表尾部 */
GsNode* used_head; /* 使用信号列表头部*/
GsNode* used_tail; /*使用信号列表尾部*/
int pool_size; /* 数组列表大小*/
pthread_mutex_t sigpool_lock;
} SignalPool;
typedef struct GsNode {
GsSndSignal sig_data;
struct GsNode* next;
} GsNode;
typedef struct GsSndSignal {
unsigned int signo; /* 需要处理的信号*/
gs_thread_t thread; /* 发送信号的线程ID */
GsSignalCheck check; /* 信号发送线程需要检查的信息 */
} GsSndSignal;
typedef struct GsSignalCheck {
GsSignalCheckType check_type;
uint64 debug_query_id;
uint64 session_id;
} GsSignalCheck;
信号处理几个的主要流程为初始化模拟信号机制、注册信号处理函数、发送信号和处理信号。具体的处理逻辑如下。
(1) 初始化模拟信号机制函数gs_signal_slots_init。在gs_signal_slots_init处理函数中完成如下功能。 ① 根据传入的槽位个数,分配内存。遍历每个槽位,进行初始化,初始化时调用gs_signal_init函数对每个槽位的GsSignal(GsSignal是openGauss封装的模拟信号结构体,里面包含了信号掩码和信号处理函数等成员)进行初始化。
② 在gs_signal_init函数中,对GsSignal分配内存和初始化,初始化时调用gs_signal_sigpool_init函数对信号池初始化。
③ 在gs_signal_sigpool_init函数中,对信号池进行分配内存和初始化。
(2) 注册信号处理函数gspqsignal。在gspqsignal处理函数中完成如下功能。 ① 调用“gs_signal_register_handler(t_thrd.signal_slot->gssignal, signo, func);”函数把信号对应的处理函数注册到GsSignal中。在注册之前,需要为线程分配一个signal_slot,这个是在gs_signal_startup_siginfo函数中完成的。
② 在gs_signal_startup_siginfo函数中,会调用gs_signal_alloc_slot_for_new_thread函数为线程分配一个signal_slot。该函数的功能是遍历“g_instance.signal_base->slots”,找到一个未使用的slot(thread_id为0表示未使用),然后设置本线程ID和线程名称。
(3) 发送信号函数gs_signal_send。在gs_signal_send处理函数中完成如下功能。 ① 调用函数gs_signal_find_slot找到要发送线程所在的GsSignalSlot。
② 调用函数gs_signal_set_signal_by_threadid设置模拟信号。该函数首先检查信号在使用列表中是否已经存在,如果已经存在,则直接返回。如果不存在,则在空闲列表中找到一个空闲GsNode,设置信号值,发送线程ID,check_type到sig_data中,最后把空闲GsNode移到使用列表中。
③ 调用函数gs_signal_thread_kill发送信号通知。该函数遍历GsSignalSlot,找到匹配的线程ID,然后调用“gs_signal_thread_kill(thread_id, RES_SIGNAL);”函数给具体线程发送信号通知。语句“#define RES_SIGNAL SIGUSR2”表示内部统一都使用SIGUSR2发送通知。
(4) 处理信号函数gs_signal_handle。在函数gs_signal_handle中完成如下功能。 ① 遍历信号池使用列表,找到一个需要处理的信号。
② 找到这个信号对应的信号处理函数。把GsNode移到空闲列表中。
③ 调用gs_signal_handle_check函数检查当前的条件是否仍然满足。如果仍然有效,回调处理函数。
八、小结
本章主要介绍了openGauss的一些公共组件机制,每个内容都比较独立。系统表是openGauss数据库的元数据,主要介绍了系统表的定义和syscache访问机制。数据库初始化是数据库安装后的第一步,它负责创建数据库的模板数据库和数据目录。多线程架构是openGauss数据库启动后的运行机制,介绍了主线程的初始化流程、后台线程的启动、各个线程的功能和线程之间的通信机制。线程池技术是解决大并发链接的有效方法,介绍了线程池机制的原理,各个类之间的关系和设计原因。内存管理是openGauss的内存资源管理组件,介绍了openGauss的3级内存管理机制。多维监控是openGauss性能调优手段的基础,介绍了性能视图的基本实现原理。模拟信号机制是openGauss多线程处理紧急事件的机制,介绍了这个机制的实现原理。
Gauss松鼠会是汇集数据库爱好者和关注者的大本营, 大家共同学习、探索、分享数据库前沿知识和技术, 互助解决问题,共建数据库技术交流圈。