[转帖]MYSQL源码分析(二)--主要代码流程_MySQL, Oracle及数据库讨论区_Weblogic技术|Tuxedo技术|中间件技术|Oracle论坛|JAVA论坛|Linux/Unix技术|hadoop论坛_联动北方技术论坛  
网站首页 | 关于我们 | 服务中心 | 经验交流 | 公司荣誉 | 成功案例 | 合作伙伴 | 联系我们 |
联动北方-国内领先的云技术服务提供商
»  游客             当前位置:  论坛首页 »  自由讨论区 »  MySQL, Oracle及数据库讨论区 »
总帖数
1
每页帖数
101/1页1
返回列表
0
发起投票  发起投票 发新帖子
查看: 2026 | 回复: 0   主题: [转帖]MYSQL源码分析(二)--主要代码流程        下一篇 
ad222888
注册用户
等级:新兵
经验:66
发帖:134
精华:0
注册:2016-9-25
状态:离线
发送短消息息给ad222888 加好友    发送短消息息给ad222888 发消息
发表于: IP:您无权察看 2018-8-15 9:35:00 | [全部帖] [楼主帖] 楼主

我们从MySQL启动开始,看一下MySQL业务流程。
★ 首先入口函数在目录sql/main.cc中:

int main(int argc, char **argv)
return mysqld_main(argc, argv);


调用了mysqld_main(argc, argv)函数,这个函数在sql/mysqld.cc中

接着开始初始化:
★ MY_INIT(argv[0]);函数位于sql/my_init.cc,用于初始化mysql内部的系统库,MYSQL定义了大量的全局变量用于系统的状态值,这样省去很多函数之间的交互参数,这些全局变量定义在my_static.cc中。
★ 接着初始化变量sys_var_init();
★ 初始化审计接口(这部分用的不多,暂时不深入)mysql_audit_initialize();
★ 初始化日志功能logger.init_base(); 日志是系统的一个重要地方,mysql定义了一个logger类,在文件sql/Logger.hpp中,里面定义了一系列的成员函数,具体可以参照logger.hpp中的注释,很详细。(后续会专门针对日志模块做一次分析)
★ 接着就初始化配置信息了,
开始之后,又是一大堆全局变量初始化。
init_thread_environment()初始化线程环境。(这个就是线程池了)
mysql_init_variables()初始化配置变量,
这边给我最大的感悟就是,MYSQL在获取或者修改一些变量时,一定会先与之相关的系统变量都读出来确认一遍,全部没问题后才修改,有问题都会有相应的日志(所以日志模块必须最先初始化)
如果初始化失败,会调用mysql_server_end() ,而mysql_server_end()实际上是一个宏定义

#define mysql_server_end() mysql_client_plugin_deinit() //宏定义也可以这样用


mysql_client_plugin_deinit()用于终止mysql server的线程。
★ 初始化server的模块init_server_components()

table_def_init() || hostname_cache_init(); //初始化table cache
query_cache_init(); //初始化查询cache
#ifdef HAVE_REPLICATION
init_slave_list(); //如果有复制,还要初始化备机的状态list
#endif


后面还有一些cache初始化如

 ★ network_init();//初始化网络模块,创建socket监听


这个流程和传统的tcp服务端代码基本一样,单独开启一个线程用于监听, set_ports();
函数用于初始化端口,默认为3306。

ip_sock= create_socket(ai, AF_INET, &a);
if (ip_sock == INVALID_SOCKET)
ip_sock= create_socket(ai, AF_INET6, &a);
// Report user-error if we failed to create a socket.
if (ip_sock == INVALID_SOCKET)
sql_perror(ER_DEFAULT(ER_IPSOCK_ERROR)); /* purecov: tested */
unireg_abort(1); /* purecov: tested */


现在IPV4上创建侦听,如果失败了,再在ipv6上创建侦听。
有时候,mysql所用的端口不会及时释放(重启,关闭的时候),这里有个算法

