Mysql源代碼分析系列(2): 源代碼結構
Mysql源代碼主要包括客戶端程序代碼,服務器端代碼,測試工具和一些庫構成,下面我們對比較重要的目錄做些介紹。
BUILD
這個目錄在本系列的上篇文章中我們仔細看過,內含各種平臺的編譯腳本,這裏就不仔細說了。
client
這個目錄下有如下比較讓人眼熟的文件: mysql.cc, mysqlcheck.c, mysqladmin.cc, mysqlshow.c,等等,如果你編譯一下就會發現那些眼熟的程序也出現了,比如mysql。明白了吧,這個目錄就是那些客戶端程序所在的目錄。這個目錄的內容也比較少,而且也不是我們閱讀的重點。
Docs
這個目錄包含了文檔。
storage
這個目錄包含了所謂的Mysql存儲引擎 (storage engine)。存儲引擎是數據庫系統的核心,封裝了數據庫文件的操作,是數據庫系統是否強大最重要的因素。Mysql實現了一個抽象接口層,叫做handler(sql/handler.h),其中定義了接口函數,比如:ha_open, ha_index_end, ha_create等等,存儲引擎需要實現這些接口才能被系統使用。這個接口定義超級複雜,有900多行 :-(,不過我們暫時知道它是幹什麼的就好了,沒必要深究每行代碼。對於具體每種引擎的特點,我推薦大家去看mysql的在線文檔:
http://dev.mysql.com/doc/refman/5.1/en/storage-engines.html
應該能看到如下的目錄:
* innobase, innodb的目錄,當前最流行的存儲引擎
* myisam, 最早的Mysql存儲引擎,一直到innodb出現以前,使用最廣的引擎。
* heap, 基於內存的存儲引擎
* federated, 一個比較新的存儲引擎
* example, csv,這幾個大家可以作爲自己寫存儲引擎時的參考實現,比較容易讀懂
mysys
包含了對於系統調用的封裝,用以方便實現跨平臺。大家看看文件名就大概知道是什麼情況了。
sql
這個目錄是另外一個大塊頭,你應該會看到mysqld.cc,沒錯,這裏就是數據庫主程序mysqld所在的地方。大部分的系統流程都發生在這裏。你還能看到sql_insert.cc, sql_update.cc, sql_select.cc,等等,分別實現了對應的SQL命令。後面我們還要經常提到這個目錄下的文件。
大概有如下及部分:
SQL解析器代碼: sql_lex.cc, sql_yacc.yy, sql_yacc.cc, sql_parse.cc等,實現了對SQL語句的解析操作。
"handler"代碼: handle.cc, handler.h,定義了存儲引擎的接口。
"item"代碼:item_func.cc, item_create.cc,定義了SQL解析後的各個部分。
SQL語句執行代碼: sql_update.cc, sql_insert.cc sql_select.cc, sql_show.cc, sql_load.cc,執行SQL對應的語句。當你要看"SELECT ..."的執行的時候,直接到sql_select.cc去看就OK了。
輔助代碼: net_serv.cc實現網絡操作
還有其他很多代碼。
vio
封裝了virtual IO接口,主要是封裝了各種協議的網絡操作。
plugin
插件的目錄,目前有一個全文搜索插件(只能用在myisam存儲引擎)。
libmysqld
Mysql連接庫源代碼。
開源函數庫目錄
和所有的開源項目一樣,Mysql也使用了一些開源的庫,在其代碼庫中我們能看到dbug、pstack、strings、 zlib等。
多說無益,主要是對於mysql的代碼目錄有個概念,要找的時候也有個方向。萬一要找某個東西找不到了就只能grep了...
Mysql源代碼分析系列(3): 主要調用流程
引言
本文主要介紹Mysql主要的調用流程,將從代碼的角度來看一個從用戶發出的"select * from test" SQL命令在服務器內部是如何被執行的。從我個人的經驗來看,閱讀理解大規模項目的代碼最重要的兩個方面,一是瞭解主要的數據結構,二是瞭解數據流,在這裏主要是調用流程。把這兩個主線把握住以後,大部分代碼都是比較容易閱讀的,Mysql的源代碼屬於比較好讀的類型,因爲函數的調用關係比較明確。難讀的代碼一般都充斥着大量的回調、異步調用,很可能你極難找到某個函數在哪裏或什麼時候被調用了。當然,算法的實現代碼也很難讀。幸好Mysql不是那種難讀的類型,所以我們也不要害怕,大步向前吧!
主要執行過程
從架構上來看,Mysql服務器對於一條SQL語句的執行過程可以分成如下幾部分:
接受命令 包括用戶驗證,資源申請等
|
V
命令解析 解析SQL語句,生成語法樹
|
V
尋找執行計劃 根據解析出來的語法樹,找到可能的執行計劃。對於一條SQL語句,很可能會有多種執行方案,特別是在SQL語句比較複雜的時候。這裏需要對於各種可能的方案進行代價評估,最快的找到最有的執行方案。
|
V
優化執行計劃 優化執行計劃。這是SQL執行中最複雜的部分之一,據說全都是由數學博士們寫出來的,而且比較難懂。我目前還處於不懂的狀態。
|
V
執行 沒啥可說的,只剩執行及返回結果了
系統啓動
所有的程序都從main開始,mysqld也不例外,打開sql/mysqld.cc,稍加搜索,你就能看到熟悉的main函數,我們可以將其進行如下簡寫:
int main(int argc, char* argv[]) {
logger.init_base();
init_common_variables(MYSQL_CONFIG_NAME, argc, argv, load_default_groups)); // 解析配置文件和命令行參數,將配置文件中的內容轉行成命令行參數
init_signals();
user_info= check_user(mysqld_user);
set_user(mysqld_user, user_info);
init_server_components(); // 初始化服務器模塊
network_init(); // 初始化網絡模塊,根據配置,打開IP socket/unix socket/windows named pipe來進行監聽。
start_signal_handler(); // 開始接收信號
acl_init(...); // 初始化ACL (Access Control List)
servers_init(0); // 服務器初始化
init_status_vars(); // 狀態變量初始化
create_shutdown_thread(); // 創建關閉線程
create_maintenance_thread(); // 創建維護線程
sql_print_information(...); // 打印一些信息
handle_connections_sockets(0); // 主要的服務處理函數,循環等待並接受命令,進行查詢,返回結果,也是我們要詳細關注的函數
wait for exit; // 服務要退出
cleanup;
exit(0);
}
可以仔細的看看這個簡寫的main函數,邏輯很清楚,就算沒有我的這些註釋大部分人也能容易的理解整個系統的執行流程。其實完整的main函數有接近300行,但是中心思想已經被包含在這裏簡短的十幾行代碼中了。
通過看這些代碼,讀者會發現mysqld是通過多線程來處理任務的,這點和Apache服務器是不一樣的。
等待命令
mysqld等待和處理命令主要在handle_connections_sockets(0);來完成,這裏我們仔細看看這個函數調用發生了什麼。該函數也在mysqld.cc中,也有大概300行,我們繼續簡寫。
爲了方便分析,這裏我們假定配置服務器通過unix domain socket來監聽接受命令,其他方式類同。
pthread_handler_t handle_connections_sockets(void *arg __attribute__((unused)))
{
FD_ZERO(&clientFDs);
FD_SET(unix_sock,&clientFDs); // unix_socket在network_init中被打開
socket_flags=fcntl(unix_sock, F_GETFL, 0);
while (!abort_loop) { // abort_loop是全局變量,在某些情況下被置爲1表示要退出。
readFDs=clientFDs; // 需要監聽的socket
select((int) max_used_connection,&readFDs,0,0,0); // select異步監聽,當接收到時間以後返回。
sock = unix_sock;
flags= socket_flags;
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
new_sock = accept(sock, my_reinterpret_cast(struct sockaddr *) (&cAddr), &length); // 接受請求
getsockname(new_sock,&dummy, &dummyLen);
thd= new THD; // 創建mysqld任務線程描述符,它封裝了一個客戶端連接請求的所有信息
vio_tmp=vio_new(new_sock, VIO_TYPE_SOCKET, VIO_LOCALHOST); // 網絡操作抽象層
my_net_init(&thd->net,vio_tmp)); // 初始化任務線程描述符的網絡操作
create_new_thread(thd); // 創建任務線程
}
}
看到這裏,大家應該已經基本清楚mysqld如何啓動並進入監聽狀態,真正的命令處理就是在create_new_thread裏面,看名字也知道就是創建一個新線程來處理任務。
怎麼樣,是不是覺得mysql的代碼很好懂呢?呵呵,更堅定了要繼續讀下去的信心。
一條語句的執行
下面具體看看服務器如何執行語句"insert"語句的。
上一節我們提到create_new_thread是所有處理的入口,這裏我們仔細看看它到底幹了什麼。幸運的是,它也在mysqld.cc裏面,我們不費吹灰之力就找他了它:
static void create_new_thread(THD *thd) {
NET *net=&thd->net;
if (connection_count >= max_connections + 1 || abort_loop) { // 看看當前連接數是不是超過了系統配置允許的最大值,如果是就斷開連接。
close_connection(thd, ER_CON_COUNT_ERROR, 1);
delete thd;
}
++connection_count;
thread_scheduler.add_connection(thd); // 將新連接加入到thread_scheduler的連接隊列中。
}
現在看來關鍵還是在thread_scheduler幹了什麼,現在打開sql/scheduler.cc文件:
void one_thread_per_connection_scheduler(scheduler_functions* func) {
func->max_threads= max_connections;
func->add_connection= create_thread_to_handle_connection;
func->end_thread= one_thread_per_connection_end;
}
再看create_thread_to_handle_connection,它還是在mysqld.cc中,哈哈:
void create_thread_to_handle_connection(THD *thd) {
if (cached_thread_count > wake_thread) {
thread_cache.append(thd);
pthread_cond_signal(&COND_thread_cache);
} else {
threads.append(thd);
pthread_create(&thd->real_id,&connection_attrib, handle_one_connection, (void*) thd)));
}
}
恩,看來先是看當前工作線程緩存(thread_cache)中有否空餘的線程,有的話,讓他們來處理,否則創建一個新的線程,該線程執行handle_one_connection函數
很好,繼續往下看,到了sql/sql_connection.cc中。
pthread_handler_t handle_one_connection(void *arg) {
thread_scheduler.init_new_connection_thread();
setup_connection_thread_globals(thd);
for (;;) {
lex_start(thd);
login_connection(thd); // 進行連接身份驗證
prepare_new_connection_state(thd);
do_command(thd); // 處理命令
end_connection(thd);
}
}
do_command在sql/sql_parse.cc中。
bool do_command(THD *thd) {
NET *net= &thd->net;
packet_length= my_net_read(net);
packet= (char*) net->read_pos;
command= (enum enum_server_command) (uchar) packet[0]; // 解析客戶端穿過來的命令類型
dispatch_command(command, thd, packet+1, (uint) (packet_length-1));
}
再看dispatch_command:
bool dispatch_command(enum enum_server_command command, THD *thd, char* packet, uint packet_length) {
NET *net= &thd->net;
thd->command=command;
switch (command) {
case COM_INIT_DB: ...;
case COM_TABLE_DUMP: ...;
case COM_CHANGE_USER: ...;
...
case COM_QUERY:
alloc_query(thd, packet, packet_length);
mysql_parse(thd, thd->query, thd->query_length, &end_of_stmt);
}
}
進行sql語句解析
void mysql_parse(THD *thd, const char *inBuf, uint length, const char ** found_semicolon) {
lex_start(thd);
if (query_cache_send_result_to_client(thd, (char*) inBuf, length) <= 0) { // 看query cache中有否命中,有就直接返回結果,否則進行查找
Parser_state parser_state(thd, inBuf, length);
parse_sql(thd, & parser_state, NULL); // 解析sql語句
mysql_execute_command(thd); // 執行
}
}
總算開始執行了,mysql_execute_command函數超長,接近3k行:-(,我們還是按需分析吧。還是覺得這種代碼不應該出現在這種高水平的開源軟件裏面,至少在linux kernel中很少看見這麼長的函數,而在mysql裏面確實是常常看到。
int mysql_execute_command(THD *thd) {
LEX *lex= thd->lex; // 解析過後的sql語句的語法結構
TABLE_LIST *all_tables = lex->query_tables; // 該語句要訪問的表的列表
switch (lex->sql_command) {
...
case SQLCOM_INSERT:
insert_precheck(thd, all_tables);
mysql_insert(thd, all_tables, lex->field_list, lex->many_values, lex->update_list, lex->value_list, lex->duplicates, lex->ignore);
break;
...
case SQLCOM_SELECT:
check_table_access(thd, lex->exchange ? SELECT_ACL | FILE_ACL : SELECT_ACL, all_tables, UINT_MAX, FALSE); // 檢查用戶對數據表的訪問權限
execute_sqlcom_select(thd, all_tables); // 執行select語句
break;
}
}
Mysql源代碼分析系列(4): 主要調用流程(續)
在上一篇文章中我們講到了的mysql_execute_command,這個函數根據解析出來的SQL命令分別調用不同的函數做進一步處理。我們這裏先看"INSERT"命令的處理流程。其對應的處理函數是mysql_insert,在sql/sql_insert.cc中,還是很長,大概300多行。
bool mysql_insert(THD *thd,
TABLE_LIST *table_list, // 該命令要用到的表
List<Item> &fields, // 使用的域
List<List_item> &values_list,
List<Item> &update_fields,
List<Item> &update_values,
enum_duplicates duplic,
bool ignored) {
open_and_lock_tables(thd, table_list);
mysql_prepare_insert(...);
foreach value in values_list {
write_record(...);
}
}
其實裏面還有很多處理trigger,錯誤,view之類的,我們暫時都忽略。
// 寫數據記錄
int write_record(THD *thd, TABLE *table,COPY_INFO *info) {
if (info->handle_duplicates == DUP_REPLACE || info->handle_duplicates == DUP_UPDATE) {
table->file->ha_write_row(table->record[0]);
table->file->ha_update_row(table->record[1], table->record[0]));
} else {
table->file->ha_write_row(table->record[0]);
}
}
不用說,這裏我們還是省略了好多東西,要注意的是這裏調用的table->file->ha_write_row和table->file->ha_update_row。在sql/table.h可以看到table的定義,其中file被定義成handler *file; 那handler是什麼?對了,這就是我們前面提到的數據存儲抽象層,所有的存儲引擎都必須事先這裏定義的接口,才能被mysql使用。在這裏使用的具體的接口操作是ha_write_row和ha_update_row。這兩個函數可以在sql/handler.cc中看到。比如ha_write_row:
int handler::ha_write_row(uchar *buf) {
write_row(buf); // 調用具體的實現
binlog_log_row(table, 0, buf, log_func)); // 寫binlog
}
下面我們看看在myisam中是怎麼實現的文件操作,代碼在storage/myisam/ha_myisam.cc中。先看write_row:
723 int ha_myisam::write_row(uchar *buf)
724 {
725 ha_statistic_increment(&SSV::ha_write_count);
726
727 /* If we have a timestamp column, update it to the current time */
728 if (table->timestamp_field_type & TIMESTAMP_AUTO_SET_ON_INSERT) // 如果有timestamp域,寫入當前時間。
729 table->timestamp_field->set_time();
730
731 /*
732 If we have an auto_increment column and we are writing a changed row
733 or a new row, then update the auto_increment value in the record.
734 */
735 if (table->next_number_field && buf == table->record[0]) // 更新auto_increment列
736 {
737 int error;
738 if ((error= update_auto_increment()))
739 return error;
740 }
741 return mi_write(file,buf); // 真正寫文件
742 }
再看mi_write函數,很好找,就在storage/myisam/mi_write.c,我們就不再分析下去了,具體實現和myisam使用的文件格式相關,有興趣的可以參考myisam的相關文檔。
結語
寫到這裏,我們對於mysql執行sql語句的主要調用流程就有了大概的瞭解,但是真正的難點往往在細節中,我也刻意逃避了這些細節,但是還是應該列一下相關的內容:
+ Sql語句的解析和相關數據結構
+ Sql語句的描述數據結構
+ 執行優化相關算法
+ 數據存儲殷勤抽象層的定義和交互
+ 存儲引擎的具體操作和文件格式
必須要提到的是,這些地方的代碼都比較難懂,而且核心函數都很長,非常不便與理解,有需要的人可以選一些方面具體深入,但要面面俱到就需要很多時間了。
Mysql源代碼分析(5): Plugin架構介紹
Mysql現在很多模塊都是通過plugin的方式連接到Mysql核心中的,除了大家熟悉的存儲引擎都是Plugin之外,Mysql還支持其他類型的plugin。本文將對相關內容做一些簡單介紹。主要還是以架構性的介紹爲主,具體細節會提到一點,但是肯定不會包括所有的細節。
主要數據結構和定義
大部分的數據接口,宏和常量都定義在include/mysql/plugin.h中,我們來慢慢看。
先看plugin的類型:
#define MYSQL_UDF_PLUGIN 0 /* User-defined function */
#define MYSQL_STORAGE_ENGINE_PLUGIN 1 /* Storage Engine */
#define MYSQL_FTPARSER_PLUGIN 2 /* Full-text parser plugin */
#define MYSQL_DAEMON_PLUGIN 3 /* The daemon/raw plugin type */
#define MYSQL_INFORMATION_SCHEMA_PLUGIN 4 /* The I_S plugin type */
開發者開發的plugin必須指定上述類型之一。類型包括用戶自定義函數,存儲引擎,全文解析,原聲plugin和information schema plugin。最常見的是前三個,daemon plugin一般用來在mysqld中啓動一個線程,在某些時候幹活兒。
一個plugin的描述數據接口是:
struct st_mysql_plugin
{
int type; /* the plugin type (a MYSQL_XXX_PLUGIN value) */
void *info; /* pointer to type-specific plugin descriptor */
const char *name; /* plugin name */
const char *author; /* plugin author (for SHOW PLUGINS) */
const char *descr; /* general descriptive text (for SHOW PLUGINS ) */
int license; /* the plugin license (PLUGIN_LICENSE_XXX) */
int (*init)(void *); /* the function to invoke when plugin is loaded */
int (*deinit)(void *);/* the function to invoke when plugin is unloaded */
unsigned int version; /* plugin version (for SHOW PLUGINS) */
struct st_mysql_show_var *status_vars;
struct st_mysql_sys_var **system_vars;
void * __reserved1; /* reserved for dependency checking */
};
主要內容包括類型,名字,初始化/清理函數,狀態變量和系統變量的定義等等。但是在使用的時候一般不是直接使用這個數據結構,而是使用大量的宏來輔助。
一個plugin的開始:
#define mysql_declare_plugin(NAME) \
__MYSQL_DECLARE_PLUGIN(NAME, \
builtin_ ## NAME ## _plugin_interface_version, \
builtin_ ## NAME ## _sizeof_struct_st_plugin, \
builtin_ ## NAME ## _plugin)
plugin定義結束:
#define mysql_declare_plugin_end ,{0,0,0,0,0,0,0,0,0,0,0,0}}
__MYSQL_DECLARE_PLUGIN根據plugin是動態鏈接plugin還是靜態鏈接plugin有不同的定義:
#ifndef MYSQL_DYNAMIC_PLUGIN
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS) \
int VERSION= MYSQL_PLUGIN_INTERFACE_VERSION; \
int PSIZE= sizeof(struct st_mysql_plugin); \
struct st_mysql_plugin DECLS[]= {
#else
#define __MYSQL_DECLARE_PLUGIN(NAME, VERSION, PSIZE, DECLS) \
int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION; \
int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin); \
struct st_mysql_plugin _mysql_plugin_declarations_[]= {
#endif
特別要注意的是“#ifndef MYSQL_DYNAMIC_PLUGIN”,如果你要寫的plugin是動態加載的話,需要在編譯的時候定義這個宏。
總體而言,mysql_declare_plugin申明瞭一個struct st_mysql_plugin數組,開發者需要在該宏之後填寫plugin自定義的st_mysql_plugin各個成員,並通過mysql_declare_plugin_end結束這個數組。
看個例子plugin/daemon_example/daemon_example.cc,這是個動態MYSQL_DAEMON_PLUGIN類型的plugin,注意到plugin/daemon_example/Makefile.am裏面有-DMYSQL_DYNAMIC_PLUGIN。具體定義如下:
mysql_declare_plugin(daemon_example)
{
MYSQL_DAEMON_PLUGIN,
&daemon_example_plugin,
"daemon_example",
"Brian Aker",
"Daemon example, creates a heartbeat beat file in mysql-heartbeat.log",
PLUGIN_LICENSE_GPL,
daemon_example_plugin_init, /* Plugin Init */ // plugin初始化入口
daemon_example_plugin_deinit, /* Plugin Deinit */ // plugin清理函數
0x0100 /* 1.0 */,
NULL, /* status variables */
NULL, /* system variables */
NULL /* config options */
}
mysql_declare_plugin_end;
這個定義經過preprocess被展開後定義爲:
int _mysql_plugin_interface_version_= MYSQL_PLUGIN_INTERFACE_VERSION; \
int _mysql_sizeof_struct_st_plugin_= sizeof(struct st_mysql_plugin); \
struct st_mysql_plugin _mysql_plugin_declarations_[]= {
{ MYSQL_DAEMON_PLUGIN,
&daemon_example_plugin,
"daemon_example",
"Brian Aker",
"Daemon example, creates a heartbeat beat file in mysql-heartbeat.log",
PLUGIN_LICENSE_GPL,
daemon_example_plugin_init, /* Plugin Init */ // plugin初始化入口
daemon_example_plugin_deinit, /* Plugin Deinit */ // plugin清理函數
0x0100 /* 1.0 */,
NULL, /* status variables */
NULL, /* system variables */
NULL /* config options */
} , {0,0,0,0,0,0,0,0,0,0,0,0}};
靜態鏈接plugin也類似,只不過plugin宏展開出來的變量都有自己的名字,對於myisam,生成了一個叫builtin_myisam_plugin的plugin數組。
plugin可以定義自己的變量,包括系統變量和狀態變量。具體的例子可以看看storage/innobase/handler/ha_innodb.cc裏面對於innodb插件的申明,結合plugin.h,還是比較容易看懂的。
在mysql的源代碼裏面grep一把mysql_declare_plugin,看看都有哪些plugin:
$grep "mysql_declare_plugin(" --include=*.cc -rni *
plugin/daemon_example/daemon_example.cc:187:mysql_declare_plugin(daemon_example)
sql/ha_partition.cc:6269:mysql_declare_plugin(partition)
sql/log.cc:5528:mysql_declare_plugin(binlog)
sql/ha_ndbcluster.cc:10533:mysql_declare_plugin(ndbcluster)
storage/csv/ha_tina.cc:1603:mysql_declare_plugin(csv)
storage/example/ha_example.cc:893:mysql_declare_plugin(example)
storage/myisam/ha_myisam.cc:2057:mysql_declare_plugin(myisam)
storage/heap/ha_heap.cc:746:mysql_declare_plugin(heap)
storage/innobase/handler/ha_innodb.cc:8231:mysql_declare_plugin(innobase)
storage/myisammrg/ha_myisammrg.cc:1186:mysql_declare_plugin(myisammrg)
storage/blackhole/ha_blackhole.cc:356:mysql_declare_plugin(blackhole)
storage/federated/ha_federated.cc:3368:mysql_declare_plugin(federated)
storage/archive/ha_archive.cc:1627:mysql_declare_plugin(archive)
呵呵,連binlog都是plugin哦,不過還是storage plugin佔大多數。
Plugin初始化
在見面的介紹main函數的文章中我也提到了其中有個函數plugin_init()是初始化的一部分,這個東東就是所有靜態鏈接初始化plugin的初始化入口。該函數定義在"sql/sql_plugin.cc"中。
int plugin_init(int *argc, char **argv, int flags) {
// 初始化內存分配pool。
init_alloc_root(&plugin_mem_root, 4096, 4096);
init_alloc_root(&tmp_root, 4096, 4096);
// hash結構初始化
...
// 初始化運行時plugin數組,plugin_dl_array用來保存動態加載plugin,plugin_array保存靜態鏈接plugin。而且最多各自能有16個plugin。
my_init_dynamic_array(&plugin_dl_array, sizeof(struct st_plugin_dl *),16,16);
my_init_dynamic_array(&plugin_array, sizeof(struct st_plugin_int *),16,16);
// 初始化靜態鏈接plugin
for (builtins= mysqld_builtins; *builtins; builtins++) {
// 每一個plugin還可以有多個子plugin,參見見面的plugin申明。
for (plugin= *builtins; plugin->info; plugin++) {
register_builtin(plugin, &tmp, &plugin_ptr); // 將plugin放到plugin_array和plugin_hash中。
// 這個時候只初始化csv或者myisam plugin。
plugin_initialize(plugin_ptr); // 初始化plugin,調用plugin的初始化函數,將plugin的狀態變量加入到狀態變量列表中,將系統變量的plugin成員指向當前的活動plugin。
}
}
// 根據用戶選項初始化動態加載plugin
if (!(flags & PLUGIN_INIT_SKIP_DYNAMIC_LOADING))
{
if (opt_plugin_load)
plugin_load_list(&tmp_root, argc, argv, opt_plugin_load); // 根據配置加載制定的plugin。包括找到dll,加載,尋找符號並設置plugin結構。
if (!(flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE))
plugin_load(&tmp_root, argc, argv); // 加載系統plugin table中的plugin。
}
// 初始化剩下的plugin。
for (i= 0; i < plugin_array.elements; i++) {
plugin_ptr= *dynamic_element(&plugin_array, i, struct st_plugin_int **);
if (plugin_ptr->state == PLUGIN_IS_UNINITIALIZED)
{
if (plugin_initialize(plugin_ptr))
{
plugin_ptr->state= PLUGIN_IS_DYING;
*(reap++)= plugin_ptr;
}
}
}
...
}
這個函數執行結束以後,在plugin_array,plugin_dl_array,plugin_hash中保存了當前加載了的所有的plugin。到此plugin初始化結束。
在plugin_initialize函數裏面,調用了每個plugin自己的init函數(參見前面的內容)。特別要提到的是對於各種不同類型的plugin,初始化函數的參數也不一樣,這是通過一個全局的plugin_type_initialize間接層來實現的。這個數組對於每種類型的plugin定義了一個函數,比如對於storage plugin對應的是ha_initialize_handlerton,對於information scheme對應的是initialize_schema_table,然後在這些函數中再調用plugin的初始化函數。暫時對於其他類型的plugin沒有定義這個中間層初始化函數,所以就直接調用了plugin的初始化函數。
Mysql源代碼分析(6): Plugin架構介紹-續
上篇文章我們分析了Mysql的Plugin接口以及plugin的初始化過程,這裏我們繼續看plugin怎麼被使用的。基本還是通過例子看問題,主要分析myisam如何通過plugin接口被調用的。
myisam是mysql最早的和默認的storage engine,前面我們也看到在plugin初始化的時候是優先初始化myisam,然後才初始化其他的存儲引擎。這裏我們假定用戶要對一個myisam的表做操作,具體看看其中涉及的調用過程。
myisam的初始化
myisam plugin的定義可以在storage/myisam/ha_isam.cc中找到:
mysql_declare_plugin(myisam)
{
MYSQL_STORAGE_ENGINE_PLUGIN,
&myisam_storage_engine,
"MyISAM",
"MySQL AB",
"Default engine as of MySQL 3.23 with great performance",
PLUGIN_LICENSE_GPL,
myisam_init, /* Plugin Init */
NULL, /* Plugin Deinit */
0x0100, /* 1.0 */
NULL, /* status variables */
NULL, /* system variables */
NULL /* config options */
}
mysql_declare_plugin_end;
初始化函數是myisam_init。在前面文章中提到,storage engine類型的plugin均是通過ha_initialize_handlerton初始化。myisam_init的輸入參數是void *p,實際上是handlerton*。handlerton在mysql中封裝了訪問一個存儲引擎需要的接口,每個存儲引擎在全局空間有一個handlerton對象,保存在對應的內存中plugin結構的data域中。該結構具體定義可以在sql/handler.h中找到。myisam_init做的事情很簡單,設置handlerton中的各個域,其中最重要的域是create,被指向了一個函數myisam_create_handler,這個函數用來創建handler,用來對於數據庫文件進行操作。
打開一個表
數據庫表是數據庫中所有操作的基礎,我們看看打開一個表需要做些什麼。當一個select命令進來的時候,sql_parse.cc中的execute_sqlcom_select被執行,並被傳入parse出來的所有該命令要用的到表。它會調用open_and_lock_tables來打開指定的表,然後調用open_and_lock_tables_derived,再調用open_tables,再調用open_table(sql_base.cc)。一大堆調用之後真正開始幹實事兒的是open_unireg_entry,名字很奇怪,但是確實就是它開始打開表了,我們仔細將仔細看這個函數,以及它調用的函數。這個函數很長,其實大部分都是在做錯誤處理,最重要的就以下幾行:
static int open_unireg_entry(THD *thd, TABLE *entry, TABLE_LIST *table_list, const char *alias, char *cache_key, uint cache_key_length, MEM_ROOT *mem_root, uint flags) {
...
share= get_table_share_with_create(thd, table_list, cache_key, cache_key_length, OPEN_VIEW |table_list->i_s_requested_object, &error);
open_table_from_share(thd, share, alias, ...);
...
}
get_table_share_with_create是創建一個table_share結構,包括了同一個類型的表公用的數據結構,open_table_from_share則通過這個公用結構打開對應的要操作的表。
TABLE_SHARE *get_table_share(THD *thd, TABLE_LIST *table_list, ...) {
share= alloc_table_share(table_list, key, key_length)); //分配內存
my_hash_insert(&table_def_cache, (uchar*) share); // 加入cache,以後可以直接用
open_table_def(thd, share, db_flags); // 代開表的定義,需要讀frm文件
}
open_table_def是用來打開存儲表定義的文件。mysql中,每個表都有一個.frm文件,存儲了表的定義,這個函數就是要打開表對應的frm文件,讀入定義信息,填入TABLE_SHARE結構。
int open_table_def(THD *thd, TABLE_SHARE *share, uint db_flags) {
file= my_open(path, O_RDONLY | O_SHARE, MYF(0))
open_binary_frm(thd, share, head, file);
}
open_binary_frm讀入二進制的frm文件信息。這個函數超長,但是我們暫時只是對與plugin相關的部分感興趣。因爲每個表的storage engine信息就是從frm文件中讀出來的,我們看相關的代碼片段:
open_binary_frm(...) {
...
plugin_ref tmp_plugin= ha_resolve_by_name(thd, &name); // name就是storage engine的名字,比如"myisam"。這裏根據名字找到對應的plugin。
share->db_plugin= my_plugin_lock(NULL, &tmp_plugin); // 保存plugin的引用,供以後使用。plugin中的"data"域就是handlerton*,這將是主要的使用plugin的入口。
...
}
好了,TABLE_SHARE設置好了,我們回到open_unireg_entry中,繼續看open_table_from_share。這纔是真正打開表的地方。這個函數還是在sql/table.cc中。這個函數還是超長...,萬幸的是我們還是隻想關注plugin相關的內容。TABLE中有一個file結構,類型是handler*,我們以前提到過,handler就是一個打開的表的引用,顯然open_table_from_share的責任之一就是要設置這個域。
int open_table_from_share(THD *thd, TABLE_SHARE *share, ... TABLE *outparam, ...) { // outparam是打開後的表信息。
...
outparam->file= get_new_handler(share, &outparam->mem_root, share->db_type())); // 直奔主題,獲取一個handler。 share->db_type()返回plugin對應的handlerton,其實就是將plugin->data強制轉換成handlerton.
...
outparam->file->ha_open(outparam, ...); // 調用plugin的handler定義的open函數,做自定義的open操作。
...
}
get_new_handler負責根據TABLE_SHARE的內容構造一個handler對象。這個函數在sql/handler.cc中。
handler *get_new_handler(TABLE_SHARE *share, MEM_ROOT *alloc, handlerton *db_type) {
file= db_type->create(db_type, share, alloc); // 調用plugin的create函數,來創建handler成員。
file->init();
}
前面我們提到過對於myisam對應的create函數是myisam_create_handler,這個函數就是new了一個ha_myisam對象,而ha_myisam又是從handler繼承下來的,重載了handler對應的函數。
這樣一個對於應數據庫表文件的handler就可以使用啦,它的第一個使用就是在open_table_from_share中被調用ha_open。ha_open在handler.cc中定義,其實就是調用了重載後了open函數。在ha_myisam中,我們可以看到open函數的定義,這裏我們就不仔細看了,實現細節和myisam的文件結構相關。
看到這裏一個"SELECT * from test"語句如何打開表的部分就基本清楚了,主要過程包括:
從frm文件尋找storage engine的名字,並獲取對應的storage engine plugin的handlerton
調用handlerton的create函數生成handler
通過handler重載的open函數打開表文件
挺清楚的。
到了這裏,我們就有了表的handler,以後凡是涉及到存儲引擎的操作,都通過這個接口調用來做,這樣,storage engine plugin就和mysql核心緊密結合到了一起,各司其事,共同完成複雜的sql操作。
Mysql源代碼分析(7): MYISAM的數據文件處理
好久沒寫分析文章了,一個是比較忙,另一個是因爲餘下的內容都是硬骨頭,需要花時間慢慢理解。剩下的比較有意思的內容有:
select語句的執行和優化過程。大家關心數據庫的查詢性能,主要是對着部分比較感興趣,特別是其中的查詢優化部分。
Mysql的replication。Mysql的master/slave架構是大部分使用mysql的高性能網站架構的不二選擇,replication則是這個架構的基礎。
具體數據庫引擎的實現。這部分也是很多關心mysql性能的人會比較感興趣的部分,不過這個工作比較複雜,特別是流行的innodb,這個工作量尤其浩大,而且難度頗高。其中涉及到transaction的部分,也是特別複雜。
另外,我發現我寫的文章被一些地方轉摘了,感謝大家的閱讀,但是我也希望轉摘要註明出處,至少給個原文鏈接吧,也不枉我幸苦一場。
今天主要寫寫Myisam的數據文件的處理。
Myisam是最早實現的Mysql數據庫引擎,也是人們心中的性能最好的引擎(雖然不是功能最強的,沒辦法,現實往往要求性能和功能做權衡)。這裏選擇分析它,主要原因是其實現還算比較簡單明瞭,而且最近我對數據文件的格式比較感興趣,特別是變長數據的處理。要注意的是本文不會介紹myisam的索引文件格式。
基本知識
對於每一個以Myisam做數據引擎的表,在<%data_dir%>/<database>目錄下會有如下幾個文件來保存其相關信息:
.frm文件。 這個文件是跨引擎的,描述了該表的元信息,其中最重要的是表定義和表的數據庫引擎。
.MYD文件。這是我們要看的重點文件,包含了數據庫record信息,就是數據庫中的每個行。
.MYI文件。索引文件,用來加速查找。
而對於MYD中的每個record,可以是fixed,dynamic以及packed三種類型之一。fixed表示record的大小是固定的,沒有VARCHAR, blob之類的東東。dynamic則剛好相反,有變長數據類型。packed類型是通過myisampack處理過的record。參見:http://dev.mysql.com/doc/refman/5.1/en/myisam-table-formats.html。
需要注意的是record類型是針對表的設置,而不是對每個column的設置。
record處理接口
record的類型是表級別的設置,所以在一個表被打開的時候,myisam會檢查元數據的選項,看該表的record是什麼類型,然後設置對應的處理函數,具體處理在storage/myisam/mi_open.c的mi_setup_functions中,我們看其中的一個片段:
746 void mi_setup_functions(register MYISAM_SHARE *share)
747 {
....
759 else if (share->options & HA_OPTION_PACK_RECORD)
760 {
761 share->read_record=_mi_read_dynamic_record;
762 share->read_rnd=_mi_read_rnd_dynamic_record;
763 share->delete_record=_mi_delete_dynamic_record;
764 share->compare_record=_mi_cmp_dynamic_record;
765 share->compare_unique=_mi_cmp_dynamic_unique;
766 share->calc_checksum= mi_checksum;
767
768 /* add bits used to pack data to pack_reclength for faster allocation */
769 share->base.pack_reclength+= share->base.pack_bits;
770 if (share->base.blobs)
771 {
772 share->update_record=_mi_update_blob_record;
773 share->write_record=_mi_write_blob_record;
774 }
775 else
776 {
777 share->write_record=_mi_write_dynamic_record;
778 share->update_record=_mi_update_dynamic_record;
779 }
780 }
...
這是針對pack類型的處理函數設置。設置了share結構中的一堆函數接口。順便說一句,這種方式是C語言編程中常用的實現”多態“的辦法:申明函數接口,動態設置接口實現,思想上和C++的動態綁定是一致的。這段代碼對於dynamic類型的表的record處理函數做了設置。比較有趣的是HA_OPTION_PACK_RECORD用來指定dynamic類型。
看到這些函數名大家可以猜想出他們都是幹嘛的,下面主要看看fixed類型和dynamic類型的具體處理。
Fixed類型
顧名思義,fixed類型的表中的所有字段都是定長的,不能出現TEXT, VARCHAR之類的東東。這種嚴格限制帶來的好處就是更快更直接的數據record操作,想想也知道,每個數據都是定長的,在文件操作的時候多方便啊。
看看一個數據的函數_mi_write_static_record,它在mi_statrec.c中,所有對於fixed record的操作的實現都定義在這個文件中。
21 int _mi_write_static_record(MI_INFO *info, const uchar *record)
22 {
...
24 if (info->s->state.dellink != HA_OFFSET_ERROR &&
25 !info->append_insert_at_end)
26 {
檢查dellink中是否有record。dellink是所有被刪除的數據構成的鏈表。當一個record被刪除的時候,它
所佔的文件大小不是被馬上釋放,而是被放入dellink中,等候下次使用。
27 my_off_t filepos=info->s->state.dellink;
讀入dellink所指向的數據空間的信息。
33 更新dellink,將使用了的數據空間移除。
將record寫入找到的已刪除的數據的空間中。
40 }
41 else
42 {
43 檢查數據文件是否過大。
49 如果使用的寫緩衝,則寫入寫緩衝。
將新數據寫入文件最後。
更新元數據。
...
86 }
因爲所有的數據都是一樣大小,處理起來很簡單。特別是當一個數據被刪除的時候,它所佔的空間被放入一個回收鏈表中,下次要寫入新數據的時候,如果回收鏈表不爲空,直接從其中找一個寫入新數據即可,不用分配新的存儲空間。
Fixed類型的其他處理也都很簡單,這裏不再多說了。需要提出的是,不管用的什麼類型的數據,當數據被刪除的時候,其所佔的空間並不是馬上被釋放的,那樣操作代價太大,要把該數據後面的所有數據向前移位,肯定無法忍受。一般的做法都是將這些空間用鏈表穿起來,供以後使用,所以數據文件一般是不會主動縮小的.....即使是innodb也是這樣。
Dynamic類型
Dynamic類型是相對於fixed的類型而言,這種類型可以容忍變長數據類型的存在。隨之而來的是更復雜的數據文件的操作。Dynamic類型中被刪除的數據塊也不是馬上被釋放,也被鏈表連起來。下次要寫入新數據的時候,還是優先從這個鏈表中找。不同於fixed類型的處理在於新來的數據和鏈表中的空間的大小可能不一樣。如果新數據大了,就會找好幾個空餘空間,將數據分散於多個數據塊中,如果新數據小了,則會將空餘數據塊分成兩個,一個寫入新數據,一個還是放在空餘鏈表中供後來者使用。
看一下mi_dynrec.c中的write_dynamic_record函數。
320 static int write_dynamic_record(MI_INFO *info, const uchar *record,
321 ulong reclength)
322 {
檢查是否有足夠的空間來存放新數據,空間滿了返回錯誤。
351
352 do
353 {
// 找一個可以寫入數據的地方。注意這裏是在一個循環裏面,也就是說每次找到的
// 空間不一定能夠寫入整個數據,只能寫入部分的話,剩下的還要繼續找地方寫。
354 if (_mi_find_writepos(info,reclength,&filepos,&length))
355 goto err;
// 寫入能夠放入找到的空間的數據。
356 if (_mi_write_part_record(info,filepos,length,
357 (info->append_insert_at_end ?
358 HA_OFFSET_ERROR : info->s->state.dellink),
359 (uchar**) &record,&reclength,&flag))
360 goto err;
361 } while (reclength);
...
}
其中的循環說明了一切,很有可能一個數據會被分成幾塊兒,寫到不同的地方,但是他們合起來才構成了整個數據。
再看_mi_find_writepos。
371 static int _mi_find_writepos(MI_INFO *info,
372 ulong reclength, /* record length */
373 my_off_t *filepos, /* Return file pos */
374 ulong *length) /* length of block at filepos */
375 {
376 MI_BLOCK_INFO block_info;
...
// 先檢查dellink中是否有空餘的空間。
380 if (info->s->state.dellink != HA_OFFSET_ERROR &&
381 !info->append_insert_at_end)
382 {
383 /* Deleted blocks exists; Get last used block */
存在空餘空間,那就把鏈表中的頭找出來,把其中的空間用來寫入新數據。
將這塊空間的描述返回給調用者。
....
398 }
399 else
400 {
401 /* No deleted blocks; Allocate a new block */
沒有已刪除的空間,那就在數據文件的最後分配空間,並返回給調用者。
421 }
...
}
如果有已刪除的空間的話,那就直接把鏈表頭描述的空間返回。這個算法很簡單,但是我覺得這樣簡單的算法可能會趙成一些問題,比如存儲的碎片化,一塊兒大空間被切的越來越小,到後來寫入一個數據要使用好幾個空間。這些問題在操作系統的內存管理中也同樣存在,所以產生了大量的內存管理算法,這裏也應該可以借用吧。
具體的寫入是在_mi_write_part_record中完成的。這個函數比較長,我就直接簡寫如下了。
int _mi_write_part_record(MI_INFO *info,
my_off_t filepos, /* points at empty block */
ulong length, /* length of block */
my_off_t next_filepos,/* Next empty block */
uchar **record, /* pointer to record ptr */
ulong *reclength, /* length of *record */
int *flag) /* *flag == 0 if header */
{
如果給出的空間空間大於數據長度的話,計算填完數據後剩餘的空間。
如果空間剛好,準備一些元數據。
如果空間太小,則找到下一個寫入空間的位置(要麼是下一個dellink,要麼是文件末尾),並準備這些元數據。如果是第一部分的數據的話,要寫入更多的信息。
如果空間太大,有剩餘空間的話,先看這個空間能否與和下一個空閒空間連接起來形成一個大空間,如果能的話就合併。將其相關的元數據,比如空間的位置,大小之類的,準備好。
開始寫數據羅,如果啓用了寫緩衝,則寫入緩衝,否則寫入找出來的空間。
更新dellink的相關信息。
}
邏輯很清楚,主要是要處理空間過大或者過小帶來的複雜性。
好了,到了這裏大部分的處理都很清楚了,還是很直接的。剩下的就是在刪除一個數據的時候,將其所佔的空間放到dellink中,要注意的是,如果其數據塊可以和dellink中的其他數據塊合併,合併操作也是在刪除數據的操作中調用的,而且合併出來的數據塊還可能和其他數據塊繼續合併