经常有朋友会问,DBA要不要了解钻研源码,我的答案是基础的源码阅读能力和调试能力对DBA来说是很有必要的:
一方面通过阅读源码能释疑很多你平时很多的细节困惑;
另外一方面可以提升职场竞争力,需要知道很多技能可以不用,但是不能不会。
本篇文章就来介绍一下MySQL信号量和源码的调试,知识结构图如下:

1 gdb工具
gdb(gnu debugger)是一个开源的调试工具,可以用来调试程序,查看程序执行,设置断点,以及分析程序崩溃问题,因此,对于想深入研究MySQL的DBA或者开发人员,掌握gdb的安装和使用是很有必要的。
1.1 gdb安装
gdb可以在线安装,也可以通过下载对应版本源码包进行编译安装。
在线安装,以CentOS为例:
yum install gdb
自编译安装:
1) 下载安装,下载地址:
https://sourceware.org/pub/gdb/releases/?C=M;O=D
2) 使用tar命令解压
3) 配置,执行/configure –prefix=/usr/local
4) 编译,执行make
5) 安装:make install
1.2 gdb常见命令
关联调试MySQL:
[root@localhost ~]# gdb /root/mysql-5.7.41/build/sql/mysqldGNU gdb (GDB) Red Hat Enterprise Linux 8.0.1-36.el7Copyright (C) 2017 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law. Type "show copying"and "show warranty" for details.This GDB was configured as "x86_64-redhat-linux-gnu".Type "show configuration" for configuration details.For bug reporting instructions, please see:<http://www.gnu.org/software/gdb/bugs/>.Find the GDB manual and other documentation resources online at:<http://www.gnu.org/software/gdb/documentation/>.For help, type "help".Type "apropos word" to search for commands related to "word"...Reading symbols from /root/mysql-5.7.41/build/sql/mysqld...done.(gdb) set non-stop on(gdb) attach 12428
gdb关联mysqld进程不建议直接对生产环境直接操作,会影响MySQL正常运行,可以通过attach pid关联已运行的mysqld服务。
通过set non-stop on命令打开non-stop模式,使其在多个线程或多个进程运行时不会自动停止在第一个遇到的断点上。对于调试多线程程序很有帮助,可以单独控制每个线程的调试过程。
比较常见的命令如下:
b rpl_master.cc:64 #设置断点info threads #查看所有线程thread xxx #调试指定线程bt xxx #查看指定线程的调用堆栈info breakpoints #查看所有断点del xxx #删除断点,xxx表示断点序号run #运行程序c/continue #触发断点后继续运行p/print xxx #打印变量值,xxx表示变量名
2 系统进程
2.1 系统进程介绍
MySQL中除了master进程外,还有很多后台线程,然后具体有哪些后台线程你清楚嘛,我们可以在源代码中找到答案,如下:

那么对于这些线程具体的功能是什么呢,可以从srv0srv.h文件中找到答案,例如:
srv_monitor_thread,则是一个可以打印各种InnoDB监控器信息的后台线程;
srv_error_monitor_thread,则是一个打印关于信号量等待持续过长时间的警告信息的后台线程。
对于其他的后台线程,也能一一在文件中找到对应的详细说明。

2.2 信号量等待
生产中你可能经常遇到过MySQL数据库日志中有这种报错,信号量超时过长导致MySQL实例夯住或崩溃,那么为什么超过600s就会崩溃呢,可以从源码中探究一番。
2024-07-04T17:12:18.408636+08:00 0 [ERROR] [FATAL] InnoDB: Semaphore wait has lasted > 600 seconds. We intentionally crash the server because it appears to be hung.
在ha_innodb.cc文件中可以看到,600 秒是由以下参数决定的。
srv_fatal_semaphore_wait_threadshold
同时需要满足fatal_cnt大于10,连续超过10次检测到信号量等待大于600秒,则会触发自杀行为。

2.3 信号量查看
信号量信息在show engine innodb status中也有展示,类似信息如:
#OS thread为139890665957120的线程在ha_innodb.cc文件5660行处有长达274s的信号量等待。需要的DICT_SYS已被持有,内存地址为0x515a188#lock var 1 表示该锁已被持有,0则表示该锁的状态为空闲--Thread 139890665957120 has waited at ha_innodb.cc line 5660 for 254 seconds the semaphore:Mutex at 0x515a188, Mutex DICT_SYS created dict0dict.cc:1217, lock var 1--Thread 139890666227456 has waited at dict0dict.cc line 505 for 274 seconds the semaphore:Mutex at 0x515a188, Mutex DICT_SYS created dict0dict.cc:1217, lock var 1--Thread 139891984103168 has waited at ha_innodb.cc line 5660 for 259 seconds the semaphore:Mutex at 0x515a188, Mutex DICT_SYS created dict0dict.cc:1217, lock var 1--Thread 139890514241280 has waited at buf0flu.cc line 1437 for 0 seconds the semaphore:Mutex at 0x3584b98, Mutex BUF_POOL created buf0buf.cc:1747, lock var 1--Thread 139890442557184 has waited at ha_innodb.cc line 5660 for 197 seconds the semaphore:Mutex at 0x515a188, Mutex DICT_SYS created dict0dict.cc:1217, lock var 1--Thread 139892000057088 has waited at ha_innodb.cc line 5660 for 263 seconds the semaphore:Mutex at 0x515a188, Mutex DICT_SYS created dict0dict.cc:1217, lock var 1--Thread 139891985454848 has waited at ha_innodb.cc line 5660 for 258 seconds the semaphore:Mutex at 0x515a188, Mutex DICT_SYS created dict0dict.cc:1217, lock var 1
对应的源码函数为sync_array_cell_print:

此时,我们则可以根据信息中的锁被创建的位置点,例如dict0dict.cc:1217初步查看是什么操作持有了锁。
3 内核锁
3.1 锁类别
Linux中的内核锁作用是保护关键资源,常见的内核锁有互斥锁(mutex)、读写锁(rwlock)、自旋锁(spinlock)、信号量(semaphore)。
互斥锁(mutex):保护共享资源,使得某个时刻只有一个线程或进程可以访问它,是一种二元锁,其实现使用了原子操作,性能较高,但是容易出现死锁情况。
读写锁(rwlock):可以同时允许多个线程或进程读取共享资源,但是只允许一个线程或进程写入它,这样可以减少读冲突。通过两个计数器分别记录当前持有锁的读线程数和写线程数。
自旋锁(spinlock):解决多线程同步问题的锁,是一种排它锁,可以在多线程环境下保护共享资源,以防止多个线程同时对该资源进行访问,自旋锁的基本原理是当一个线程试图获取锁时,会不断尝试获取锁,直到成功为止,在此期间,线程不会进入休眠状态,而是一直处于忙等待(busy-waiting)状态。在等待期间,会一直占用CPU,适用于代码临界区比较小的情况且共享资源的独占时间较短,以避免上下文切换的开销。
信号量(semaphore):一种常用的同步机制,用于控制多个线程对共享资源的访问,以确保同一时间只有一个线程能够访问共享资源,从而避免资源冲突和竞争。可以分为二元信号量和计数信号量,二元信号量只有0和1两种状态,通常用于互斥锁的实现,计数信号量可以允许多个进程同时访问同一共享资源,只要申请的信号量数量不超过该资源所允许的最大数量即可。
在InnoDB中,也会有相同的概念,但是InnoDB并没有直接使用系统的内核锁,而是实现了自己的系统锁。
3.2 互斥锁
我们经常会遇到的互斥锁有trx_sys->mutex,dict_sys->mutex等,以其为例,分享其使用的一些场景。
trx_sys->mutex,trx_sys_t结构体中的mutex字段,保护此结构体中的大多数字段,应用场景:
查看事务列表最小事务id; 判断事务是否需要回滚; mtr中写最大事务id; 创建readview时候; MySQL关闭会话; 事务提交改变对其他会话可见性。
通过调用mutex_enter(&trx_sys->mutex)函数获取mutex,通过trx_sys_mutex_exit()释放mutex。如下:

dict_sys->mutex,dict_sys_t数据结构,保护数据字典,及基于磁盘的字典系统表,对create/drop table操作,以及从系统表读取字典数据进行序列化,应用场景有:
打开二级索引游标; 基于表id打开innoDB表; 减少表打开的句柄数; 丢弃表空间; drop表操作。
通过调用mutex_enter(&dict_sys->mutex)函数获取mutex;
通过调用mutex_own(&dict_sys->mutex)函数判断是否持有mutex;
通过调用mutex_exit(&dict_sys->mutex)函数释放mutex。
如下:

4 源码调试
4.1 打印lsn
在MySQL中,redo日志对应的数据结构是log_t,对应的对象是log_sys,具体定义在storage\innobase\include\log0log.h文件中,如下:

而对于lsn字段,则是log sequence number,定义也是在log0log.h文件中。

在使用gdb进行调试时,可以通过p log_sys->lsn打印lsn的值,也可以通过p *log_sys打印整体log_sys的值,参考如下:
(gdb) p log_sys->lsn$24 = 4031743907(gdb)$25 = 4031743907(gdb) p *log_sys$26 = {pad1 = '\000' <repeats 63 times>, lsn = 4031743907, ... write_lsn = 4031743907,current_flush_lsn = 4031743907, flushed_to_disk_lsn = 4031743907, n_pending_flushes = 0, flush_event = 0xc8ea738, n_log_ios = 10, n_log_ios_old = 10,last_printout_time = 1718721780, log_group_capacity = 2899097396, max_modified_age_async = 2282419885, max_modified_age_sync = 2445449877,max_checkpoint_age_async = 2526964873, max_checkpoint_age = 2608479868, next_checkpoint_no = 96, last_checkpoint_lsn = 4031743898,next_checkpoint_lsn = 4031743898, ..."/root/mysql-5.7.41/storage/innobase/log/log0log.cc",last_s_file_name = 0x2255980 "/root/mysql-5.7.41/storage/innobase/log/log0log.cc",last_x_file_name = 0x2255980 "/root/mysql-5.7.41/storage/innobase/log/log0log.cc"...
4.2 打印mutex
MySQL中,当排查mutex相关问题时,对于Server层的Mutex,我们可以通过gdb调试打印其值,根据输出结果中的__owner值,找到对应的mutex持有者,从而定位对应的线程和SQL。
(gdb) p LOCK_status$11 = {m_mutex = {__data = {__lock = 2, __count = 0, __owner = 102188, __nusers = 1, __kind = 3, __spins = 85, __list = {__prev = 0x0, __next = 0x0}},__size = "\002\000\000\000\000\000\000\000,\217\001\000\001\000\000\000\003\000\000\000U", '\000' <repeats 18 times>, __align = 2}, m_psi = 0x0}
4.3 show slave status调试
当我们在客户端执行show slave status命令时,在源码中的流程如下:
1) 首先会来到mysql_execute_command函数,其是所有操作的入口,进入SQLCOM_SHOW_SLAVE_STAT分支,对应文件为sql\sql_parse.cc;
2) 接着会来到show_slave_status_cmd函数,其是show slave status 命令的入口,对应文件为sql\rpl_slave.cc;
3) 然后show_slave_status函数负责执行show slave status语句,对应文件为sql\rpl_slave.cc;
4) 而show_slave_status_send_data函数将数据发送到Master_info的客户端,对应文件为sql\rpl_slave.cc。
在阅读源码的时候,我们可以根据自己想验证的点在对应代码处设置断点,模拟show slave status阻塞的场景。以我的测试环境为例,如果我想模拟show slave status阻塞的场景,则我可以在下图申请锁之前设置断点,例如:
b rpl_slave.cc:3766

5 总结
该篇的内容到这里也就结束了,小结一下。
本篇从源码调试工具入手,介绍了gdb的安装和gdb常见命令;
然后引入了系统进程和信号量相关的知识,并介绍了内核锁以及常见互斥锁的使用场景;
最后,结合具体场景,案例介绍如何如何使用gdb去打印lsn值及使用gdb排查互斥锁的持有问题,还有模拟show slave status阻塞场景。
当然,MySQL源码是一个庞大而复杂的体系,需要长期的学习和投入,我所做的只是希望让你入门源码开头能简单一点。
最后,跟大家推荐一下我的个人专栏课程,已经更新2/3了,涵盖MySQL、Redis、国产化三个部分,共30节。
基于实战出发,想要提升自己的可以入手啦。