for (waited= 0, retry= 1; ; retry++, waited+= this_wait)
if (((ret= bind(ip_sock, a->ai_addr, a->ai_addrlen)) >= 0 ) ||
(socket_errno != SOCKET_EADDRINUSE) ||
(waited >= mysqld_port_timeout))
break;
sql_print_information("Retrying bind on TCP/IP port %u", mysqld_port);
this_wait= retry * retry / 3 + 1;
sleep(this_wait);


他会按照

 Sleep intervals: 1, 2, 4, 6, 9, 13, 17, 22, ...
Retry at second: 1, 3, 7, 13, 22, 35, 52, 74, ...


来省资源。
接着就是三种系统的侦听代码,原理都是一样的。

★ start_signal_handler();// 创建pid文件


调用create_pid_file(),调用了自己封装的mysql_file_write()把内容写到了文件,mysql_file_write()也是个宏定义。

★ if (mysql_rm_tmp_tables() || acl_init(opt_noacl) ||
my_tz_init((THD *)0, default_tz_name, opt_bootstrap))


删除tmp_table并初始化数据库级别的权限。


init_status_vars(); //初始化mysql中的status变量
inlog_unsafe_map_init();
initialize_information_schema_acl();
execute_ddl_log_recovery();
create_shutdown_thread();


未启动先保护,这样异常出现时,系统不会直接崩掉,同时会尽可能的多保留下信息。


start_handle_manager(); //创建manager线程
mysql_thread_create(key_thread_handle_manager, &hThread, &connection_attrib, handle_manager, 0)
★ handle_connections_sockets();


主要处理函数,
一系列异常保护之后,如果系统收到新的连接,就创建新的线程处理之create_new_thread(thd);
这里可以看出在创建线程之前,有大部分的代码用来检查内容是否有问题,大约占了2/3,所以异常保护,入参校验是很重要的。

★ 对当前连接数检测connection_count,先对互斥量LOCK_connection_count枷锁,如果大于``max_connections+1,则报错,没有问题,才新建线程,(一个典型的互斥线程),ok,现在线程创建成功了。

之后的一段代码一度让我很费解。前前后后看了很久,终于搞明白流程是怎么继续往下面走的。以下是整个过程:
★ 在create_new_thread(thd);的末尾,有一行代码:

MYSQL_CALLBACK(thread_scheduler, add_connection, (thd));同样他是一个宏:

#define MYSQL_CALLBACK(OBJ, FUNC, PARAMS) \
do { \
      if ((OBJ) && ((OBJ)->FUNC)) \
      (OBJ)->FUNC PARAMS; \
} while (0)


这样,这个代码就是调用thread_scheduler的add_connection函数,参数是(thd)。
★ 继续看的定义:(代码在scheduler.cc里面)

 scheduler_functions *thread_scheduler= NULL;


scheduler_functions的定义:

struct scheduler_functions
uint max_threads;
bool (*init)(void);
bool (*init_new_connection_thread)(void);
void (*add_connection)(THD *thd);
void (*thd_wait_begin)(THD *thd, int wait_type);
void (*thd_wait_end)(THD *thd);
void (*post_kill_notification)(THD *thd);
bool (*end_thread)(THD *thd, bool cache_thread);
void (*end)(void);


可以看得出来,add_connection是他这个结构的第四个成员,是一个成员变量。
★ Mysql给thread_scheduler提供了两种值,一种单线程的情况,一种是多线程的情况。单线程就是每次只有一个用户连接,多线程用的比较多。
单线程:

static scheduler_functions one_thread_scheduler_functions=
1, // max_threads
NULL, // init
init_new_connection_handler_thread, // init_new_connection_thread
#ifndef EMBEDDED_LIBRARY
handle_connection_in_main_thread, // add_connection
#else
NULL, // add_connection
#endif // EMBEDDED_LIBRARY
NULL, // thd_wait_begin
NULL, // thd_wait_end
NULL, // post_kill_notification
no_threads_end, // end_thread
NULL, // end


多线程:

#ifndef EMBEDDED_LIBRARY
static scheduler_functions one_thread_per_connection_scheduler_functions=
0, // max_threads
NULL, // init
init_new_connection_handler_thread, // init_new_connection_thread
create_thread_to_handle_connection, // add_connection
NULL, // thd_wait_begin
NULL, // thd_wait_end
NULL, // post_kill_notification
one_thread_per_connection_end, // end_thread
NULL, // end
#endif // EMBEDDED_LIBRARY


