What this post covers
This is the third post in the "Digging into MariaDB's Source" series. In part 2 we drew the map: the server layer (sql/) and the storage engines (storage/) are separated by the handler/handlerton boundary, and one query travels connection → THD (the object holding all the state of one connection) → parsing → preparation → optimization → execution → handler → engine. This post zooms into the very front of the part 2 map — the area before any query arrives: from starting mariadbd until ready for connections. shows up in the error log, and from mariadb-admin shutdown until the process exits. We also look at the moment the bottom box of the map, the engine, gets attached to the server (the moment its handlerton is filled in).
The blue boxes are this post's scope. The startup and plugins box, gray on the part 2 map, is the one we zoom into this time. The gray ones are zoomed into one by one from part 4 on.
There are four questions to answer.
- When you start
mariadbd, what gets ready in what order, and why that order? - Where in the source do a line in
my.cnf, a--max-connections=200on the command line and aSET GLOBALall reach the same variable? - InnoDB is said to be a "plugin" — what is a plugin, and how does the server find and initialize one?
- Where does an engine fill in its
handlerton, and in what order is everything cleaned up at shutdown?
| Item | Value |
|---|---|
| Project | MariaDB Server |
| Version | 13.2.0 (main, alpha) |
| Commit | c894d97781 (2026-10-19) |
| Main files | sql/main.cc, sql/mysqld.cc, sql/sql_plugin.cc, sql/handler.cc, sql/set_var.cc, sql/sys_vars.cc, include/mysql/plugin.h, storage/innobase/handler/ha_innodb.cc |
All line numbers in this post refer to this commit. How to get the same source and build the Docker image for the examples is in part 1. sql/mysqld.cc is 10,607 lines and sql/sql_plugin.cc is 4,646. This post follows only the main path of startup and plugins. Everything left out is listed by name and location in "What this post leaves out" at the end.
Terms to know first
mysqld_main() is the server's real main. The main() in sql/main.cc does nothing but call it, because the server body (almost every file in sql/) is built into a library and only main.cc becomes the executable on its own. The executable is called mariadbd, but the source files and function names keep the old name mysqld.
Option groups and load_defaults. my.cnf is split into bracketed groups such as [mysqld], [mariadbd] and [client-server]. load_defaults is a mysys/ function that collects the lines of the groups the server reads, turns them into command-line arguments (argv) of the form --socket=/tmp/mariadb.sock, and inserts them before the real command line. That is why, inside the server, the config file and the command line are read by the same code (handle_options()).
System variables (sys_var) are the things SHOW VARIABLES lists, such as max_connections and innodb_buffer_pool_size. The server's own ones are declared one by one in sql/sys_vars.cc as global objects of classes like Sys_var_ulong and Sys_var_enum; a plugin's ones are created when the plugin is loaded. One variable is one command-line option (section 2).
A plugin is a part you can plug in without changing the server executable. Storage engines, information_schema tables, authentication methods, audit logs, data types and SQL functions are all plugins. A plugin introduces itself with a single struct, st_maria_plugin. There are built-in (static) plugins linked into mariadbd, and dynamic plugins that live in .so files until INSTALL SONAME or --plugin-load-add loads them.
handlerton, as we saw in part 2, is a struct with one instance per storage engine — a table of function pointers such as the engine's commit, rollback and create (which makes the handler object that opens a table). In this post we look at who creates that table and who fills it. Since an engine is a plugin too, "initializing an engine plugin" means "creating a handlerton and letting the engine fill it".
The error log is the file set with log-error (/tmp/mariadbd.err in this series' examples). Despite the name, it records not only errors but also startup and shutdown progress as [Note] lines. In this post we connect its lines, one by one, to places in the source.
1. From main() to the first connection — the order at a glance
main() really is a one-liner.
int main(int argc, char **argv)
{
#ifdef _WIN32
return mysqld_win_main(argc, argv);
#else
return mysqld_main(argc, argv);
#endif
}
mysqld_main() is lines 6181–6634 of sql/mysqld.cc, about 450 lines. Let's see the order as a picture first.
Blue boxes are preparation steps; the main thread stays in the yellow box for the whole life of the server.
Here is the middle section in the source, starting where the engines are switched on, after init_common_variables() (line 6354) and init_signals() (line 6357).
if (init_server_components())
unireg_abort(1);
init_ssl();
network_init();
// ... (WSREP recovery block omitted)
/*
init signals
After this we can't quit by a simple unireg_abort
*/
start_signal_handler(); // Creates pidfile
if (mysql_rm_tmp_tables() || acl_init(opt_noacl) ||
my_tz_init((THD *)0, default_tz_name, opt_bootstrap))
unireg_abort(1);
if (!opt_noacl)
(void) grant_init();
udf_init();
unireg_abort(1) means "give up on startup and exit" (where [ERROR] Aborting appears in the error log). The order we can read here:
init_server_components()opens the error log file (lines 5266–5299), creates server parts such as metadata locking and the table definition cache, and initializes every plugin withplugin_init(). InnoDB's buffer pool allocation and crash recovery finish in here (sections 4 and 5).init_ssl(),network_init()prepare TLS andbind()/listen()on port 3306 and the Unix socket.start_signal_handler()starts a dedicated thread that receivesSIGTERMandSIGHUP(section 6). As the comment says, from here on you can no longer simply quit withunireg_abort().acl_init(),grant_init()read the privilege tables (mysql.global_privand others) into memory, which part 7 uses. Thenudf_init()andEvents::init()(line 6477) run.
After that, lines 6543–6560 log ready for connections., and run_main_loop() at line 6590 enters poll() in handle_connections_sockets(). When a stop request makes that function return, cleanup starts at line 6605 (section 7).
Example 1: watching the startup order in the error log
All examples in this post were run with MariaDB 13.2.0 (13.2.0-MariaDB-debug), a Debug build of the base commit (c894d97781) as is, in a Docker container (Ubuntu 24.04). We create an empty data directory, start the server and print the error log as it is. The helpers lab-init, lab-start and lab-stop inside the container are explained in part 1.
# Example written for this post (not part of the MariaDB source)
set -e
lab-init
lab-start
echo "===== 1. error log right after startup ====="
cat /tmp/mariadbd.err
echo "===== 2. TLS is on, with a certificate made at startup ====="
mariadb -t -e "SELECT @@have_ssl, @@ssl_cert, @@ssl_key;
SHOW GLOBAL STATUS WHERE Variable_name IN ('Ssl_server_not_before','Ssl_server_not_after')"
echo "===== 3. shutdown with mariadb-admin (COM_SHUTDOWN) ====="
lab-stop
sed -n '/Normal shutdown/,$p' /tmp/mariadbd.err
echo "===== 4. shutdown with kill -TERM (signal_hand thread) ====="
lab-start
kill -TERM "$(cat /tmp/mariadbd.pid)"
while [ -e /tmp/mariadbd.pid ] && kill -0 "$(cat /tmp/mariadbd.pid)" 2>/dev/null; do sleep 0.2; done
sed -n '/Normal shutdown/,$p' /tmp/mariadbd.err
[Example ex/u03-startup-log/run.sh]
The output of the first group. A few [Note] lines, mostly InnoDB ones, are shortened to ....
===== 1. error log right after startup =====
2026-11-07 3:43:21 0 [Note] Starting MariaDB 13.2.0-MariaDB-debug source revision server_uid vBnYwkGsBSVPH/FWuKqBU0vfwIc= as process 68
2026-11-07 3:43:21 0 [Note] Help others discover MariaDB. Star it on GitHub: https://github.com/MariaDB/server
...
2026-11-07 3:43:21 0 [Note] InnoDB: innodb_buffer_pool_size_max=8388608m, innodb_buffer_pool_size=128m
2026-11-07 3:43:21 0 [Note] InnoDB: Completed initialization of buffer pool
2026-11-07 3:43:21 0 [Note] InnoDB: Buffered log writes (block size=512 bytes)
2026-11-07 3:43:21 0 [Note] InnoDB: End of log at LSN=52261
...
2026-11-07 3:43:21 0 [Note] InnoDB: log sequence number 52261; transaction id 14
2026-11-07 3:43:21 0 [Note] InnoDB: Loading buffer pool(s) from /var/lib/mariadb-lab/ib_buffer_pool
2026-11-07 3:43:21 0 [Note] Plugin 'FEEDBACK' is disabled.
2026-11-07 3:43:21 0 [Note] InnoDB: Buffer pool(s) load completed at 261107 3:43:21
2026-11-07 3:43:26 0 [Note] Server socket created on IP: '0.0.0.0', port: '3306'.
2026-11-07 3:43:26 0 [Note] Server socket created on IP: '::', port: '3306'.
2026-11-07 3:43:26 0 [Note] mariadbd: Event Scheduler: Loaded 0 events
2026-11-07 3:43:26 0 [Note] mariadbd: ready for connections.
Version: '13.2.0-MariaDB-debug' socket: '/tmp/mariadb.sock' port: 3306 Source distribution
The times, server_uid, process number and LSN (52261 — a number marking a position in the redo log, part 12) can differ from run to run. Matching each line to its place in the source:
| Error log | Printed by | Step in section 1 |
|---|---|---|
Starting MariaDB 13.2.0-MariaDB-debug … |
init_server_components(), right after opening the error log (lines 5313–5316) |
1 |
Help others discover MariaDB … |
Lines 5317–5320 just below, only when SERVER_MATURITY_LEVEL is below stable — printed because this commit is alpha |
1 |
Several InnoDB: … lines, ending with log sequence number 52261; transaction id 14 |
plugin_init() → InnoDB's innodb_init() → srv_start(). The last line is the end of srv_start() (srv0start.cc lines 2010–2017) |
1 |
Plugin 'FEEDBACK' is disabled. |
test_plugin_options(), which reads plugin options (sql_plugin.cc lines 4393–4398) |
1 |
Server socket created on IP: … |
network_init() → activate_tcp_port() (line 2591) |
2 |
Event Scheduler: Loaded 0 events |
Events::load_events_from_db(), called by Events::init() (events.cc line 1288) |
4 |
ready for connections. |
mysqld_main() lines 6543–6560; the message template is ER_STARTUP |
end |
The order matches the picture in section 1 exactly. In particular, every InnoDB line comes before Server socket created.
Easy to get wrong: during crash recovery there is no port yet
network_init(), which creates the sockets, is called after init_server_components() returns. InnoDB's crash recovery (part 12) runs inside srv_start(), that is, inside init_server_components(), so while a long recovery is running, port 3306 is not open. A client that connects then is not told to wait — the connection is simply refused (there is no port). Conversely, from listen() until ready for connections. (reading privilege tables and so on) the port is open but the accept() loop is not running yet, so connections made in that window wait in the kernel's queue (back_log). If monitoring treats "the port is open" as "the server is ready", this small gap appears.
Easy to get wrong: the few-second gap in the log is TLS key generation
In the log above there is a 5-second gap between Buffer pool(s) load completed (3:43:21) and Server socket created (3:43:26). The length of the gap varies per run (when I ran it again it was 2 seconds, then 4). At first I assumed the Debug build was just slow, but starting the same image with --skip-ssl, which turns TLS off, printed both lines in the same second and the gap disappeared. In this version opt_use_ssl defaults to 1 (mysqld.cc line 1672), and if you give no certificate or key file, the server generates a new 4096-bit RSA key and a self-signed certificate on every start.
if (!cert_file && !key_file)
{
if (!is_client)
{
EVP_PKEY *pkey;
X509 *x509;
if (!(pkey= vio_keygen()) || SSL_CTX_use_PrivateKey(ctx, pkey) < 1)
{
*error= SSL_INITERR_KEY;
fprintf(stderr, "SSL error: %s\n", sslGetErrString(*error));
DBUG_RETURN(1);
}
if (!(x509= vio_gencert(pkey)) || SSL_CTX_use_certificate(ctx, x509) < 1)
vio/viosslfactories.c lines 115–128
vio_keygen() (lines 50–70) makes the RSA key and vio_gencert() (lines 72–105) makes a certificate with CN=MariaDB Server, valid for 10 years (60*60*24*365*10 seconds). RSA key generation means searching for primes, so its time varies. The second group of example 1 confirms it.
===== 2. TLS is on, with a certificate made at startup =====
+------------+------------+-----------+
@@have_ssl | @@ssl_cert | @@ssl_key |
+------------+------------+-----------+
YES | NULL | NULL |
+------------+------------+-----------+
+-----------------------+--------------------------+
Variable_name | Value |
+-----------------------+--------------------------+
Ssl_server_not_after | Nov 4 03:43:26 2036 GMT |
Ssl_server_not_before | Nov 7 03:43:26 2026 GMT |
+-----------------------+--------------------------+
We gave no ssl_cert or ssl_key, yet have_ssl is YES, the certificate starts exactly at that startup time and ends exactly 10 years (3,650 days) later. If you point to certificate files in production, this time shrinks to reading the files.
2. Options and system variables — how a line in my.cnf becomes a global variable
load_defaults — turning the config file into a command line
The very beginning of mysqld_main():
orig_argc= argc;
orig_argv= argv;
my_defaults_mark_files= TRUE;
load_defaults_or_exit(MYSQL_CONFIG_NAME, load_default_groups, &argc, &argv);
// ...
sys_var_init();
// ...
int ho_error= handle_early_options();
load_defaults_or_exit() looks up the groups in load_default_groups — "mysqld", "server", "mariadb", "mariadbd", "client-server" plus version-numbered groups from include/mysqld_default_groups.h — in my.cnf and prepends them to argv. my_defaults_mark_files= TRUE asks it to "insert markers saying which line came from which file". To check, we stopped at sys_var_init() with gdb and printed argv (part of example 5; the whole example is in section 5).
Breakpoint 1, sys_var_init () at /usr/src/mariadb/sql/set_var.cc:64
64 DBUG_ENTER("sys_var_init");
#1 0x0000555555f9633d in mysqld_main (argc=11, argv=0x5555589d3f60) at /usr/src/mariadb/sql/mysqld.cc:6224
6224 sys_var_init();
$1 = 11
$2 = {0x7fffffffef06 "/opt/mariadb/bin/mariadbd", 0x55555773cad0 "----file-marker----", 0x5555589d3e70 "/etc/my.cnf", 0x5555589d3e80 "--socket=/tmp/mariadb.sock", 0x5555589d3ea0 "--port=3306", 0x5555589d3eb0 "--user=root", 0x5555589d3ec0 "--datadir=/var/lib/mariadb-lab", 0x5555589d3ee0 "--log-error=/tmp/mariadbd.err", 0x5555589d3f00 "--pid-file=/tmp/mariadbd.pid", 0x55555773cad0 "----file-marker----", 0x55555773cb3f ""}
We started main() with no arguments (argc=1), yet argc in mysqld_main() became 11. The six lines of /etc/my.cnf came in as --socket=… and friends, wrapped by ----file-marker---- and the file name. The empty string ("") after the last ----file-marker---- means "the real command line starts here" (my_default.c lines 48–53, lines 470–474). Thanks to these markers, the server can later tell that "this value came from my.cnf".
Options are read in four passes
The resulting argv is not read in one go.
Each pass keeps unknown options in argv instead of dropping them, and hands them to the next pass. Only what is left at the very end is an error.
handle_early_options()(line 6981) reads only variables markedPARSE_EARLY. To sizeperformance_schema's memory, values such asmax_connectionsmust be known before anything else (lines 6252–6270).get_options()(line 9188, called from line 4331 ininit_common_variables()) reads themy_long_options[]table and every server system variable. The comment at lines 9203–9204 gives the reason: "Skip unknown options so that they may be processed later by plugins".test_plugin_options()is called per plugin insideplugin_init()(section 4). The--innodb-…options are read here, right before InnoDB is initialized.- Finally,
init_server_components()checks what is left.
/* we do want to exit if there are any other unknown options */
if (remaining_argc > 1)
{
int ho_error;
struct my_option removed_opts[]=
// ... (removed_opts[] — table of options removed in older versions omitted)
if ((ho_error= handle_options(&remaining_argc, &remaining_argv, removed_opts,
mysqld_get_one_option))) {
#ifdef WITH_WSREP
Wsrep_server_state::instance().disable_node_reset();
#endif
unireg_abort(ho_error);
}
removed_opts[] is a table of options that existed in MySQL 5.6 or older MariaDB and were removed (innodb-file-format, secure-auth removed in 13.0, and so on). Options in that table pass with a warning instead of an error; if something not in the table is left over, handle_options() reports unknown variable (my_getopt.c lines 412–421). An option with a value (one containing =) ends with EXIT_UNKNOWN_VARIABLE (7); one without a value ends with unknown option and EXIT_UNKNOWN_OPTION (2). With a loose- prefix, the same place leaves only a warning instead of an error (lines 415–419).
Example 2: a mistyped option name — InnoDB is already up by then
We compared three cases: innodb-buffer-pool-size mistyped as sise; the same typo with a loose- prefix plus --validate-config (check the configuration and exit); and --skip-innodb, which turns InnoDB off.
# Example written for this post (not part of the MariaDB source)
lab-init
try() {
echo "===== mariadbd $* ====="
rm -f /tmp/mariadbd.err
timeout 120 mariadbd "$@" > /tmp/out.txt 2>&1
echo "exit code: $?"
cat /tmp/mariadbd.err /tmp/out.txt 2>/dev/null | sed -E 's/^[0-9-]+ +[0-9:]+ [0-9]+ //' | grep -v "^\[Note\] InnoDB: \(!!!\|Compressed\|Number of\|Using\|Setting file\|File './ibtmp1'\|Buffer pool(s) load\|Loading buffer\|Opened 3\|128 rollback\|Completed init\|Buffered log\|End of log\)\|O_TMPFILE\|Help others" | head -20
}
try --innodb-buffer-pool-sise=64M
try --loose-innodb-buffer-pool-sise=64M --validate-config
try --skip-innodb
[Example ex/u03-options/run.sh]
===== mariadbd --innodb-buffer-pool-sise=64M =====
exit code: 7
[Note] Starting MariaDB 13.2.0-MariaDB-debug source revision server_uid Qpcwx0GWB78W6kgk4K2JAgHnRoI= as process 75
[Note] InnoDB: innodb_buffer_pool_size_max=8388608m, innodb_buffer_pool_size=128m
[Note] InnoDB: log sequence number 52261; transaction id 14
[Note] Plugin 'FEEDBACK' is disabled.
[ERROR] mariadbd: unknown variable 'innodb-buffer-pool-sise=64M'
[ERROR] Aborting
===== mariadbd --loose-innodb-buffer-pool-sise=64M --validate-config =====
exit code: 0
[Warning] mariadbd: unknown variable 'loose-innodb-buffer-pool-sise=64M'
Configuration is valid, but warnings were reported; see above.
===== mariadbd --skip-innodb =====
exit code: 1
[Note] Starting MariaDB 13.2.0-MariaDB-debug source revision server_uid Qpcwx0GWB78W6kgk4K2JAgHnRoI= as process 100
[Note] Plugin 'InnoDB' is disabled.
[Note] Plugin 'FEEDBACK' is disabled.
[ERROR] Unknown/unsupported storage engine: InnoDB
[ERROR] Aborting
The noisy InnoDB [Note] lines were removed with grep -v. The server_uid, process numbers and LSN can differ per run.
- The typo (first group): InnoDB's
log sequence number … transaction idline comes before theunknown variableerror. Because the pass-4 check sits afterplugin_init(), the typo is only noticed after the buffer pool has been allocated and the redo log read. Exit code 7 is theEXIT_UNKNOWN_VARIABLEwe saw above. On a server with a lot of data, restarting after editingmy.cnfand failing on a typo means waiting through the recovery time just to fail. loose-+--validate-config(second group): just one warning and exit code 0. With--validate-config,get_options()setsopt_abort= 1(lines 9211–9214), andplugin_init()receives thePLUGIN_INIT_SKIP_INITIALIZATIONflag, so it reads options but does not initialize anything (lines 5618–5620). That is why there is not a single InnoDB line. After editingmy.cnf, checking with this option first is faster. Note, though, that withloose-even the check ends in a mere warning; to catch typos, check withoutloose-.--skip-innodb(third group): afterPlugin 'InnoDB' is disabled.it fails withUnknown/unsupported storage engine: InnoDB. We look at this in section 4.
One system variable object = one option = one global variable
The declaration of max_connections in sql/sys_vars.cc:
// Default max_connections of 151 is larger than Apache's default max
// children, to avoid "too many connections" error in a common setup
static Sys_var_on_access_global<Sys_var_ulong,
PRIV_SET_SYSTEM_GLOBAL_VAR_MAX_CONNECTIONS>
Sys_max_connections(
"max_connections", "The number of simultaneous clients allowed",
PARSED_EARLY GLOBAL_VAR(max_connections), CMD_LINE(REQUIRED_ARG),
VALID_RANGE(10, 100000),
DEFAULT(MAX_CONNECTIONS_DEFAULT), BLOCK_SIZE(1), NO_MUTEX_GUARD,
NOT_IN_BINLOG, ON_CHECK(0), ON_UPDATE(fix_max_connections));
sql/sys_vars.cc lines 1922–1931
It states the variable's name, description, range (10 to 100000), default (MAX_CONNECTIONS_DEFAULT = 151, sql/sql_const.h line 168) and GLOBAL_VAR(max_connections) — the C++ global variable that holds the value — all at once. The comment above explains the default of 151: it is larger than Apache httpd's default number of child processes, so a common setup does not hit "too many connections". httpd's number of child processes is decided by the MPM we saw in httpd part 8.
What the constructor of this object (the constructor of the parent class sys_var) does is the key.
bzero(&option, sizeof(option));
option.name= name_arg;
option.id= getopt_id;
option.comment= comment;
option.arg_type= getopt_arg_type;
option.value= (uchar **)global_var_ptr();
option.def_value= def_val;
option.app_type= this;
option.var_type= flags & AUTO_SET ? GET_AUTO : 0;
option.deprecation_substitute= substitute;
// ...
if (chain->last)
chain->last->next= this;
else
chain->first= this;
chain->last= this;
- It fills in one command-line option (
my_option) itself.option.valuepoints toglobal_var_ptr()— that very global variablemax_connections. Whenhandle_options()reads--max-connections=200, it writes 200 to that address. - It appends itself to the global linked list
all_sys_vars(lines 193–197).sys_var_init()puts this list into a hash, andsys_var_add_options()inget_options()walks the list to build the option table (set_var.cc lines 89–107).
SET GLOBAL max_connections=300 ends up in sys_var::update() → global_update() and changes the same global variable, and SELECT @@max_connections reads the same place. The config file, the command line and SQL all use the same single slot of memory.
Easy to get wrong: these objects are created before main()
Sys_max_connections is a static global object, so its constructor runs before main() even starts, when the C++ runtime initializes global objects. That is why the all_sys_vars list is already full when sys_var_init() is called at line 6224 of mysqld_main(). If you look for a function call that "registers this variable", you won't find one. Plugin variables (innodb_buffer_pool_size and so on) are different: test_plugin_options() in section 4 creates sys_var_pluginvar objects at run time when it loads a plugin, naming them <plugin name>_<variable name> (sql_plugin.cc lines 4318–4331). Two option names are created for each: --innodb-buffer-pool-size and --plugin-innodb-buffer-pool-size (the comment at line 4281).
The server remembers where each value came from
mysqld_get_one_option(), called for every option that is read:
if (opt->app_type)
{
sys_var *var= (sys_var*) opt->app_type;
// ...
if (*filename)
{
var->origin_filename= filename;
var->value_origin= sys_var::CONFIG;
}
else
var->value_origin= sys_var::COMMAND_LINE;
If filename is not empty — thanks to the file markers we saw — it records CONFIG; if empty, COMMAND_LINE. Values the server sizes on its own are AUTO, and a SET GLOBAL makes sys_var::update() change it to SQL (set_var.cc line 244). This is the GLOBAL_VALUE_ORIGIN column of information_schema.SYSTEM_VARIABLES.
Example 3: value origins and SET GLOBAL
We started the server with --max-connections=200 on the command line and asked where a few variables came from.
-- Example written for this post (not part of the MariaDB source)
-- 1. Where did each value come from: compile-time default / my.cnf / command line / auto-sized
SELECT '--- 1. where did each value come from' AS step;
SELECT VARIABLE_NAME, GLOBAL_VALUE, GLOBAL_VALUE_ORIGIN, DEFAULT_VALUE, READ_ONLY
FROM information_schema.SYSTEM_VARIABLES
WHERE VARIABLE_NAME IN ('MAX_CONNECTIONS','SOCKET','PORT','THREAD_HANDLING',
'INNODB_BUFFER_POOL_SIZE','TABLE_DEFINITION_CACHE','DEFAULT_STORAGE_ENGINE')
ORDER BY VARIABLE_NAME;
SELECT VARIABLE_NAME, GLOBAL_VALUE_PATH
FROM information_schema.SYSTEM_VARIABLES WHERE GLOBAL_VALUE_ORIGIN='CONFIG' ORDER BY VARIABLE_NAME;
-- 2. SET GLOBAL changes the same global variable, and the origin becomes SQL
SELECT '--- 2. SET GLOBAL' AS step;
SET GLOBAL max_connections = 300;
SELECT VARIABLE_NAME, GLOBAL_VALUE, GLOBAL_VALUE_ORIGIN
FROM information_schema.SYSTEM_VARIABLES WHERE VARIABLE_NAME='MAX_CONNECTIONS';
[Example ex/u03-sysvars/demo.sql — run.sh starts the server with lab-start --max-connections=200, runs this, and finally tries SET GLOBAL thread_handling=… once]
Part of the output:
+-------------------------+---------------------------+---------------------+---------------------------+-----------+
VARIABLE_NAME | GLOBAL_VALUE | GLOBAL_VALUE_ORIGIN | DEFAULT_VALUE | READ_ONLY |
+-------------------------+---------------------------+---------------------+---------------------------+-----------+
DEFAULT_STORAGE_ENGINE | InnoDB | COMPILE-TIME | InnoDB | NO |
INNODB_BUFFER_POOL_SIZE | 134217728 | COMPILE-TIME | 134217728 | NO |
MAX_CONNECTIONS | 200 | COMMAND-LINE | 151 | NO |
PORT | 3306 | CONFIG | 0 | YES |
SOCKET | /tmp/mariadb.sock | CONFIG | | YES |
TABLE_DEFINITION_CACHE | 400 | AUTO | 400 | NO |
THREAD_HANDLING | one-thread-per-connection | COMPILE-TIME | one-thread-per-connection | YES |
+-------------------------+---------------------------+---------------------+---------------------------+-----------+
...
DATADIR | /etc/my.cnf |
LOG_ERROR | /etc/my.cnf |
PID_FILE | /etc/my.cnf |
PORT | /etc/my.cnf |
SOCKET | /etc/my.cnf |
...
MAX_CONNECTIONS | 300 | SQL |
...
ERROR 1238 (HY000) at line 1: Variable 'thread_handling' is a read only variable
MAX_CONNECTIONSisCOMMAND-LINE;PORTandSOCKET, written in/etc/my.cnf, areCONFIG, andGLOBAL_VALUE_PATHeven keeps the file name.TABLE_DEFINITION_CACHEisAUTO: lines 6247–6250 ofmysqld_main()re-sized it from other values (SYSVAR_AUTOSIZE).- After
SET GLOBALit becomesSQL. thread_handlingisREAD_ONLY, soSET GLOBALis refused with error 1238. Which thread model takes connections is decided once at startup and cannot be changed; the thread models this variable chooses are covered in part 4.
3. What a plugin is — one declaration, two ways to build it
There are four plugin-related structs.
The plugin writes only the st_maria_plugin at the bottom. The server creates the other three.
st_maria_plugin(include/mysql/plugin.hlines 567–588) is the declaration a plugin exports: its type (type), type-specific info (info), name,init/deinitfunctions, system variable table and so on.st_plugin_int(sql/sql_plugin.hlines 109–123) is the record the server creates for each plugin. It holds the state (state), how it is switched on (load_option), and the type-specific datadata— ahandlertonfor engines.st_plugin_dl(lines 94–105) is onedlopen()-ed.so. For a built-in plugin,st_plugin_int::plugin_dlis 0. In section 4's example,UNINSTALLanswers "Built-in plugins cannot be deleted" by checking exactly whether this is 0 (sql_plugin.cc lines 2433–2437).handlerton— only for engines. See section 5.
maria_declare_plugin — the same source compiles two ways
InnoDB's declaration, near the very end of the file:
maria_declare_plugin(innobase)
{
MYSQL_STORAGE_ENGINE_PLUGIN,
&innobase_storage_engine,
innobase_hton_name,
plugin_author,
"Supports transactions, row-level locking, foreign keys and encryption for tables",
PLUGIN_LICENSE_GPL,
innodb_init, /* Plugin Init */
NULL, /* Plugin Deinit */
MYSQL_VERSION_MAJOR << 8 | MYSQL_VERSION_MINOR,
innodb_status_variables_export,/* status variables */
innobase_system_variables, /* system variables */
PACKAGE_VERSION,
MariaDB_PLUGIN_MATURITY_STABLE /* maturity */
},
i_s_innodb_trx,
i_s_innodb_locks,
// ... (26 entries from i_s_innodb_lock_waits to i_s_innodb_sys_virtual omitted)
i_s_innodb_tablespaces_encryption
maria_declare_plugin_end;
storage/innobase/handler/ha_innodb.cc lines 20762–20807
One pair of braces is one st_maria_plugin. The first element is the storage engine InnoDB (innodb_init in the init slot), followed by 29 more from i_s_innodb_trx to i_s_innodb_tablespaces_encryption, so one array holds 30 plugins. The 29 are INFORMATION_SCHEMA plugins that create tables such as information_schema.INNODB_TRX, INNODB_LOCKS and INNODB_BUFFER_PAGE. The INNODB_TRX table we use in part 11 is also something InnoDB exports as a plugin.
The maria_declare_plugin macro expands differently depending on how it is built.
#ifndef MYSQL_DYNAMIC_PLUGIN
// ...
#define MARIA_DECLARE_PLUGIN__(NAME, VERSION, PSIZE, DECLS) \
MYSQL_PLUGIN_EXPORT int VERSION; \
int VERSION= MARIA_PLUGIN_INTERFACE_VERSION; \
MYSQL_PLUGIN_EXPORT int PSIZE; \
int PSIZE= sizeof(struct st_maria_plugin); \
MYSQL_PLUGIN_EXPORT struct st_maria_plugin DECLS[]; \
struct st_maria_plugin DECLS[]= {
#else
// ... (mysql_ variant of the macro omitted)
#define MARIA_DECLARE_PLUGIN__(NAME, VERSION, PSIZE, DECLS) \
MYSQL_PLUGIN_EXPORT int _maria_plugin_interface_version_; \
int _maria_plugin_interface_version_= MARIA_PLUGIN_INTERFACE_VERSION; \
MYSQL_PLUGIN_EXPORT int _maria_sizeof_struct_st_plugin_; \
int _maria_sizeof_struct_st_plugin_= sizeof(struct st_maria_plugin); \
MYSQL_PLUGIN_EXPORT struct st_maria_plugin _maria_plugin_declarations_[]; \
struct st_maria_plugin _maria_plugin_declarations_[]= {
#endif
include/mysql/plugin.h lines 136–167
- When linked in statically (without
MYSQL_DYNAMIC_PLUGIN), the array name differs per plugin, likebuiltin_maria_innobase_plugin, so that many plugins in one executable don't clash. - When built as a
.so, the array is always named_maria_plugin_declarations_, because afterdlopen()the server only has todlsym()this one name (sql_plugin.cc lines 169–170;dlopen(dlpath, RTLD_NOW)at line 783 inplugin_dl_add(), anddlsym()at line 685 inread_maria_plugin_info()).
CMake decides which. MYSQL_ADD_PLUGIN() in cmake/plugin.cmake links a static plugin into the server without MYSQL_DYNAMIC_PLUGIN and adds its name to a list (lines 255–268); for a dynamic one it defines MYSQL_DYNAMIC_PLUGIN and builds a MODULE library (lines 269–280). The template that list goes into is sql/sql_builtin.cc.in. Lines 37–47 of that file hold two arrays, mysql_optional_plugins[] and mysql_mandatory_plugins[], and CMake fills the @mysql_optional_plugins@ placeholders with lines like builtin_maria_innobase_plugin, to produce sql_builtin.cc in the build directory. The first three of the mandatory array (binlog, mysql_password, mhnsw) are written into the template directly. Since this generated file is not in the repository (and the example image did not keep its build directory), we printed the arrays inside the executable with gdb — reading only the executable, without starting a process.
===== 0. built-in plugin arrays generated at build time (no process) =====
0x294daa0 <mysql_mandatory_plugins>: 0x29487a0 <builtin_maria_binlog_plugin> 0x291f020 <builtin_maria_mysql_password_plugin>
0x294dab0 <mysql_mandatory_plugins+16>: 0x2948c20 <builtin_maria_mhnsw_plugin> 0x2954220 <builtin_maria_csv_plugin>
...
0x294db20 <mysql_mandatory_plugins+128>: 0x294dca0 <builtin_maria_online_alter_log_plugin> 0x0
0x294da40 <mysql_optional_plugins>: 0x2956560 <builtin_maria_innobase_plugin> 0x2950860 <builtin_maria_perfschema_plugin>
0x294da50 <mysql_optional_plugins+16>: 0x2b635a0 <builtin_maria_sequence_plugin> 0x2b636c0 <builtin_maria_auth_socket_plugin>
...
0x294da70 <mysql_optional_plugins+48>: 0x2b652a0 <builtin_maria_thread_pool_info_plugin> 0x2950460 <builtin_maria_partition_plugin>
0x294da80 <mysql_optional_plugins+64>: 0x0
Addresses differ per build, and the middle lines are shortened to .... The mandatory array has 17 entries and the optional one 8, both ending in 0. Each entry is one of the "plugin arrays" above, so there are more plugins than entries (InnoDB's array alone has 30). Remember that InnoDB, perfschema and partition are on the optional side; it explains LOAD_OPTION in section 4. The ap_prelinked_modules[] we saw in httpd part 6 was also an array in a build-generated modules.c. The two servers put static modules into the executable in almost the same way.
4. plugin_init() — register, switch on type by type, read mysql.plugin
Let's first picture what plugin_init() (lines 1619–1890), called at line 5618 of init_server_components(), does.
In the yellow box the server reads one of its own tables (mysql.plugin). For that, the table's engine must already be running.
First, register everything — just register
for (builtins= mysql_mandatory_plugins; *builtins || mandatory; builtins++)
{
if (!*builtins)
{
builtins= mysql_optional_plugins;
mandatory= false;
if (!*builtins)
break;
}
for (plugin= *builtins; plugin->info; plugin++)
{
// ... (option that skips the built-in InnoDB omitted)
bzero(&tmp, sizeof(tmp));
tmp.plugin= plugin;
tmp.name= tmp_plugin_name;
tmp.state= 0;
tmp.load_option= mandatory ? PLUGIN_FORCE : PLUGIN_ON;
// ... (applying override_plugin_load_policy omitted)
tmp.state= PLUGIN_IS_UNINITIALIZED;
if (register_builtin(plugin, &tmp, &plugin_ptr))
goto err_unlock;
}
}
sql/sql_plugin.cc lines 1685–1722
It walks the two arrays in turn and only puts each plugin into plugin_array and the per-type hash plugin_hash[type] with register_builtin() (lines 1893–1913). The state is PLUGIN_IS_UNINITIALIZED, and plugins from the mandatory array get PLUGIN_FORCE, those from the optional array PLUGIN_ON (line 1705). The omitted lines 1709–1716 apply an exception table, override_plugin_load_policy (lines 213–234), which changes performance_schema to FORCE and feedback to OFF. Example 1's Plugin 'FEEDBACK' is disabled. starts here.
Next, lines 1724–1744 initialize MyISAM alone first and make it the first value of global_system_variables.table_plugin (the default engine). The comment gives the reason: "that should almost always succeed". Switching to the real default engine, InnoDB, happens after plugin_init() returns, in init_default_storage_engine() (mysqld.cc line 5871). Lines 1747–1751 open the .so files given with --plugin-load-add through plugin_load_list() and only register them.
Initialize type by type, read mysql.plugin, go around once more
for(;;)
{
int error;
for (i=0; i < MYSQL_MAX_PLUGIN_TYPE_NUM; i++)
{
HASH *hash= plugin_hash + plugin_type_initialization_order[i];
for (uint idx= 0; idx < hash->records; idx++)
{
plugin_ptr= (struct st_plugin_int *) my_hash_element(hash, idx);
if (plugin_ptr->state == PLUGIN_IS_UNINITIALIZED)
{
bool plugin_table_engine= lex_string_eq(&plugin_table_engine_name,
&plugin_ptr->name);
bool opts_only= flags & PLUGIN_INIT_SKIP_INITIALIZATION &&
(flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE ||
!plugin_table_engine);
error= plugin_initialize(&tmp_root, plugin_ptr, argc, argv,
opts_only);
// ... (failures go to the reap list, HA_ERR_RETRY_INIT to the retry list — omitted)
/* load and init plugins from the plugin table (unless done already) */
if (flags & PLUGIN_INIT_SKIP_PLUGIN_TABLE ||
plugin_table_engine_name.length == 0)
break;
mysql_mutex_unlock(&LOCK_plugin);
plugin_load(&tmp_root);
flags|= PLUGIN_INIT_SKIP_PLUGIN_TABLE;
mysql_mutex_lock(&LOCK_plugin);
}
sql/sql_plugin.cc lines 1774–1860
- Inside the outer
for(;;), it follows the type order tableplugin_type_initialization_order[], walks each type's hash and runsplugin_initialize()on everything stillUNINITIALIZED. The order table (lines 142–156) is DAEMON → encryption → data type → function → storage engine → INFORMATION_SCHEMA → full-text parser → authentication → password validation → audit → replication → UDF. Encryption key plugins (MariaDB_ENCRYPTION_PLUGIN) must come before engines so InnoDB can open encrypted tablespaces, and engines must come beforeINFORMATION_SCHEMAso tables likeINNODB_TRXcan rely on their engine. - Failed plugins are collected in the
reaplist. Those that returnedHA_ERR_RETRY_INITbecause a plugin they depend on is not there yet are collected separately and retried (lines 1805–1849). - When a pass ends,
plugin_load()reads themysql.plugintable and registers the plugins loaded withINSTALL SONAME, sets thePLUGIN_INIT_SKIP_PLUGIN_TABLEflag and goes around once more. In the second pass only the newly registered ones areUNINITIALIZED, so only they get initialized. - At the end it cleans up the
reaplist. If any of them isFORCE,plugin_init()fails and startup stops (lines 1865–1879).
mysql.plugin is read after the first pass because it is an ordinary server table. plugin_load() creates a connection-less THD with new THD(0), opens the table with open_and_lock_tables() and reads it row by row with READ_RECORD (lines 1933–2042). Opening a table needs its engine to be running, so the built-in engines have to be switched on in the first pass. That is also why lines 1753–1760 read the engine name from mysql/plugin.frm in advance. As the example below shows, this table's engine is Aria.
plugin_initialize() and plugin_do_initialize()
plugin_initialize() (lines 1523–1555) first reads that plugin's options with test_plugin_options() (pass 3 in section 2), and if the plugin is not disabled, calls plugin_do_initialize().
static int plugin_do_initialize(struct st_plugin_int *plugin, uint &state)
{
DBUG_ENTER("plugin_do_initialize");
mysql_mutex_assert_not_owner(&LOCK_plugin);
plugin_type_init init= plugin_type_initialize[plugin->plugin->type];
if (!init)
init= plugin->plugin->init;
if (init)
{
if (int ret= init(plugin))
{
/* Plugin init failed and did not requested a retry */
if (ret != HA_ERR_RETRY_INIT)
print_init_failed_error(plugin);
DBUG_RETURN(ret);
}
}
state= PLUGIN_IS_READY; // plugin->init() succeeded
sql/sql_plugin.cc lines 1483–1500
Line 1487 is the key. If the table has a per-type init function, that is called; otherwise the plugin's own init. The table:
plugin_type_init plugin_type_initialize[MYSQL_MAX_PLUGIN_TYPE_NUM]=
{
0, ha_initialize_handlerton, 0, 0,initialize_schema_table,
initialize_audit_plugin, 0, 0, 0, initialize_encryption_plugin,
initialize_data_type_plugin, 0
};
sql/sql_plugin.cc lines 123–128
The slot number is the type number in include/mysql/plugin.h. Slot 1 (MYSQL_STORAGE_ENGINE_PLUGIN) is ha_initialize_handlerton, slot 4 (MYSQL_INFORMATION_SCHEMA_PLUGIN) is initialize_schema_table, and slots 5 (audit), 9 (encryption) and 10 (data type) have functions too. For the zero slots (daemon, authentication and so on) the plugin's init is called directly. An engine plugin's init (InnoDB's innodb_init) is not called by the server directly; ha_initialize_handlerton() calls it after preparing a handlerton — the topic of section 5. On success the state becomes PLUGIN_IS_READY and the plugin's status variables (the Innodb_… entries in SHOW STATUS) are registered (lines 1500–1519).
Example 4: the plugin list, INSTALL SONAME, and a restart
-- Example written for this post (not part of the MariaDB source)
-- 1. Right after startup: plugins per type (PLUGIN_LIBRARY IS NULL means built into mariadbd)
SELECT '--- 1. plugins right after startup' AS step;
SELECT PLUGIN_TYPE, COUNT(*) AS n, SUM(PLUGIN_LIBRARY IS NULL) AS builtin, SUM(PLUGIN_STATUS='ACTIVE') AS active
FROM information_schema.PLUGINS GROUP BY PLUGIN_TYPE ORDER BY n DESC, PLUGIN_TYPE;
SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE, LOAD_OPTION
FROM information_schema.PLUGINS
WHERE PLUGIN_TYPE='STORAGE ENGINE' OR PLUGIN_STATUS<>'ACTIVE' ORDER BY PLUGIN_TYPE, PLUGIN_NAME;
-- Which engine holds the mysql.plugin table itself?
SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA='mysql' AND TABLE_NAME='plugin';
-- 2. Load a dynamic plugin: the server dlopen()s a .so from plugin_dir
SELECT '--- 2. INSTALL SONAME' AS step;
SELECT @@plugin_dir;
INSTALL SONAME 'metadata_lock_info';
SELECT PLUGIN_NAME, PLUGIN_STATUS, PLUGIN_TYPE, PLUGIN_LIBRARY, LOAD_OPTION
FROM information_schema.PLUGINS WHERE PLUGIN_LIBRARY IS NOT NULL;
SELECT * FROM mysql.plugin;
-- A new information_schema table now exists
SELECT COUNT(*) AS locks_held FROM information_schema.METADATA_LOCK_INFO;
[Example ex/u03-install-soname/demo1.sql]
After running this, run.sh restarts the server and prints step 3 (asking again only for PLUGIN_LIBRARY IS NOT NULL) and step 4 (UNINSTALL PLUGIN InnoDB), then restarts with lab-start --plugin-load-add=ha_blackhole and prints step 5. Only part of the output is shown (at the ... marks we removed the step title tables and table headers, and for steps 2, 3 and 5 kept only the result rows).
+--------------------+----+---------+--------+
PLUGIN_TYPE | n | builtin | active |
+--------------------+----+---------+--------+
INFORMATION SCHEMA | 42 | 42 | 41 |
FUNCTION | 13 | 13 | 13 |
STORAGE ENGINE | 10 | 10 | 10 |
DATA TYPE | 6 | 6 | 6 |
AUTHENTICATION | 3 | 3 | 3 |
DAEMON | 3 | 3 | 3 |
+--------------------+----+---------+--------+
+--------------------+---------------+--------------------+-------------+
PLUGIN_NAME | PLUGIN_STATUS | PLUGIN_TYPE | LOAD_OPTION |
+--------------------+---------------+--------------------+-------------+
FEEDBACK | DISABLED | INFORMATION SCHEMA | OFF |
Aria | ACTIVE | STORAGE ENGINE | FORCE |
CSV | ACTIVE | STORAGE ENGINE | FORCE |
InnoDB | ACTIVE | STORAGE ENGINE | ON |
MEMORY | ACTIVE | STORAGE ENGINE | FORCE |
MRG_MyISAM | ACTIVE | STORAGE ENGINE | FORCE |
MyISAM | ACTIVE | STORAGE ENGINE | FORCE |
partition | ACTIVE | STORAGE ENGINE | ON |
PERFORMANCE_SCHEMA | ACTIVE | STORAGE ENGINE | FORCE |
SEQUENCE | ACTIVE | STORAGE ENGINE | ON |
SQL_SEQUENCE | ACTIVE | STORAGE ENGINE | FORCE |
+--------------------+---------------+--------------------+-------------+
+------------+--------+
TABLE_NAME | ENGINE |
+------------+--------+
plugin | Aria |
+------------+--------+
...
METADATA_LOCK_INFO | ACTIVE | INFORMATION SCHEMA | metadata_lock_info.so | ON |
...
METADATA_LOCK_INFO | ACTIVE | metadata_lock_info.so | ON |
...
ERROR 1619 (HY000) at line 1: Built-in plugins cannot be deleted
...
BLACKHOLE | ACTIVE | ha_blackhole.so | ON |
METADATA_LOCK_INFO | ACTIVE | metadata_lock_info.so | ON |
- Right after startup there are 77 plugins, all built in (
builtin=n). One is inactive:FEEDBACK, withLOAD_OPTIONOFF— exactly theoverride_plugin_load_policywe saw. - The engines'
LOAD_OPTIONsplits in two. Aria, CSV, MEMORY, MyISAM, MRG_MyISAM and SQL_SEQUENCE came from the mandatory array and areFORCE; InnoDB, partition and SEQUENCE came from the optional array and areON.PERFORMANCE_SCHEMAis in the optional array, but the exception table changed it toFORCE. INSTALL SONAME 'metadata_lock_info'goes tomysql_install_plugin()(lines 2326–2416).plugin_add()dlopen()s the.sofromplugin_dirand registers it,finalize_install()initializes it withplugin_initialize()and then writes a row tomysql.plugin(ha_write_row()at lines 2314–2316). As a result, theinformation_schema.METADATA_LOCK_INFOtable appeared. We use that table to watch metadata locks in part 7.- It is still there after the restart (step 3) because
plugin_load()readmysql.plugin. - BLACKHOLE, loaded with
--plugin-load-add=ha_blackhole(step 5), is in the list but not inmysql.plugin.plugin_load_list()only registers and never writes to the table, so it disappears if you drop the option at the next start. The option for "just this run",INSTALLfor "from now on" — that is where the two paths differ.
Easy to get wrong: FORCE and ON — why --skip-innodb works but --skip-aria doesn't
test_plugin_options() creates one more option per plugin, named after the plugin (--innodb, --feedback …), which decides whether to switch it on. But the code that uses this value is wrapped in if (!plugin_is_forced(tmp)).
if (!plugin_is_forced(tmp))
{
plugin_load_option= (enum_plugin_load_option) *(ulong*) opts[0].value;
disable_plugin= (plugin_load_option == PLUGIN_OFF);
tmp->load_option= plugin_load_option;
}
// ...
if (disable_plugin)
{
if (global_system_variables.log_warnings && !opt_help)
sql_print_information("Plugin '%s' is disabled.",
tmp->name.str);
goto err;
sql/sql_plugin.cc lines 4373–4398
A FORCE plugin is not affected by this option, so --skip-aria cannot turn Aria off. InnoDB is ON, so --skip-innodb turns it off and, as in the third group of example 2, Plugin 'InnoDB' is disabled. is printed. But since the default of the default engine (default_storage_engine) is "InnoDB" (mysqld.cc lines 4285–4286), init_default_storage_engine() after plugin_init() cannot find the engine and stops startup with Unknown/unsupported storage engine: InnoDB (lines 5091–5096). To turn InnoDB off, you also have to change --default-storage-engine.
5. ha_initialize_handlerton() — where an engine fills its handlerton
Drawn as a sequence, the initialization of one engine plugin looks like this.
The server mallocs the handlerton; the engine fills it.
Server side: create an empty table and hand it over
hton= (handlerton *)my_malloc(key_memory_handlerton, sizeof(handlerton),
MYF(MY_WME | MY_ZEROFILL));
// ... (out-of-memory handling omitted)
hton->tablefile_extensions= hton_no_exts;
hton->discover_table_names= hton_ext_based_table_discovery;
hton->drop_table= hton_drop_table;
hton->slot= HA_SLOT_UNDEF;
/* Historical Requirement */
plugin->data= hton; // shortcut for the future
if (plugin->plugin->init && (ret= plugin->plugin->init(hton)))
goto err;
// ... (discovery defaults and db_type conflict check omitted)
if ((ret= setup_transaction_participant(plugin)))
goto err_deinit;
installed_htons[hton->db_type]= hton;
- It
mallocs a zero-filledhandlertonand puts defaults in a few slots (lines 725–739). Zero-filling matters: function pointers the engine does not fill stayNULL, and the server checks forNULLbefore using them (for exampleif (hton->panic)inha_finalize_handlerton()). - It hangs it on the
st_plugin_intwithplugin->data= htonand callsplugin->plugin->init(hton)— the engine'sinit(lines 741–743).inittakes avoid *, but an engine plugin receives ahandlerton *. - On return it checks that the
db_typethe engine chose does not clash, and assigns a free number if it clashes or is missing (lines 767–787).db_typeis the old engine number written into.frmfiles. setup_transaction_participant()(lines 674–715) assigns aslot. The slot is the index of the per-engine cell insideTHD(thd->ha_data[slot]); InnoDB keeps each connection's transaction objecttrx_tin that cell (ha_innodb.cc line 1810,thd_get_ha_data(thd, innodb_hton_ptr)). If the engine hasprepare, it also increments the number of engines taking part in two-phase commit (total_ha_2pc) — which connects to the XA story in part 12.installed_htons[db_type]= htonputs it on the numbered list.
Engine side: the smallest example, and InnoDB
How simple the engine's job is shows in the init of SQL_SEQUENCE, the internal engine behind CREATE SEQUENCE.
static int sequence_initialize(void *p)
{
handlerton *local_sequence_hton= (handlerton *)p;
DBUG_ENTER("sequence_initialize");
local_sequence_hton->db_type= DB_TYPE_SEQUENCE;
local_sequence_hton->create= sequence_create_handler;
local_sequence_hton->panic= sequence_end;
local_sequence_hton->flags= (HTON_NOT_USER_SELECTABLE |
// ... (remaining HTON_ flags omitted)
HTON_NO_PARTITION);
DBUG_RETURN(0);
}
sql/ha_sequence.cc lines 544–561
It puts a number, two function pointers (create, panic) and property flags (flags) into the handlerton it received, and that's it. Because of HTON_HIDDEN and HTON_NOT_USER_SELECTABLE, users cannot pick it with ENGINE=SQL_SEQUENCE.
InnoDB's innodb_init() does the same job, much more of it.
static int innodb_init(void* p)
{
DBUG_ENTER("innodb_init");
handlerton* innobase_hton= static_cast<handlerton*>(p);
innodb_hton_ptr = innobase_hton;
innobase_hton->db_type = DB_TYPE_INNODB;
// ...
innobase_hton->commit = innobase_commit;
innobase_hton->rollback = innobase_rollback;
innobase_hton->prepare = innobase_xa_prepare;
// ... (… dozens of function pointers, innodb_init_params(), performance_schema registration omitted)
bool create_new_db = false;
/* Check whether the data files exist. */
dberr_t err = srv_sys_space.check_file_spec(&create_new_db, 5U << 20);
// ...
err = srv_start(create_new_db);
if (err != DB_SUCCESS) {
if (!recv_sys.rpo) {
recv_sys.rpo = srv_read_only_mode;
}
innodb_shutdown();
goto init_abort;
}
srv_was_started = true;
storage/innobase/handler/ha_innodb.cc lines 4295–4475
The first part (lines 4301–4390) is all innobase_hton->… = …: innobase_commit for commit, innobase_rollback for rollback, innobase_xa_prepare for prepare, innobase_create_handler for create (which makes the handler that opens a table), and innobase_end for panic (called at shutdown, section 7). The function pointers of a recently added feature that keeps the binlog inside InnoDB (binlog_storage_engine) are lined up here too (lines 4365–4390, part 12).
The second part tidies up the innodb_… variables with innodb_init_params() (line 4416), checks whether the data files exist (line 4451) and calls srv_start() (line 4465). srv_start() (srv0start.cc line 1303) creates the buffer pool (buf_pool.create() at line 1383), prepares the redo log (line 1390), reads the redo log from the checkpoint and re-applies it if needed (recv_recovery_from_checkpoint_start() at line 1570). Example 1's InnoDB: Completed initialization of buffer pool and End of log at LSN=52261 were printed in there. The buffer pool is covered in part 10, redo and recovery in part 12. If srv_start() fails, innodb_shutdown() cleans up and an error is returned; since InnoDB is ON rather than FORCE, the server tries to go on without InnoDB and — as we saw — stops because it cannot find the default engine.
Example 5: stopping at ha_initialize_handlerton with gdb and printing engine names
We started mariadbd under gdb, put a dprintf (print a line without stopping) on ha_initialize_handlerton to print engine names, and stopped once at srv_start to see the backtrace. Then we stopped at handle_connections_sockets to print the thread list (section 6), and in a second run stopped at innodb_shutdown during shutdown (section 7).
# Example written for this post (not part of the MariaDB source)
set pagination off
set print thread-events off
break sys_var_init
dprintf ha_initialize_handlerton,"ha_initialize_handlerton: %s\n", ((st_plugin_int*)plugin_)->name.str
break srv_start
break handle_connections_sockets
run
up
print argc
print *argv@argc
continue
bt
continue
info threads
kill
[Example ex/u03-gdb-handlerton/gdb-start.cmds — run.sh runs gdb -batch -x gdb-start.cmds --args /opt/mariadb/bin/mariadbd after lab-init, and removes the [New Thread …] lines printed whenever a thread starts with grep -v. The shutdown file gdb-stop.cmds is a short one that sets break innodb_shutdown and does run, bt, continue; while that gdb runs in the background, the script sends mariadb-admin shutdown]
The engine names and the backtrace:
ha_initialize_handlerton: MyISAM
ha_initialize_handlerton: MEMORY
ha_initialize_handlerton: CSV
ha_initialize_handlerton: PERFORMANCE_SCHEMA
ha_initialize_handlerton: Aria
ha_initialize_handlerton: MRG_MyISAM
ha_initialize_handlerton: partition
ha_initialize_handlerton: InnoDB
Thread 1 "mariadbd" hit Breakpoint 3, srv_start (create_new_db=false) at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:1304
1304 {
#0 srv_start (create_new_db=false) at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:1304
#1 0x0000555556a6a9c8 in innodb_init (p=0x5555589d7230) at /usr/src/mariadb/storage/innobase/handler/ha_innodb.cc:4465
#2 0x000055555661ec05 in ha_initialize_handlerton (plugin_=0x555558c6d3b0) at /usr/src/mariadb/sql/handler.cc:742
#3 0x00005555561b7762 in plugin_do_initialize (plugin=0x555558c6d3b0, state=@0x7fffffffb7a8: 4) at /usr/src/mariadb/sql/sql_plugin.cc:1492
#4 0x00005555561b7b09 in plugin_initialize (tmp_root=0x7fffffffbe00, plugin=0x555558c6d3b0, argc=0x5555580c26d0 <remaining_argc>, argv=0x5555589d3f60, options_only=false) at /usr/src/mariadb/sql/sql_plugin.cc:1546
#5 0x00005555561b892a in plugin_init (argc=0x5555580c26d0 <remaining_argc>, argv=0x5555589d3f60, flags=0) at /usr/src/mariadb/sql/sql_plugin.cc:1790
#6 0x0000555555f94dbd in init_server_components () at /usr/src/mariadb/sql/mysqld.cc:5618
#7 0x0000555555f967e1 in mysqld_main (argc=11, argv=0x5555589d3f60) at /usr/src/mariadb/sql/mysqld.cc:6425
#8 0x0000555555f88d2d in main (argc=1, argv=0x7fffffffed38) at /usr/src/mariadb/sql/main.cc:34
ha_initialize_handlerton: SQL_SEQUENCE
ha_initialize_handlerton: SEQUENCE
Addresses and argument values depend on the environment (gdb starts the program with address randomization turned off, so rerunning on the same image gave the same values).
- Read from bottom to top, the backtrace connects sections 1, 4 and 5 in one line:
main→mysqld_main(line 6425, theinit_server_components()call) →init_server_components(line 5618,plugin_init()) →plugin_init(line 1790) →plugin_initialize(line 1546) →plugin_do_initialize(line 1492,init(plugin)) →ha_initialize_handlerton(line 742,plugin->plugin->init(hton)) →innodb_init(line 4465) →srv_start. - MyISAM is the very first engine — exactly the "MyISAM alone first" we saw (lines 1724–1744).
- The order of the rest (MEMORY, CSV, PERFORMANCE_SCHEMA, Aria, …) is not an order the code defines (rerunning on the same image gave the same order). Within a type, plugins are visited in the element order of the hash
plugin_hash[type](lines 1780–1782,my_hash_element(hash, idx)). If you write engines that depend on each other, you must not rely on this order; if you need to wait, returnHA_ERR_RETRY_INITand ask to be called again. - The
SQL_SEQUENCEandSEQUENCElines appear after InnoDB's backtrace because we stopped atsrv_startand the same loop carried on aftercontinue.
Easy to get wrong: InnoDB's deinit is NULL
In InnoDB's declaration (lines 20762–20777), the deinit slot is NULL. So where is InnoDB shut down? innodb_init() set innobase_hton->panic = innobase_end (line 4329), and the engines' per-type cleanup function ha_finalize_handlerton() calls hton->panic(hton, HA_PANIC_CLOSE). An engine's way out goes through a handlerton function pointer, not the plugin's deinit. Section 7's backtrace confirms it.
6. Waiting for the first connection — the signal thread and the poll() loop
Signals are received by one dedicated thread
init_signals() (lines 3349–3419) puts SIGTERM, SIGHUP, SIGQUIT and SIGPIPE — plus SIGINT and SIGTSTP unless started with --gdb — into a set and blocks them with sigprocmask() and pthread_sigmask() (lines 3385–3417). Every thread created after this inherits the mask, so no connection thread is interrupted by these signals. Then the single signal_hand thread started by start_signal_handler() waits for and receives them with sigwait.
while ((error= my_sigwait(&set, &sig, &origin)) == EINTR) /* no-op */;
if (abort_loop)
break;
switch (sig) {
case SIGTERM:
case SIGQUIT:
#ifdef EXTRA_DEBUG
sql_print_information("Got signal %d to shutdown server",sig);
#endif
/* switch to the old log message processing */
logger.set_handlers(global_system_variables.sql_log_slow ? LOG_FILE:LOG_NONE,
opt_log ? LOG_FILE:LOG_NONE);
break_connect_loop();
DBUG_ASSERT(abort_loop);
break;
case SIGHUP:
// ... (SIGHUP: reload_acl_and_cache() and reopening logs omitted)
break;
On SIGTERM or SIGQUIT it wakes the main thread's loop with break_connect_loop() (section 7); on SIGHUP it reopens the logs and flushes the privilege and table caches (reload_acl_and_cache(), similar to FLUSH LOGS). A signal handler can do very little safely, but receiving signals with sigwait in an ordinary thread lets you take locks and do heavy work. This thread also writes the pid file (create_pid_file() at lines 3474–3476), so /tmp/mariadbd.pid appears only after start_signal_handler(). httpd in httpd part 5 went around main()'s loop once more on a signal to re-read its configuration, but MariaDB's SIGHUP does not re-read the config file; the flags passed to reload_acl_and_cache() above are only the log, table, privilege, thread and host caches.
The poll() loop
handle_connections_sockets(), called by run_main_loop():
int termination_fds[2];
if (pipe(termination_fds))
// ... (pipe creation failure handling omitted)
struct pollfd event_fd;
event_fd.fd= termination_fds[0];
event_fd.events= POLLIN;
fds.push(event_fd);
sd_notify(0, "READY=1\n"
"STATUS=Taking your SQL requests now...\n");
DBUG_PRINT("general",("Waiting for connections."));
while (!abort_loop)
{
retval= poll(fds.get_pos(0), fds.size(), -1);
// ... (poll() error handling and finding which socket omitted)
new_sock= mysql_socket_accept(key_socket_client_connection, sock,
(struct sockaddr *)(&cAddr),
&length);
if (mysql_socket_getfd(new_sock) != INVALID_SOCKET)
handle_accepted_socket(new_sock, sock);
- It puts the listening sockets created by
network_init()(TCP IPv4 and IPv6, the Unix socket) into apollfdarray (lines 6856–6864). - It creates one more pipe and puts its read end into the same array (lines 6865–6882). When a stop request comes, someone writes to this pipe, and
poll()wakes up (section 7). - It tells
systemdREADY=1(lines 6884–6885). This is the momentsystemctl start mariadbreturns. poll(…, -1)waits with no timeout. When a new connection arrives on a socket, itaccept()s it and passes it tohandle_accepted_socket(). Attaching a thread to each connection after that is covered in part 4.
The thread that receives signals and the thread that waits for connections are separate, and a single pipe links them. The list of awaited signals was checked against signal_hand's sigaddset calls (mysqld.cc lines 3466–3472).
The main thread stays in this loop for the server's whole life. The thread list when example 5 stopped at handle_connections_sockets:
Thread 1 "mariadbd" hit Breakpoint 4, handle_connections_sockets () at /usr/src/mariadb/sql/mysqld.cc:6846
6846 {
Id Target Id Frame
* 1 Thread 0x7ffff6fe2a00 (LWP 94) "mariadbd" handle_connections_sockets () at /usr/src/mariadb/sql/mysqld.cc:6846
2 Thread 0x7ffff63e86c0 (LWP 97) "statement_timer" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
3 Thread 0x7fffe1d8e6c0 (LWP 98) "checkpoint_bg" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
4 Thread 0x7fffe118c6c0 (LWP 99) "io_uring_wait" 0x00007ffff7edfab0 in ?? () from /lib/x86_64-linux-gnu/liburing.so.2
5 Thread 0x7fffe098b6c0 (LWP 100) "ib_tpool_worker" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
6 Thread 0x77ffdbfff6c0 (LWP 101) "page_cleaner" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
...
11 Thread 0x7ffff41aa6c0 (LWP 106) "manager" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
12 Thread 0x7ffff41596c0 (LWP 107) "signal_handler" 0x00007ffff72b7fc8 in sigtimedwait () from /lib/x86_64-linux-gnu/libc.so.6
13 Thread 0x7ffff410e6c0 (LWP 108) "mariadbd" 0x00007ffff730ae51 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
LWP numbers and some thread addresses differ per run, and repeated names are shortened to .... Even before the first connection there are 13 threads. Number 1 is the main thread (now in handle_connections_sockets), and number 12, signal_handler, is the signal_hand above, waiting in sigtimedwait. my_sigwait() (include/my_pthread.h lines 176–187) calls sigwaitinfo() when HAVE_SIGWAITINFO is set; seeing sigtimedwait in the backtrace suggests the C library (glibc) implements it that way (I did not check this in the glibc source). page_cleaner, ib_tpool_worker, io_uring_wait and checkpoint_bg are InnoDB threads started by srv_start(), and manager is the server's manager thread. There is not a single connection thread yet. In part 4 we compare this list with one where each connection gets a thread.
7. Shutdown — startup in reverse
The way to stop comes in through two doors and meets in one function.
The two doors (gray) meet at break_connect_loop(). The actual cleanup is always done by the main thread.
break_connect_loop() — one flag and 8 bytes
static void break_connect_loop()
{
abort_loop= 1;
// ... (Windows version omitted)
mysql_mutex_lock(&LOCK_start_thread);
if (termination_event_fd >= 0)
{
uint64_t u= 1;
if(write(termination_event_fd, &u, sizeof(uint64_t)) < 0)
{
sql_print_error("Couldn't send event to terminate listen loop");
abort();
}
}
mysql_mutex_unlock(&LOCK_start_thread);
#endif /* _WIN32 */
}
It sets abort_loop= 1 and writes 8 bytes (one uint64_t) to section 6's pipe. That's all it does, and the comment above the function (lines 1845–1847) is firm about it: "Shutdown as such is supposed to be performed by main thread itself". Three places call it: SIGTERM/SIGQUIT in signal_hand (line 3513), kill_mysql() (line 1914), and end_mysqld_signal() (line 3066), which handles ^C when started with --gdb. kill_mysql() is called by the COM_SHUTDOWN command that mariadb-admin shutdown sends (sql_parse.cc lines 2241–2268) and by the SQL statement SHUTDOWN (lines 5517–5523); both check the SHUTDOWN privilege first. kill_mysql() also records who shut the server down (shutdown_user, lines 1886–1895).
The bytes written to the pipe wake poll() in the main thread, which sees abort_loop, leaves the loop and returns to mysqld_main().
run_main_loop();
/* Shutdown requested */
{
char *user= shutdown_user.load(std::memory_order_relaxed);
sql_print_information(ER_DEFAULT(ER_NORMAL_SHUTDOWN), my_progname,
user ? user : "unknown");
}
// ... (WSREP block omitted)
close_connections();
termination:
my_free(shutdown_user.load(std::memory_order_relaxed));
/*
The signal thread can use server resources, e.g. when processing SIGHUP,
and it must end gracefully before clean_up()
*/
wait_for_signal_thread_to_end();
ha_pre_shutdown();
clean_up(1, true);
- It logs
Normal shutdown. Afterinitiated by:comes the userkill_mysql()recorded; if a signal stopped it,unknown. close_connections()(from line 1918) closes the listening sockets and removes the Unix socket file (lines 1928–1944), tells every connection thread to finish (kill_thread_phase_1, line 1954), and waits up to 1000 × 20 ms = 20 seconds (lines 1987–1992). The comment explains: "From 10.0, we increase this to 1000 rounds / 20 seconds".- It waits for the signal thread with
wait_for_signal_thread_to_end(), warns the engines in advance withha_pre_shutdown(), andclean_up()tears down the server parts in reverse. Line 2183 in there isplugin_shutdown().
plugin_shutdown() — unused ones first
while (reap_needed && (count= plugin_array.elements))
{
reap_plugins();
for (i= 0; i < count; i++)
{
plugin= *dynamic_element(&plugin_array, i, struct st_plugin_int **);
if (plugin->state == PLUGIN_IS_READY)
{
plugin->state= PLUGIN_IS_DELETED;
reap_needed= true;
}
}
if (!reap_needed)
{
/*
release any plugin references held.
*/
unlock_variables(NULL, &global_system_variables);
unlock_variables(NULL, &max_system_variables);
}
}
sql/sql_plugin.cc lines 2160–2180
It repeats marking every READY plugin as DELETED and calling reap_plugins(). reap_plugins() (lines 1348–1390) picks only those with ref_count 0 and runs plugin_deinitialize() on them. A plugin someone still holds is pushed to the next round. The typical one is the default engine: global_system_variables.table_plugin keeps InnoDB locked, so it is cleaned up in the round after lines 2177–2178 release it with unlock_variables(), once everything else is gone. That is the "reasonable order" in the comment (lines 2153–2158).
plugin_deinitialize() mirrors plugin_do_initialize(). If there is a per-type cleanup function (plugin_type_deinitialize[], lines 130–135), it calls that — ha_finalize_handlerton() for engines (lines 1307–1311). And, as we saw in section 5, ha_finalize_handlerton() calls hton->panic.
if (installed_htons[hton->db_type] == hton)
installed_htons[hton->db_type]= NULL;
if (hton->panic)
hton->panic(hton, HA_PANIC_CLOSE);
if (plugin->plugin->deinit)
deinit_status= plugin->plugin->deinit(NULL);
Example 6: two kinds of shutdown, and a backtrace
Groups 3 and 4 of example 1 stopped the same server once with mariadb-admin shutdown and once with kill -TERM. The identical lines in the middle are shortened to ....
===== 3. shutdown with mariadb-admin (COM_SHUTDOWN) =====
2026-11-07 3:43:27 0 [Note] mariadbd (initiated by: root[root] @ localhost []): Normal shutdown
2026-11-07 3:43:27 0 [Note] InnoDB: FTS optimize thread exiting.
2026-11-07 3:43:27 0 [Note] InnoDB: Starting shutdown...
...
2026-11-07 3:43:27 0 [Note] Shutdown completed; log sequence number 52261; transaction id 15
2026-11-07 3:43:27 0 [Note] mariadbd: Shutdown complete
===== 4. shutdown with kill -TERM (signal_hand thread) =====
2026-11-07 3:43:29 0 [Note] mariadbd (initiated by: unknown): Normal shutdown
...
2026-11-07 3:43:29 0 [Note] Shutdown completed; log sequence number 52261; transaction id 15
2026-11-07 3:43:29 0 [Note] mariadbd: Shutdown complete
The two logs differ only in the initiated by: of the first line: COM_SHUTDOWN leaves root[root] @ localhost [], the signal leaves unknown. The rest is the same, because after the two doors meet at break_connect_loop() the path is one. InnoDB: Starting shutdown... is printed in logs_empty_and_mark_files_at_shutdown(), called by innodb_shutdown() (log0log.cc lines 2106–2112), which also kicks off the buffer pool dump with buf_dump_start() at line 2129 (the dump messages themselves come from buf_dump() in buf/buf0dump.cc). The function's comment reads "Make a checkpoint at the latest lsn on shutdown". That is why there is nothing to recover at the next start after a clean shutdown; what happens when kill -9 skips this step is covered in part 12.
The backtrace from the second run of example 5, stopped at innodb_shutdown:
Thread 1 "mariadbd" hit Breakpoint 1, innodb_shutdown () at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:2105
2105 ut_ad(!srv_read_only_mode || recv_sys.rpo);
#0 innodb_shutdown () at /usr/src/mariadb/storage/innobase/srv/srv0start.cc:2105
#1 0x0000555556a6ab83 in innobase_end () at /usr/src/mariadb/storage/innobase/handler/ha_innodb.cc:4542
#2 0x000055555661e4a2 in ha_finalize_handlerton (plugin_=0x555558c6d3b0) at /usr/src/mariadb/sql/handler.cc:603
#3 0x00005555561b6a07 in plugin_deinitialize (plugin=0x555558c6d3b0, ref_check=true) at /usr/src/mariadb/sql/sql_plugin.cc:1311
#4 0x00005555561b6f4b in reap_plugins () at /usr/src/mariadb/sql/sql_plugin.cc:1382
#5 0x00005555561b9d68 in plugin_shutdown () at /usr/src/mariadb/sql/sql_plugin.cc:2162
#6 0x0000555555f8cb02 in clean_up (print_message=true, use_dummy_thd=true) at /usr/src/mariadb/sql/mysqld.cc:2183
#7 0x0000555555f96e20 in mysqld_main (argc=11, argv=0x5555589d3f60) at /usr/src/mariadb/sql/mysqld.cc:6616
#8 0x0000555555f88d2d in main (argc=1, argv=0x7fffffffed38) at /usr/src/mariadb/sql/main.cc:34
From bottom to top: mysqld_main (line 6616, clean_up()) → clean_up (line 2183, plugin_shutdown()) → plugin_shutdown (line 2162, reap_plugins()) → reap_plugins → plugin_deinitialize (line 1311) → ha_finalize_handlerton (line 603, hton->panic) → innobase_end → innodb_shutdown. It pairs up with section 5's startup backtrace (plugin_init → plugin_do_initialize → ha_initialize_handlerton → innodb_init).
What this post leaves out
performance_schemainitialization early inmysqld_main()(pre_initialize_performance_schema(),initialize_performance_schema()): mysqld.cc lines 6199–6314--bootstrapmode (the pathmariadb-install-dbuses to create system tables) and--init-file: lines 6512–6539;bootstrap()is insql/sql_parse.cc- Starting as root and dropping to
user=(check_user(),set_user()): lines 6391–6399 ddl_log_initialize(), DDL recovery andsignal_ddl_recovery_done: mysqld.cc lines 5602–5603,sql/ddl_log.cc- Opening the binlog and
tc_log(the two-phase commit coordinator): around lines 5342–5590 ofinit_server_components()and from line 5897 — part 12 - Starting replication (
init_slave()): lines 6530–6533 — part 12 - The Windows startup path (
mysqld_win_main(),handle_connections_win()):sql/winmain.cc,sql/handle_connections_win.cc UNINSTALL PLUGIN(mysql_uninstall_plugin()) and plugin locking (plugin_lock()/plugin_unlock(),ref_count):sql/sql_plugin.cc- Plugin services (
list_of_services, the table through which plugins call server functions):sql/sql_plugin_services.inl,plugin_dl_add()lines 816–837 - Initializing schedulers such as the thread pool (
thread_scheduler->initat line 2918 innetwork_init()): part 4
Summary
| Concept | In one line |
|---|---|
main() |
Just calls mysqld_main(); the server body is a library |
| Startup order | options → block signals → init_server_components() (error log, plugin_init(), leftover option check) → TLS and sockets → signal thread → privileges → ready for connections. → poll() |
load_defaults |
Inserts the my.cnf groups as --x=y before argv, with file markers recording the origin |
| Four option passes | early → get_options() → test_plugin_options() per plugin → leftover check. Unknown options are passed along to the end |
| The price of a typo | The leftover check comes after plugin_init(), so unknown variable stops startup only after InnoDB is fully up. --validate-config is faster |
sys_var |
One global object = one option = one global variable. my.cnf, the command line and SET GLOBAL use the same memory |
st_maria_plugin |
A plugin's declaration. maria_declare_plugin expands to builtin_maria_<name>_plugin when static, _maria_plugin_declarations_ when dynamic |
plugin_init() |
register all → MyISAM first → initialize type by type → read mysql.plugin → one more pass → clean up failures |
FORCE/ON |
The mandatory array is FORCE (cannot be turned off), the optional array ON (turned off with --skip-<name>) |
ha_initialize_handlerton() |
The server creates a zero-filled handlerton and the engine's init(hton) fills the function pointers. The server assigns slot and db_type |
innodb_init() |
Fills hton and calls srv_start() — buffer pool, redo and recovery happen here, before the sockets open |
| Signals | Blocked in every thread; one signal_hand thread receives them with sigwait |
| Shutdown | break_connect_loop() wakes poll() through a pipe, then the main thread cleans up in reverse: connections → plugin_shutdown() → hton->panic |
What's next
At the end of this post the main thread was waiting in poll() for the first connection. When one arrives, the accept()ed socket goes through handle_accepted_socket() and either gets a thread of its own (the default, one-thread-per-connection) or joins a thread pool group. A THD is created for each connection, and an authentication plugin checks the password. It continues in part 4: Connections and Threads — One Thread per Connection, and the Thread Pool.
Compared with the earlier series: httpd in httpd part 5 read its configuration twice inside main() and handled a restart as "one more round of a loop that re-reads the configuration from scratch". MariaDB reads its configuration once and, with no notion of a restart, changes only what can be changed through SET GLOBAL. Compared with modules in httpd part 6, there is a lot in common: a build-generated static list (ap_prelinked_modules[] ↔ mysql_mandatory_plugins[]), a way to open a .so later (LoadModule ↔ INSTALL SONAME/--plugin-load-add), and a module attaching functions to hook arrays in register_hooks() versus an engine filling the function pointers of its handlerton in init(). The difference is that an httpd hook is an array where many modules line up at one spot, while a handlerton is a table, one per engine. In Tomcat part 3 every component followed the Lifecycle state machine and the shutdown command came over port 8005; MariaDB's plugin states (UNINITIALIZED → READY → DELETED → DYING) are much simpler, and the shutdown command comes over an SQL connection (COM_SHUTDOWN) or as a Unix signal.
- Part 1: How I Read It with AI, and the Map
- Part 2: The Big Picture — the Server Layer, Storage Engines, and the Life of One Query
- Part 7: Preparation — Checking Privileges, Opening Tables, Resolving Names
- Part 10: InnoDB Part 1 — Pages, the Buffer Pool and B-trees
- Part 12: Two Logs — the Redo Log, the Binlog and Crash Recovery, and Wrapping Up