之前寫了一個 禁止創建 MyISAM 表的插件(blog),有報 issue,在 MySQL 重啟後 uninstall plugin 會引發 crash。
測試了一下果然可以復現。掛上 gdb 後,backstace 如下:
(gdb) bt
#0 0x00000000006f7658 in column_bitmaps_set (write_set_arg=0x189ad98, read_set_arg=0x189ad98, this=0x7fc3a800fa60)
at /root/mysql-5.6.24-tp/sql/table.h:1228
#1 use_all_columns (this=0x7fc3a800fa60) at /root/mysql-5.6.24-tp/sql/table.h:1238
#2 mysql_uninstall_plugin (thd=thd@entry=0x1d1b030, name=0x1d1d858) at /root/mysql-5.6.24-tp/sql/sql_plugin.cc:2077
#3 0x00000000006e8daf in mysql_execute_command (thd=thd@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:4910
#4 0x00000000006ed9d8 in mysql_parse (thd=thd@entry=0x1d1b030, rawbuf=, length=,
parser_state=parser_state@entry=0x7fc3c61b02f0) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:6391
#5 0x00000000006ef1cd in dispatch_command (command=COM_QUERY, thd=0x1d1b030, packet=, packet_length=)
at /root/mysql-5.6.24-tp/sql/sql_parse.cc:1340
#6 0x00000000006f0f24 in do_command (thd=) at /root/mysql-5.6.24-tp/sql/sql_parse.cc:1037
#7 0x00000000006bd662 in do_handle_one_connection (thd_arg=thd_arg@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_connect.cc:982
#8 0x00000000006bd710 in handle_one_connection (arg=arg@entry=0x1d1b030) at /root/mysql-5.6.24-tp/sql/sql_connect.cc:898
#9 0x000000000095dea3 in pfs_spawn_thread (arg=0x1dbb410) at /root/mysql-5.6.24-tp/storage/perfschema/pfs.cc:1860
#10 0x00007fc3f7258182 in start_thread (arg=0x7fc3c61b1700) at pthread_create.c:312
#11 0x00007fc3f676530d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111
進一步追蹤這個 column_bitmaps_set 的調用
inline void column_bitmaps_set(MY_BITMAP *read_set_arg,
MY_BITMAP *write_set_arg)
{
read_set= read_set_arg;
write_set= write_set_arg;
if (file && created)
file->column_bitmaps_signal();
}
發現是在 file->column_bitmaps_signal 這一步出錯,反匯編以後,也確實是在 callq 指令處。注意到反匯編後,gdb 在 callq 指令所調用函數的名字沒有能顯示出來,說明這個地址很可能已經無效。這裡 file 成員變量即是插件生成的替換了 create 操作的 ha_mysiam 的 wrapper。打印其內容發現 _vptr.handler = 0x7fc3d44768d0 ,這裡也未能指出指向的類名。
閱讀了 sql_handler.cc 中 mysql_uninstall_plugin 的代碼後,明白了原因。卸載插件會刪除 mysql.plugin 表中的相應記錄。在 mysql_uninstall_plugin 中,先打開了 mysql.plugin 表,然後卸載插件,然後刪除相應記錄。問題就出在 plugin 表也是一個 MyISAM 表。如果打開的時候插件是啟用的,那麼其對應的 handler 是插件中的 wrapper。而插件卸載以後,對應地址的代碼已經不復存在,因此就造成了 crash。那麼既然是這個原因,又想到了,如果在插件加載期間打開過表,卸載插件後訪問這些表應該也會出錯。試了一下果然如此,一陣冷汗。
接著就是考慮如何解決這個問題。既然插件卸載後無法內存,那就需要在卸載時將原有的指向 wrapper 的 handler 都換回原有的 ha_myisam。handler 本身的內存是由 mysql 管理的,並不會因為插件卸載而釋放。所以可以修改。wrapper 是 ha_myisam 的派生類,而且沒有增加成員,因此內存布局和 ha_myisam 是完全相同的。唯一差異就在虛函數表。C++ 似乎沒有這種強制轉換到父類的語法。於是就考慮直接替換虛函數表指針。而 C++ 虛函數指針中總是對象最前面。於是直接建一個真的 ha_myisam 對象,然後把它的虛函數指針復制回去。試下來果然可以。接著就是在 wrapper 的構造和析構函數中維持一個當前現有 handler 的集合,用於在卸載時處理即可