EMBEDDED_LIBRARY是一种用于开发板的库。
★ 那么MYSQL是怎么知道当前thread_scheduler的值呢,这个就是我忽视的问题。在上文所述,logger.init()之后,mysql调用了一个init_common_variables()函数,里面初始化了这个变量的值。
在init_common_variables()中调用了一个get_options()函数:

#ifdef EMBEDDED_LIBRARY
one_thread_scheduler();
#else
if (thread_handling <= SCHEDULER_ONE_THREAD_PER_CONNECTION)
one_thread_per_connection_scheduler();
else /* thread_handling == SCHEDULER_NO_THREADS) */
one_thread_scheduler();
#endif


我们这边多线程用的比较多,所以从上述的函数,参照多线程的结构体,上述回调函数就是调用了create_thread_to_handle_connection这个函数。
找到这个函数(在mysqld.cc里),流程继续:

★ 在create_thread_to_handle_connection中:

if ((error= mysql_thread_create(key_thread_one_connection,
&thd->real_id, &connection_attrib,
handle_one_connection,
(void*) thd)))


创建一个线程,并调用handle_one_connection函数
★ 在handle_one_connection中继续调用do_handle_one_connection()
★ 之后又是一个回调函数:

 MYSQL_CALLBACK_ELSE(thread_scheduler, init_new_connection_thread, (), 0)


★ 找到init_new_connection_thread(),里面调用了my_thread_init()到目前为止,才算创建了一个新的线程
★ 在进行一些确认过程后,终于来到了do_command(thd)
★ 在接收到command的时候,mysql对command做了一些简单的处理(后来我才知道,这个叫做预处理。。。),就开始分发dispatch_command()
★ 在dispatch_command()中,有一个巨大的switch语句,里面涵盖了MYSQL支持的所有语句,这些语句在mysql_com.h中定义

enum enum_server_command
COM_SLEEP, COM_QUIT, COM_INIT_DB, COM_QUERY, COM_FIELD_LIST,
COM_CREATE_DB, COM_DROP_DB, COM_REFRESH, COM_SHUTDOWN, COM_STATISTICS,
COM_PROCESS_INFO, COM_CONNECT, COM_PROCESS_KILL, COM_DEBUG, COM_PING,
COM_TIME, COM_DELAYED_INSERT, COM_CHANGE_USER, COM_BINLOG_DUMP,
COM_TABLE_DUMP, COM_CONNECT_OUT, COM_REGISTER_SLAVE,
COM_STMT_PREPARE, COM_STMT_EXECUTE, COM_STMT_SEND_LONG_DATA, COM_STMT_CLOSE,
COM_STMT_RESET, COM_SET_OPTION, COM_STMT_FETCH, COM_DAEMON,
/* don't forget to update const char *command_name[] in sql_parse.cc */
/* Must be last */
COM_END


★ 我们以查询语句为例
在收到查询指令后,mysql还是先预处理,然后在处理:

 mysql_parse(thd, thd->query(), thd->query_length(), &end_of_stmt);


解析查询语句
★ 在mysql_parse函数中,有段注释:

Warning.
The purpose of query_cache_send_result_to_client() is to lookup the
query in the query cache first, to avoid parsing and executing it.
So, the natural implementation would be to:
- first, call query_cache_send_result_to_client,
- second, if caching failed, initialise the lexical and syntactic parser.
The problem is that the query cache depends on a clean initialization
of (among others) lex->safe_to_cache_query and thd->server_status,
which are reset respectively in
- lex_start()
- mysql_reset_thd_for_next_command()
So, initializing the lexical analyser *before* using the query cache
is required for the cache to work properly.
FIXME: cleanup the dependencies in the code to simplify this.


