mysql分析(一)源代码安装及简单调试
一、源代码安装
安装方式有两种,一种是二进制文件的安装,另一种是源代码级别安装。不同的二进制安装只是将同一份源代码在不同的机器上编译并且按照指定的运行格式进行尽量简单的配置,所以二进制安装的发行包命名中包含有操作系统的信息,源代码发行版本中中只有版本信息,不包含OS信息。在下载的5.5.28版本自带的INSTALL-SOURCE文件中的一些摘录内容
Standard distributions are available as compressed tar files,
Zip archives, or RPM packages. Distribution files have names
of the form mysql-VERSION.tar.gz, mysql-VERSION.zip, or
mysql-VERSION.rpm, where VERSION is a number like 5.5.29. File
names for source distributions can be distinguished from those
for precompiled binary distributions in that source
distribution names are generic and include no platform name,
whereas binary distribution names include a platform name
indicating the type of system for which the distribution is
intended (for example, pc-linux-i686 or winx64).
在mysql的官方网站中下载mysql源代码
http://dev.mysql.com/downloads/mysql/#downloads
其中选择最新的源代码文件包下载即可。
之后的安装在网上都可以搜索到对应的安装操作手册,虽然比较麻烦,但是有兴趣的同学可以尝试一下。对于我们调试来说,希望在编译的任何过程中都禁止优化功能,也就是通常在编译的时候可以看到的-O2选项。mysql的安装需要依赖cmake,这个在redhat的发型版本中默认是没有安装的,其中的c代表Crossing,也就是可以在不同的平台上编译。可能是mysql为了保证在所有的平台上都可以顺利编译,所以从一开始就选择了一个可以跨平台的编译工具。这些都不是问题,主要是这个工具估计熟悉的人不多,而我也没有兴趣再看他的用法和实现。只是在它的安装手册中说明了一下如何编译出一个DEBUG版本的mysql环境。可惜在看到这个文档之前,我已经通过其它简单粗暴的方法实现了删除优化选项,这个自带的选项也就没有机会尝试一下。在mysql自带的INSTALL-SOURCE文件中,其中关于编译debug版本的说明
* -DCMAKE_BUILD_TYPE=type
The type of build to produce:
+ RelWithDebInfo: Enable optimizations and generate
debugging information. This is the default MySQL build
type.
+ Debug: Disable optimizations and generate debugging
information. This build type is also used if the
WITH_DEBUG option is enabled. That is, -DWITH_DEBUG=1 has
the same effect as -DCMAKE_BUILD_TYPE=Debug.
我使用的简单粗暴、不科学但是能行的方法就是搜索整个文件下所有的文件,将所有文件中出现的-O2字符串全部删除,使用的命令为
grep -rl -e '-O2' . | xargs sed -i "s/-O2//g"
说它不科学是因为这并不能保证删除的都是优化编译选项,可能有其它的文本内容碰巧是这种格式,但是自己测试的时候暂时没有出现什么异常。
二、mysql客户端连接时选项
mysql只是一个和mysqld通讯的独立进程,它同样遵守mysqld的通讯协议,但是它功能更加专一,因此也更加专业。在一个系统里,例如我自己的调试环境,启动mysql的时候我没有加任何参数,这样mysql如何知道以什么样的方式连接服务器进程mysqld呢?
在可以调试的情况下,这写都不是问题,无论是通过TCP套接口,还是通过Unix socket,它们和服务器的连接都必须经过connect系统调用,所以在该函数设置一个断点,立刻可以看到它执行的过程。
(gdb) bt
#0 0x00399130 in connect () from /lib/libpthread.so.0
#1 0x080645d0 in my_connect (fd=8, name=0xbfffed96, namelen=110, timeout=0)
at /home/tsecer/Downloads/mysql-5.5.28/sql-common/client.c:168
#2 0x08067879 in mysql_real_connect (mysql=0x833bc60,
host=0x80babb7 "localhost", user=0x80ba6a5 "", passwd=0x80ba6a5 "", db=0x0,
port=3306, unix_socket=0x8343330 "/tmp/mysql.sock", client_flag=66560)
at /home/tsecer/Downloads/mysql-5.5.28/sql-common/client.c:3163
#3 0x080562f6 in sql_real_connect (host=0x0, database=0x0, user=0x0,
password=0x0, silent=0)
at /home/tsecer/Downloads/mysql-5.5.28/client/mysql.cc:4345
#4 0x080563f9 in sql_connect (host=0x0, database=0x0, user=0x0, password=0x0,
silent=0) at /home/tsecer/Downloads/mysql-5.5.28/client/mysql.cc:4383
#5 0x0804f612 in main (argc=4, argv=0x8343220)
at /home/tsecer/Downloads/mysql-5.5.28/client/mysql.cc:1152
这个地方有一个比较诡异的地方,因为我启动mysql的时候没有添加任何命令行选项,这也就意味着在main函数中看到的argc应该等于1,但是这里的数量却是等于4。虽然不太常见,但是完全合法的做法就是在main函数中修改main函数的各个参数,mysql就是这么做的,在整个函数的最开始
if (load_defaults("my",load_default_groups,&argc,&argv))
{
my_end(0);
exit(1);
}
这里通过load_defaults修改了mysql启动时的参数内容。它的功能就是搜索系统中存在的mysql的配置文件,配置文件的名称是确定my,不确定的是文件存放的位置以及文件的后缀,函数init_default_directories完成了对这个缺省文件夹的搜索,这个比较重要的位置就是
errors += add_directory(alloc, "/etc/", dirs);
errors += add_directory(alloc, "/etc/mysql/", dirs);
……
if ((env= getenv("MYSQL_HOME")))
errors += add_directory(alloc, env, dirs);
……
errors += add_directory(alloc, "~/", dirs);
其中的一些变量的调试态数值配置为:
(gdb) p argv[0]
$17 = 0xbffff58e "/usr/local/mysql/bin/mysql"
(gdb) p argv[1]
$18 = 0x0
(gdb) p argv[2]
$19 = 0x83431c8 "--socket=/tmp/mysql.sock"
(gdb) p argv[3]
$20 = 0x83431e8 "--no-auto-rehash"
(gdb) p load_default_groups
$21 = {0x80b73aa "mysql", 0x80b73b0 "client", 0x0}
(gdb)
也就是说,mysql会在启动的时候将my.cnf中所有的mysql和client节中的键值都作为选项添加到自己启动的命令行中。
我的/etc/my.cnf文件内容
18 [client]
19 #password = your_password
20 port = 3306
21 socket = /tmp/mysql.sock
……
132 [mysql]
133 no-auto-rehash
134 # Remove the next comment character if you are not familiar with SQL
135 #safe-updates
三、什么时候使用TCP连接
在真正的连接函数CLI_MYSQL_REAL_CONNECT中,执行操作
#if defined(HAVE_SYS_UN_H)
if (!net->vio &&
(!mysql->options.protocol ||
mysql->options.protocol == MYSQL_PROTOCOL_SOCKET) &&
(unix_socket || mysql_unix_port) &&
(!host || !strcmp(host,LOCAL_HOST)))
{
可以看到,这里其关键作用的是命令行中的host选项,如果没有指定host或者host指定的是localhost,那么此时是一定会通过本地的unix socket连接的。这里顺便科普一下,unix socket是只能在计算机内部生效的一种套接字,它的地址使用的是和文件系统相同的路径名。默认变量mysql_unix_port的初始化在main-->>mysql_server_init函数中完成初始化,再进一步,两个变量的值在mysql_version.cmake中动态配置,如果没有指定,使用默认路径,也就是tmp文件夹下的mysql.sock文件
IF(NOT MYSQL_UNIX_ADDR)
SET(MYSQL_UNIX_ADDR "/tmp/mysql.sock")
ENDIF()
这里顺便粘贴一下我编译生成的配置文件主要信息,文件路径在mysql-5.5.28/include/mysql_version.h
11 #define PROTOCOL_VERSION 10
12 #define MYSQL_SERVER_VERSION "5.5.28"
13 #define MYSQL_BASE_VERSION "mysqld-5.5"
14 #define MYSQL_SERVER_SUFFIX_DEF ""
15 #define FRM_VER 6
16 #define MYSQL_VERSION_ID 50528
17 #define MYSQL_PORT 3306
18 #define MYSQL_PORT_DEFAULT 0
19 #define MYSQL_UNIX_ADDR "/tmp/mysql.sock"
20 #define MYSQL_CONFIG_NAME "my"
21 #define MYSQL_COMPILATION_COMMENT "Source distribution"
四、何时‘;’是必须的
简单执行一下mysql的help命令,可以发现输出的内容并不多,但是help select却同样可以拉取很多的帮助信息。这里的区别就在于help打印的只是mysql这个程序内置的帮助命令。所谓内置,是指区别于mysqld中通过自己数据库中存放的信息。这些help的命令只有在mysql这个命令解释器中可以使用,对应地,help select这个帮助却可以在所有的连接上了mysqld的程序中都可以使用。默认连接上mysql,通过 show databases命令可以看到其中有一个mysql,
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| test |
+--------------------+
4 rows in set (0.47 sec)
mysql> use mysql
Database changed
mysql> show tables;
+---------------------------+
| Tables_in_mysql |
+---------------------------+
| columns_priv |
| db |
| event |
| func |
| general_log |
| help_category |
| help_keyword |
| help_relation |
| help_topic |
| host |
| ndb_binlog_index |
| plugin |
| proc |
| procs_priv |
| proxies_priv |
| servers |
| slow_log |
| tables_priv |
| time_zone |
| time_zone_leap_second |
| time_zone_name |
| time_zone_transition |
| time_zone_transition_type |
| user |
+---------------------------+
24 rows in set (0.00 sec)
mysql> select * from help_keyword limit 1;
+-----------------+------+
| help_keyword_id | name |
+-----------------+------+
| 0 | JOIN |
+-----------------+------+
1 row in set (0.35 sec)
mysql>
对于mysql来说,它对于这个处理也相对比较简单,在read_and_execute中,通过readline工具读入用户的输入,在用户执行的每一次回车之后,readline就会返回,返回之后如何判断一个命令结束有下面逻辑
if ((named_cmds || glob_buffer.is_empty())
&& !ml_comment && !in_string && (com=find_command(line,0)))
在find_command中,其中最为关键的逻辑判断为
for (uint i= 0; commands[i].name; i++)
{
if (commands[i].func &&
也就是说,这个定义的内容必须是有func函数实现,观察mysql.cc中定义的commands变量,其中只有help显示的命令才有func定义,其它的SELECT等均没有实现该成员。
static COMMANDS commands[] = {
{ "?", '?', com_help, 1, "Synonym for `help'." },
{ "clear", 'c', com_clear, 0, "Clear the current input statement."},
{ "connect",'r', com_connect,1,
"Reconnect to the server. Optional arguments are db and host." },
{ "delimiter", 'd', com_delimiter, 1,
"Set statement delimiter." },
#ifdef USE_POPEN
{ "edit", 'e', com_edit, 0, "Edit command with $EDITOR."},
#endif
{ "ego", 'G', com_ego, 0,
"Send command to mysql server, display result vertically."},
{ "exit", 'q', com_quit, 0, "Exit mysql. Same as quit."},
{ "go", 'g', com_go, 0, "Send command to mysql server." },
{ "help", 'h', com_help, 1, "Display this help." },
#ifdef USE_POPEN
{ "nopager",'n', com_nopager,0, "Disable pager, print to stdout." },
#endif
{ "notee", 't', com_notee, 0, "Don't write into outfile." },
#ifdef USE_POPEN
{ "pager", 'P', com_pager, 1,
"Set PAGER [to_pager]. Print the query results via PAGER." },
#endif
{ "print", 'p', com_print, 0, "Print current command." },
{ "prompt", 'R', com_prompt, 1, "Change your mysql prompt."},
{ "quit", 'q', com_quit, 0, "Quit mysql." },
{ "rehash", '#', com_rehash, 0, "Rebuild completion hash." },
{ "source", '.', com_source, 1,
"Execute an SQL script file. Takes a file name as an argument."},
{ "status", 's', com_status, 0, "Get status information from the server."},
#ifdef USE_POPEN
{ "system", '!', com_shell, 1, "Execute a system shell command."},
#endif
{ "tee", 'T', com_tee, 1,
"Set outfile [to_outfile]. Append everything into given outfile." },
{ "use", 'u', com_use, 1,
"Use another database. Takes database name as argument." },
{ "charset", 'C', com_charset, 1,
"Switch to another charset. Might be needed for processing binlog with multi-byte charsets." },
{ "warnings", 'W', com_warnings, 0,
"Show warnings after every statement." },
{ "nowarning", 'w', com_nowarnings, 0,
"Don't show warnings after every statement." },
/* Get bash-like expansion for some commands */
{ "create table", 0, 0, 0, ""},
{ "create database", 0, 0, 0, ""},
如果一个readline返回的时候,没有从commands中找到其字符内容,那么此时就需要等到一个delimiter来表示一个输入的结束,在add_line函数中:
else if (!*ml_comment && !*in_string && is_prefix(pos, delimiter))
{
……
if ((com= find_command(buffer.c_ptr(), 0)))
{
if ((*com->func)(&buffer, buffer.c_ptr()) > 0)
DBUG_RETURN(1); // Quit
}
else
{
if (com_go(&buffer, 0) > 0) // < 0 is not fatal 这个地方是真正的向服务器发送请求字符串的命令,这个也是之前通过help显示的go命令的执行内容。
DBUG_RETURN(1);
}
buffer.length(0);
……
}
所以说deLimiter是一个字符串,而不仅可以是一个字符。在mysql中可以通过delimiter命令来设置这个分隔符。
五、为什么commands中有select之类的命令
既然在本地命令查找的时候只会从func有定义的表项中查找,那为什么还要把那些select之类的命令列举上来呢?好在作者有注释,是为了保证可以进行和bash中类似命令行的展开。我试了一下,这个功能并不可行,但是会想到之前在my.cnf中出现的
132 [mysql]
133 no-auto-rehash
134 # Remove the next comment character if you are not familiar with SQL
135 #safe-updates
觉得可能和这个有关,那么为什么默认没有打开这个选项呢?可能是为了节省物理内存的使用量?不过在mysql启动的时候可以通过--auto-rehash 选项来使能该功能,并且在help输出中有rehash选项,所以手动执行一下这个命令也可以打开自动补齐功能。
六、终端和非终端输出格式的不同
使用过mysql的同学可能会知道,通过mysql中执行命令和在mysql执行之后输出到另一个位置的时候,输出的格式是不同的。输出在终端上的内容会比较丰富一些,比较适合操作人员来阅读,反过来如果不是,则格式比较简单。
看一下在mysql中内容
mysql> select * from help_keyword limit 1;
+-----------------+------+
| help_keyword_id | name |
+-----------------+------+
| 0 | JOIN |
+-----------------+------+
1 row in set (0.00 sec)
如果把这个命令的输出重定向到一个管道中,显示的格式会自动变化
[root@Harry mysql]# ./bin/mysql -e "use mysql;select * from help_keyword limit 1;" | more
help_keyword_id name
0 JOIN
可以看到这个格式是相对比较简洁的,没有各种减号和加号的区分。
在mysql的main函数开始,这个判断执行的内容为
if (!isatty(0) || !isatty(1))
{
status.batch=1; opt_silent=1;
ignore_errors=0;
}
else
status.add_to_history=1;
如果mysql的标准输入或者标准输入两个文件描述符中只要有一个不是终端设备,那么就启用silent模式,这个模式将会影响到query内容的输出。在执行命令的com_go函数中,其操作为:
{
init_pager();
if (opt_html)
print_table_data_html(result);
else if (opt_xml)
print_table_data_xml(result);
else if (vertical || (auto_vertical_output && (terminal_width < get_result_width(result))))
print_table_data_vertically(result);
else if (opt_silent && verbose <= 2 && !output_tables)
print_tab_data(result);
else
print_table_data(result);
sprintf(buff,"%ld %s in set",
(long) mysql_num_rows(result),
(long) mysql_num_rows(result) == 1 ? "row" : "rows");
end_pager();
if (mysql_errno(&mysql))
error= put_error(&mysql);
}
有兴趣的同学可以看一下两个函数的区别,大致的意思就是table的输出中会加上加号和减号的格式化点缀,而对于tab输出,各个字段只是以简单的'\t'来分隔,这样便于自动化处理工具处理和定位。
七、pager命令的作用
在一些数据库中,可能我们查看的内容非常多,此时如果执行select就会一闪而过,这样大家就会想念起可以使用more进行分屏的功能。好在mysql也提供了这样的功能,就是那个诡异而不起眼的pager命令。这个命令很容易和操作系统中的分页管理功能混淆,因为sqlite中也有pager的说法,所以这个名字还是有一定欺骗性的。在mysql的代码中可以看到这个命令的意义,在
static void init_pager()
{
#ifdef USE_POPEN
if (!opt_nopager)
{
if (!(PAGER= popen(pager, "w")))
{
tee_fprintf(stdout, "popen() failed! defaulting PAGER to stdout!\n");
PAGER= stdout;
}
}
else
#endif
PAGER= stdout;
}
man手册中关于popen的说明
The popen() function opens a process by creating a pipe, forking, and invoking
the shell. Since a pipe is by definition unidirectional, the type argument may
specify only reading or writing, not both; the resulting stream is correspond-
ingly read-only or write-only.
mysql> pager more
PAGER set to 'more'
mysql> select * from help_keyword;
+-----------------+-------------------------------+
| help_keyword_id | name |
+-----------------+-------------------------------+
| 0 | JOIN |
| 1 | HOST |
| 2 | REPEAT |
| 3 | SERIALIZABLE |
| 4 | REPLACE |
| 5 | AT |
| 6 | SCHEDULE |
| 7 | RETURNS |
| 8 | STARTS |
| 9 | MASTER_SSL_CA |
| 10 | NCHAR |
| 11 | COLUMNS |
| 12 | COMPLETION |
| 13 | WORK |
| 14 | DATETIME |
| 15 | MODE |
| 16 | OPEN |
| 17 | INTEGER |
| 18 | ESCAPE |
| 19 | VALUE |
--More--
此时程序会暂时停止,等待用户输入空格键继续。如果要禁止掉该功能,可以使用nopager禁用。这里注意的是table还是tab格式输出只在main函数开始的时候判断一次,此处虽然标准输出变化为管道,但是格式并没有变化。