MySQL内部手册/MySQL源代码指南/服务器代码的框架

1.9服务器代码的框架

现在我们将讨论一些更难的内容,即服务器。

警告:代码不断变化,所以名称和参数可能在您阅读这篇文章的时候已经发生了变化。

我们将浏览的重要文件:

/ sql / mysqld。cc / sql / sql_parse。cc / sql / sql_prepare。cc / sql / sql_insert。cc / sql / ha_myisam。cc / myisam / mi_write.c

这可不像我们刚才做的那么简单。事实上,我们需要多页来浏览这一个,尽管我们再次使用了截断和冷凝。但是服务器很重要,如果你能理解我们用它做什么,你就能理解MySQL源代码的本质。

我们将主要关注sql目录中的程序,这是存储mysqld和sql引擎代码的大多数程序的地方。

我们的目标是跟踪服务器,从它启动时开始,通过从客户机接收到的单个INSERT语句,直到它最终在MyISAM文件中执行低级写操作。

遍历服务器代码:/sql/mysqld.cc

Int main(Int argc, char **argv) {_cust_check_startup();(空白)thr_setconcurrency(并发);init_ssl ();server_init ();// 'bind' + 'listen' init_server_components();start_signal_handler ();acl_init((THD *)0, opt_noacl);init_slave ();create_shutdown_thread ();create_maintenance_thread (); handle_connections_sockets(0); // ! DBUG_PRINT("quit",("Exiting main thread")); exit(0); }

这里是所有开始的地方,在mysqld.cc的主函数中。

注意,我们在这个代码片段上面显示了目录名和程序名。我们将对本系列中的所有代码片段进行同样的操作。

花几秒钟看一下这个代码片段,您可能会看到main函数在启动时执行一些初始检查,正在初始化一些组件,正在调用名为handle_connections_sockets的函数,然后退出。acl可能代表“访问控制”,有趣的是,DBUG_PRINT来自Fred Fish的调试库,我们之前已经提到过。但我们不能离题。

实际上,主函数中有150行代码,而我们只展示13行代码。这样你就能知道我们修剪了多少了。我们抛弃了错误检查、辅助路径、可选代码和变量。但我们没有改变剩下的东西。如果使用编辑器访问mysqld,就可以找到这些行。Cc程序,这同样适用于本系列代码片段中的所有其他例程。

在实际的源代码中,您不会看到一个小标记“// !”。该标记将始终位于作为下一个代码片段主题的函数所在的行上。在本例中,这意味着下一个代码片段将显示handle_connection_sockets函数。为了证明这一点,让我们看下一个片段。

遍历服务器代码:/sql/mysqld.cc

handle_connections_sockets (arg __attribute__((未使用)){if (ip_sock != INVALID_SOCKET) {FD_SET(ip_sock,& clientfd);DBUG_PRINT("general",("Waiting for connections."));while (!abort_loop) {new_sock = accept(sock, my_reinterpret_cast(struct sockaddr*) (&cAddr), &length);thd= new thd;If (sock == unix_sock) thd->host=(char*) localhost;create_new_thread (thd);/ / !}

在handle_connections_sockets中,您将看到经典客户机/服务器体系结构的特征。在经典的客户机/服务器中,服务器有一个主线程,它总是监听来自新客户机的传入请求。一旦它接收到这样的请求,它就会分配资源,这些资源将专属于该客户端。特别是,主线程将生成一个新线程来处理连接。然后,主服务器将循环并监听新的连接——但我们将离开它,并遵循新的线程。

除了我们选择在这里显示的套接字代码之外,这个线程循环还有几个变体,因为客户机可以选择以其他方式连接,例如使用命名管道或共享内存。但本节需要注意的重要事项是,服务器正在生成新线程。

遍历服务器代码:/sql/mysqld.cc

create_new_thread(THD * THD) {pthread_mutex_lock(&LOCK_thread_count);Pthread_create (&thd->real_id,&connection_attrib, handle_one_connection, // !(void *) (thd));pthread_mutex_unlock (&LOCK_thread_count);}

下面是生成新线程的例程的详细介绍。值得注意的细节是,如您所见,它使用互斥或互斥对象。MySQL有各种各样的互斥锁,用来防止所有线程的动作相互冲突。

遍历服务器代码:/sql/sql_parse.cc

handle_one_connection(THD * THD) {init_sql_alloc(&thd->mem_root, MEM_ROOT_BLOCK_SIZE, MEM_ROOT_PREALLOC);而(!净- >错误& & - > vio ! = 0 & & ! (thd - >杀){如果(do_command (thd)) / / !打破;} close_connection(净);end_thread (thd), 1);包= (char *)净- > read_pos;

通过这个代码片段,我们已经脱离了mysqld.cc。现在,我们在sql_parse文件中,仍然在sql目录中。这就是会话的大循环所在之处。

循环重复地获取和执行命令。当它结束时,连接关闭。此时,线程将结束,用于线程的资源将被释放。

但我们更感兴趣的是,当调用do_command函数时,在循环内部发生了什么。

图形:客户端<===== MESSAGE ====>服务器<======PACKETS ====>示例:INSERT INTO Table1 VALUES (1);

从图形化的角度来说,此时客户机和一个服务器线程之间存在长期连接。消息包将通过此连接在它们之间来回传输。对于今天的介绍,让我们假设客户机传递图形上显示的INSERT语句,以便服务器进行处理。

遍历服务器代码:/sql/sql_parse.cc

bool do_command(THD * THD) {net_new_transaction(net);packet_length = my_net_read(净);包= (char *)净- > read_pos;命令= (enum enum_server_command) (uchar) packet[0];Dispatch_command (command,thd, packet+1, (uint) packet_length);/ / !}

现在您可能已经注意到,每当调用较低级的例程时,我们都会传递一个名为thd的参数,这是单词thread(我们认为)的缩写。这是我们决不能失去的重要背景。

my_net_read函数在另一个名为net_serv.cc的文件中。该函数从客户端获取一个包,解压缩它,然后去掉头。

完成此操作后,我们就得到了一个名为packet的多字节变量,其中包含客户机发送的内容。第一个字节很重要,因为它包含标识消息类型的代码。

我们将把它和包的其余部分传递给dispatch_command函数。

遍历服务器代码:/sql/sql_parse.cc

bool dispatch_command(enum enum_server_command command, THD * THD, char* packet, uint packet_length) {switch(命令){case COM_INIT_DB:…case COM_REGISTER_SLAVE:…case:…实例COM_CHANGE_USER:…mysql_stmt_execute(thd,packet);case COM_LONG_DATA:…mysql_stmt_prepare(thd, packet, packet_length);/ / !*/ default: send_error(thd, ER_UNKNOWN_COM_ERROR);打破; }

这是sql_parse.cc中一个很大的switch语句的一部分。这个代码片段没有足够的空间来展示其余的内容,但是当您查看dispatch_command函数时,您将看到在这里看到的语句之后还有更多的case语句。

将会有—我们现在进入列表模式,只列出switch语句中的其余项—用于准备、关闭语句、查询、退出、创建数据库、删除数据库、转储二进制日志、刷新、统计、获取进程信息、杀死进程、睡眠、连接和一些次要命令的代码。这是一个大连接点。

除了COM_EXECUTE和COM_PREPARE两种情况外,我们已经删除了所有情况的代码。

遍历服务器代码:/sql/sql_prepare.cc

我们不打算遵循COM_PREPARE的情况。相反,我们将遵循COM_EXECUTE之后的代码。但是我们必须暂时离开我们的主线,解释一下prepare的作用。

分配一个新的语句,将其保存在'thd->prepared statements'池中。返回给客户端参数的总数和结果集元数据信息(如果有)。

准备是在执行发生之前必须发生的步骤。它包括检查语法错误,查找语句中引用的任何表和列,以及设置供执行使用的表。一旦准备完成,执行就可以多次执行,而不必再次进行语法检查和表查找。

因为我们不打算遍述COM_PREPARE代码,所以我们决定在这里不显示它的代码。相反,我们剪切并粘贴了一些描述prepare的代码注释。我们在这里演示的是代码中有注释,所以当您更努力地查看prepare代码时,您将得到帮助。

遍历服务器代码:/sql/sql_parse.cc

bool dispatch_command(enum enum_server_command command, THD * THD, char* packet, uint packet_length) {switch(命令){case COM_INIT_DB:…case COM_REGISTER_SLAVE:…case:…实例COM_CHANGE_USER:…mysql_stmt_execute(thd,packet);/ / !case COM_LONG_DATA:…mysql_stmt_prepare(thd, packet, packet_length);*/ default: send_error(thd, ER_UNKNOWN_COM_ERROR);打破; }

让我们在sql_parse中再次回到大中心结点。抄送一会儿。在这段代码中需要注意的是,我们真正要遵循的行是COM_EXECUTE的情况。

遍历服务器代码:/sql/sql_prepare.cc

void mysql_stmt_execute(THD * THD, char *packet) {if (!(stmt=find_prepared_statement(thd, stmt_id, "execute"))) {send_error(thd);DBUG_VOID_RETURN;} init_stmt_execute(支撑);mysql_execute_command (thd);/ / !}

在本例中,我们所跟随的行是执行语句的行。

注意我们是如何一直携带THD线程和包的,并注意我们希望找到一个准备好的语句在等着我们,因为这是执行阶段。还要注意,我们继续使用以字母DBUG开头的与错误相关的函数,以供调试库使用。最后,请注意标识符“stmt”与ODBC用于等效对象的名称相同。我们尽量使用合适的标准名称。

遍历服务器代码:/sql/sql_parse.cc

void mysql_execute_command(THD * THD) switch (lex->sql_command) {case SQLCOM_SELECT:…SQLCOM_SHOW_ERRORS:…SQLCOM_CREATE_TABLE:…case SQLCOM_UPDATE:…SQLCOM_INSERT:…/ / !SQLCOM_DELETE:…SQLCOM_DROP_TABLE:…}

在mysql_execute_command函数中。我们遇到了另一个路口。switch语句中的一个项名为SQLCOM_INSERT。

遍历服务器代码:/sql/sql_parse.cc

{my_bool update=(lex->value_list. case SQLCOM_INSERT: {my_bool update=元素?Update_acl: 0);ulong privilege= (lex->duplicate == DUP_REPLACE ?删除acl: INSERT_ACL |;If (check_access(thd,privilege,tables->db,&tables->grant.privilege))If (grant_option && check_grant(thd,privilege,tables)) goto错误;如果(select_lex - > item_list。= lex->value_list.elements) {send_error(thd,ER_WRONG_VALUE_COUNT);DBUG_VOID_RETURN;} res = mysql_insert(thd,tables,lex->field_list,lex->many_values, select_lex->item_list, lex->value_list,(更新? DUP_UPDATE : lex->duplicates)); // ! if (thd->net.report_error) res= -1; break; }

对于这段代码,我们在mysql_execute_command函数中扩展了SQLCOM_INSERT案例周围的代码。首先要做的是检查用户是否具有向表中执行INSERT操作所需的适当权限,这是服务器通过调用check_access和check_grant函数进行检查的地方。遵循这些功能是很诱人的,但这些都是支线。相反,我们将遵循工作正在进行的路径。

浏览服务器代码:导航辅助

/sql目录下的一些程序名:

程序名SQL语句类型  ------------ ------------------ sql_delete。cc DELETE sql_do。cc DO sql_handler。cc HANDLER sql_help。cc HELP sql_insert。插入// !sql_load。cc LOAD sql_rename. cccc RENAME sql_select。cc SELECT sql_show。cc SHOW sql_update。cc更新

问:mysql_insert()在哪里?

接下来的这一行将把我们带到一个名为mysql_insert的例程。有时很难猜测一个例程将在什么程序中,因为MySQL没有一致的命名约定。但是,这里有一个导航辅助工具,它适用于某些语句类型。在sql目录中,一些程序的名称与语句类型对应。例如,INSERT就是这种情况。因此,mysql_insert程序将位于sql_insert.cc程序中。但是没有可靠的规则。

(让我们在这里添加几句关于标签“标签”程序的句子。当编辑器支持标签时(列表很长,但当然有vi和emacs),函数定义只需按一个键——不涉及猜测。在上面的例子中,vim用户可以在mysql_insert名称上按^],vim将打开sql_insert。Cc,并将光标放在mysql_insert()函数的第一行。标签的帮助在日常工作中是不可或缺的。)

遍历服务器代码:/sql/sql_insert.cc

int mysql_insert(THD * THD,TABLE_LIST * TABLE_LIST, List &fields, List &values_list, enum_duplduplic){表= open_ltable(THD,TABLE_LIST,lock_type);If (check_insert_fields(thd,table,fields,*values,1) || setup_tables(table_list) || setup_fields(thd,table_list,*values,0,0,0)) goto abort;fill_record(表- >字段,*值);错误= write_record(表、信息);/ / !Query_cache_invalidate3 (thd, table_list, 1);If (transactional_table) error=ha_autocommit_or_rollback(thd,error);Query_cache_invalidate3 (thd, table_list, 1);官mysql_unlock_tables (thd) - >锁);}

对于mysql_insert例程,我们将只读取代码片段中的内容。我们在这里要强调的是函数名和变量名几乎都是英文的。

好的,我们先打开一张表。然后,如果INSERT中的字段检查失败,或者设置表的尝试失败,或者设置字段的尝试失败,我们将中止。

接下来,我们将用值填充记录缓冲区。然后我们写唱片。然后我们将使查询缓存失效。顺便说一下,请记住,MySQL将经常使用的选择语句和结果集作为优化存储在内存中,但是一旦插入成功,存储的结果集就无效了。最后,我们将解锁表。

遍历服务器代码:/sql/sql_insert.cc

int write_record(TABLE * TABLE,COPY_INFO *info) {TABLE ->file->write_row(TABLE ->record[0];/ / !}

你可以从我们的记号笔上看到,我们要沿着含有“write row”字样的那一行写。但这并不是一个普通的函数调用,因此在没有调试器的帮助下只读代码的人很容易错过这里执行行的下一个点。事实上,'write_row'可以把我们带到几个不同的地方之一。

遍历服务器代码:/sql/handler.h

/*表类型的处理器。将包含在表结构*/ handler(TABLE *table_arg):表(table_arg),active_index(MAX_REF_PARTS), ref(0),ref_length(sizeof(my_off_t)), block_size(0),records(0),deleted(0), data_file_length(0), max_data_file_length(0), index_file_length(0), delete_length(0), auto_increment_value(0), raid_type(0), key_used_on_scan(MAX_KEY), create_time(0), check_time(0), update_time(0), mean_rec_length(0), ft_handler(0){}…虚拟int write_row(byte * buf)=0;

要了解write_row语句在做什么,我们必须查看其中一个包含文件。在sql目录的handler.h中,我们发现write_row与表的处理程序相关联。这个定义告诉我们write_row中的地址会变化,它会在运行时被填充。事实上,有几种可能的地址。

每个处理程序都有一个地址。在我们的例子中,由于我们使用默认值,此时的值将是MyISAM处理程序中write_row的地址。

遍历服务器代码:/sql/ha_myisam.cc

int ha_myisam::write_row(byte * buf) {statistic_increment(ha_write_count,&LOCK_status);/*如果我们有一个时间戳列,更新到当前时间*/ If (table->time_stamp) update_timestamp(buf+table->time_stamp-1);/*如果我们有一个auto_increment列,并且我们正在写入更改的行或新行,那么更新记录中的auto_increment值。*/ if (table->next_number_field && buf == table->record[0]) update_auto_increment();返回mi_write(文件、buf);/ / !}

这就把我们带到了ha_myisam中的write_row。cc程序。还记得我们告诉过您,这些以字母ha开头的程序是到处理程序的接口,而这个是到myisam处理程序的接口。我们终于可以在处理程序包中调用一些东西了。

遍历服务器代码:/myisam/mi_write.c

int mi_write(MI_INFO *info, byte *record) {_mi_readinfo(info,F_WRLCK,1);_mi_mark_file_changed(信息);计算并检查所有唯一约束*/ for (i=0;I < share->state.header。暗金物品;i++) {mi_check_unique(info,share->uniqueinfo+i,record, mi_unique_hash(share->uniqueinfo+i,record), HA_OFFSET_ERROR);}……下一个片段继续

注意,此时不再引用表,注释是关于文件和索引键的。我们终于到达了最底层。还要注意,我们现在是在一个C程序中,而不是c++程序。

在mi_write函数的前半部分,我们看到一个明显注释的调用。这是检查唯一性的地方(不是UNIQUE约束,而是内部问题)。

遍历服务器代码:/myisam/mi_write.c

...(i=0;I < share->base。键;I ++) {share->keyinfo[I]。Ck_insert (info,i,buff, _mi_make_key(info,i,buff,record,filepos)} (*share->write_record)(info,record);If (share->base.auto_key) update_auto_increment(info,record);}

在mi_write函数的后半部分,我们看到了另一个明确的注释,大意是这是为任何索引列创建新键的地方。然后我们看到最后20个片段所准备的一切的高潮,我们所有人都在等待的时刻,唱片的写作。

而且,由于INSERT语句的对象最终是导致对文件中的记录进行写操作,所以就这样了。服务器已经完成了工作。

遍历服务器代码:堆栈跟踪

Main在/sql/mysqld中。Cc /sql/mysqld中的handle_connections_sockets。在/sql/mysqld中Cc create_new_thread。Cc handle_one_connection在/sql/sql_parse中。Cc do_command在/sql/sql_parse中。/sql/sql_parse. Cc dispatch_command。Cc mysql_stmt_execute在/sql/sql_prepare中。在/sql/sql_parse. Cc mysql_execute_command。Cc mysql_insert在/sql/mysql_insert中。在/sql/mysql_insert中写入记录。Cc ha_myisam::write_row in /sql/ha_myisam。Cc mi_write in /myisam/mi_write.c

现在让我们来看看堆栈上面是什么,或者至少让我们了解一下我们是如何到达这里的。我们从mysqld.cc中的主程序开始。我们继续为客户端创建线程、确定前进方向的几个连接进程、解析和SQL语句的初始执行、决定调用MyISAM处理程序以及写行。我们在一个低级的地方结束,在那里我们调用写文件的例程。这是我们今天应该达到的最低价格了。

当然,服务器程序会连续多次返回,向客户端发送一个包,说“ok”,最后返回handle_one_connection函数内部的循环中。

相反,我们将暂停片刻,对我们刚刚跳过的大量代码感到敬畏。这将结束我们对服务器代码的研究。

CREATE TABLE Table1 (column1 CHAR(1), column2 CHAR(1), column3 CHAR(1));INSERT INTO Table1 VALUES ('a', 'b', 'c');INSERT INTO Table1 VALUES ('d', NULL, 'e');F1 61 62 63 00 F5 64 00 66 600 ... .abc..d e。

继续我们的虫眼视图,让我们看一下MyISAM文件中记录的结构。

这个图上的SQL语句显示了一个表定义和一些我们用来填充表的插入语句。

图形上的最后一行是我们最终得到的两条记录的十六进制转储显示,它们来自Table1的MyISAM文件。

这里要注意的是,记录的存储非常紧凑。在每个记录的开始处有一个字节,F1用于第一个记录,F5用于第二个记录,其中包含位列表。

当一个位是on时,这意味着它对应的字段是NULL。这就是为什么第二行(在第二列或字段中有一个NULL)具有与第一行不同的头字节。

复杂的情况是可能的,但一个简单的记录确实看起来如此简单。

图形:一块InnoDB文件19 17 15 13 0C 06字段开始偏移量/*第一行*/ 00 00 78 0D 02 BF额外字节00 00 00 00 00 04 21系统列#1 00 00 00 00 00 00 09 2A系统列#2 80 00 00 00 00 2D 00 84系统列#3 50 50 Field1 'PP' 50 50 Field2 'PP' 50 50 Field3 'PP'

另一方面,如果你看一个InnoDB文件,你会发现它的存储更复杂。详细内容在本文档的其他地方。但这里有一个介绍性的介绍。

这里的标头以偏移量开始,不像MyISAM,它没有偏移量。所以你必须先通过第一列才能到达第二列。

然后有一个固定的头额外的字节。

然后是正式的唱片。典型记录的第一个字段包含用户看不到的信息,如行ID、事务ID和回滚指针。如果用户在CREATE TABLE语句期间定义了一个主键,这部分看起来就会不同。

最后是列内容片段末尾的一串Ps。你可以看到InnoDB做了更多的管理工作。

InnoDB最近有了一些变化;上面所看到的是5.0版本之前的数据库。

图形:报文头行数ID状态长度消息内容

我们最后对物理结构的观察将是对包的观察。

所谓包,我们的意思是:客户端通过tcp/ip线发送给服务器的消息的格式是什么,服务器发回什么?

这里我们没有显示转储。如果您想查看包内容的十六进制转储,本文档提供了大量此类转储。我们要注意的是,典型的消息将有一个标题、一个标识符和一个长度,后面是消息内容。

不可否认,这并没有遵循像ISO的RDA或IBM的DRDA这样的标准,但它是有文档记录的,所以如果您想要编写自己的type 4 JDBC驱动程序,这里就有了您需要的东西。(当然,受许可证限制。)但关于最后一点,我有一个建议:这已经被做过了。最初是Mark Matthews写的,都在“MySQL Connector/J”中。