他大概的意思是:本来mysql应该先调用query_cache_send_result_to_client()在query_cache中查询该语句,加快查询速度。如果失败,在调用lex_start()和mysql_reset_thd_for_next_command()来初始化thd,在解析sql。但是查询cache也需要干净的thd,只能先调用lex_start()和mysql_reset_thd_for_next_command()来初始化thd了,这样导致代码和逻辑有悖。
★ 好,作者说的很清楚了,那么我们接下来,就应该先查cache,查询不到,再进行命令解析。

lex_start(thd);
query_cache_send_result_to_client(thd, (char*) inBuf, length) sql_command
mysql_execute_command(thd);


★ 在mysql_execute_command()中,先确定command要对哪张表操作

 lex->first_lists_tables_same();


根据该表的状态,会做一些预处理,尽量减少之后的操作对表的影响(因为目前还不知道这条指令执行之后,会对数据库产生什么样的影响)做好保护是必须的。
★ 然后有个switch语句,他决定了command属于哪种类型,
这些类型定义在sql_lex.h中:

enum enum_sql_command {
SQLCOM_SELECT, SQLCOM_CREATE_TABLE, SQLCOM_CREATE_INDEX, SQLCOM_ALTER_TABLE,
SQLCOM_UPDATE, SQLCOM_INSERT, SQLCOM_INSERT_SELECT,
SQLCOM_DELETE, SQLCOM_TRUNCATE, SQLCOM_DROP_TABLE, SQLCOM_DROP_INDEX,
SQLCOM_SHOW_DATABASES, SQLCOM_SHOW_TABLES, SQLCOM_SHOW_FIELDS,
SQLCOM_SHOW_KEYS, SQLCOM_SHOW_VARIABLES, SQLCOM_SHOW_STATUS,
SQLCOM_SHOW_ENGINE_LOGS, SQLCOM_SHOW_ENGINE_STATUS, SQLCOM_SHOW_ENGINE_MUTEX,
SQLCOM_SHOW_PROCESSLIST, SQLCOM_SHOW_MASTER_STAT, SQLCOM_SHOW_SLAVE_STAT,
SQLCOM_SHOW_GRANTS, SQLCOM_SHOW_CREATE, SQLCOM_SHOW_CHARSETS,
SQLCOM_SHOW_COLLATIONS, SQLCOM_SHOW_CREATE_DB, SQLCOM_SHOW_TABLE_STATUS,
SQLCOM_SHOW_TRIGGERS,
SQLCOM_LOAD,SQLCOM_SET_OPTION,SQLCOM_LOCK_TABLES,SQLCOM_UNLOCK_TABLES,
SQLCOM_GRANT,
SQLCOM_CHANGE_DB, SQLCOM_CREATE_DB, SQLCOM_DROP_DB, SQLCOM_ALTER_DB,
SQLCOM_REPAIR, SQLCOM_REPLACE, SQLCOM_REPLACE_SELECT,
SQLCOM_CREATE_FUNCTION, SQLCOM_DROP_FUNCTION,
SQLCOM_REVOKE,SQLCOM_OPTIMIZE, SQLCOM_CHECK,
SQLCOM_ASSIGN_TO_KEYCACHE, SQLCOM_PRELOAD_KEYS,
SQLCOM_FLUSH, SQLCOM_KILL, SQLCOM_ANALYZE,
SQLCOM_ROLLBACK, SQLCOM_ROLLBACK_TO_SAVEPOINT,
SQLCOM_COMMIT, SQLCOM_SAVEPOINT, SQLCOM_RELEASE_SAVEPOINT,
SQLCOM_SLAVE_START, SQLCOM_SLAVE_STOP,
SQLCOM_BEGIN, SQLCOM_CHANGE_MASTER,
SQLCOM_RENAME_TABLE,
SQLCOM_RESET, SQLCOM_PURGE, SQLCOM_PURGE_BEFORE, SQLCOM_SHOW_BINLOGS,
SQLCOM_SHOW_OPEN_TABLES,
SQLCOM_HA_OPEN, SQLCOM_HA_CLOSE, SQLCOM_HA_READ,
SQLCOM_SHOW_SLAVE_HOSTS, SQLCOM_DELETE_MULTI, SQLCOM_UPDATE_MULTI,
SQLCOM_SHOW_BINLOG_EVENTS, SQLCOM_DO,
SQLCOM_SHOW_WARNS, SQLCOM_EMPTY_QUERY, SQLCOM_SHOW_ERRORS,
SQLCOM_SHOW_STORAGE_ENGINES, SQLCOM_SHOW_PRIVILEGES,
SQLCOM_HELP, SQLCOM_CREATE_USER, SQLCOM_DROP_USER, SQLCOM_RENAME_USER,
SQLCOM_REVOKE_ALL, SQLCOM_CHECKSUM,
SQLCOM_CREATE_PROCEDURE, SQLCOM_CREATE_SPFUNCTION, SQLCOM_CALL,
SQLCOM_DROP_PROCEDURE, SQLCOM_ALTER_PROCEDURE,SQLCOM_ALTER_FUNCTION,
SQLCOM_SHOW_CREATE_PROC, SQLCOM_SHOW_CREATE_FUNC,
SQLCOM_SHOW_STATUS_PROC, SQLCOM_SHOW_STATUS_FUNC,
SQLCOM_PREPARE, SQLCOM_EXECUTE, SQLCOM_DEALLOCATE_PREPARE,
SQLCOM_CREATE_VIEW, SQLCOM_DROP_VIEW,
SQLCOM_CREATE_TRIGGER, SQLCOM_DROP_TRIGGER,
SQLCOM_XA_START, SQLCOM_XA_END, SQLCOM_XA_PREPARE,
SQLCOM_XA_COMMIT, SQLCOM_XA_ROLLBACK, SQLCOM_XA_RECOVER,
SQLCOM_SHOW_PROC_CODE, SQLCOM_SHOW_FUNC_CODE,
SQLCOM_ALTER_TABLESPACE,
SQLCOM_INSTALL_PLUGIN, SQLCOM_UNINSTALL_PLUGIN,
SQLCOM_SHOW_AUTHORS, SQLCOM_BINLOG_BASE64_EVENT,
SQLCOM_SHOW_PLUGINS,
SQLCOM_SHOW_CONTRIBUTORS,
SQLCOM_CREATE_SERVER, SQLCOM_DROP_SERVER, SQLCOM_ALTER_SERVER,
SQLCOM_CREATE_EVENT, SQLCOM_ALTER_EVENT, SQLCOM_DROP_EVENT,
SQLCOM_SHOW_CREATE_EVENT, SQLCOM_SHOW_EVENTS,
SQLCOM_SHOW_CREATE_TRIGGER,
SQLCOM_ALTER_DB_UPGRADE,
SQLCOM_SHOW_PROFILE, SQLCOM_SHOW_PROFILES,
SQLCOM_SIGNAL, SQLCOM_RESIGNAL,
SQLCOM_SHOW_RELAYLOG_EVENTS,
When a command is added here, be sure it's also added in mysqld.cc
in "struct show_var_st status_vars[]= {" ...
/* This should be the last !!! */
SQLCOM_END


★ 我们仍然以查询命令为例。找到

case SQLCOM_SELECT:
thd->status_var.last_query_cost= 0.0;
lex->exchange != NULL implies SELECT .. INTO OUTFILE and this
requires FILE_ACL access.
ulong privileges_requested= lex->exchange ? SELECT_ACL | FILE_ACL :
SELECT_ACL;
if (all_tables)
res= check_table_access(thd,
privileges_requested,
all_tables, FALSE, UINT_MAX, FALSE);
else
res= check_access(thd, privileges_requested, any_db, NULL, NULL, 0, 0);
if (res)
break;
res= execute_sqlcom_select(thd, all_tables);
break;


重置查询消耗时间:thd->status_var.last_query_cost= 0.0;
检查用户权限:check_table_access
执行select命令execute_sqlcom_select
之后就是命令的解析,处理,以及然后查询,规整结果集。下次笔记再说。




赞(0)    操作        顶端 
总帖数
1
每页帖数
101/1页1
返回列表
发新帖子
请输入验证码: 点击刷新验证码
您需要登录后才可以回帖 登录 | 注册
技术讨论