主要執行過程從架構上來看,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);
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_insert(thd, all_tables, lex->field_list, lex->many_values, lex->update_list, lex->value_list, lex->duplicates, lex->ignore); break; ... case SQLCOM_SELECT: