3.7 模拟信号机制
信号是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函数检查当前的条件是否仍然满足。如果仍然有效,回调处理函数。
3.8 小结
本章主要介绍了openGauss的一些公共组件机制,每个内容都比较独立。系统表是openGauss数据库的元数据,主要介绍了系统表的定义和syscache访问机制。数据库初始化是数据库安装后的第一步,它负责创建数据库的模板数据库和数据目录。多线程架构是openGauss数据库启动后的运行机制,介绍了主线程的初始化流程、后台线程的启动、各个线程的功能和线程之间的通信机制。线程池技术是解决大并发链接的有效方法,介绍了线程池机制的原理,各个类之间的关系和设计原因。内存管理是openGauss的内存资源管理组件,介绍了openGauss的3级内存管理机制。多维监控是openGauss性能调优手段的基础,介绍了性能视图的基本实现原理。模拟信号机制是openGauss多线程处理紧急事件的机制,介绍了这个机制的实现原理。