作为DBA关心的更多的可能是原理、机制,对于源码一般大家也不是特别关心,也不太用得上。而且对于对于源代码级别的细节也很难用文字表达自己的理解,最终理解必须还是需要自己去看看每一行代码到底是怎样的。也读过《Understanding.MySQL.Internals》的大部分章节,作者也是偏重于从代码的实现目的(原理、机制)来介绍的,国内的《MySQL核心内幕》(个人对于“核心内幕”这个词有莫名的反感)算是更多的从源码开始介绍MySQL了,可是这本书也许是授予篇幅的限制,介绍的东西也并不多。
开始写这篇文章,不能期待自己能写多少,也不能期待自己能研究多少,不过至少走出了自己探索的第一步。文章的宗旨不在于能够多么细致的分析MySQL的源代码,而是希望能给自己,能给他人打开走向源代码的第一道门。
我是BNU数学系毕业的,对C语言知道甚少,C语言的经历大概就是“计算方法”课程中实现的求解各种方程的撇脚程序了。所以虽然我会极力避免错误,但是相信还是会犯一堆错误,希望各位看官能够宽容点,有错误指出来,慢慢改正。
InnoDB的代码通过宏定义考虑很多平台的兼容问题,这里分析的主要是类Unix/Linux平台(POSIX标准)的代码段,所以下面的很多代码段也是删除相关兼容性代码的。本文InnoDB源码是Plugin1.0.6版本。
MySQL是一个多线程的实现的DBMS,多线程编程需要解决的一个重要问题就是线程间并发访问的控制。一般某个线程更改需要更改某个内存块(变量)时,首先要先将数据从内存读取到寄存器,然后计算更改之,最后回写到内存块。如果有多个线程同时在修改内存块,而且读取同时(“同时”表示在两个线程修改该值之前读取)发生,那么两个线程将有一个会丢失修改。
所以,希望保持内存数据(变量)“一致”、“准确”时,线程在操作这些变量时,需要先获取对应的锁。利用各个变量对应的“锁机制”来保证数据访问的并发控制。
多线程并发访问控制中,InnoDB使用标准POSIX线程模型中的mutex(condition variable)实现基本的排他访问。在对mutex(condition variable)的封装的基础上,又实现了相应的读写锁。
pthread_mutex_t封装在os_fast_mutex_t中,condition variable则封装在了os_event_struct中。
对应的mutex操作分别封装在了os_fast_mutex_init / os_fast_mutex_lock / os_fast_mutex_unlock / os_fast_mutex_free中,这里我们看看os_fast_mutex_trylock的封装函数实现:
ulint os_fast_mutex_trylock( os_fast_mutex_t* fast_mutex) { #ifdef __WIN__ EnterCriticalSection(fast_mutex); return(0); #else return((ulint) pthread_mutex_trylock(fast_mutex)); #endif }
Condition variable较之pthread_mutex_t要复杂一点,对应的封装也更复杂。os_event_struct封装了condition variable全部操作,主要包括初始化,陷入等待,广播唤醒:os_event_create(初始化)/os_event_wait_low(等待)/os_event_set(广播唤醒)。下面是基本数据结构struct os_event_struct:
struct os_event_struct { os_fast_mutex_t os_mutex; ibool is_set; ib_longlong signal_count; pthread_cond_t cond_var; /* condition variable is used in waiting for the event */ ...... };
这里抽取一个广播唤醒开函数os_event_set看看:
void os_event_set( os_event_t event) /*!< in: event to set */ { os_fast_mutex_lock(&(event->os_mutex)); if (event->is_set) { /* Do nothing */ } else { event->is_set = TRUE; event->signal_count += 1; ut_a(0 == pthread_cond_broadcast(&(event->cond_var))); } os_fast_mutex_unlock(&(event->os_mutex)); }
可以看到上面函数,没有什么神秘的地方,就是判断了一下是否需要广播,然后调用了pthread_cond_broadcast函数广播。
数据结构os_fast_mutex_t和os_event_t看到,InnoDB并不是那么复杂,但是需要对Linux(或者说POSIX)有一定的理解。
另外,算是最底层的数据结构,事实上,这个封装还没结束,InnoDB其他模块中用的较多的是mutex_struct和rw_lock结构,mutex_t是对os_event_t封装,rw_lock是对mutex_t的封装。
struct mutex_struct { os_event_t event; /* Used by sync0arr.c for the wait queue */ ulint lock_word; /* This ulint is the target of the atomic test-and-set instruction in Win32 */ ulint waiters; //如果有线程在等待,设置为1 UT_LIST_NODE_T(mutex_t) list; const char* cfile_name;/* File name where mutex created */ ulint cline; /* Line where created */ #ifndef UNIV_HOTBACKUP ulong count_os_wait; /* count of os_wait */ #endif /* !UNIV_HOTBACKUP */ };
mutex_struct相关的函数有很多,一般需要使用的有:mutex_create、mutex_enter、mutex_exit;还有一些其他的函数:mutex_spin_wait、mutex_free、mutex_signal_object。
InnoDB中一个典型的mutex_struct使用如下:
mutex_create(&rw_lock_list_mutex); mutex_enter(&rw_lock_list_mutex); ... //这里,对rw_lock_list_mutex所保护的对象,这里可以进行一致的、排他操作 ... mutex_exit(&rw_lock_list_mutex);
mutex_create函数相对简单,做了一些初始化的工作。我们继续沿着线索来看看mutex_enter的实现。这个函数实现:
typedef struct mutex_struct mutex_t; void mutex_enter_func( mutex_t* mutex, const char* file_name, ulint line) { ut_d(mutex->count_using++); if (!mutex_test_and_set(mutex)) { ut_d(mutex->thread_id = os_thread_get_curr_id()); return; /* Succeeded! */ } mutex_spin_wait(mutex, file_name, line); }
代码说明:首先,尝试使用mutex_test_and_set(这是对os_fast_mutex_trylock的封装)获得锁,成功则返回。失败,则调用mutex_spin_wait陷入“spin lock的方式”获取锁。spin lock的方式是指,自己不释放cpu资源,而是自己空循环一段时间后,在重新尝试获取锁。
线索到了mutex_spin_wait了,该函数是InnoDB里面实现了spin lock的主要部分。这里简单介绍一下原理:首先线程检查lock_word,如果被设置0(表示当前并没有变量并没有被锁住),则直接调用mutex_test_and_set(其实是os_fast_mutex_trylock)来尝试获取锁。如果lock_word=1那么,则表示当前的锁已经被某个线程获取,则线程进入空循环,延迟一段时间(spin lock),等待lock_word被设置为0后,立刻调用mutex_test_and_set尝试获取锁。如果lock_word一直1(锁一直在被占用),那么线程将在延迟SYNC_SPIN_ROUNDS个单位时间后,线程将尝试调用mutex_test_and_set一次(碰运气),如果仍失败,这时线程将陷入等待(这时Condition variables开始登上舞台),如果其他线程释放该锁,则会将lock_word被设置为0,并进行一次广播,那么前面陷入等待的线程将会被唤醒,并重新调用mutex_test_and_set尝试获取锁。
下面是mutex_spin_wait的一个简单流程图:
再回头看看mutex_struct的一般使用方法,先是mutex_create,然后是mutex_enter,最后是mutex_exit,该函数会调用os_event_set进行一次广播(广播函数本身是会释放pthread_mutex_t的)。
至此,我们看到,InnoDB一般通过数据结构mutex_struct来实现对内存块(变量)的完全排他访问。如果只是排他访问,可能会导致效率较低,因为很多时候,不需要排他,只需要共享方式访问就可以了。InnoDB在mutex_struct基础上,再做了一次封装,对应的数据结构为rw_lock_struct。
好了,这篇文章已经够长了….欲知后事如何,且听下回分解。
广告时间:工作机会–MySQL Hacker
Leave a Reply