网络初始化
MYSQL 在 main 函数中通过调用 network_init 初始化网络。
cpp
static bool network_init(void) {
if (opt_initialize) return false;
#ifdef HAVE_SYS_UN_H
std::string const unix_sock_name(mysqld_unix_port ? mysqld_unix_port : "");
#else
std::string const unix_sock_name("");
#endif
std::list<Bind_address_info> bind_addresses_info;
if (!opt_disable_networking || unix_sock_name != "") {
if (my_bind_addr_str != nullptr &&
check_bind_address_has_valid_value(my_bind_addr_str,
&bind_addresses_info)) {
LogErr(ERROR_LEVEL, ER_INVALID_VALUE_OF_BIND_ADDRESSES, my_bind_addr_str);
return true;
}
Bind_address_info admin_address_info;
if (!opt_disable_networking) {
if (my_admin_bind_addr_str != nullptr &&
check_admin_address_has_valid_value(my_admin_bind_addr_str,
&admin_address_info)) {
LogErr(ERROR_LEVEL, ER_INVALID_ADMIN_ADDRESS, my_admin_bind_addr_str);
return true;
}
/*
Port 0 is interpreted by implementations of TCP protocol
as a hint to find a first free port value to use and bind to it.
On the other hand, the option mysqld_admin_port can be assigned
the value 0 if a user specified a value that is out of allowable
range of values. Therefore, to avoid a case when an operating
system binds admin interface to am arbitrary selected port value,
set it explicitly to the value MYSQL_ADMIN_PORT in case it has value 0.
*/
if (mysqld_admin_port == 0) mysqld_admin_port = MYSQL_ADMIN_PORT;
}
Mysqld_socket_listener *mysqld_socket_listener = new (std::nothrow)
Mysqld_socket_listener(bind_addresses_info, mysqld_port,
admin_address_info, mysqld_admin_port,
admin_address_info.address.empty()
? false
: listen_admin_interface_in_separate_thread,
back_log, mysqld_port_timeout, unix_sock_name);
if (mysqld_socket_listener == nullptr) return true;
mysqld_socket_acceptor = new (std::nothrow)
Connection_acceptor<Mysqld_socket_listener>(mysqld_socket_listener);
if (mysqld_socket_acceptor == nullptr) {
delete mysqld_socket_listener;
mysqld_socket_listener = nullptr;
return true;
}
if (mysqld_socket_acceptor->init_connection_acceptor())
return true; // mysqld_socket_acceptor would be freed in unireg_abort.
if (report_port == 0) report_port = mysqld_port;
if (!opt_disable_networking) assert(report_port != 0);
}
#ifdef _WIN32
// Create named pipe
if (opt_enable_named_pipe) {
const std::string pipe_name = mysqld_unix_port ? mysqld_unix_port : "";
named_pipe_listener = new (std::nothrow) Named_pipe_listener(&pipe_name);
if (named_pipe_listener == nullptr) return true;
named_pipe_acceptor = new (std::nothrow)
Connection_acceptor<Named_pipe_listener>(named_pipe_listener);
if (named_pipe_acceptor == nullptr) {
delete named_pipe_listener;
named_pipe_listener = nullptr;
return true;
}
if (named_pipe_acceptor->init_connection_acceptor())
return true; // named_pipe_acceptor would be freed in unireg_abort.
}
// Setup shared_memory acceptor
if (opt_enable_shared_memory) {
const std::string shared_mem_base_name =
shared_memory_base_name ? shared_memory_base_name : "";
Shared_mem_listener *shared_mem_listener =
new (std::nothrow) Shared_mem_listener(&shared_mem_base_name);
if (shared_mem_listener == nullptr) return true;
shared_mem_acceptor = new (std::nothrow)
Connection_acceptor<Shared_mem_listener>(shared_mem_listener);
if (shared_mem_acceptor == nullptr) {
delete shared_mem_listener;
shared_mem_listener = nullptr;
return true;
}
if (shared_mem_acceptor->init_connection_acceptor())
return true; // shared_mem_acceptor would be freed in unireg_abort.
}
#endif // _WIN32
return false;
}
这段代码主要负责MySQL服务器启动时的网络服务初始化,包括:
-
解析绑定地址配置
-
创建和管理各种连接监听器
-
初始化连接接收器
Unix域套接字处理
cpp
#ifdef HAVE_SYS_UN_H
std::string const unix_sock_name(mysqld_unix_port ? mysqld_unix_port : "");
-
只在支持Unix域套接字的系统上(Linux/Unix)启用Unix套接字
-
mysqld_unix_port指定套接字文件路径,如/tmp/mysql.sock
TCP/IP网络服务初始化
解析绑定地址
cpp
if (!opt_disable_networking || unix_sock_name != "") {
if (my_bind_addr_str != nullptr &&
check_bind_address_has_valid_value(my_bind_addr_str,
&bind_addresses_info)) {
LogErr(ERROR_LEVEL, ER_INVALID_VALUE_OF_BIND_ADDRESSES, my_bind_addr_str);
return true;
}
-
进行必要的前置条件检查
- 检查是否禁用网络(
opt_disable_networking) - 检查是否开启了 unix 套接字
- 检查是否禁用网络(
-
如果满足前置条件,则调用 check_bind_address_has_valid_value 函数,对 bind-address 就行参数检验,并将其转化为
bind_addresses_info 结构。
cpp
/**
Check acceptable value(s) of parameter bind-address
@param bind_address Value of the parameter bind-address
@param[out] valid_bind_addresses List of addresses to listen and their
corresponding network namespaces if set.
@return false on success, true on failure
*/
static bool check_bind_address_has_valid_value(
const char *bind_address,
std::list<Bind_address_info> *valid_bind_addresses) {
if (strlen(bind_address) == 0)
// Empty value for bind_address is an error
return true;
const char *comma_separator = strchr(bind_address, ',');
const char *begin_of_value = bind_address;
const bool multiple_bind_addresses = (comma_separator != nullptr);
if (comma_separator == begin_of_value)
// Return an error if a value of bind_address begins with comma
return true;
while (comma_separator != nullptr) {
/*
Wildcard value is not allowed in case multi-addresses value specified
for the option bind-address.
*/
// 检查当前片段是否为通配符('*' 或 '0.0.0.0' 或 '::')
if (check_address_is_wildcard(begin_of_value,
comma_separator - begin_of_value)) {
LogErr(ERROR_LEVEL, ER_WILDCARD_NOT_ALLOWED_FOR_MULTIADDRESS_BIND);
return true;
}
// 将当前片段转换为 Bind_address_info 并加入列表
if (create_bind_address_info_from_string(begin_of_value, comma_separator,
valid_bind_addresses))
return true;
// 移动到下一个片段的开始
begin_of_value = comma_separator + 1;
comma_separator = strchr(begin_of_value, ',');
if (comma_separator == begin_of_value)
// Return an error if a value of bind_address has two adjacent commas
return true;
}
/*
Wildcard value is not allowed in case multi-addresses value specified
for the option bind-address.
*/
if (multiple_bind_addresses &&
(check_address_is_wildcard(begin_of_value, strlen(begin_of_value)) ||
strlen(begin_of_value) == 0))
return true;
if (create_bind_address_info_from_string(begin_of_value, comma_separator,
valid_bind_addresses))
return true;
return false;
}
MySQL 8.0 开始支持 bind-address 接受多个逗号分隔的地址,允许服务器同时监听多个不同的 IP 地址(例如同时监听 IPv4 和 IPv6 特定地址)。这个 check_bind_address_has_valid_value 函数就是为了验证和解析这种多地址格式,同时保证语义的正确性:
-
空字符串检查: 如果
bind-address被设置为空字符串,直接返回true(失败)。因为绑定地址不能为空。 -
初始化指针和标志
-
comma_separator指向字符串中第一个逗号的位置,若不存在则为nullptr。 -
begin_of_value用于遍历时指向当前待解析片段的起始位置。 -
multiple_bind_addresses标记是否包含多个地址(即是否有逗号)。
-
-
禁止以逗号开头: 如果第一个字符就是逗号(例如
",192.168.1.1"),说明格式错误,直接失败。 -
while 循环解析每个逗号分隔的片段:
-
通配符禁止 :在多地址模式下,不允许使用通配符
*、0.0.0.0或::。因为通配符表示"监听所有接口",与多地址的精确指定冲突。 -
创建地址信息【重要】 :调用
create_bind_address_info_from_string函数解析单个地址片段,并处理可能附带的网络命名空间(格式如"eth0:192.168.1.1"或"@namespace:address")。 -
连续逗号检测:如果解析完一个片段后下一个字符又是逗号(即空片段),则报错。
-
-
处理最后一个片段(或单地址情况)
-
如果是多地址模式,最后一个片段同样不能是通配符,也不能是空字符串(例如以逗号结尾的情况已在循环中捕获)。
-
最后,将最后一个片段(或整个单地址)转换为
Bind_address_info并入列表。
-
管理端口处理
cpp
if (mysqld_admin_port == 0) mysqld_admin_port = MYSQL_ADMIN_PORT;
-
管理员端口特殊处理:端口0表示系统分配,这里强制设为默认管理端口
-
管理员连接用于特殊管理命令,与普通业务连接隔离
创建套接字监听器
cpp
Mysqld_socket_listener *mysqld_socket_listener = new (std::nothrow)
Mysqld_socket_listener(bind_addresses_info, mysqld_port,
admin_address_info, mysqld_admin_port,
admin_address_info.address.empty()
? false
: listen_admin_interface_in_separate_thread,
back_log, mysqld_port_timeout, unix_sock_name);
///////////////////////////////////////////////////////////////////////////
// Mysqld_socket_listener implementation
///////////////////////////////////////////////////////////////////////////
Mysqld_socket_listener::Mysqld_socket_listener(
const std::list<Bind_address_info> &bind_addresses, uint tcp_port,
const Bind_address_info &admin_bind_addr, uint admin_tcp_port,
bool use_separate_thread_for_admin, uint backlog, uint port_timeout,
std::string unix_sockname)
: m_bind_addresses(bind_addresses),
m_admin_bind_address(admin_bind_addr),
m_tcp_port(tcp_port),
m_admin_tcp_port(admin_tcp_port),
m_use_separate_thread_for_admin(use_separate_thread_for_admin),
m_backlog(backlog),
m_port_timeout(port_timeout),
m_unix_sockname(unix_sockname),
m_unlink_sockname(false),
m_admin_interface_listen_socket(mysql_socket_invalid()) {
#ifdef HAVE_LIBWRAP
/*
Set up syslog parameters on behalf of the TCP-wrappers.
The loadable component that logs server errors to syslog
may re-open it with user-defined attributes (logging of
PIDS / ident) later, but we establish a sensible baseline
here in case that log-sink is not used. Note that the
wrapper is hard-coded to use LOG_AUTH in the syslog()
call below, which lets the wrapper log to a different
facility than the rest of the server (the facility of
which defaults to LOG_DAEMON and is user-configurable)
if desired.
*/
libwrap_name = my_progname + dirname_length(my_progname);
openlog(libwrap_name, LOG_PID, LOG_AUTH);
#endif /* HAVE_LIBWRAP */
}
创建 Mysqld_socket_listener 对象,构造器参数如下:
|---------------------------------|--------------------------------|------------------------|
| 构造器参数名 | 类型 | |
| m_bind_addresses | std::list<Bind_address_info> | 业务连接绑定地址列表 |
| m_tcp_port | uint | 业务端口(默认3306) |
| m_admin_bind_address | Bind_address_info | 管理连接绑定地址 |
| m_admin_tcp_port | uint | 管理端口(默认33062) |
| m_use_separate_thread_for_admin | bool | 是否在独立线程监听管理接口 |
| m_backlog | uint | 连接待处理队列大小 |
| m_port_timeout | uint | 端口绑定超时 |
| m_unix_sockname | std::string | Unix套接字路径 |
| m_unlink_sockname | bool | |
| m_admin_interface_listen_socket | MYSQL_SOCKET | |
| m_poll_info | poll_info_t | 条件编译,当支持poll时候才启用这个参数 |
| m_select_info | select_info_t | 条件编译,当不支持poll时候才启用这个参数 |
创建连接接收器
cpp
mysqld_socket_acceptor = new (std::nothrow)
Connection_acceptor<Mysqld_socket_listener>(mysqld_socket_listener);
Connection_acceptor 是一个模板类,它提供了一种统一的接口 来管理不同类型的网络监听器,并运行一个连接事件循环。它的设计目的是将"监听并接受连接"这一通用流程与具体的监听器实现(如 TCP、Unix 套接字、Windows 命名管道等)解耦。
cpp
/**
This class presents a generic interface to initialize and run
a connection event loop for different types of listeners and
a callback functor to call on the connection event from the
listener that listens for connection. Listener type should
be a class providing methods setup_listener, listen_for_
connection_event and close_listener. The Connection event
callback functor object would on receiving connection event
from the client to process the connection.
*/
template <typename Listener>
class Connection_acceptor {
Listener *m_listener;
public:
Connection_acceptor(Listener *listener) : m_listener(listener) {}
~Connection_acceptor() { delete m_listener; }
/**
Initialize a connection acceptor.
@retval return true if initialization failed, else false.
*/
bool init_connection_acceptor() { return m_listener->setup_listener(); }
/**
Connection acceptor loop to accept connections from clients.
*/
void connection_event_loop() {
Connection_handler_manager *mgr =
Connection_handler_manager::get_instance();
while (!connection_events_loop_aborted()) {
Channel_info *channel_info = m_listener->listen_for_connection_event();
if (channel_info != nullptr) mgr->process_new_connection(channel_info);
}
}
/**
Spawn admin connection handler to accept admin connections from clients if
create-admin-listener-thread is specified by user on commandline.
@return true unable to spawn admin connection handler thread else false.
*/
bool check_and_spawn_admin_connection_handler_thread() const {
return m_listener->check_and_spawn_admin_connection_handler_thread();
}
/**
Close the listener.
*/
void close_listener() { m_listener->close_listener(); }
};
初始化套接字连接器
cpp
if (mysqld_socket_acceptor->init_connection_acceptor())
return true; // mysqld_socket_acceptor would be freed in unireg_abort.
这一行代码,在底层涉及的比较多,单独拆出来讲解。
cpp
template <typename Listener>
class Connection_acceptor {
Listener *m_listener;
bool init_connection_acceptor() { return m_listener->setup_listener(); }
}
在 Connection_acceptor 的 init_connection_acceptor 函数声明,其具体实现委托为了套接字监听器的 setup_listener 方法。
我们直接去看 Mysqld_socket_listener 的 setup_listener 实现。
cpp
bool Mysqld_socket_listener::setup_listener() {
/*
It's matter to add a socket for admin connection listener firstly,
before listening sockets for other connection types be added.
It is done in order to check availability of new incoming connection
on admin interface with higher priority than on other interfaces..
*/
if (!m_admin_bind_address.address.empty()) {
TCP_socket tcp_socket(m_admin_bind_address.address,
m_admin_bind_address.network_namespace,
m_admin_tcp_port, m_backlog, m_port_timeout);
MYSQL_SOCKET mysql_socket = tcp_socket.get_listener_socket();
if (mysql_socket.fd == INVALID_SOCKET) return true;
m_admin_interface_listen_socket = mysql_socket;
if (!m_use_separate_thread_for_admin) {
m_socket_vector.emplace_back(mysql_socket, Socket_type::TCP_SOCKET,
&m_admin_bind_address.network_namespace,
Socket_interface_type::ADMIN_INTERFACE);
}
}
// Setup tcp socket listener
if (m_tcp_port) {
for (const auto &bind_address_info : m_bind_addresses) {
TCP_socket tcp_socket(bind_address_info.address,
bind_address_info.network_namespace, m_tcp_port,
m_backlog, m_port_timeout);
MYSQL_SOCKET mysql_socket = tcp_socket.get_listener_socket();
if (mysql_socket.fd == INVALID_SOCKET) return true;
m_socket_vector.emplace_back(mysql_socket, Socket_type::TCP_SOCKET,
&bind_address_info.network_namespace,
Socket_interface_type::DEFAULT_INTERFACE);
}
}
#if defined(HAVE_SYS_UN_H)
// Setup unix socket listener
if (m_unix_sockname != "") {
Unix_socket unix_socket(&m_unix_sockname, m_backlog);
MYSQL_SOCKET mysql_socket = unix_socket.get_listener_socket();
if (mysql_socket.fd == INVALID_SOCKET) return true;
Listen_socket s(mysql_socket, Socket_type::UNIX_SOCKET);
m_socket_vector.push_back(s);
m_unlink_sockname = true;
}
#endif /* HAVE_SYS_UN_H */
setup_connection_events(m_socket_vector);
return false;
}
Mysqld_socket_listener::setup_listener() 是 MySQL 服务器网络初始化过程中最核心的函数之一,它负责实际创建并配置所有监听套接字,为后续接受客户端连接做好准备。下面我将详细解析这个函数的每一步。
该函数依次处理三种类型的监听器(按顺序):
-
管理接口(admin interface,如果配置了独立地址)
-
普通 TCP 接口(业务端口,默认 3306,可能绑定多个 IP)
-
Unix 域套接字(仅当系统支持且路径非空)
函数执行完毕后,所有成功的监听套接字会被收集到 m_socket_vector 中,并最终通过 setup_connection_events() 注册到 I/O 多路复用机制(如 poll 或 select),使服务器能够响应连接请求。
我们主要关注普通 TCP 端口部分:
// Setup tcp socket listener
if (m_tcp_port) {
for (const auto &bind_address_info : m_bind_addresses) {
TCP_socket tcp_socket(bind_address_info.address,
bind_address_info.network_namespace, m_tcp_port,
m_backlog, m_port_timeout);
MYSQL_SOCKET mysql_socket = tcp_socket.get_listener_socket();
if (mysql_socket.fd == INVALID_SOCKET) return true;
m_socket_vector.emplace_back(mysql_socket, Socket_type::TCP_SOCKET,
&bind_address_info.network_namespace,
Socket_interface_type::DEFAULT_INTERFACE);
}
}
m_bind_addresses 参数是在实例化 Mysqld_socket_listener 传入的,他是由启动参数 bind-address 转化出来的一个 std::list<Bind_address_info>。
- 循环 m_bind_addresses ,该参数是在实例化 Mysqld_socket_listener 传入的,他是由启动参数 bind-address 转化出来的一个
std::list<Bind_address_info>。 - 构造 TCP_socket
- 由 TCP_socket 衍生出 MYSQL_SOCKET ,你就把 MYSQL_SOCKET 相像为一个由 TCP_socket 和 性能模式组合起来的对象
TCP_socket
cpp
/**
TCP_socket class represents the TCP sockets abstraction. It provides
the get_listener_socket that setup a TCP listener socket to listen.
*/
class TCP_socket {
std::string m_bind_addr_str; // IP address as string.
std::string m_network_namespace; // Network namespace if specified
uint m_tcp_port; // TCP port to bind to
uint m_backlog; // Backlog length for queue of pending connections.
uint m_port_timeout; // Port timeout
MYSQL_SOCKET create_socket(const struct addrinfo *addrinfo_list,
int addr_family, struct addrinfo **use_addrinfo) {
for (const struct addrinfo *cur_ai = addrinfo_list; cur_ai != nullptr;
cur_ai = cur_ai->ai_next) {
if (cur_ai->ai_family != addr_family) continue;
MYSQL_SOCKET sock =
mysql_socket_socket(key_socket_tcpip, cur_ai->ai_family,
cur_ai->ai_socktype, cur_ai->ai_protocol);
char ip_addr[INET6_ADDRSTRLEN];
if (vio_getnameinfo(cur_ai->ai_addr, ip_addr, sizeof(ip_addr), nullptr, 0,
NI_NUMERICHOST)) {
ip_addr[0] = 0;
}
if (mysql_socket_getfd(sock) == INVALID_SOCKET) {
LogErr(ERROR_LEVEL, ER_CONN_TCP_NO_SOCKET,
(addr_family == AF_INET) ? "IPv4" : "IPv6",
(const char *)ip_addr, (int)socket_errno);
} else {
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_CREATED, (const char *)ip_addr);
*use_addrinfo = const_cast<addrinfo *>(cur_ai);
return sock;
}
}
return MYSQL_INVALID_SOCKET;
}
public:
/**
Constructor that takes tcp port and ip address string and other
related parameters to set up listener tcp to listen for connection
events.
@param bind_addr_str ip address as string value.
@param network_namespace_str network namespace as string value
@param tcp_port tcp port number.
@param backlog backlog specifying length of pending connection queue.
@param port_timeout port timeout value
*/
TCP_socket(std::string bind_addr_str, std::string network_namespace_str,
uint tcp_port, uint backlog, uint port_timeout)
: m_bind_addr_str(bind_addr_str),
m_network_namespace(network_namespace_str),
m_tcp_port(tcp_port),
m_backlog(backlog),
m_port_timeout(port_timeout) {}
/**
Set up a listener to listen for connection events.
@retval valid socket if successful else MYSQL_INVALID_SOCKET on failure.
*/
MYSQL_SOCKET get_listener_socket() {
const char *bind_address_str = nullptr;
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_ADDRESS, m_bind_addr_str.c_str(),
m_tcp_port);
// Get list of IP-addresses associated with the bind-address.
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC;
char port_buf[NI_MAXSERV];
snprintf(port_buf, NI_MAXSERV, "%d", m_tcp_port);
if (!m_network_namespace.empty()) {
#ifdef HAVE_SETNS
if (set_network_namespace(m_network_namespace))
return MYSQL_INVALID_SOCKET;
#else
LogErr(ERROR_LEVEL, ER_NETWORK_NAMESPACES_NOT_SUPPORTED);
return MYSQL_INVALID_SOCKET;
#endif
}
// Create a RAII guard for addrinfo struct.
AddrInfoPtr ai_ptr{nullptr};
if (native_strcasecmp(m_bind_addr_str.c_str(), MY_BIND_ALL_ADDRESSES) ==
0) {
/*
That's the case when bind-address is set to a special value ('*'),
meaning "bind to all available IP addresses". If the box supports
the IPv6 stack, that means binding to '::'. If only IPv4 is available,
bind to '0.0.0.0'.
*/
bool ipv6_available = false;
ai_ptr = GetAddrInfoPtr(ipv6_all_addresses, port_buf, &hints);
if (ai_ptr) {
/*
IPv6 might be available (the system might be able to resolve an IPv6
address, but not be able to create an IPv6-socket). Try to create a
dummy IPv6-socket. Do not instrument that socket by P_S.
*/
const MYSQL_SOCKET s = mysql_socket_socket(0, AF_INET6, SOCK_STREAM, 0);
ipv6_available = mysql_socket_getfd(s) != INVALID_SOCKET;
if (ipv6_available) mysql_socket_close(s);
}
if (ipv6_available &&
DBUG_EVALUATE_IF("sim_ipv6_unavailable", false, true)) {
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_IPV6_AVAILABLE);
// Address info (ai) for IPv6 address is already set.
bind_address_str = ipv6_all_addresses;
} else {
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_IPV6_UNAVAILABLE);
// Retrieve address info (ai) for IPv4 address.
ai_ptr = GetAddrInfoPtr(ipv4_all_addresses, port_buf, &hints);
if (!ai_ptr) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)msg_buff, sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, strerror(errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_CANT_RESOLVE_HOSTNAME);
#ifdef HAVE_SETNS
if (!m_network_namespace.empty())
(void)restore_original_network_namespace();
#endif
return MYSQL_INVALID_SOCKET;
} // !ai_ptr
bind_address_str = ipv4_all_addresses;
}
} else {
ai_ptr = GetAddrInfoPtr(m_bind_addr_str.c_str(), port_buf, &hints);
if (!ai_ptr) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)msg_buff, sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, strerror(errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_CANT_RESOLVE_HOSTNAME);
#ifdef HAVE_SETNS
if (!m_network_namespace.empty())
(void)restore_original_network_namespace();
#endif
return MYSQL_INVALID_SOCKET;
} // !ai_ptr
bind_address_str = m_bind_addr_str.c_str();
}
// Log all the IP-addresses
for (struct addrinfo *cur_ai = ai_ptr.get(); cur_ai != nullptr;
cur_ai = cur_ai->ai_next) {
char ip_addr[INET6_ADDRSTRLEN];
if (vio_getnameinfo(cur_ai->ai_addr, ip_addr, sizeof(ip_addr), nullptr, 0,
NI_NUMERICHOST)) {
LogErr(ERROR_LEVEL, ER_CONN_TCP_IP_NOT_LOGGED);
continue;
}
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_RESOLVE_INFO, bind_address_str,
ip_addr);
}
/*
If the 'bind-address' option specifies the hostname, which resolves to
multiple IP-address, use the following rule:
- if there are IPv4-addresses, use the first IPv4-address
returned by getaddrinfo();
- if there are IPv6-addresses, use the first IPv6-address
returned by getaddrinfo();
*/
struct addrinfo *a = nullptr;
MYSQL_SOCKET listener_socket = create_socket(ai_ptr.get(), AF_INET, &a);
if (mysql_socket_getfd(listener_socket) == INVALID_SOCKET)
listener_socket = create_socket(ai_ptr.get(), AF_INET6, &a);
#ifdef HAVE_SETNS
if (!m_network_namespace.empty() && restore_original_network_namespace())
return MYSQL_INVALID_SOCKET;
#endif
// Report user-error if we failed to create a socket.
if (mysql_socket_getfd(listener_socket) == INVALID_SOCKET) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, strerror(errno));
#endif
return MYSQL_INVALID_SOCKET;
}
mysql_socket_set_thread_owner(listener_socket);
#ifndef _WIN32
/*
We should not use SO_REUSEADDR on windows as this would enable a
user to open two mysqld servers with the same TCP/IP port.
*/
{
int option_flag = 1;
(void)mysql_socket_setsockopt(listener_socket, SOL_SOCKET, SO_REUSEADDR,
(char *)&option_flag, sizeof(option_flag));
}
#endif
#ifdef IPV6_V6ONLY
/*
For interoperability with older clients, IPv6 socket should
listen on both IPv6 and IPv4 wildcard addresses.
Turn off IPV6_V6ONLY option.
NOTE: this will work starting from Windows Vista only.
On Windows XP dual stack is not available, so it will not
listen on the corresponding IPv4-address.
*/
if (a->ai_family == AF_INET6) {
int option_flag = 0;
if (mysql_socket_setsockopt(listener_socket, IPPROTO_IPV6, IPV6_V6ONLY,
(char *)&option_flag, sizeof(option_flag))) {
LogErr(WARNING_LEVEL, ER_CONN_TCP_CANT_RESET_V6ONLY, (int)socket_errno);
}
}
#endif
/*
Sometimes the port is not released fast enough when stopping and
restarting the server. This happens quite often with the test suite
on busy Linux systems. Retry to bind the address at these intervals:
Sleep intervals: 1, 2, 4, 6, 9, 13, 17, 22, ...
Retry at second: 1, 3, 7, 13, 22, 35, 52, 74, ...
Limit the sequence by m_port_timeout (set --port-open-timeout=#).
*/
uint this_wait = 0;
int ret = 0;
for (uint waited = 0, retry = 1;; retry++, waited += this_wait) {
if (((ret = mysql_socket_bind(listener_socket, a->ai_addr,
a->ai_addrlen)) >= 0) ||
(socket_errno != SOCKET_EADDRINUSE) || (waited >= m_port_timeout))
break;
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_BIND_RETRY, mysqld_port);
this_wait = retry * retry / 3 + 1;
sleep(this_wait);
}
if (ret < 0) {
DBUG_PRINT("error", ("Got error: %d from bind", socket_errno));
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_BIND_FAIL, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_BIND_FAIL, strerror(socket_errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_IS_THERE_ANOTHER_USING_PORT, m_tcp_port);
mysql_socket_close(listener_socket);
return MYSQL_INVALID_SOCKET;
}
if (mysql_socket_listen(listener_socket, static_cast<int>(m_backlog)) < 0) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_START_FAIL, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_START_FAIL, strerror(errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_LISTEN_FAIL, socket_errno);
mysql_socket_close(listener_socket);
return MYSQL_INVALID_SOCKET;
}
#if !defined(NO_FCNTL_NONBLOCK)
(void)mysql_sock_set_nonblocking(listener_socket);
#endif
return listener_socket;
}
};
#if defined(HAVE_SYS_UN_H)
TCP_socket 类,它是对 TCP 监听套接字 的高级封装。它的核心任务是:根据给定的绑定地址、端口、网络命名空间等参数,创建一个已经完成 socket()、bind()、listen() 步骤的监听套接字 ,并返回一个 MYSQL_SOCKET 对象供上层使用。
成员变量
| 成员 | 类型 | 含义 |
|---|---|---|
m_bind_addr_str |
std::string |
绑定的 IP 地址字符串,如 "0.0.0.0"、"127.0.0.1" 或 "*"。 |
m_network_namespace |
std::string |
网络命名空间标识(仅在 Linux 且支持 setns 时有效),用于将套接字绑定到特定容器网络空间。 |
m_tcp_port |
uint |
要监听的 TCP 端口号。 |
m_backlog |
uint |
listen() 的 backlog 参数,即等待连接队列的最大长度。 |
m_port_timeout |
uint |
端口绑定超时时间(秒),用于端口被占用时的重试策略。 |
create_socket 辅助方法
cpp
MYSQL_SOCKET create_socket(const struct addrinfo *addrinfo_list,
int addr_family, struct addrinfo **use_addrinfo) {
for (const struct addrinfo *cur_ai = addrinfo_list; cur_ai != nullptr;
cur_ai = cur_ai->ai_next) {
if (cur_ai->ai_family != addr_family) continue;
MYSQL_SOCKET sock =
mysql_socket_socket(key_socket_tcpip, cur_ai->ai_family,
cur_ai->ai_socktype, cur_ai->ai_protocol);
char ip_addr[INET6_ADDRSTRLEN];
if (vio_getnameinfo(cur_ai->ai_addr, ip_addr, sizeof(ip_addr), nullptr, 0,
NI_NUMERICHOST)) {
ip_addr[0] = 0;
}
if (mysql_socket_getfd(sock) == INVALID_SOCKET) {
LogErr(ERROR_LEVEL, ER_CONN_TCP_NO_SOCKET,
(addr_family == AF_INET) ? "IPv4" : "IPv6",
(const char *)ip_addr, (int)socket_errno);
} else {
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_CREATED, (const char *)ip_addr);
*use_addrinfo = const_cast<addrinfo *>(cur_ai);
return sock;
}
}
return MYSQL_INVALID_SOCKET;
}
create_socket 函数是 TCP_socket 类的一个辅助方法,它的作用是在 getaddrinfo 返回的地址信息链表中,为指定的地址族(IPv4 或 IPv6)尝试创建套接字,并返回成功创建的第一个套接字,同时记录详细的日志。
-
遍历地址链表:
getaddrinfo返回的链表可能包含多个地址族(如同时包含 IPv4 和 IPv6 地址)。循环跳过与addr_family不匹配的条目,只处理指定地址族的地址。 -
创建套接字:
-
调用 MySQL 封装的套接字创建函数
mysql_socket_socket,它内部调用系统的socket(),并集成性能监控(Performance Schema)。 -
参数直接取自当前
addrinfo结构体的字段:地址族、套接字类型(SOCK_STREAM)、协议(通常为 0)。
-
-
获取 IP 地址字符串(用于日志)
-
vio_getnameinfo是 MySQL 对getnameinfo的封装,用于将套接字地址结构转换为可读的 IP 字符串。 -
如果转换失败,
ip_addr被置为空字符串,但仍会记录日志(可能显示空 IP)。
-
-
检查创建结果并记录日志
mysql_socket_getfd(sock) == INVALID_SOCKET-
创建失败:记录错误日志,说明是 IPv4 还是 IPv6 失败,以及失败的 IP 地址和系统错误码。
-
创建成功 :记录信息日志,显示成功创建的 IP 地址;将当前
addrinfo指针赋给输出参数use_addrinfo;并立即返回新套接字。
-
get_listener_socket 方法
这是 TCP_socket 的主入口,它执行了从地址解析到套接字最终可用的完整流程。
cpp
/**
Set up a listener to listen for connection events.
@retval valid socket if successful else MYSQL_INVALID_SOCKET on failure.
*/
MYSQL_SOCKET get_listener_socket() {
const char *bind_address_str = nullptr;
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_ADDRESS, m_bind_addr_str.c_str(),
m_tcp_port);
// Get list of IP-addresses associated with the bind-address.
struct addrinfo hints;
memset(&hints, 0, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_socktype = SOCK_STREAM;
hints.ai_family = AF_UNSPEC;
char port_buf[NI_MAXSERV];
snprintf(port_buf, NI_MAXSERV, "%d", m_tcp_port);
if (!m_network_namespace.empty()) {
#ifdef HAVE_SETNS
if (set_network_namespace(m_network_namespace))
return MYSQL_INVALID_SOCKET;
#else
LogErr(ERROR_LEVEL, ER_NETWORK_NAMESPACES_NOT_SUPPORTED);
return MYSQL_INVALID_SOCKET;
#endif
}
// Create a RAII guard for addrinfo struct.
AddrInfoPtr ai_ptr{nullptr};
if (native_strcasecmp(m_bind_addr_str.c_str(), MY_BIND_ALL_ADDRESSES) ==
0) {
/*
That's the case when bind-address is set to a special value ('*'),
meaning "bind to all available IP addresses". If the box supports
the IPv6 stack, that means binding to '::'. If only IPv4 is available,
bind to '0.0.0.0'.
*/
bool ipv6_available = false;
ai_ptr = GetAddrInfoPtr(ipv6_all_addresses, port_buf, &hints);
if (ai_ptr) {
/*
IPv6 might be available (the system might be able to resolve an IPv6
address, but not be able to create an IPv6-socket). Try to create a
dummy IPv6-socket. Do not instrument that socket by P_S.
*/
const MYSQL_SOCKET s = mysql_socket_socket(0, AF_INET6, SOCK_STREAM, 0);
ipv6_available = mysql_socket_getfd(s) != INVALID_SOCKET;
if (ipv6_available) mysql_socket_close(s);
}
if (ipv6_available &&
DBUG_EVALUATE_IF("sim_ipv6_unavailable", false, true)) {
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_IPV6_AVAILABLE);
// Address info (ai) for IPv6 address is already set.
bind_address_str = ipv6_all_addresses;
} else {
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_IPV6_UNAVAILABLE);
// Retrieve address info (ai) for IPv4 address.
ai_ptr = GetAddrInfoPtr(ipv4_all_addresses, port_buf, &hints);
if (!ai_ptr) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)msg_buff, sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, strerror(errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_CANT_RESOLVE_HOSTNAME);
#ifdef HAVE_SETNS
if (!m_network_namespace.empty())
(void)restore_original_network_namespace();
#endif
return MYSQL_INVALID_SOCKET;
} // !ai_ptr
bind_address_str = ipv4_all_addresses;
}
} else {
ai_ptr = GetAddrInfoPtr(m_bind_addr_str.c_str(), port_buf, &hints);
if (!ai_ptr) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)msg_buff, sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, strerror(errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_CANT_RESOLVE_HOSTNAME);
#ifdef HAVE_SETNS
if (!m_network_namespace.empty())
(void)restore_original_network_namespace();
#endif
return MYSQL_INVALID_SOCKET;
} // !ai_ptr
bind_address_str = m_bind_addr_str.c_str();
}
// Log all the IP-addresses
for (struct addrinfo *cur_ai = ai_ptr.get(); cur_ai != nullptr;
cur_ai = cur_ai->ai_next) {
char ip_addr[INET6_ADDRSTRLEN];
if (vio_getnameinfo(cur_ai->ai_addr, ip_addr, sizeof(ip_addr), nullptr, 0,
NI_NUMERICHOST)) {
LogErr(ERROR_LEVEL, ER_CONN_TCP_IP_NOT_LOGGED);
continue;
}
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_RESOLVE_INFO, bind_address_str,
ip_addr);
}
/*
If the 'bind-address' option specifies the hostname, which resolves to
multiple IP-address, use the following rule:
- if there are IPv4-addresses, use the first IPv4-address
returned by getaddrinfo();
- if there are IPv6-addresses, use the first IPv6-address
returned by getaddrinfo();
*/
struct addrinfo *a = nullptr;
MYSQL_SOCKET listener_socket = create_socket(ai_ptr.get(), AF_INET, &a);
if (mysql_socket_getfd(listener_socket) == INVALID_SOCKET)
listener_socket = create_socket(ai_ptr.get(), AF_INET6, &a);
#ifdef HAVE_SETNS
if (!m_network_namespace.empty() && restore_original_network_namespace())
return MYSQL_INVALID_SOCKET;
#endif
// Report user-error if we failed to create a socket.
if (mysql_socket_getfd(listener_socket) == INVALID_SOCKET) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_ERROR_WITH_STRERROR, strerror(errno));
#endif
return MYSQL_INVALID_SOCKET;
}
mysql_socket_set_thread_owner(listener_socket);
#ifndef _WIN32
/*
We should not use SO_REUSEADDR on windows as this would enable a
user to open two mysqld servers with the same TCP/IP port.
*/
{
int option_flag = 1;
(void)mysql_socket_setsockopt(listener_socket, SOL_SOCKET, SO_REUSEADDR,
(char *)&option_flag, sizeof(option_flag));
}
#endif
#ifdef IPV6_V6ONLY
/*
For interoperability with older clients, IPv6 socket should
listen on both IPv6 and IPv4 wildcard addresses.
Turn off IPV6_V6ONLY option.
NOTE: this will work starting from Windows Vista only.
On Windows XP dual stack is not available, so it will not
listen on the corresponding IPv4-address.
*/
if (a->ai_family == AF_INET6) {
int option_flag = 0;
if (mysql_socket_setsockopt(listener_socket, IPPROTO_IPV6, IPV6_V6ONLY,
(char *)&option_flag, sizeof(option_flag))) {
LogErr(WARNING_LEVEL, ER_CONN_TCP_CANT_RESET_V6ONLY, (int)socket_errno);
}
}
#endif
/*
Sometimes the port is not released fast enough when stopping and
restarting the server. This happens quite often with the test suite
on busy Linux systems. Retry to bind the address at these intervals:
Sleep intervals: 1, 2, 4, 6, 9, 13, 17, 22, ...
Retry at second: 1, 3, 7, 13, 22, 35, 52, 74, ...
Limit the sequence by m_port_timeout (set --port-open-timeout=#).
*/
uint this_wait = 0;
int ret = 0;
for (uint waited = 0, retry = 1;; retry++, waited += this_wait) {
if (((ret = mysql_socket_bind(listener_socket, a->ai_addr,
a->ai_addrlen)) >= 0) ||
(socket_errno != SOCKET_EADDRINUSE) || (waited >= m_port_timeout))
break;
LogErr(INFORMATION_LEVEL, ER_CONN_TCP_BIND_RETRY, mysqld_port);
this_wait = retry * retry / 3 + 1;
sleep(this_wait);
}
if (ret < 0) {
DBUG_PRINT("error", ("Got error: %d from bind", socket_errno));
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_BIND_FAIL, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_BIND_FAIL, strerror(socket_errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_IS_THERE_ANOTHER_USING_PORT, m_tcp_port);
mysql_socket_close(listener_socket);
return MYSQL_INVALID_SOCKET;
}
if (mysql_socket_listen(listener_socket, static_cast<int>(m_backlog)) < 0) {
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_TCP_START_FAIL, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_TCP_START_FAIL, strerror(errno));
#endif
LogErr(ERROR_LEVEL, ER_CONN_TCP_LISTEN_FAIL, socket_errno);
mysql_socket_close(listener_socket);
return MYSQL_INVALID_SOCKET;
}
#if !defined(NO_FCNTL_NONBLOCK)
(void)mysql_sock_set_nonblocking(listener_socket);
#endif
return listener_socket;
}
-
准备
addrinfohints: 设置getaddrinfo的提示,AF_UNSPEC表示允许返回任何地址族。- struct addrinfo hints;
- hints.ai_flags = AI_PASSIVE :用于绑定
- hints.ai_socktype = SOCK_STREAM:TCP相关
- hints.ai_family = AF_UNSPEC:允许 IPv4 或 IPv6
-
处理网络命名空间(Linux 特有): 如果指定了网络命名空间,调用
set_network_namespace()切换到该命名空间。这一步必须在调用getaddrinfo和创建套接字之前完成,因为地址解析和套接字创建都与当前网络命名空间相关。若切换失败,直接返回无效套接字。 -
地址解析(通配符
*特殊处理):将绑定的地址解析为 AddrInfoPtr 结构,这一步充分体现了 MySQL 对 IPv6 的智能检测和回退策略。-
如果是通配符
"*":-
先尝试获取 IPv6 通配地址
"::"的addrinfo(通过GetAddrInfoPtr(ipv6_all_addresses, ...))。 -
接着通过创建一个临时 IPv6 套接字来检测系统是否真正支持 IPv6 (因为
getaddrinfo可能成功但实际无法创建 IPv6 套接字)。 -
如果 IPv6 可用且未通过调试标志模拟不可用,则使用 IPv6 地址列表;否则回退到 IPv4 通配地址
"0.0.0.0",并重新获取 IPv4 的addrinfo。
-
-
如果是普通地址(非
*):- 直接调用
GetAddrInfoPtr(m_bind_addr_str.c_str(), ...)解析。
- 直接调用
-
-
记录所有解析出的 IP 地址:
对刚生成的 AddrInfoPtr 进行迭代,因为它底层是一个指向 addrinfo 的独占指针将 vio_
getaddrinfo(它是对系统 getnameinfo() 函数的封装,因为不同的操作系统 对 getnameinfo() 的实现有所不同,所以这里做了一个适配层)返回的所有 IP 地址(一个主机名可能对应多个 IP)记录到日志,便于调试。 -
尝试创建套接字
-
struct addrinfo *a = nullptr;
-
create_socket(ai_ptr.get(), AF_INET, &a)
-
create_socket(ai_ptr.get(), AF_INET6, &a)
-
遵循 优先使用 IPv4 的原则:如果 IPv4 创建失败,再尝试 IPv6。同时,
a被设置为实际使用的addrinfo结构,后续bind()需要用到其中的地址。
-
-
**恢复原始网络命名空间:**创建完套接字后,立即切换回原始命名空间,避免影响后续操作。
-
最终检查与设置套接字选项(通过 mysql_socket_setsockopt 方法设置)
-
如果套接字无效,记录错误并返回。
-
mysql_socket_set_thread_owner(listener_socket):标记套接字属于当前线程(用于性能模式)。 -
SO_REUSEADDR(非 Windows):允许端口重用,便于服务器快速重启。 -
IPV6_V6ONLY(仅 IPv6 套接字):默认可能启用该选项,导致 IPv6 套接字不能接收 IPv4 连接。MySQL 显式将其关闭(option_flag = 0),使得 IPv6 套接字可以同时处理 IPv4 连接(双栈),提高兼容性。
-
-
绑定并处理端口占用
-
mysql_socket_bind(listener_socket, a->ai_addr,a->ai_addrlen)
这是一个智能重试机制 :当
bind()返回EADDRINUSE(端口被占用)且未超时,会按照递增的间隔(1, 2, 4, 6, 9, 13, ... 秒)等待后重试,直到成功或超时。超时由m_port_timeout控制(对应--port-open-timeout系统变量)。这大大提高了在繁忙测试环境或快速重启场景下的成功率。 -
-
开始监听
-
mysql_socket_listen(listener_socket, static_cast<int>(m_backlog)
-
调用
listen()使套接字真正进入监听状态。
-
-
设置为非阻塞模式
-
mysql_sock_set_nonblocking(listener_socket);
-
将套接字设为非阻塞,以便与
poll/select等 I/O 多路复用机制配合使用。
-
-
返回
using AddrInfoPtr = std::unique_ptr<addrinfo, FreeAddrInfoDeleter>;这行代码定义了一个类型别名,用于管理
addrinfo结构体的生命周期。让我们分解它的每个部分:
using:C++11 引入的别名声明,等价于typedef,但更直观,可以为复杂类型起一个简短的名字。
AddrInfoPtr:新类型的名称,代表一个指向addrinfo的独占智能指针。
std::unique_ptr<addrinfo, FreeAddrInfoDeleter>:这是标准库的unique_ptr模板,具有两个模板参数:
addrinfo:指针指向的对象类型。addrinfo是 POSIX 网络编程中用于存储地址信息的结构体,通常由getaddrinfo()函数动态分配并返回一个链表。
FreeAddrInfoDeleter:自定义的删除器类型。unique_ptr默认使用delete释放内存,但addrinfo是由 C 函数getaddrinfo()分配的,必须用freeaddrinfo()来释放。因此需要提供一个自定义删除器,它会在unique_ptr析构时调用freeaddrinfo来正确释放资源。这样设计的目的:
RAII(资源获取即初始化) :将动态分配的
addrinfo链表的生命周期绑定到AddrInfoPtr对象。当AddrInfoPtr离开作用域时(例如函数返回或异常抛出),删除器自动调用freeaddrinfo,避免内存泄漏。异常安全 :在可能抛出异常的函数中,无需手动编写
freeaddrinfo的清理代码,智能指针会自动处理。代码简洁 :无需显式调用
freeaddrinfo,降低出错概率。
MYSQL_SOCKET
cpp
/**
An instrumented socket.
@c MYSQL_SOCKET is a replacement for @c my_socket.
*/
struct MYSQL_SOCKET {
/** The real socket descriptor. */
my_socket fd;
/**
The instrumentation hook.
Note that this hook is not conditionally defined,
for binary compatibility of the @c MYSQL_SOCKET interface.
*/
struct PSI_socket *m_psi;
};
成员变量
| 成员名称 | 类型 | 说明 | 作用 |
|---|---|---|---|
fd |
my_socket |
底层操作系统的套接字描述符。在 Unix/Linux 平台上通常为 int 类型的文件描述符,在 Windows 平台上为 SOCKET 类型(本质也是无符号整数)。my_socket 是 MySQL 为跨平台兼容而定义的类型别名。 |
标识实际的网络套接字句柄,用于调用系统级的套接字函数(如 send、recv、accept 等)。所有网络 I/O 操作最终都通过该成员所代表的套接字进行。 |
m_psi |
struct PSI_socket * |
指向性能模式(Performance Schema)套接字监控结构体的指针。PSI_socket 是 MySQL 内部用于收集和报告套接字相关性能数据的结构,其中记录了该套接字的 I/O 操作次数、字节数、等待时间、连接状态等统计信息。 |
将 MYSQL_SOCKET 对象与性能监控数据关联起来,使得服务器能够实时跟踪每个套接字的资源消耗情况,并通过 Performance Schema 表(如 socket_instances、socket_summary_by_event_name 等)向用户展示这些统计信息。此字段的存在保证了即使在不启用性能模式时,MYSQL_SOCKET 的内存布局也能保持稳定(二进制兼容)。 |
setup_connection_events
cpp
void Mysqld_socket_listener::setup_connection_events(
const socket_vector_t &socket_vector) {
#ifdef HAVE_POLL
const socket_vector_t::size_type total_number_of_addresses_to_bind =
socket_vector.size();
m_poll_info.m_fds.reserve(total_number_of_addresses_to_bind);
m_poll_info.m_pfs_fds.reserve(total_number_of_addresses_to_bind);
#endif
for (const auto &socket_element : socket_vector)
add_socket_to_listener(socket_element.m_socket);
}
struct poll_info_t {
std::vector<struct pollfd> m_fds;
std::vector<MYSQL_SOCKET> m_pfs_fds;
};
Mysqld_socket_listener::setup_connection_events 方法负责将已经创建好的所有监听套接字注册到 I/O 多路复用机制中,使它们能够被 listen_for_connection_event 监视并接受新连接。
-
条件编译与预分配内存:
-
HAVE_POLL:编译时宏,表示当前系统支持poll系统调用。如果定义,则使用poll作为 I/O 多路复用后端;否则回退到select。 -
total_number_of_addresses_to_bind:获取需要添加的监听套接字总数。 -
m_poll_info是一个内部数据结构,包含两个向量:-
m_fds:类型为std::vector<pollfd>,用于存储传递给poll()的pollfd结构数组。 -
m_pfs_fds:类型为std::vector<MYSQL_SOCKET>,用于存储对应的套接字对象,以便在事件发生时能够获取性能监控信息。
-
-
reserve的作用是提前分配足够的内存空间,避免在后续循环中多次添加元素时因容量不足而触发重新分配,从而提高性能。
-
-
循环注册套接字
-
遍历
socket_vector中的每一个元素(每个元素代表一个监听套接字)。 -
对每个套接字调用
add_socket_to_listener方法,将该套接字加入 I/O 多路复用的监听集合中。
-
cpp
void Mysqld_socket_listener::add_socket_to_listener(
MYSQL_SOCKET listen_socket) {
mysql_socket_set_thread_owner(listen_socket);
#ifdef HAVE_POLL
m_poll_info.m_fds.emplace_back(
pollfd{mysql_socket_getfd(listen_socket), POLLIN, 0});
m_poll_info.m_pfs_fds.push_back(listen_socket);
#else // HAVE_POLL
FD_SET(mysql_socket_getfd(listen_socket), &m_select_info.m_client_fds);
if ((uint)mysql_socket_getfd(listen_socket) >
m_select_info.m_max_used_connection)
m_select_info.m_max_used_connection = mysql_socket_getfd(listen_socket);
#endif // HAVE_POLL
}
Mysqld_socket_listener::add_socket_to_listener 函数的作用是将单个监听套接字添加到 I/O 多路复用机制中 ,使其能够被后续的 listen_for_connection_event 监视到新连接。根据系统是否支持 poll(由 HAVE_POLL 宏控制),该函数会分别使用 poll 或 select 两种不同的后端实现。
情况 A:系统支持 poll
cpp
#ifdef HAVE_POLL
m_poll_info.m_fds.emplace_back(
pollfd{mysql_socket_getfd(listen_socket), POLLIN, 0});
m_poll_info.m_pfs_fds.push_back(listen_socket);
-
m_poll_info.m_fds是一个std::vector<pollfd>,用于存储传递给poll()系统调用的结构体数组。-
使用
emplace_back直接在尾部构造一个pollfd对象,其成员:-
fd:通过mysql_socket_getfd获取的底层文件描述符。 -
events:设置为POLLIN,表示监听"可读"事件(对于监听套接字,可读即意味着有新连接)。 -
revents:初始为 0,poll()返回后由内核填充实际发生的事件。
-
-
-
m_poll_info.m_pfs_fds是一个std::vector<MYSQL_SOCKET>,用于保存对应的MYSQL_SOCKET对象。这样在poll()返回后,可以通过索引快速获取完整的套接字信息(包括性能监控指针m_psi),以便进行后续处理。
情况 B:系统不支持 poll,回退到 select
cpp
#else // HAVE_POLL
FD_SET(mysql_socket_getfd(listen_socket), &m_select_info.m_client_fds);
if ((uint)mysql_socket_getfd(listen_socket) >
m_select_info.m_max_used_connection)
m_select_info.m_max_used_connection = mysql_socket_getfd(listen_socket);
#endif // HAVE_POLL
-
FD_SET将监听套接字的文件描述符加入fd_set集合m_select_info.m_client_fds中,表示需要监视该描述符的可读事件。 -
更新最大描述符值 :
select()的第一个参数需要传入"最大描述符值 + 1"。因此这里记录当前所有描述符中的最大值m_select_info.m_max_used_connection,如果新加入的描述符比之前记录的最大值还大,就更新它。这保证了后续调用select()时可以正确设置nfds参数。
连接事件监听
cpp
int mysqld_main(int argc, char **argv){
...
if (network_init()) unireg_abort(MYSQLD_ABORT_EXIT);
...
mysqld_socket_acceptor->connection_event_loop();
}
connection_event_loop 方法是 Connection_acceptor 模板类的核心成员,它实现了连接接受的主循环。
cpp
template <typename Listener>
class Connection_acceptor {
Listener *m_listener;
public:
/**
Connection acceptor loop to accept connections from clients.
*/
void connection_event_loop() {
Connection_handler_manager *mgr =
Connection_handler_manager::get_instance();
while (!connection_events_loop_aborted()) {
Channel_info *channel_info = m_listener->listen_for_connection_event();
if (channel_info != nullptr) mgr->process_new_connection(channel_info);
}
}
}
| 代码行 | 作用 | 说明 |
|---|---|---|
Connection_handler_manager::get_instance() |
获取连接处理器管理器单例 | Connection_handler_manager 负责管理如何为新连接分配处理资源(例如创建新线程、使用线程池等)。单例模式保证整个服务器只有一个管理器实例。 |
while (!connection_events_loop_aborted()) |
循环条件检查 | connection_events_loop_aborted() 是一个全局或成员函数,返回一个布尔值指示是否应该停止接受新连接(通常因服务器关闭或重启)。只要未中止,循环继续。 |
channel_info = m_listener->listen_for_connection_event() |
等待新连接 | 调用具体监听器(如 Mysqld_socket_listener)的 listen_for_connection_event 方法。该方法使用 I/O 多路复用(poll/select)阻塞等待任一监听套接字上的连接请求,成功后返回一个封装了客户端信息的 Channel_info 对象(包含客户端套接字、地址等)。若被信号中断或出错,可能返回 nullptr。 |
if (channel_info != nullptr) mgr->process_new_connection(channel_info) |
分发新连接 | 如果成功获得新连接,则通过管理器处理。process_new_connection 会根据配置(如 thread_handling)决定是创建新线程、放入线程池还是直接处理。该函数通常不阻塞,将连接快速移交给工作线程。 |
| 循环继续 | 等待下一个连接 | 处理完一个连接后立即回到等待状态,保证能够及时响应后续连接请求。 |
Connection_handler_manager
`Connection_handler_manager::init()` 函数是一个静态成员函数,负责在类 MySQL 数据库服务器中初始化全局连接处理器管理器。它根据服务器配置设定相应的连接处理策略,初始化必要的同步原语,并向线程调度器注册回调函数。
cpp
bool Connection_handler_manager::init() {
/*
This is a static member function.
Per_thread_connection_handler's static members need to be initialized
even if One_thread_connection_handler is used instead.
*/
Per_thread_connection_handler::init();
Connection_handler *connection_handler = nullptr;
switch (Connection_handler_manager::thread_handling) {
case SCHEDULER_ONE_THREAD_PER_CONNECTION:
connection_handler = new (std::nothrow) Per_thread_connection_handler();
break;
case SCHEDULER_NO_THREADS:
connection_handler = new (std::nothrow) One_thread_connection_handler();
break;
default:
assert(false);
}
if (connection_handler == nullptr) {
// This is a static member function.
Per_thread_connection_handler::destroy();
return true;
}
m_instance =
new (std::nothrow) Connection_handler_manager(connection_handler);
if (m_instance == nullptr) {
delete connection_handler;
// This is a static member function.
Per_thread_connection_handler::destroy();
return true;
}
#ifdef HAVE_PSI_INTERFACE
int count = static_cast<int>(array_elements(all_conn_manager_mutexes));
mysql_mutex_register("sql", all_conn_manager_mutexes, count);
count = static_cast<int>(array_elements(all_conn_manager_conds));
mysql_cond_register("sql", all_conn_manager_conds, count);
#endif
mysql_mutex_init(key_LOCK_connection_count, &LOCK_connection_count,
MY_MUTEX_INIT_FAST);
mysql_cond_init(key_COND_connection_count, &COND_connection_count);
max_threads = connection_handler->get_max_threads();
// Init common callback functions.
thr_set_lock_wait_callback(scheduler_wait_lock_begin,
scheduler_wait_lock_end);
thr_set_sync_wait_callback(scheduler_wait_sync_begin,
scheduler_wait_sync_end);
return false;
}
-
初始化 Per_thread_connection_handler 的静态数据
Per_thread_connection_handler::init();- 即使服务器未配置为对每个连接使用专用线程,此调用仍会初始化 `Per_thread_connection_handler` 的静态成员(例如计数器或全局列表)。根据注释表明,无论选用何种处理器,这些静态成员都必须被初始化。
-
选择并初始化连接处理器
-
`thread_handling` 是一个静态枚举,用于确定线程模型:
`SCHEDULER_ONE_THREAD_PER_CONNECTION` → 每个连接独占一个线程。
`SCHEDULER_NO_THREADS` → 单一线程处理所有连接(适用于嵌入式服务器或调试场景)。
-
如果无法分配连接处理器,此前已初始化的 Per_thread_connection_handler 静态数据将被销毁,且函数返回 true(表示失败)。
-
-
创建单例的连接管理器 Connection_handler_manager(connection_handler);
-
Connection_handler_manager 单例被实例化,并接管了所选 connection_handler 的所有权。
-
如果无法分配管理器,则删除连接处理器【connection_handler】,销毁 Per_thread_connection_handler 的静态数据,并且该函数返回 true。
-
-
如果服务器编译时启用了性能模式检测 (HAVE_PSI_INTERFACE),则连接管理器使用的互斥锁和条件变量将被注册。这允许对这些同步对象进行性能监控。
-
初始化连接计数原语,这些互斥锁和条件变量用于保护活动连接数量,并对该数量的变化发出信号(例如,用于等待直至有可用的连接槽位)。
mysql_mutex_init(key_LOCK_connection_count, &LOCK_connection_count, MY_MUTEX_INIT_FAST); mysql_cond_init(key_COND_connection_count, &COND_connection_count) -
设置最大允许线程:连接管理器存储由所选处理器报告的允许的最大线程数(或连接数)。
-
注册全局调度器等待回调函数,这些函数设置线程级回调函数,当线程开始或结束等待锁或同步对象时,这些回调函数会被调用。它们用于跟踪等待事件,并可能用于更新性能指标。
thr_set_lock_wait_callback(scheduler_wait_lock_begin, scheduler_wait_lock_end); thr_set_sync_wait_callback(scheduler_wait_sync_begin, scheduler_wait_sync_end); -
返回
Per_thread_connection_handler
Per_thread_connection_handler 是 MySQL 中实现每连接一线程 模型的连接处理器。它继承自 Connection_handler 基类,并利用线程缓存机制来优化线程的创建与销毁。该类的核心在于其静态成员,这些成员构成了全局共享的线程缓存基础设施,即使服务器实际采用单线程处理器,也必须初始化它们。
类的职责
-
管理每个连接对应的线程:为每个新连接创建或分配一个工作线程。
-
实现线程缓存:维护一组空闲线程,避免频繁的 pthread_create/destroy 开销。
-
排队等待连接:当所有线程忙碌时,将新连接放入等待队列,待空闲线程出现时再处理。
-
提供状态变量 :记录阻塞线程数、慢启动线程数等统计信息,供
SHOW STATUS等命令查询。
静态成员详解
| 静态成员 | 类型 | 用途 |
|---|---|---|
waiting_channel_info_list |
std::list<Channel_info *> * |
指向待处理连接列表的指针。当无空闲线程时,新连接被加入此列表,由空闲线程取出处理。由 LOCK_thread_cache 保护。 |
LOCK_thread_cache |
mysql_mutex_t |
保护线程缓存所有共享数据(包括等待列表、统计变量、空闲线程池)的互斥锁。 |
COND_thread_cache |
mysql_cond_t |
用于线程在缓存条件上等待/唤醒的条件变量。例如,主线程等待空闲线程时使用。 |
COND_flush_thread_cache |
mysql_cond_t |
用于触发线程缓存刷新的条件变量(例如 FLUSH THREADS 命令)。 |
blocked_pthread_count |
ulong |
空闲线程数量 。受 LOCK_thread_cache 保护。 该字段在连接处理的生命周期中起着关键的协调作用: * 进入缓存(线程休眠) : * 在线程完成任务准备结束前,会检查 blocked_pthread_count 是否小于系统变量 max_blocked_pthreads(通常对应 thread_cache_size)。 * 如果符合条件,执行 blocked_pthread_count++,线程调用 pthread_cond_wait 阻塞在条件变量 COND_thread_cache 上。 * 唤醒复用(分配连接) : * 当有新连接进入时,系统调用 check_idle_thread_and_enqueue_connection。 * 它会对比 blocked_pthread_count 和正在准备唤醒的线程数(wake_pthread)。如果有足够的空闲线程,则向 waiting_channel_info_list 放入连接信息并发出信号唤醒线程。 * 退出缓存(恢复执行) : * 被唤醒的线程从阻塞状态恢复后,会执行 blocked_pthread_count--,并接手新的连接任务。 |
slow_launch_threads |
ulong |
累计慢启动线程的次数(创建线程耗时超过 slow_launch_time 的计数)。 |
max_blocked_pthreads |
ulong |
系统变量,允许的最大阻塞线程数(可配置)。超过此值时新连接可能被拒绝。 |
shrink_cache |
bool |
标志是否需要收缩线程缓存(例如空闲线程过多时销毁多余线程)。受 LOCK_thread_cache 保护。 |
| wake_pthread | uint | 它是一个静态计数器,表示 已经被唤醒但尚未取走连接的线程数量 (即已放入队列但未处理的连接数)。每次成功入队一个连接并发出信号时,wake_pthread 增加;当空闲线程被唤醒并从队列中取出连接时,wake_pthread 减少。该变量同样受 LOCK_thread_cache 保护。 |
静态方法 init() 与 destroy()
cpp
void Per_thread_connection_handler::init() {
#ifdef HAVE_PSI_INTERFACE
int count = static_cast<int>(array_elements(all_per_thread_mutexes));
mysql_mutex_register("sql", all_per_thread_mutexes, count);
count = static_cast<int>(array_elements(all_per_thread_conds));
mysql_cond_register("sql", all_per_thread_conds, count);
#endif
mysql_mutex_init(key_LOCK_thread_cache, &LOCK_thread_cache,
MY_MUTEX_INIT_FAST);
mysql_cond_init(key_COND_thread_cache, &COND_thread_cache);
mysql_cond_init(key_COND_flush_thread_cache, &COND_flush_thread_cache);
waiting_channel_info_list = new (std::nothrow) std::list<Channel_info *>;
assert(waiting_channel_info_list != nullptr);
}
在 MySQL 的源码中,连接管理器的初始化由两个关键静态函数协作完成:Connection_handler_manager::init() 负责全局管理器的设置,而 Per_thread_connection_handler::init() 则为具体的线程模型准备必要的基础设施。
-
注册性能监控对象(PSI)
-
若编译时启用了 Performance Schema,则将该处理器内部使用的所有互斥量(
all_per_thread_mutexes)和条件变量(all_per_thread_conds)注册到性能监控框架中。 -
这使得 DBA 能够通过
performance_schema表观察这些同步对象的等待事件,便于调优和诊断。
-
-
初始化线程缓存同步原语,这些同步原语是线程缓存机制的核心,负责协调工作线程的创建、销毁和复用。
mysql_mutex_init(key_LOCK_thread_cache, &LOCK_thread_cache, MY_MUTEX_INIT_FAST); mysql_cond_init(key_COND_thread_cache, &COND_thread_cache); mysql_cond_init(key_COND_flush_thread_cache, &COND_flush_thread_cache);-
LOCK_thread_cache:保护线程缓存(例如空闲线程列表)的互斥锁。 -
COND_thread_cache:用于等待线程缓存条件变化的信号量(例如当没有空闲线程时,新连接可能需要等待)。 -
COND_flush_thread_cache:可能用于触发线程缓存的清理或刷新操作(例如管理命令FLUSH THREADS时使用)。
-
-
创建等待连接信息列表
waiting_channel_info_list = new (std::nothrow) std::list<Channel_info *>; assert(waiting_channel_info_list != nullptr);-
waiting_channel_info_list是一个静态成员,类型为std::list<Channel_info *>,用于存放尚未分配工作线程的连接请求信息(Channel_info对象)。 -
当所有工作线程都忙碌时,新到达的连接会被暂存在该列表中,待有空闲线程时再取出处理。
-
cpp
void Per_thread_connection_handler::destroy() {
if (waiting_channel_info_list != nullptr) {
delete waiting_channel_info_list;
waiting_channel_info_list = nullptr;
mysql_mutex_destroy(&LOCK_thread_cache);
mysql_cond_destroy(&COND_thread_cache);
mysql_cond_destroy(&COND_flush_thread_cache);
}
}
-
检查初始化标志
if (waiting_channel_info_list != nullptr)判断列表指针是否非空。由于init()在成功分配列表后才设置该指针,因此此条件用于确认静态成员是否已初始化,避免对未初始化的对象进行销毁操作。 -
释放等待列表
delete waiting_channel_info_list释放动态分配的std::list<Channel_info *>对象,并将指针置nullptr,防止悬挂指针。 -
销毁同步原语
-
mysql_mutex_destroy(&LOCK_thread_cache)销毁保护线程缓存的互斥锁。 -
mysql_cond_destroy(&COND_thread_cache)和mysql_cond_destroy(&COND_flush_thread_cache)销毁两个条件变量。
-
核心方法 add_connection
cpp
bool Per_thread_connection_handler::add_connection(Channel_info *channel_info) {
int error = 0;
my_thread_handle id;
DBUG_TRACE;
// Simulate thread creation for test case before we check thread cache
DBUG_EXECUTE_IF("fail_thread_create", error = 1; goto handle_error;);
if (!check_idle_thread_and_enqueue_connection(channel_info)) return false;
/*
There are no idle threads available to take up the new
connection. Create a new thread to handle the connection
*/
channel_info->set_prior_thr_create_utime();
error =
mysql_thread_create(key_thread_one_connection, &id, &connection_attrib,
handle_connection, (void *)channel_info);
#ifndef NDEBUG
handle_error:
#endif // !NDEBUG
if (error) {
connection_errors_internal++;
if (!create_thd_err_log_throttle.log())
LogErr(ERROR_LEVEL, ER_CONN_PER_THREAD_NO_THREAD, error);
channel_info->send_error_and_close_channel(ER_CANT_CREATE_THREAD, error,
true);
Connection_handler_manager::dec_connection_count();
return true;
}
Global_THD_manager::get_instance()->inc_thread_created();
DBUG_PRINT("info", ("Thread created"));
return false;
}
-
DBUG 模拟失败
DBUG_EXECUTE_IF("fail_thread_create", ...)用于测试:在调试模式下,若设置了该标记,则直接跳到错误处理,模拟线程创建失败。 -
尝试使用线程缓存
if (!check_idle_thread_and_enqueue_connection(channel_info)) return false;-
check_idle_thread_and_enqueue_connection是一个私有辅助函数,它检查当前是否有空闲线程可用。 -
若存在空闲线程 :它会将
channel_info加入等待列表(waiting_channel_info_list),并唤醒一个空闲线程去处理该连接。该函数返回true(表示已通过缓存处理),因此!true == false,add_connection直接返回false(成功)。 -
若无空闲线程 :函数返回
false,表示未能使用缓存,需要创建新线程。此时!false == true,继续执行后续创建线程的代码。
-
-
创建新线程
-
记录线程创建前的微秒时间戳(用于性能统计,在下面讲线程慢启动时候会用到)。
-
调用
mysql_thread_create创建线程,线程函数为handle_connection,参数为channel_info。 -
connection_attrib是线程属性(通常为默认)。 -
若创建成功,增加全局线程创建计数(
Global_THD_manager::inc_thread_created),并返回false表示成功。
-
-
错误处理
-
若线程创建失败,内部错误计数器
connection_errors_internal增加。 -
通过日志限速记录错误。
-
向客户端发送错误(
ER_CANT_CREATE_THREAD),并关闭通道。 -
调用
Connection_handler_manager::dec_connection_count()减少活动连接计数,因为之前连接计数已增加,但现在线程未能创建,需回退。 -
返回
true表示失败。
-
核心方法 check_idle_thread_and_enqueue_connection(提供者)
cpp
bool Per_thread_connection_handler::check_idle_thread_and_enqueue_connection(
Channel_info *channel_info) {
bool res = true;
mysql_mutex_lock(&LOCK_thread_cache);
if (Per_thread_connection_handler::blocked_pthread_count > wake_pthread) {
DBUG_PRINT("info", ("waiting_channel_info_list->push %p", channel_info));
waiting_channel_info_list->push_back(channel_info);
wake_pthread++;
mysql_cond_signal(&COND_thread_cache);
res = false;
}
mysql_mutex_unlock(&LOCK_thread_cache);
return res;
}
关键变量含义
-
blocked_pthread_count静态成员,表示当前在条件变量
COND_thread_cache上等待工作的 空闲线程数量 。这些线程在block_until_new_connection()中阻塞,等待新连接到来。 -
wake_pthread它是一个静态计数器,表示 已经被唤醒但尚未取走连接的线程数量 (即已放入队列但未处理的连接数)。每次成功入队一个连接并发出信号时,
wake_pthread增加;当空闲线程被唤醒并从队列中取出连接时,wake_pthread减少。该变量同样受LOCK_thread_cache保护。 -
waiting_channel_info_list静态成员,指向存放待处理连接的列表(
std::list<Channel_info*>)。空闲线程从此列表获取连接。 -
COND_thread_cache条件变量,用于唤醒等待的空闲线程。
核心逻辑
-
加锁 :
mysql_mutex_lock(&LOCK_thread_cache)保护所有共享数据。 -
检查空闲线程可用性 :
if (blocked_pthread_count > wake_pthread)-
blocked_pthread_count当前被阻塞的线程数,wake_pthread是已经被唤醒但尚未取走连接的线程数量。 -
当空闲线程数量(
blocked_pthread_count) 大于 已经被唤醒但尚未取走连接的线程数量 (wake_pthread)时,说明还有真正的空闲线程在等待,可以将新连接交给它们,而无需创建新线程。
-
-
若条件成立:
-
将
channel_info追加到等待列表(waiting_channel_info_list)的尾部。 -
wake_pthread++:记录又多了一个待处理的连接。 -
mysql_cond_signal(&COND_thread_cache):唤醒一个等待的空闲线程(该线程被唤醒后会从队列中取出连接并处理)。 -
将
res设为false(表示成功使用缓存)。
-
-
若条件不成立 :
这意味着所有空闲线程都已分配了任务(或没有空闲线程),此时不应再入队,而是让调用者创建新线程。
res保持为true。 -
解锁 ,返回
res。
并发与同步的处理:
-
所有操作均在
LOCK_thread_cache保护下进行,保证了多线程环境下数据的一致性。 -
mysql_cond_signal唤醒一个等待的空闲线程,该线程随后会从waiting_channel_info_list中取出连接并处理,同时减少wake_pthread(具体在block_until_new_connection()函数中完成)。 -
blocked_pthread_count在空闲线程进入等待时递增,在唤醒后递减,同样受锁保护。
核心方法 block_until_new_connection(消费者)
cpp
static mysql_mutex_t LOCK_thread_cache;
/**
An instrumented mutex structure.
@c mysql_mutex_t is a drop-in replacement for @c my_mutex_t.
@sa mysql_mutex_assert_owner
@sa mysql_mutex_assert_not_owner
@sa mysql_mutex_init
@sa mysql_mutex_lock
@sa mysql_mutex_unlock
@sa mysql_mutex_destroy
*/
struct mysql_mutex_t {
/** The real mutex. */
my_mutex_t m_mutex;
/**
The instrumentation hook.
Note that this hook is not conditionally defined,
for binary compatibility of the @c mysql_mutex_t interface.
*/
struct PSI_mutex *m_psi{nullptr};
};
#ifdef _WIN32
typedef CRITICAL_SECTION native_mutex_t;
typedef int native_mutexattr_t;
#else
typedef pthread_mutex_t native_mutex_t;
typedef pthread_mutexattr_t native_mutexattr_t;
#endif
struct safe_mutex_t;
struct my_mutex_t {
union u {
native_mutex_t m_native;
safe_mutex_t *m_safe_ptr;
} m_u;
};
typedef struct my_mutex_t my_mutex_t;
mysql_mutex_t 是 MySQL 代码中普遍使用的互斥锁类型,例如在 Per_thread_connection_handler::init() 中通过 mysql_mutex_init 初始化 LOCK_thread_cache。
-
m_mutex:实际执行锁定操作的互斥锁,类型为my_mutex_t(见下文)。 -
m_psi:指向 Performance Schema 仪表(instrument)的指针。如果启用了 PSI 并且该互斥锁被注册,这个指针用于记录锁的获取、释放等事件,便于监控和分析性能瓶颈。
my_mutex_t 是一个联合体,根据编译选项(是否定义 SAFE_MUTEX)决定使用哪个成员:
-
在非调试模式 (未定义
SAFE_MUTEX)下,m_u.m_native被直接用作原生互斥锁。例如在 Linux 上,它就是pthread_mutex_t。 -
在调试模式 (定义了
SAFE_MUTEX)下,m_u.m_safe_ptr指向一个safe_mutex_t结构,该结构封装了原生互斥锁并添加了调试信息(如持有者线程、加锁位置等)。这时的互斥锁操作(如mysql_mutex_lock)会通过safe_mutex_lock进行额外的检查,帮助开发者发现死锁、重复加锁等问题。
MYSQL 对 native_mutex_t 做了基于平台的抽象:
- 在 Windows 上,原生互斥锁是临界区(
CRITICAL_SECTION)。 - 在 Unix-like 系统(Linux、macOS 等)上,原生互斥锁是 POSIX 线程库的
pthread_mutex_t。
这种抽象使得 MySQL 的同步代码可以跨平台运行,而无需关心底层实现细节。
了解了基本的互斥锁,我们来看 block_until_new_connection 方法的实现:
cpp
/**
Block the current pthread for reuse by new connections.
@retval NULL Too many pthreads blocked already or shutdown in progress.
@retval !NULL Pointer to Channel_info object representing the new connection
to be served by this pthread.
*/
Channel_info *Per_thread_connection_handler::block_until_new_connection() {
Channel_info *new_conn = nullptr;
mysql_mutex_lock(&LOCK_thread_cache);
if (blocked_pthread_count < max_blocked_pthreads && !shrink_cache) {
/* Don't kill the pthread, just block it for reuse */
DBUG_PRINT("info", ("Blocking pthread for reuse"));
/*
mysys_var is bound to the physical thread,
so make sure mysys_var->dbug is reset to a clean state
before picking another session in the thread cache.
*/
DBUG_POP();
assert(!_db_is_pushed_());
// Block pthread
blocked_pthread_count++;
while (!connection_events_loop_aborted() && !wake_pthread && !shrink_cache)
mysql_cond_wait(&COND_thread_cache, &LOCK_thread_cache);
blocked_pthread_count--;
if (shrink_cache && blocked_pthread_count <= max_blocked_pthreads) {
mysql_cond_signal(&COND_flush_thread_cache);
}
if (wake_pthread) {
wake_pthread--;
if (!waiting_channel_info_list->empty()) {
new_conn = waiting_channel_info_list->front();
waiting_channel_info_list->pop_front();
DBUG_PRINT("info", ("waiting_channel_info_list->pop %p", new_conn));
} else {
assert(0); // We should not get here.
}
}
}
mysql_mutex_unlock(&LOCK_thread_cache);
return new_conn;
}
Per_thread_connection_handler::block_until_new_connection() 是线程缓存机制中消费者 (空闲工作线程)的核心函数。当一个工作线程完成了当前连接的请求处理后,它会调用此函数尝试进入空闲状态,以便被复用处理未来的新连接,而不是立即退出。该函数与之前讨论的 check_idle_thread_and_enqueue_connection()(生产者)成对出现,共同实现高效的线程池管理。
-
加锁 :通过条件 LOCK_thread_cache ,保护所有静态成员(
blocked_pthread_count、wake_pthread、waiting_channel_info_list等)。 -
条件判断:
-
blocked_pthread_count < max_blocked_pthreads:当前空闲线程数未达到系统允许的上限,可以继续增加空闲线程。 -
!shrink_cache:未设置"收缩缓存"标志(通常由管理命令FLUSH THREADS或动态调整线程缓存大小触发)。 -
如果条件不满足,则跳过整个阻塞逻辑,直接返回
nullptr,线程将退出。
-
-
调试状态清理 :
mysys_var是与物理线程绑定的调试上下文。由于该线程即将处理一个新的连接(可能来自不同的会话),需要将调试堆栈恢复到干净状态,避免前一个会话的调试信息干扰。DBUG_POP()用于弹出所有调试标记,断言确保没有残留。 -
增加空闲计数 :
blocked_pthread_count++表示当前线程即将进入空闲等待状态。 -
等待条件(有阻塞,blocking):
-
!connection_events_loop_aborted():服务器未关闭。 -
!wake_pthread:没有待处理的连接(wake_pthread为0,即没有新连接在排队等待分配)。如果有待处理的连接,那么需要立即去处理。 -
!shrink_cache:未要求收缩缓存。 -
如果以上条件全为真,线程在
COND_thread_cache上等待,释放锁,直到被mysql_cond_signal唤醒。
-
-
唤醒后 :首先减少空闲计数
blocked_pthread_count--,表示线程不再处于阻塞状态。 -
处理收缩缓存 :如果线程被唤醒是因为
shrink_cache变为true(例如收到FLUSH THREADS命令),且当前空闲线程数未超过最大值,则发送COND_flush_thread_cache信号,通知其他可能也在等待的线程继续收缩流程。这通常用于协调多个空闲线程同时退出。 -
取走待处理连接:
-
wake_pthread > 0表示至少有一个连接在等待被分配(由生产者放入列表)。 -
wake_pthread--:减少待处理计数。 -
从
waiting_channel_info_list前端弹出一个Channel_info对象,赋值给new_conn。 -
如果列表为空(正常情况下不应发生,因为
wake_pthread应该与列表中的元素数一致),则触发断言。
-
-
解锁并返回 :如果线程成功取到了新连接,则返回该连接对象;否则返回
nullptr(例如因服务器关闭或缓存收缩而退出)。
cppstatic inline int inline_mysql_cond_wait(mysql_cond_t *that, mysql_mutex_t *mutex, const char *src_file [[maybe_unused]], int src_line [[maybe_unused]]) { int result; #ifdef HAVE_PSI_COND_INTERFACE if (that->m_psi != nullptr) { if (that->m_psi->m_enabled) { /* Instrumentation start */ PSI_cond_locker *locker; PSI_cond_locker_state state; locker = PSI_COND_CALL(start_cond_wait)( &state, that->m_psi, mutex->m_psi, PSI_COND_WAIT, src_file, src_line); /* Instrumented code */ result = my_cond_wait(&that->m_cond, &mutex->m_mutex #ifdef SAFE_MUTEX , src_file, src_line #endif ); /* Instrumentation end */ if (locker != nullptr) { PSI_COND_CALL(end_cond_wait)(locker, result); } return result; } } #endif /* Non instrumented code */ result = my_cond_wait(&that->m_cond, &mutex->m_mutex #ifdef SAFE_MUTEX , src_file, src_line #endif ); return result; }下面是在开启性能模式下的执行轨迹,对应代码里 instrumented code ,而 Non instrumented code 就不列举了,它可以说是 instrumented code 的一个子集。
条件编译 :
#ifdef HAVE_PSI_COND_INTERFACE确保只有在编译时启用了 Performance Schema 的条件变量监控时才包含此段代码。检查 PSI 元数据:
that->m_psi指向条件变量对应的 PSI 仪表(instrumentation)对象。如果为空,说明该条件变量未被注册到 Performance Schema,则跳过监控。
that->m_psi->m_enabled表示该仪表是否在运行时启用(可通过performance_schema表动态控制)。如果未启用,也跳过监控。开始监控:
PSI_cond_locker *locker:定义一个"锁存器"指针,用于记录本次等待事件。
PSI_cond_locker_state state:为锁存器分配状态存储空间(通常用于避免动态内存分配)。
PSI_CALL(start_cond_wait)是一个宏,调用 Performance Schema 提供的函数来开始记录一个条件等待事件。它传入参数:状态存储、条件变量的 PSI 句柄、互斥锁的 PSI 句柄、操作类型(此处为PSI_COND_WAIT)、源文件名和行号。函数返回一个锁存器指针;如果由于资源限制等原因无法记录,可能返回nullptr。执行实际等待(从这里开始看):
my_cond_wait(&that->m_cond, &mutex->m_mutex, ...)是 MySQL 对底层条件等待函数的封装(例如在 Unix 上通常调用pthread_cond_wait)。它将当前线程挂起,直到条件变量被唤醒,同时自动释放互斥锁(并在唤醒后重新获取)。如果定义了
SAFE_MUTEX(调试模式),额外传入src_file和src_line,以便在发生错误(如死锁)时报告位置信息。结束监控:
- 如果
locker不为空,则调用PSI_CALL(end_cond_wait)结束等待事件的记录,并传入结果(result,即my_cond_wait的返回值)。Performance Schema 会统计等待时间、成功/失败等信息。提前返回 :直接返回
result,不再执行后面的非监控代码。
cppstatic inline int my_cond_wait(native_cond_t *cond, my_mutex_t *mp #ifdef SAFE_MUTEX , const char *file, uint line #endif ) { #ifdef SAFE_MUTEX return safe_cond_wait(cond, mp->m_u.m_safe_ptr, file, line); #else return native_cond_wait(cond, &mp->m_u.m_native); #endif } // 非调试模式走这里 static inline int native_cond_wait(native_cond_t *cond, native_mutex_t *mutex) { #ifdef _WIN32 if (!SleepConditionVariableCS(cond, mutex, INFINITE)) return ETIMEDOUT; return 0; #else return pthread_cond_wait(cond, mutex); #endif }
- 非调试模式(未定义
SAFE_MUTEX)
直接调用
native_cond_wait,它是对操作系统原生条件变量等待函数的直接封装(例如pthread_cond_wait)。参数
&mp->m_u.m_native从my_mutex_t联合体中取出原生的互斥锁对象(通常是一个pthread_mutex_t)。这种模式追求最大性能,无额外检查或记录。
- 调试模式(定义了
SAFE_MUTEX)
调用
safe_cond_wait,这是一个增加了运行时检查的版本。
mp->m_u.m_safe_ptr指向一个包含调试信息的互斥锁包装结构(例如记录哪个线程持有锁、加锁位置等)。
safe_cond_wait会执行额外的校验,例如:
确保当前线程确实持有该互斥锁(因为条件等待要求调用时已加锁)。
记录等待开始和结束的时间戳,帮助检测死锁或长时间等待。
在发生错误时,输出
file和line指示调用位置,便于开发者定位问题。这种模式会带来一定的性能开销,但大大增强了调试能力,常用于开发测试环境。
方法 modify_thread_cache_size
cpp
// thread_cache_size:新的线程缓存大小,即允许的最大空闲线程数。
void Per_thread_connection_handler::modify_thread_cache_size(
const ulong thread_cache_size) {
mysql_mutex_lock(&LOCK_thread_cache);
if (thread_cache_size >= blocked_pthread_count) {
mysql_mutex_unlock(&LOCK_thread_cache);
return;
}
shrink_cache = true;
if (thread_cache_size == 0) {
mysql_cond_broadcast(&COND_thread_cache);
} else {
const ulong num_threads = blocked_pthread_count - thread_cache_size;
for (ulong i = 0; i < num_threads; i++)
mysql_cond_signal(&COND_thread_cache);
}
// Wait until threads have been unblocked from thread cache.
while (blocked_pthread_count > thread_cache_size)
mysql_cond_wait(&COND_flush_thread_cache, &LOCK_thread_cache);
shrink_cache = false;
mysql_mutex_unlock(&LOCK_thread_cache);
}
Per_thread_connection_handler::modify_thread_cache_size 是用于动态调整线程缓存大小 的函数。当管理员通过 SET GLOBAL thread_cache_size = N 修改系统变量时,MySQL 会调用此函数,使当前空闲线程数(blocked_pthread_count)适应新的配置值,必要时唤醒多余的空闲线程并等待它们退出。
-
加锁:LOCK_thread_cache保护所有线程缓存的共享数据,包括blocked_pthread_count、shrink_cache以及等待队列等。 -
检查是否需要调整:如果新大小(thread_cache_size) 大于等于 当前空闲线程数(blocked_pthread_count),说明空闲线程数量未超标,无需操作,直接解锁返回。
-
如果不满足前面的调整条件,则设置
shrink_cache标志,指示正在收缩缓存。这个标志在空闲线程等待时会被检查(见block_until_new_connection),让它们知道应该退出而不是继续等待。 -
唤醒多余的空闲线程
if (thread_cache_size == 0) { mysql_cond_broadcast(&COND_thread_cache); } else { const ulong num_threads = blocked_pthread_count - thread_cache_size; for (ulong i = 0; i < num_threads; i++) mysql_cond_signal(&COND_thread_cache); }-
需要减少的空闲线程数 =
blocked_pthread_count - thread_cache_size。 -
如果新大小为 0 ,则使用
broadcast唤醒所有空闲线程(它们都应该退出)。 -
否则,逐个发送信号(
signal)唤醒恰好num_threads个线程。每个被唤醒的空闲线程会在block_until_new_connection中检测到shrink_cache为真,从而返回NULL并退出。
-
-
等待空闲线程退出
while (blocked_pthread_count > thread_cache_size) mysql_cond_wait(&COND_flush_thread_cache, &LOCK_thread_cache);-
当前线程(即执行修改的线程,通常是用户线程或管理线程)在此处等待,直到空闲线程数降到新大小以下。
-
每次被唤醒的空闲线程退出时,会在
block_until_new_connection中减少blocked_pthread_count,并发送COND_flush_thread_cache信号,通知等待的修改线程继续检查。 -
条件变量
COND_flush_thread_cache专用于此目的,确保修改线程不会忙等待。
-
-
清理并返回:当循环退出(即
blocked_pthread_count <= thread_cache_size),清除shrink_cache标志,解锁并返回。
通常,当用户执行 SET GLOBAL thread_cache_size = N 时,MYSQL 会:
-
更新系统变量对应的静态成员(如
Per_thread_connection_handler::max_blocked_pthreads)为新值。 -
调用
modify_thread_cache_size(N)来收缩当前超出新上限的空闲线程(如果当前空闲线程数大于新上限)。 -
后续所有空闲线程的进入和退出都将以新的
max_blocked_pthreads为准。
因此,modify_thread_cache_size 只负责处理当前超出的部分,而未来的行为由上限变量自动保证。
监听连接 listen_for_connection_event
listen_for_connection_event 是 Mysqld_socket_listener 类的一个成员函数,其主要作用是监听已绑定的服务器套接字,等待新的客户端连接请求,接受连接并封装成 Channel_info 对象返回。该函数是服务器网络层核心部分,处理不同平台、不同网络命名空间及安全机制(如 TCP Wrapper)下的连接接受逻辑。
cpp
Channel_info *Mysqld_socket_listener::listen_for_connection_event() {
#ifdef HAVE_POLL
int retval = poll(&m_poll_info.m_fds[0], m_socket_vector.size(), -1);
#else
m_select_info.m_read_fds = m_select_info.m_client_fds;
int retval = select((int)m_select_info.m_max_used_connection,
&m_select_info.m_read_fds, nullptr, nullptr, nullptr);
#endif
if (retval < 0 && socket_errno != SOCKET_EINTR) {
/*
select(2)/poll(2) failed on the listening port.
There is not much details to report about the client,
increment the server global status variable.
*/
++connection_errors_query_block;
if (!select_errors++ && !connection_events_loop_aborted())
LogErr(ERROR_LEVEL, ER_CONN_SOCKET_SELECT_FAILED, socket_errno);
}
if (retval < 0 || connection_events_loop_aborted()) return nullptr;
/* Is this a new connection request ? */
const Listen_socket *listen_socket = get_listen_socket();
/*
When poll/select returns control flow then at least one ready server socket
must exist. Check that get_ready_socket() returns a valid socket.
*/
assert(listen_socket != nullptr);
MYSQL_SOCKET connect_sock;
#ifdef HAVE_SETNS
/*
If a network namespace is specified for a listening socket then set this
network namespace as active before call to accept().
It is not clear from manuals whether a socket returned by a call to
accept() borrows a network namespace from a server socket used for
accepting a new connection. For that reason, assign a network namespace
explicitly before calling accept().
*/
std::string network_namespace_for_listening_socket;
if (listen_socket->m_socket_type == Socket_type::TCP_SOCKET) {
network_namespace_for_listening_socket =
(listen_socket->m_network_namespace != nullptr
? *listen_socket->m_network_namespace
: std::string(""));
if (!network_namespace_for_listening_socket.empty() &&
set_network_namespace(network_namespace_for_listening_socket))
return nullptr;
}
#endif
if (accept_connection(listen_socket->m_socket, &connect_sock)) {
#ifdef HAVE_SETNS
if (!network_namespace_for_listening_socket.empty())
(void)restore_original_network_namespace();
#endif
return nullptr;
}
#ifdef HAVE_SETNS
if (!network_namespace_for_listening_socket.empty() &&
restore_original_network_namespace())
return nullptr;
#endif
#ifdef HAVE_LIBWRAP
if ((listen_socket->m_socket_type == Socket_type::TCP_SOCKET) &&
check_connection_refused_by_tcp_wrapper(connect_sock)) {
return nullptr;
}
#endif // HAVE_LIBWRAP
Channel_info *channel_info = nullptr;
if (listen_socket->m_socket_type == Socket_type::UNIX_SOCKET)
channel_info = new (std::nothrow) Channel_info_local_socket(connect_sock);
else
channel_info = new (std::nothrow) Channel_info_tcpip_socket(
connect_sock, (listen_socket->m_socket_interface ==
Socket_interface_type::ADMIN_INTERFACE));
if (channel_info == nullptr) {
(void)mysql_socket_shutdown(connect_sock, SHUT_RDWR);
(void)mysql_socket_close(connect_sock);
connection_errors_internal++;
return nullptr;
}
#ifdef HAVE_SETNS
if (listen_socket->m_socket_type == Socket_type::TCP_SOCKET &&
!network_namespace_for_listening_socket.empty())
static_cast<Channel_info_tcpip_socket *>(channel_info)
->set_network_namespace(network_namespace_for_listening_socket);
#endif
return channel_info;
}
等待连接事件
cpp
#ifdef HAVE_POLL
int retval = poll(&m_poll_info.m_fds[0], m_socket_vector.size(), -1);
#else
m_select_info.m_read_fds = m_select_info.m_client_fds;
int retval = select((int)m_select_info.m_max_used_connection,
&m_select_info.m_read_fds, nullptr, nullptr, nullptr);
#endif
根据平台是否支持 poll 选择不同的多路复用方式:
-
HAVE_POLL :调用
poll,监视m_poll_info.m_fds数组中的所有文件描述符(即所有监听套接字),超时时间为-1(无限等待,直到有事件发生)。 -
否则 :使用
select,首先将m_select_info.m_client_fds赋值给m_select_info.m_read_fds(读集),然后调用select等待读就绪事件。m_select_info.m_max_used_connection是最大文件描述符值 + 1,用于优化select范围。
获取就绪的监听套接字
cpp
if (retval < 0 || connection_events_loop_aborted()) return nullptr;
/* Is this a new connection request ? */
const Listen_socket *listen_socket = get_listen_socket();
代码中调用的是 MySQL 服务器 Mysqld_socket_listener 类的成员函数 get_listen_socket(),其作用是在多个监听套接字中找出当前有连接请求就绪的那个,并返回其对应的 Listen_socket 对象指针 。它通过 poll 或 select 返回的就绪事件信息,优先处理管理接口(admin interface)的连接请求,然后遍历其他普通监听套接字。
cpp
const Listen_socket *Mysqld_socket_listener::get_listen_socket() const {
/*
In case admin interface was set up, then first check whether an admin socket
ready to accept a new connection. Doing this way provides higher priority
to admin interface over other listeners.
*/
#ifdef HAVE_POLL
uint start_index = 0;
if (!m_admin_bind_address.address.empty() &&
!m_use_separate_thread_for_admin) {
if (m_poll_info.m_fds[0].revents & POLLIN) {
return &m_socket_vector[0];
} else
start_index = 1;
}
for (uint i = start_index; i < m_socket_vector.size(); ++i) {
if (m_poll_info.m_fds[i].revents & POLLIN) {
return &m_socket_vector[i];
}
}
#else // HAVE_POLL
if (!m_admin_bind_address.address.empty() &&
!m_use_separate_thread_for_admin &&
FD_ISSET(mysql_socket_getfd(m_admin_interface_listen_socket),
const_cast<fd_set *>(&m_select_info.m_read_fds))) {
return &m_socket_vector[0];
}
for (const auto &socket_element : m_socket_vector) {
if (FD_ISSET(mysql_socket_getfd(socket_element.m_socket),
const_cast<fd_set *>(&m_select_info.m_read_fds))) {
return &socket_element;
}
}
#endif // HAVE_POLL
return nullptr;
;
}
m_poll_info.m_fds是与m_socket_vector一一对应的pollfd数组,其中每个元素的revents字段记录了就绪事件(POLLIN表示有连接请求)。
m_socket_vector是std::vector<Listen_socket>,按顺序存储所有监听套接字。索引 0 通常对应管理接口(若存在),其余为普通监听套接字(如 TCP 端口、Unix 域套接字等)。
定义了 HAVE_POLL(使用 poll)
-
若管理地址(m_admin_bind_address)非空 且 管理接口未使用单独线程 (即由当前线程负责管理接口,m_use_separate_thread_for_admin),则先检查索引 0 的套接字是否就绪(
revents & POLLIN)。若是,直接返回管理套接字。 -
否则(管理接口未就绪或不存在),设置
start_index = 1,跳过索引 0,然后遍历索引 1 到末尾,返回第一个就绪的套接字。 -
如果遍历完所有套接字都没有发现就绪事件,返回
nullptr,由调用者处理(例如在listen_for_connection_event中,这种情况理论上不会发生,因为poll/select已返回至少一个就绪事件,但防御性编程保留)。
接受连接
cpp
if (accept_connection(listen_socket->m_socket, &connect_sock)) {
#ifdef HAVE_SETNS
if (!network_namespace_for_listening_socket.empty())
(void)restore_original_network_namespace();
#endif
return nullptr;
}
代码中调用的是 MySQL 中用于接受新连接的静态函数 accept_connection()。它封装了底层 accept 系统调用,处理可能的中断和资源不足等错误,并包含重试机制和错误统计,从 listen_socket->m_socket 接受一个新连接,并将客户端套接字存入参数 connect_sock。该函数被 Mysqld_socket_listener::listen_for_connection_event() 调用,当检测到监听套接字就绪后,调用此函数接受连接。
cpp
/**
Accept a new connection on a ready listening socket.
@param listen_sock Listening socket ready to accept a new connection
@param [out] connect_sock Socket corresponding to a new accepted connection
@return operation result
@retval true on error
@retval false on success
*/
static bool accept_connection(MYSQL_SOCKET listen_sock,
MYSQL_SOCKET *connect_sock) {
struct sockaddr_storage c_addr;
for (uint retry = 0; retry < MAX_ACCEPT_RETRY; retry++) {
socket_len_t length = sizeof(struct sockaddr_storage);
*connect_sock =
mysql_socket_accept(key_socket_client_connection, listen_sock,
(struct sockaddr *)(&c_addr), &length);
if (mysql_socket_getfd(*connect_sock) != INVALID_SOCKET ||
(socket_errno != SOCKET_EINTR && socket_errno != SOCKET_EAGAIN))
break;
}
if (mysql_socket_getfd(*connect_sock) == INVALID_SOCKET) {
/*
accept(2) failed on the listening port, after many retries.
There is not much details to report about the client,
increment the server global status variable.
*/
if ((connection_errors_accept++ & 255) == 0) { // This can happen often
#ifdef _WIN32
Socket_error_message_buf msg_buff;
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, socket_errno,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)msg_buff,
sizeof(msg_buff), nullptr);
LogErr(ERROR_LEVEL, ER_CONN_SOCKET_ACCEPT_FAILED, msg_buff);
#else
LogErr(ERROR_LEVEL, ER_CONN_SOCKET_ACCEPT_FAILED, strerror(errno));
#endif
}
if (socket_errno == SOCKET_ENFILE || socket_errno == SOCKET_EMFILE)
sleep(1); // Give other threads some time
return true;
}
return false;
}
-
MAX_ACCEPT_RETRY:预定义的最大重试次数(通常为 10 或类似值),用于应对可恢复错误。 -
mysql_socket_accept:是 MySQL 对系统accept的封装,接收一个性能键(key_socket_client_connection)用于统计,并返回MYSQL_SOCKET类型。 -
c_addr:存储客户端地址的结构体。 -
length:传入地址结构体大小,accept返回实际客户端地址长度。
重试循环 :如果 accept 返回有效套接字(INVALID_SOCKET 非真),或错误码不是 SOCKET_EINTR(被信号中断)且不是 SOCKET_EAGAIN(资源临时不可用,通常非阻塞模式下),则跳出循环。
- 如果错误码是
EINTR或EAGAIN,循环继续重试,直到达到MAX_ACCEPT_RETRY上限。 - 目的:避免因临时信号中断或非阻塞模式下暂时无连接而立即失败,提高健壮性。
错误处理: 若重试后 connect_sock 仍然无效,则接受失败。
-
错误计数 :
connection_errors_accept是全局计数器,用于统计接受连接失败的次数。& 255使得仅每 256 次失败记录一次日志,避免日志洪泛(因为此类错误可能频繁发生)。 -
日志记录:
-
Windows 平台使用
FormatMessage获取系统错误信息。 -
其他平台使用
strerror(errno)输出错误描述。 -
日志消息为
ER_CONN_SOCKET_ACCEPT_FAILED。
-
-
特殊错误处理 :如果错误码是
ENFILE(系统文件表满)或EMFILE(进程文件表满),则sleep(1)休眠一秒,让其他线程有机会释放文件描述符,然后再返回错误。这可以缓解资源耗尽时的竞争。
cpp
/** mysql_socket_accept */
static inline MYSQL_SOCKET inline_mysql_socket_accept(
#ifdef HAVE_PSI_SOCKET_INTERFACE
const char *src_file, uint src_line, PSI_socket_key key,
#endif
MYSQL_SOCKET socket_listen, struct sockaddr *addr, socklen_t *addr_len) {
MYSQL_SOCKET socket_accept = MYSQL_INVALID_SOCKET;
socklen_t addr_length = (addr_len != nullptr) ? *addr_len : 0;
#ifdef HAVE_PSI_SOCKET_INTERFACE
if (socket_listen.m_psi != nullptr) {
if (socket_listen.m_psi->m_enabled) {
/* Instrumentation start */
PSI_socket_locker *locker;
PSI_socket_locker_state state;
locker = PSI_SOCKET_CALL(start_socket_wait)(&state, socket_listen.m_psi,
PSI_SOCKET_CONNECT, (size_t)0,
src_file, src_line);
/* Instrumented code */
socket_accept.fd = accept(socket_listen.fd, addr, &addr_length);
/* Instrumentation end */
if (locker != nullptr) {
PSI_SOCKET_CALL(end_socket_wait)(locker, (size_t)0);
}
if (likely(socket_accept.fd != INVALID_SOCKET)) {
/* Initialize the instrument with the new socket descriptor and address
*/
socket_accept.m_psi = PSI_SOCKET_CALL(init_socket)(
key, (const my_socket *)&socket_accept.fd, addr, addr_length);
}
return socket_accept;
}
}
#endif
/* Non instrumented code */
socket_accept.fd = accept(socket_listen.fd, addr, &addr_length);
#ifdef HAVE_PSI_SOCKET_INTERFACE
if (likely(socket_accept.fd != INVALID_SOCKET)) {
/* Initialize the instrument with the new socket descriptor and address */
socket_accept.m_psi = PSI_SOCKET_CALL(init_socket)(
key, (const my_socket *)&socket_accept.fd, addr, addr_length);
}
#endif
return socket_accept;
}
/** mysql_socket_close */
static inline int inline_mysql_socket_close(
#ifdef HAVE_PSI_SOCKET_INTERFACE
const char *src_file, uint src_line,
#endif
MYSQL_SOCKET mysql_socket) {
int result;
#ifdef HAVE_PSI_SOCKET_INTERFACE
if (mysql_socket.m_psi != nullptr) {
if (mysql_socket.m_psi->m_enabled) {
/* Instrumentation start */
PSI_socket_locker *locker;
PSI_socket_locker_state state;
locker = PSI_SOCKET_CALL(start_socket_wait)(&state, mysql_socket.m_psi,
PSI_SOCKET_CLOSE, (size_t)0,
src_file, src_line);
/* Instrumented code */
result = closesocket(mysql_socket.fd);
/* Instrumentation end */
if (locker != nullptr) {
PSI_SOCKET_CALL(end_socket_wait)(locker, (size_t)0);
}
/* Remove the instrumentation for this socket. */
PSI_SOCKET_CALL(destroy_socket)(mysql_socket.m_psi);
return result;
}
}
#endif
/* Non instrumented code */
result = closesocket(mysql_socket.fd);
#ifdef HAVE_PSI_SOCKET_INTERFACE
/* Remove the instrumentation for this socket. */
if (mysql_socket.m_psi != nullptr) {
PSI_SOCKET_CALL(destroy_socket)(mysql_socket.m_psi);
}
#endif
return result;
}
该函数封装了 accept 系统调用,接受一个新的客户端连接,并在 PSI 启用时记录等待事件和初始化新套接字的监控数据。
-
保存原始地址长度: 因为
accept会修改addr_len为实际客户端地址长度,所以先保存传入值用于后续可能的 PSI 初始化。socklen_t addr_length = (addr_len != nullptr) ? *addr_len : 0; -
PSI 监控分支(若启用且监听套接字已监控)
-
start_socket_wait创建一个锁存器(locker),记录等待开始时间,事件类型为PSI_SOCKET_CONNECT(表示accept操作)。 -
执行真正的
accept系统调用,结果存入socket_accept.fd。 -
end_socket_wait记录等待结束,将耗时计入 Performance Schema 的套接字等待事件表。 -
若
accept成功(fd != INVALID_SOCKET),调用init_socket为新套接字分配 PSI 描述符,关联到传入的key,并记录客户端地址信息(addr和实际地址长度addr_length)。这样新连接在后续操作中也能被监控。
-
-
非监控分支(PSI 未启用或监听套接字未监控): 如果 PSI 未定义,或者
socket_listen.m_psi为空/禁用,则直接调用accept。/* Non instrumented code */ socket_accept.fd = accept(socket_listen.fd, addr, &addr_length); -
统一的 PSI 初始化(即使未走监控分支): 即使前面未进入监控分支(例如监听套接字没有 PSI 句柄),只要 PSI 功能启用且
accept成功,仍然会为新套接字初始化 PSI 数据。这保证了所有新连接都能被监控,而监听套接字本身可能未被监控(如 Unix 域套接字可能未配置 PSI)。#ifdef HAVE_PSI_SOCKET_INTERFACE if (likely(socket_accept.fd != INVALID_SOCKET)) { socket_accept.m_psi = PSI_SOCKET_CALL(init_socket)( key, (const my_socket *)&socket_accept.fd, addr, addr_length); } #endif -
返回新套接字:
socket_accept结构体包含文件描述符和 PSI 指针(若成功且 PSI 启用)。
创建 Channel_info 对象
cpp
Channel_info *channel_info = nullptr;
if (listen_socket->m_socket_type == Socket_type::UNIX_SOCKET)
channel_info = new (std::nothrow) Channel_info_local_socket(connect_sock);
else
channel_info = new (std::nothrow) Channel_info_tcpip_socket(
connect_sock, (listen_socket->m_socket_interface ==
Socket_interface_type::ADMIN_INTERFACE));
-
根据监听套接字类型创建相应的
Channel_info子类:-
Unix 域套接字 :创建
Channel_info_local_socket,只传递套接字描述符。 -
其他(TCP/IP) :创建
Channel_info_tcpip_socket,额外传递一个布尔值,指示该套接字是否属于管理接口(如管理端口)。
-
cpp
///////////////////////////////////////////////////////////////////////////
// Channel_info_tcpip_socket implementation
///////////////////////////////////////////////////////////////////////////
/**
This class abstracts the info. about TCP/IP socket mode of communication with
the server.
*/
class Channel_info_tcpip_socket : public Channel_info {
// connect socket object
MYSQL_SOCKET m_connect_sock;
/*
Flag specifying whether a connection is admin connection or
ordinary connection.
*/
bool m_is_admin_conn;
#ifdef HAVE_SETNS
/*
Network namespace associated with the socket.
*/
std::string m_network_namespace;
#endif
protected:
Vio *create_and_init_vio() const override {
Vio *vio = mysql_socket_vio_new(m_connect_sock, VIO_TYPE_TCPIP, 0);
#ifdef USE_PPOLL_IN_VIO
if (vio != nullptr) {
vio->thread_id.reset();
vio->signal_mask = mysqld_signal_mask;
}
#endif
#ifdef HAVE_SETNS
strncpy(vio->network_namespace, m_network_namespace.c_str(),
sizeof(vio->network_namespace) - 1);
vio->network_namespace[sizeof(vio->network_namespace) - 1] = '\0';
#endif
return vio;
}
public:
/**
Constructor that sets the connect socket.
@param connect_socket set connect socket descriptor.
@param is_admin_conn flag specifying whether a connection is admin
connection.
*/
Channel_info_tcpip_socket(MYSQL_SOCKET connect_socket, bool is_admin_conn)
: m_connect_sock(connect_socket), m_is_admin_conn(is_admin_conn) {}
THD *create_thd() override {
THD *thd = Channel_info::create_thd();
if (thd != nullptr) {
thd->set_admin_connection(m_is_admin_conn);
init_net_server_extension(thd);
}
return thd;
}
void send_error_and_close_channel(uint errorcode, int error,
bool senderror) override {
Channel_info::send_error_and_close_channel(errorcode, error, senderror);
mysql_socket_shutdown(m_connect_sock, SHUT_RDWR);
mysql_socket_close(m_connect_sock);
}
bool is_admin_connection() const override { return m_is_admin_conn; }
#ifdef HAVE_SETNS
/**
Set a network namespace for channel.
@param network_namespace Network namespace associated with a channel.
*/
void set_network_namespace(const std::string &network_namespace) {
m_network_namespace = network_namespace;
}
#endif
};
这段代码是 MySQL 中用于封装 TCP/IP 套接字连接信息的类 Channel_info_tcpip_socket 的实现。该类继承自抽象基类 Channel_info,用于将客户端连接(TCP/IP 协议)的相关信息(如套接字描述符、是否为管理连接、网络命名空间等)打包,并在后续线程创建、VIO 初始化等过程中使用。
Channel_info_tcpip_socket 与基类的关系
-
Channel_info是一个抽象基类,定义了创建 THD、关闭通道、发送错误等接口,并为不同类型连接(Unix 域套接字、TCP/IP)提供了统一的抽象。 -
Channel_info_tcpip_socket实现了 TCP/IP 连接的具体细节,包括:-
使用
MYSQL_SOCKET保存套接字。 -
在 VIO 创建时指定
VIO_TYPE_TCPIP。 -
管理连接标志的传递。
-
网络命名空间的支持。
-
-
另一个派生类
Channel_info_local_socket用于 Unix 域套接字,逻辑类似但 VIO 类型为VIO_TYPE_SOCKET。
成员变量
|---------------------------|----------------------|--------------------------------------------------------------------------|
| 字段 | 类型 | 作用 |
| m_connect_sock | MYSQL_SOCKET | 通过 accept 返回的客户端套接字,封装为 MYSQL_SOCKET 类型,支持跨平台和性能监控(PSI)。 |
| m_is_admin_conn | bool | 标识该连接是否来自管理接口。管理接口可以配置单独的端口,拥有更高的优先级或特殊的访问权限。 |
| m_network_namespace | std::string | 仅在定义了 HAVE_SETNS(支持 Linux 网络命名空间)时存在,用于记录该连接所属的网络命名空间名称,供 VIO 和后续处理使用。 |
函数 create_and_init_vio
该方法是一个 protected 虚函数,由基类 Channel_info 在创建 THD 时调用,用于创建并初始化 VIO 对象,供后续网络通信使用。
-
mysql_socket_vio_new:创建一个 VIO 对象,包装给定的套接字,类型为VIO_TYPE_TCPIP(TCP/IP 类型),第三个参数0表示使用默认超时。 -
USE_PPOLL_IN_VIO分支 :如果编译时启用了USE_PPOLL_IN_VIO(通常表示使用ppoll实现 VIO 中的多路复用),则设置 VIO 的thread_id和signal_mask。-
vio->thread_id.reset():可能重置线程 ID 相关状态(具体取决于实现)。 -
vio->signal_mask = mysqld_signal_mask:保存服务器当前信号掩码,用于ppoll调用时恢复,确保信号处理正确。
-
-
HAVE_SETNS分支 :如果系统支持网络命名空间,则将保存的命名空间名称复制到 VIO 的network_namespace字段中。这样后续在该 VIO 上进行网络操作时(如读写),可以知道它属于哪个命名空间,但实际读写操作通常不依赖此字段,更多用于日志、审计或权限判断。 -
最后返回创建的 VIO 对象,如果创建失败则返回
nullptr。
create_thd() 方法
cpp
THD *create_thd() override {
THD *thd = Channel_info::create_thd(); // 调用基类创建基础 THD
if (thd != nullptr) {
thd->set_admin_connection(m_is_admin_conn); // 设置是否为管理连接
init_net_server_extension(thd); // 初始化网络扩展
}
return thd;
}
重写基类的纯虚函数,用于创建并初始化 THD 对象。
-
首先调用基类的
Channel_info::create_thd(),创建并初始化一个基本的THD对象(包括分配内存、设置初始状态等)。 -
若成功,则:
-
设置管理标志 :调用
thd->set_admin_connection(),根据m_is_admin_conn标记该线程是否属于管理连接,这在权限检查、连接计数等方面有影响。 -
初始化网络扩展 :
init_net_server_extension(thd)会初始化 THD 中的网络相关部分(如 VIO 的关联、网络缓冲区等),通常内部会调用create_and_init_vio()来获取 VIO 并挂载到 THD。
-
-
返回
thd,若基类创建失败则返回nullptr。
send_error_and_close_channel() 方法
cpp
void send_error_and_close_channel(uint errorcode, int error,
bool senderror) override {
Channel_info::send_error_and_close_channel(errorcode, error, senderror);
mysql_socket_shutdown(m_connect_sock, SHUT_RDWR);
mysql_socket_close(m_connect_sock);
}
用于在连接出现错误时发送错误消息并关闭通道。
-
先调用基类的
send_error_and_close_channel(),其内部会使用net_send_error_packet等函数向客户端发送错误包(如果senderror为真),并关闭网络连接(但可能只是 VIO 层面)。 -
然后显式对原始套接字执行
shutdown(SHUT_RDWR表示禁止收发)和close,确保彻底释放操作系统资源。这一步是必要的,因为基类的处理可能未完全关闭底层套接字。
分发处理新连接
cpp
if (channel_info != nullptr) mgr->process_new_connection(channel_info);
对新传入的连接,是通过全局连接处理器管理器做分发处理的,具体实现为:
cpp
void Connection_handler_manager::process_new_connection(
Channel_info *channel_info) {
if (connection_events_loop_aborted() ||
!check_and_incr_conn_count(channel_info->is_admin_connection())) {
channel_info->send_error_and_close_channel(ER_CON_COUNT_ERROR, 0, true);
delete channel_info;
return;
}
if (m_connection_handler->add_connection(channel_info)) {
inc_aborted_connects();
delete channel_info;
}
}
-
连接数限制检查
-
connection_events_loop_aborted():如果服务器的连接事件循环已被标记为中止(如正在关闭),则拒绝新连接。 -
!check_and_incr_conn_count(channel_info->is_admin_connection()):调用check_and_incr_conn_count检查当前连接数是否已达上限。此函数接受一个布尔参数,表示是否为管理连接。管理连接可能不受普通连接数的限制(取决于配置),从而保证管理员总能连入。如果检查失败(连接数超限),则返回false。 -
失败处理:
-
调用
channel_info->send_error_and_close_channel(ER_CON_COUNT_ERROR, 0, true)向客户端发送错误消息(错误码ER_CON_COUNT_ERROR表示连接数已达上限),并关闭套接字。 -
然后
delete channel_info释放该对象,避免内存泄漏。 -
最后
return返回,不继续处理。
-
-
-
将连接交给连接处理器
if (m_connection_handler->add_connection(channel_info)) { inc_aborted_connects(); delete channel_info; }-
m_connection_handler:是Connection_handler_manager持有的一个Connection_handler抽象指针,具体实现可以是:-
Per_thread_connection_handler -
One_thread_connection_handler -
其他自定义处理器。
-
-
add_connection:将channel_info交给连接处理器。如果处理器成功接收(例如成功创建线程或将任务放入队列),则返回0;否则返回非零(表示失败,如系统资源不足)。 -
失败处理:
-
若
add_connection返回非零,则调用inc_aborted_connects()增加服务器全局的aborted_connects计数器(记录因服务器内部错误而拒绝的连接数)。 -
然后
delete channel_info,释放连接信息对象(因为处理器未能接管)。
-
-
成功时 :处理器已接管
channel_info,不再在此处删除。处理器会在连接结束时负责释放相关资源。
-
代码走到这里,我们需要去 Per_thread_connection_handler 里找 add_connection 实现:
cpp
bool Per_thread_connection_handler::add_connection(Channel_info *channel_info) {
int error = 0;
my_thread_handle id;
DBUG_TRACE;
// Simulate thread creation for test case before we check thread cache
DBUG_EXECUTE_IF("fail_thread_create", error = 1; goto handle_error;);
if (!check_idle_thread_and_enqueue_connection(channel_info)) return false;
/*
There are no idle threads available to take up the new
connection. Create a new thread to handle the connection
*/
channel_info->set_prior_thr_create_utime();
error =
mysql_thread_create(key_thread_one_connection, &id, &connection_attrib,
handle_connection, (void *)channel_info);
#ifndef NDEBUG
handle_error:
#endif // !NDEBUG
if (error) {
connection_errors_internal++;
if (!create_thd_err_log_throttle.log())
LogErr(ERROR_LEVEL, ER_CONN_PER_THREAD_NO_THREAD, error);
channel_info->send_error_and_close_channel(ER_CANT_CREATE_THREAD, error,
true);
Connection_handler_manager::dec_connection_count();
return true;
}
Global_THD_manager::get_instance()->inc_thread_created();
DBUG_PRINT("info", ("Thread created"));
return false;
}
Per_thread_connection_handler::add_connection 是 MySQL 中采用 每连接一个线程 (one thread per connection)模型时的核心函数。当服务器接收到一个新连接并封装为 Channel_info 对象后,该函数负责将该连接分配给一个线程进行处理。它首先尝试复用已有的空闲线程,若无空闲线程则创建新线程。
尝试复用空闲线程:
-
check_idle_thread_and_enqueue_connection()是Per_thread_connection_handler的成员函数。-
作用 :检查是否有处于空闲状态的线程(即已创建但当前没有处理连接的线程)。如果有,则将新的
channel_info放入该线程的等待队列,并唤醒该线程去处理。 -
返回值 :
true表示没有空闲线程 ,需要创建新线程;false表示已成功将连接交给空闲线程,本函数应立即返回成功(false)。
-
-
因此,如果返回
false,函数直接结束,表示连接已被接管。
创建新线程处理连接:
-
set_prior_thr_create_utime():记录创建线程前的时间戳(微秒),用于性能统计或调试(例如测量线程创建耗时)。 -
mysql_thread_create:MySQL 封装的线程创建函数。-
key_thread_one_connection:Performance Schema 的线程键,用于标识该线程类型(普通用户连接线程)。 -
&id:输出参数,返回新线程的句柄。 -
&connection_attrib:线程属性(通常为默认值)。 -
handle_connection:线程主函数,负责处理连接(创建THD、执行协议握手、处理 SQL 命令等)。 -
(void *)channel_info:传递给线程函数的参数,即新连接的封装对象。
-
-
若创建成功,
error为 0;否则为非零错误码。
线程创建失败:
-
如果
error非零(即线程创建失败),则:-
增加全局内部错误计数器
connection_errors_internal(统计因线程创建失败等原因导致的连接错误)。 -
使用限流器
create_thd_err_log_throttle控制错误日志的输出频率,避免大量错误日志刷屏。如果限流器允许输出,则记录错误日志ER_CONN_PER_THREAD_NO_THREAD(无法创建线程),并附带系统错误码。 -
调用
channel_info->send_error_and_close_channel()向客户端发送错误包(错误码ER_CANT_CREATE_THREAD,表示无法创建线程),然后关闭套接字。 -
调用
Connection_handler_manager::dec_connection_count()减少全局连接计数器(因为在接受连接时已经增加了计数,现在失败需要回退)。 -
返回
true表示失败。
-
线程创建成功:
-
调用全局 THD 管理器的
inc_thread_created()增加已创建的线程总数统计(用于Threads_created状态变量)。 -
调试打印信息,表明线程已创建。
-
返回
false表示成功。
至此,主线程忙完自己的工作了,剩下的工作全部委托给了子线程。我们下面继续深入 mysql_thread_create 函数
处理客户端请求
cpp
static inline int inline_mysql_thread_create(
PSI_thread_key key [[maybe_unused]],
unsigned int sequence_number [[maybe_unused]], my_thread_handle *thread,
const my_thread_attr_t *attr, my_start_routine start_routine, void *arg) {
int result;
#ifdef HAVE_PSI_THREAD_INTERFACE
result = PSI_THREAD_CALL(spawn_thread)(key, sequence_number, thread, attr,
start_routine, arg);
#else
result = my_thread_create(thread, attr, start_routine, arg);
#endif
return result;
}
inline_mysql_thread_create 函数是 MySQL 内部对线程创建函数 的一个包装,主要目的是集成 Performance Schema(性能模式)的监控能力 。它实际上是之前代码中 mysql_thread_create 宏或函数背后的具体实现。
参数说明:
| 参数 | 类型 | 说明 |
|---|---|---|
key |
PSI_thread_key |
性能模式中用于标识线程类别的键(例如 key_thread_one_connection),在非性能模式构建中标记为 [[maybe_unused]]。 |
sequence_number |
unsigned int |
性能模式可能用到的序列号,通常用于区分同一类别下的不同线程实例。 |
thread |
my_thread_handle * |
输出参数,返回新创建的线程句柄。 linux下是pthread_t |
attr |
const my_thread_attr_t * |
线程属性(如栈大小、分离状态等)。 linux下是pthread_attr_t |
start_routine |
my_start_routine |
线程入口函数(如 handle_connection)。 |
arg |
void * |
传递给线程入口函数的参数(如 Channel_info *)。 |
我们大概可以认为,在linux环境下,他其实就是对pthread_create 函数的调用封装
#include <pthread.h> int pthread_create(pthread_t *restrict thread, const pthread_attr_t *restrict attr, typeof(void *(void *)) *start_routine, void *restrict arg); The pthread_create() function starts a new thread in the calling process. The new thread starts execution by invoking start_routine(); arg is passed as the sole argument of start_routine(). The new thread terminates in one of the following ways: • It calls pthread_exit(3), specifying an exit status value that is available to another thread in the same process that calls pthread_join(3). • It returns from start_routine(). This is equivalent to calling pthread_exit(3) with the value supplied in the return statement. • It is canceled (see pthread_cancel(3)). • Any of the threads in the process calls exit(3), or the main thread performs a return from main(). This causes the termination of all threads in the process. The attr argument points to a pthread_attr_t structure whose contents are used at thread creation time to determine attributes for the new thread; this structure is initialized using pthread_attr_init(3) and related functions. If attr is NULL, then the thread is created with default attributes. Before returning, a successful call to pthread_create() stores the ID of the new thread in the buffer pointed to by thread; this identifier is used to refer to the thread in subsequent calls to other pthreads functions. The new thread inherits a copy of the creating thread's signal mask (pthread_sigmask(3)). The set of pending signals for the new thread is empty (sigpending(2)). The new thread does not inherit the creating thread's alternate signal stack (sigaltstack(2)). The new thread inherits the calling thread's floating-point environment (fenv(3)). The initial value of the new thread's CPU-time clock is 0 (see pthread_getcpuclockid(3)).
cpp
int my_thread_create(my_thread_handle *thread, const my_thread_attr_t *attr,
my_start_routine func, void *arg) {
#ifndef _WIN32
return pthread_create(&thread->thread, attr, func, arg);
#else
struct thread_start_parameter *par;
unsigned int stack_size;
par = (struct thread_start_parameter *)malloc(sizeof(*par));
if (!par) goto error_return;
par->func = func;
par->arg = arg;
stack_size = attr ? attr->dwStackSize : 0;
thread->handle =
(HANDLE)_beginthreadex(nullptr, stack_size, win_thread_start, par, 0,
(unsigned int *)&thread->thread);
if (thread->handle) {
/* Note that JOINABLE is default, so attr == nullptr => JOINABLE. */
if (attr && attr->detachstate == MY_THREAD_CREATE_DETACHED) {
/*
Close handles for detached threads right away to avoid leaking
handles. For joinable threads we need the handle during
my_thread_join. It will be closed there.
*/
CloseHandle(thread->handle);
thread->handle = nullptr;
}
return 0;
}
my_osmaperr(GetLastError());
free(par);
error_return:
thread->thread = 0;
thread->handle = nullptr;
return 1;
#endif
}
my_thread_create 是 MySQL 中对底层线程创建 API 的跨平台封装,用于屏蔽 Windows 和 POSIX 系统(如 Linux、Unix)的差异,并统一错误处理、参数传递和资源管理。该函数在 my_thread.h头文件中声明,并在 my_thread.cc 源文件中实现。
请求参数:
-
my_thread_handle:平台无关的线程句柄结构体。在 POSIX 上通常为pthread_t,在 Windows 上包含一个HANDLE和一个线程 ID(DWORD)。 -
my_thread_attr_t:线程属性结构体,对应 POSIX 的pthread_attr_t或 Windows 的自定义结构(包含栈大小、分离状态等)。 -
my_start_routine:线程入口函数类型,定义为void* (*)(void*)。 -
arg:传递给线程函数的参数。
然后对应于 my_thread_create(thread, attr, start_routine, arg) 的调用,我们可以对请求参数做详细的理解。
my_thread_handle
cpp
struct my_thread_handle {
my_thread_t thread{null_thread_initializer};
#if defined(_WIN32) && !defined(MYSQL_ABI_CHECK)
HANDLE handle{INVALID_HANDLE_VALUE};
#endif
};
这个结构体用于统一表示一个线程的"句柄",封装了不同操作系统下线程标识和管理所需的数据。
my_thread_t thread
-
类型 :
my_thread_t是一个条件编译的 typedef:-
Windows:
DWORD(线程 ID) -
非 Windows:
pthread_t(POSIX 线程 ID)
-
-
初始化 :使用
null_thread_initializer(定义为my_thread_t{}值初始化),确保在所有平台上都有一个明确的"空"值。 -
作用:存储操作系统原生的线程标识符,用于线程比较、等待等操作。
HANDLE handle(仅 Windows)
-
存在条件 :仅在 Windows 平台且未定义
MYSQL_ABI_CHECK时出现。 -
类型 :
HANDLE是 Windows 内核对象句柄,实际线程对象的引用。 -
初始值 :
INVALID_HANDLE_VALUE,表示无效句柄。 -
作用 :在 Windows 上,线程操作(如
WaitForSingleObject、TerminateThread)需要HANDLE而不是DWORD线程 ID。因此额外保存HANDLE以便进行同步和管理。
为什么需要两个成员属性(仅 Windows)?
在 POSIX 系统(Linux、macOS 等)中,
pthread_t本身就可以唯一标识一个线程,并且可以通过pthread_*函数直接操作。而在 Windows 中,DWORD只是线程 ID,不能用于等待线程结束或修改线程优先级等操作------这些操作需要内核对象句柄HANDLE。因此,Windows 实现必须同时保存 ID 和句柄,以提供与 POSIX 类似的功能。结构体设计将平台差异隐藏在内,上层代码可以统一使用
my_thread_handle,而无需关心底层细节。
my_thread_attr_t
my_thread_attr_t 是 MySQL 线程抽象层中用于表示线程属性的类型。它定义了创建线程时所需的各种参数,如栈大小、是否分离等,从而在不同的操作系统上提供统一的接口。
cpp
#if defined(_WIN32) && !defined(MYSQL_ABI_CHECK)
typedef DWORD my_thread_t;
typedef struct thread_attr {
DWORD dwStackSize;
int detachstate;
} my_thread_attr_t;
#else
typedef pthread_t my_thread_t;
typedef pthread_attr_t my_thread_attr_t;
#endif
-
Windows 平台 :自定义了一个结构体
thread_attr,包含两个成员。 -
非 Windows 平台(如 Linux、macOS 等) :直接使用 POSIX 的
pthread_attr_t类型。
Windows 下的 my_thread_attr_t 成员详解
DWORD dwStackSize
-
含义:线程栈的初始大小,单位为字节。
-
对应 Windows 参数 :
CreateThread的dwStackSize参数。 -
特殊值:若为 0,则使用系统默认栈大小(通常为 1 MB)。
int detachstate
-
含义:线程的分离状态。
-
取值 :通常为
0(非分离,即 joinable)或1(分离,detached)。 -
Windows 实现差异:Windows 原生线程没有"分离"属性,分离状态通过句柄管理来实现:
-
若设置为分离状态,则在创建线程后立即关闭线程句柄(
CloseHandle),使线程在结束时不需其他线程等待。 -
若设置为非分离状态,则需要保留句柄,以便后续通过
WaitForSingleObject等待线程结束并获取退出码。
-
因此,这个成员实际上指导了 MySQL 内部线程创建函数如何处理 HANDLE。
POSIX 下的 my_thread_attr_t
在 POSIX 系统中,my_thread_attr_t 直接定义为 pthread_attr_t,并直接使用标准 pthread 函数操作:
-
初始化:
pthread_attr_init(&attr); -
设置栈大小:
pthread_attr_setstacksize(&attr, size); -
设置分离状态:
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); -
销毁:
pthread_attr_destroy(&attr);
MySQL 的上层代码(如 my_thread_create 函数)会根据平台调用相应的底层实现,从而隐藏差异。
与 my_thread_handle 的关联
-
my_thread_attr_t:用于创建线程前,描述线程应该具有的属性。 -
my_thread_handle:用于创建线程后,表示一个已经运行的线程实例(包含线程 ID 和 Windows 句柄)。
my_thread_attr_t 是 MySQL 跨平台线程封装的关键组件之一。它在 Windows 上通过自定义结构体模拟了 POSIX 线程属性,并将栈大小和分离状态这两个核心属性封装起来;在 POSIX 系统上则直接使用原生 pthread_attr_t。这种设计使得 MySQL 能够在保持高性能的同时,无缝运行在 Windows 和各种 Unix-like 系统上。
线程入口
cpp
extern "C" {
static void *handle_connection(void *arg) {
Global_THD_manager *thd_manager = Global_THD_manager::get_instance();
Connection_handler_manager *handler_manager =
Connection_handler_manager::get_instance();
Channel_info *channel_info = static_cast<Channel_info *>(arg);
bool pthread_reused [[maybe_unused]] = false;
if (my_thread_init()) {
connection_errors_internal++;
channel_info->send_error_and_close_channel(ER_OUT_OF_RESOURCES, 0, false);
handler_manager->inc_aborted_connects();
Connection_handler_manager::dec_connection_count();
delete channel_info;
my_thread_exit(nullptr);
return nullptr;
}
for (;;) {
THD *thd = init_new_thd(channel_info);
if (thd == nullptr) {
connection_errors_internal++;
handler_manager->inc_aborted_connects();
Connection_handler_manager::dec_connection_count();
break; // We are out of resources, no sense in continuing.
}
#ifdef HAVE_PSI_THREAD_INTERFACE
if (pthread_reused) {
/*
Reusing existing pthread:
Create new instrumentation for the new THD job,
and attach it to this running pthread.
*/
PSI_thread *psi = PSI_THREAD_CALL(new_thread)(key_thread_one_connection,
0 /* no sequence number */,
thd, thd->thread_id());
PSI_THREAD_CALL(set_thread_os_id)(psi);
PSI_THREAD_CALL(set_thread)(psi);
}
#endif
#ifdef HAVE_PSI_THREAD_INTERFACE
/* Find the instrumented thread */
PSI_thread *psi = PSI_THREAD_CALL(get_thread)();
/* Save it within THD, so it can be inspected */
thd->set_psi(psi);
#endif /* HAVE_PSI_THREAD_INTERFACE */
mysql_thread_set_psi_id(thd->thread_id());
mysql_thread_set_psi_THD(thd);
const MYSQL_SOCKET socket =
thd->get_protocol_classic()->get_vio()->mysql_socket;
mysql_socket_set_thread_owner(socket);
thd_manager->add_thd(thd);
if (thd_prepare_connection(thd))
handler_manager->inc_aborted_connects();
else {
while (thd_connection_alive(thd)) {
if (do_command(thd)) break;
}
end_connection(thd);
}
close_connection(thd, 0, false, false);
thd->get_stmt_da()->reset_diagnostics_area();
thd->release_resources();
// Clean up errors now, before possibly waiting for a new connection.
#if OPENSSL_VERSION_NUMBER < 0x10100000L
ERR_remove_thread_state(nullptr);
#endif /* OPENSSL_VERSION_NUMBER < 0x10100000L */
thd_manager->remove_thd(thd);
Connection_handler_manager::dec_connection_count();
#ifdef HAVE_PSI_THREAD_INTERFACE
/* Stop telemetry, while THD is still available. */
if (psi != nullptr) {
PSI_THREAD_CALL(abort_telemetry)(psi);
}
/* Decouple THD and the thread instrumentation. */
thd->set_psi(nullptr);
mysql_thread_set_psi_THD(nullptr);
#endif /* HAVE_PSI_THREAD_INTERFACE */
delete thd;
#ifdef HAVE_PSI_THREAD_INTERFACE
/* Delete the instrumentation for the job that just completed. */
PSI_THREAD_CALL(delete_current_thread)();
#endif /* HAVE_PSI_THREAD_INTERFACE */
// Server is shutting down so end the pthread.
if (connection_events_loop_aborted()) break;
channel_info = Per_thread_connection_handler::block_until_new_connection();
if (channel_info == nullptr) break;
pthread_reused = true;
if (connection_events_loop_aborted()) {
// Close the channel and exit as server is undergoing shutdown.
channel_info->send_error_and_close_channel(ER_SERVER_SHUTDOWN, 0, false);
delete channel_info;
channel_info = nullptr;
Connection_handler_manager::dec_connection_count();
break;
}
}
my_thread_end();
my_thread_exit(nullptr);
return nullptr;
}
} // extern "C"
-
获取全局单例:
-
Global_THD_manager负责管理所有 THD 对象 -
Connection_handler_manager负责统计连接计数和协调处理器,我们在上面有讲过。
-
-
pthread_reused标记当前线程是否为复用线程(即之前已处理过连接,现在等待新连接)。这个标志用于性能模式(PSI)的重新初始化。bool pthread_reused [[maybe_unused]] = false; -
my_thread_init():初始化线程局部存储(如THR_MALLOC、my_errno等),若失败则无法继续。 -
主循环:处理一个或多个连接:
-
创建并初始化 THD:
init_new_thd负责根据channel_info创建THD对象,并初始化网络 VIO、设置会话变量等。 -
性能模式(PSI)集成:
-
如果线程被复用(
pthread_reused为真),则需要为新的连接创建新的 PSI 线程描述符,并绑定到当前线程。这是因为 PSI 认为每个"逻辑线程"(即每次连接的 THD)是一个独立的 instrumentation 单元,即使物理线程是同一个。 -
随后获取当前线程的 PSI 描述符(可能已通过
new_thread设置或从之前继承),并将其保存到 THD 中,以便后续统计。 -
设置线程的 PSI ID 和关联的 THD 指针,用于性能监控。
-
获取客户端套接字,并设置其拥有者线程(用于调试和资源管理)。
-
将 THD 注册到全局 THD 管理器,以便其他组件(如 kill 操作)可以找到它。
-
-
处理连接:
-
thd_prepare_connection:执行协议握手、身份验证、初始化会话变量等。如果失败(如认证失败),则增加放弃连接计数。 -
如果准备成功,进入命令循环:
-
end_connection(thd):处理连接结束后的清理,如更新状态变量、记录二进制日志等。
-
-
关闭连接与清理
-
close_connection:发送结束包(如果合适),关闭套接字,更新状态。 -
reset_diagnostics_area:清空诊断区域。 -
release_resources:释放 THD 持有的资源(如临时表、用户变量等)。 -
从全局 THD 管理器中移除 THD。
-
减少全局连接计数(与之前
inc_connection_count对应)。 -
停止该连接的遥测数据收集(如等待时间统计),并解除 THD 与 PSI 的关联。
-
销毁 THD 对象( delete thd;)。
-
如果 PSI 启用,删除当前线程的 instrumentation(因为该逻辑线程已完成)。
-
-
决定是否等待下一个连接
-
首先检查服务器是否正在关闭(
connection_events_loop_aborted()),若是则跳出循环。 -
否则,调用
block_until_new_connection()将当前线程挂起,直到有新的连接请求被分配到该线程(通过check_idle_thread_and_enqueue_connection机制)。 -
如果返回
nullptr,表示没有更多连接(通常因为服务器关闭或线程池关闭),则跳出循环。 -
如果返回有效的
channel_info,则设置pthread_reused = true,标记此线程为复用状态。 -
再次检查服务器关闭标志:如果在此期间服务器开始关闭,则立即向客户端发送关闭错误,释放
channel_info,减少连接计数,并跳出循环。
-
-
-
线程退出前清理
-
my_thread_end()清理线程局部存储。 -
my_thread_exit结束线程执行(在 POSIX 中调用pthread_exit,在 Windows 中结束线程)。 -
返回
nullptr实际上不会被执行,但保持函数签名一致。
-
Global_THD_manager
cpp
class Global_THD_manager {
public:
/**
Value for thread_id reserved for THDs which does not have an
assigned value yet. get_new_thread_id() will never return this
value.
*/
static const my_thread_id reserved_thread_id;
/**
Retrieves singleton instance
*/
static Global_THD_manager *get_instance() {
assert(thd_manager != nullptr);
return thd_manager;
}
/**
Checks if the singleton is not already deinitialized
*/
static bool is_initialized() { return thd_manager != nullptr; }
/**
Initializes the thd manager.
Must be called before get_instance() can be used.
@return true if initialization failed, false otherwise.
*/
static bool create_instance();
/**
Destroys the singleton instance.
*/
static void destroy_instance();
/**
Internally used to bypass code.
It enables unit test scripts to create dummy THD object for testing.
*/
void set_unit_test() { unit_test = true; }
/**
Adds THD to global THD list.
@param thd THD object
*/
void add_thd(THD *thd);
/**
Removes THD from global THD list.
@param thd THD object
*/
void remove_thd(THD *thd);
/**
Retrieves thread running statistic variable.
@return int Returns the total number of threads currently running
*/
int get_num_thread_running() const { return atomic_num_thread_running; }
/**
Increments thread running statistic variable.
*/
void inc_thread_running() { atomic_num_thread_running++; }
/**
Decrements thread running statistic variable.
*/
void dec_thread_running() { atomic_num_thread_running--; }
/**
Retrieves thread created statistic variable.
@return ulonglong Returns the total number of threads created
after server start
*/
ulonglong get_num_thread_created() const { return atomic_thread_created; }
/**
Increments thread created statistic variable.
*/
void inc_thread_created() { atomic_thread_created++; }
/**
Returns an unused thread id.
*/
my_thread_id get_new_thread_id();
/**
Releases a thread id so that it can be reused.
Note that this is done automatically by remove_thd().
*/
void release_thread_id(my_thread_id thread_id);
/**
Retrieves thread id counter value.
@return my_thread_id Returns the thread id counter value
@note This is a dirty read.
*/
my_thread_id get_thread_id() const { return thread_id_counter; }
/**
Sets thread id counter value. Only used in testing for now.
@param new_id The next ID to hand out (if it's unused).
*/
void set_thread_id_counter(my_thread_id new_id);
/**
Retrieves total number of items in global THD lists (all partitions).
@return uint Returns the count of items in global THD lists.
*/
static uint get_thd_count() { return atomic_global_thd_count; }
/**
Waits until all THDs are removed from global THD lists (all partitions).
In other words, get_thd_count() to become zero.
*/
void wait_till_no_thd();
/**
This function calls func() for all THDs in every thd list partition
after taking local copy of the THD list partition. It acquires
LOCK_thd_remove to prevent removal of the THD.
@param func Object of class which overrides operator()
*/
void do_for_all_thd_copy(Do_THD_Impl *func);
/**
This function calls func() for all THDs in all THD list partitions.
@param func Object of class which overrides operator()
@note One list partition is unlocked before the next partition is locked.
*/
void do_for_all_thd(Do_THD_Impl *func);
/**
* This function calls func() for all first "n" THDs across all THD list
* partitions.
* @param func Object of class which overrides operator()
* @param n number of elements we want to call func for
*/
void do_for_first_n_thd(Do_THD_Impl *func, uint n);
/**
Returns a THD_ptr containing first THD for which operator() returns true.
@param func Object of class which overrides operator()
@return THD_ptr
@retval THD_ptr{THD*} When matching THD is found.
@retval THD_ptr{nullptr} When THD is *not* found.
*/
THD_ptr find_thd(Find_THD_Impl *func);
THD_ptr find_thd(Find_thd_with_id *func);
// Declared static as it is referenced in handle_fatal_signal()
static std::atomic<uint> atomic_global_thd_count;
// Number of THD list partitions.
static const int NUM_PARTITIONS = 8;
private:
Global_THD_manager();
~Global_THD_manager();
// Singleton instance.
static Global_THD_manager *thd_manager;
// Array of current THDs. Protected by LOCK_thd_list.
typedef Prealloced_array<THD *, 60> THD_array;
THD_array thd_list[NUM_PARTITIONS];
// Array of thread ID in current use. Protected by LOCK_thread_ids.
typedef Prealloced_array<my_thread_id, 1000> Thread_id_array;
Thread_id_array thread_ids;
mysql_cond_t COND_thd_list[NUM_PARTITIONS];
// Mutexes that guard thd_list partitions
mysql_mutex_t LOCK_thd_list[NUM_PARTITIONS];
// Mutexes used to guard removal of elements from thd_list partitions.
mysql_mutex_t LOCK_thd_remove[NUM_PARTITIONS];
// Mutex protecting thread_ids
mysql_mutex_t LOCK_thread_ids;
// Count of active threads which are running queries in the system.
std::atomic<int> atomic_num_thread_running;
// Cumulative number of threads created by mysqld daemon.
std::atomic<ulonglong> atomic_thread_created;
// Counter to assign thread id.
my_thread_id thread_id_counter;
// Used during unit test to bypass creating real THD object.
bool unit_test;
friend void thd_lock_thread_count();
friend void thd_unlock_thread_count();
};
#endif /* MYSQLD_INCLUDED */
Global_THD_manager 是 MySQL 中用于全局管理所有 THD(线程描述符) 的核心类。THD 代表一个客户端连接或后台线程的上下文,包含会话状态、事务信息、变量等。该类采用单例模式,负责线程的注册、注销、统计以及线程 ID 分配等关键任务。
主要功能
-
全局 THD 列表管理 :通过分区数组(
NUM_PARTITIONS = 8)存储所有活跃 THD 指针,减少多线程并发访问时的锁争用。 -
线程运行/创建统计 :使用原子变量记录当前正在运行的线程数(
atomic_num_thread_running)以及自启动以来累计创建的线程总数(atomic_thread_created)。 -
线程 ID 分配 :维护一个可重用的线程 ID 池,避免 ID 快速耗尽,并通过
LOCK_thread_ids保护。 -
安全遍历 THD :提供多种遍历方式(
do_for_all_thd、do_for_all_thd_copy、do_for_first_n_thd),支持在遍历期间防止 THD 被意外删除。 -
等待所有 THD 退出 :
wait_till_no_thd()用于优雅关闭等场景,直到全局 THD 计数变为零。
单例模式
-
thd_manager静态指针持有唯一实例。 -
create_instance()/destroy_instance()负责初始化和清理。 -
get_instance()返回实例,使用前必须确保is_initialized()。
静态字段
| 字段 | 类型 | 说明 |
|---|---|---|
reserved_thread_id |
const my_thread_id |
保留的线程 ID 值,不会被 get_new_thread_id() 分配出去(通常用于表示无效或未分配的 ID)。 |
atomic_global_thd_count |
std::atomic<uint> |
全局 THD 总数(所有分区中 THD 数量的总和),原子操作,用于快速获取当前活跃连接/线程数。 |
thd_manager |
Global_THD_manager* |
单例实例指针。 |
NUM_PARTITIONS |
static const int |
分区数量,固定为 8。用于将 THD 列表分片,减少锁竞争。 |
非静态字段
| 字段 | 类型 | 说明 |
|---|---|---|
thd_list[NUM_PARTITIONS] |
THD_array (Prealloced_array<THD*,60>) |
每个分区存储当前活跃的 THD 指针,预分配 60 个槽位,动态扩容。 |
thread_ids |
Thread_id_array (Prealloced_array<my_thread_id,1000>) |
已分配的线程 ID 集合,用于快速判断 ID 是否正在使用,支持复用。 |
COND_thd_list[NUM_PARTITIONS] |
mysql_cond_t |
条件变量数组,用于等待某个分区的 THD 数量变为 0。 |
LOCK_thd_list[NUM_PARTITIONS] |
mysql_mutex_t |
保护对应分区 thd_list 的读写操作。(如 add_thd / remove_thd 会哈希到对应分区)。 |
LOCK_thd_remove[NUM_PARTITIONS] |
mysql_mutex_t |
在遍历 THD 时锁定,防止遍历过程中 THD 被从分区中移除。保证遍历的稳定性。 |
LOCK_thread_ids |
mysql_mutex_t |
保护 thread_ids 数组的互斥锁。 |
atomic_num_thread_running |
std::atomic<int> |
当前正在执行查询的线程数量(活跃运行数)。每次开始执行查询时 inc_thread_running(),结束时 dec_thread_running()。 |
atomic_thread_created |
std::atomic<ulonglong> |
自服务器启动以来累计创建的线程总数。每次创建新线程(如新建连接)时 inc_thread_created()。 |
thread_id_counter |
my_thread_id |
下一个待分配的新线程 ID(单调递增,但会跳过已使用的 ID)。 |
unit_test |
bool |
单元测试标志,为 true 时允许创建"假" THD 对象绕过正常初始化。 |
静态方法
| 方法 | 返回类型 | 说明 |
|---|---|---|
get_instance() |
Global_THD_manager* |
返回单例实例指针。调用前必须确保 is_initialized() 为真。 static Global_THD_manager *get_instance() { assert(thd_manager != nullptr); return thd_manager; } |
is_initialized() |
bool |
检查单例是否已创建(thd_manager != nullptr)。 |
create_instance() |
bool |
创建单例实例。成功返回 false,失败返回 true。 bool Global_THD_manager::create_instance() { if (thd_manager == nullptr) thd_manager = new (std::nothrow) Global_THD_manager(); return (thd_manager == nullptr); } |
destroy_instance() |
void |
销毁单例实例,释放资源。 |
get_thd_count() |
uint |
返回全局 THD 总数(等价于 atomic_global_thd_count 的值)。 |
非静态方法
初始化和测试辅助
| 方法 | 说明 |
|---|---|
set_unit_test() |
将 unit_test 标志设为 true,仅供单元测试使用。 |
THD 列表管理
| 方法 | 说明 |
|---|---|
add_thd(THD* thd) |
将 THD 加入全局列表(根据 THD 的哈希值分配到某个分区)。 |
remove_thd(THD* thd) |
从全局列表中移除 THD,同时释放其占用的线程 ID。 |
运行与创建统计
| 方法 | 说明 |
|---|---|
get_num_thread_running() |
返回当前正在执行查询的线程数。 |
inc_thread_running() |
原子增加 atomic_num_thread_running。 |
dec_thread_running() |
原子减少 atomic_num_thread_running。 |
get_num_thread_created() |
返回累计创建的线程总数。 |
inc_thread_created() |
原子增加 atomic_thread_created。 |
线程 ID 管理
| 方法 | 说明 |
|---|---|
get_new_thread_id() |
返回一个未使用的线程 ID(优先复用已释放的 ID,否则使用 thread_id_counter++)。 |
release_thread_id(my_thread_id id) |
释放一个线程 ID,使其可以被后续复用(通常由 remove_thd 自动调用)。 |
get_thread_id() |
返回当前 thread_id_counter 的值(仅用于调试或测试,可能包含已分配的 ID)。 |
set_thread_id_counter(my_thread_id new_id) |
设置 thread_id_counter 的值(仅用于测试)。 |
全局等待与遍历
| 方法 | 说明 |
|---|---|
wait_till_no_thd() |
阻塞等待,直到所有分区的 THD 数量都变为 0(用于服务器关闭等场景)。 |
do_for_all_thd_copy(Do_THD_Impl* func) |
遍历所有 THD:对每个分区先加锁拷贝 THD 列表,然后立即解锁,再对拷贝的列表执行 func。适合回调中可能耗时较长或需要避免长时间持锁的场景。 |
do_for_all_thd(Do_THD_Impl* func) |
遍历所有 THD:顺序锁定每个分区,直接对分区内的 THD 执行 func,处理完一个分区才释放锁。适合快速遍历且需要看到实时列表的场景。 |
do_for_first_n_thd(Do_THD_Impl* func, uint n) |
遍历所有分区,对前 n 个 THD 执行 func(按分区顺序,每个分区内按数组顺序)。 |
find_thd(Find_THD_Impl* func) |
遍历所有 THD,返回第一个使 func 返回 true 的 THD 的 THD_ptr(智能指针,保证使用期间不被销毁)。 |
find_thd(Find_thd_with_id* func) |
重载版本,根据特定的 ID 查找 THD。 |
构造与析构(私有)
| 方法 | 说明 |
|---|---|
Global_THD_manager() |
私有构造函数,初始化互斥锁、条件变量、原子变量等。 |
~Global_THD_manager() |
私有析构函数,销毁所有锁和条件变量。 |
cpp
Global_THD_manager::Global_THD_manager()
: thd_list {
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
THD_array(PSI_INSTRUMENT_ME),
},
thread_ids(PSI_INSTRUMENT_ME),
atomic_num_thread_running(0),
atomic_thread_created(0),
thread_id_counter(reserved_thread_id + 1),
unit_test(false)
{
#ifdef HAVE_PSI_INTERFACE
int count = static_cast<int>(array_elements(all_thd_manager_mutexes));
mysql_mutex_register("sql", all_thd_manager_mutexes, count);
count = static_cast<int>(array_elements(all_thd_manager_conds));
mysql_cond_register("sql", all_thd_manager_conds, count);
#endif
for (int i = 0; i < NUM_PARTITIONS; i++) {
mysql_mutex_init(key_LOCK_thd_list, &LOCK_thd_list[i], MY_MUTEX_INIT_FAST);
mysql_mutex_init(key_LOCK_thd_remove, &LOCK_thd_remove[i],
MY_MUTEX_INIT_FAST);
mysql_cond_init(key_COND_thd_list, &COND_thd_list[i]);
}
mysql_mutex_init(key_LOCK_thread_ids, &LOCK_thread_ids, MY_MUTEX_INIT_FAST);
// The reserved thread ID should never be used by normal threads,
// so mark it as in-use. This ID is used by temporary THDs never
// added to the list of THDs.
thread_ids.push_back(reserved_thread_id);
}
初始化列表
thd_list 数组初始化:
-
thd_list是一个长度为NUM_PARTITIONS(=8)的数组,每个元素是Prealloced_array<THD*, 60>类型。 -
Prealloced_array是一个预分配内存的动态数组,模板参数60表示初始容量为 60 个指针,避免频繁扩容。 -
PSI_INSTRUMENT_ME是 Performance Schema Instrumentation 的宏,用于让该数组支持性能监控(统计内存操作、等待事件等)。 -
这里用花括号为 8 个分区分别构造了一个空的
THD_array,每个分区独立管理一部分 THD。
thread_ids 初始化:
-
thread_ids是Prealloced_array<my_thread_id, 1000>类型,用于存放已经分配且尚未释放的线程 ID。 -
同样传入
PSI_INSTRUMENT_ME,启用性能监控。
原子计数器初始化
-
atomic_num_thread_running(0):当前正在执行查询的线程数初始为 0。 -
atomic_thread_created(0):自启动以来创建的线程总数初始为 0。
线程 ID 计数器初始化
thread_id_counter(reserved_thread_id + 1):reserved_thread_id是一个静态常量,通常为 0 或一个特殊值(例如std::numeric_limits<my_thread_id>::max()或者 0)。这里将其加 1 作为第一个可分配的正常线程 ID。因为reserved_thread_id本身不会被分配给任何普通线程。
单元测试标志
unit_test(false):默认不为单元测试模式。
构造函数体
注册 Performance Schema 监控对象:
cpp
#ifdef HAVE_PSI_INTERFACE
int count = static_cast<int>(array_elements(all_thd_manager_mutexes));
mysql_mutex_register("sql", all_thd_manager_mutexes, count);
count = static_cast<int>(array_elements(all_thd_manager_conds));
mysql_cond_register("sql", all_thd_manager_conds, count);
#endif
-
如果编译时启用了 Performance Schema(
HAVE_PSI_INTERFACE),则需要注册该管理器所使用的所有互斥锁和条件变量,以便 P_S 能够监控它们的等待时间、争用情况等。 -
all_thd_manager_mutexes和all_thd_manager_conds是预定义的数组,包含了本类中所有锁和条件变量的PSI_mutex_info/PSI_cond_info描述信息。 -
mysql_mutex_register和mysql_cond_register将这些信息告知 P_S 基础设施,后续初始化每个锁时才能关联到对应的监控 key。
初始化每个分区的锁和条件变量:
cpp
for (int i = 0; i < NUM_PARTITIONS; i++) {
mysql_mutex_init(key_LOCK_thd_list, &LOCK_thd_list[i], MY_MUTEX_INIT_FAST);
mysql_mutex_init(key_LOCK_thd_remove, &LOCK_thd_remove[i],
MY_MUTEX_INIT_FAST);
mysql_cond_init(key_COND_thd_list, &COND_thd_list[i]);
}
-
LOCK_thd_list[i]:保护第i个分区的thd_list,防止并发添加/删除 THD 时数据损坏。 -
LOCK_thd_remove[i]:用于遍历时的删除保护。当需要安全遍历所有 THD 时,会先锁住这个锁,防止其他线程从该分区移除 THD。 -
COND_thd_list[i]:条件变量,用于wait_till_no_thd()等待该分区的 THD 数量变为 0。 -
key_LOCK_thd_list、key_LOCK_thd_remove、key_COND_thd_list是在其他地方定义的 P_S key,用于标识这些锁/条件变量。MY_MUTEX_INIT_FAST表示使用快速互斥体(不检查递归等)。
初始化全局线程 ID 池的锁:
cpp
mysql_mutex_init(key_LOCK_thread_ids, &LOCK_thread_ids, MY_MUTEX_INIT_FAST);
LOCK_thread_ids保护thread_ids数组,防止多个线程同时申请或释放 ID 时产生冲突。
标记保留线程 ID 为"已使用":
cpp
thread_ids.push_back(reserved_thread_id);
-
reserved_thread_id是一个特殊值(例如 0),永远不会通过get_new_thread_id()分配给普通线程。但有些临时 THD(如内部后台任务)可能会使用这个 ID,并且它们不会加入thd_list。 -
为了防止
get_new_thread_id()意外分配出这个保留 ID,构造函数将其预先放入thread_ids数组中,表示它已被占用。这样后续分配新 ID 时,如果尝试使用该值,会发现它已经存在,就会跳过并继续尝试下一个。
这个构造函数的核心任务是:
-
创建 8 个独立分区的 THD 列表,降低锁竞争。
-
初始化所有用于并发控制的互斥锁和条件变量。
-
注册 Performance Schema 监控支持。
-
设置初始计数器(运行线程数 0,累计创建线程数 0)。
-
预留第一个可分配的普通线程 ID(
reserved_thread_id + 1)。 -
将保留 ID 标记为"已使用",防止被误分配。
这样,Global_THD_manager 实例就处于一个干净、可用的初始状态,等待后续 add_thd()、inc_thread_running() 等操作。
友元
| 友元函数 | 说明 |
|---|---|
thd_lock_thread_count() |
允许该函数访问私有成员(用于某些需要全局计数锁的场合)。 |
thd_unlock_thread_count() |
对应解锁操作。 |
my_thread_init
cpp
/**
Allocate thread specific memory for the thread, used by mysys and dbug
@note This function may called multiple times for a thread, for example
if one uses my_init() followed by mysql_server_init().
@retval false ok
@retval true Fatal error; mysys/dbug functions can't be used
*/
bool my_thread_init() {
#ifndef NDEBUG
struct st_my_thread_var *tmp;
#endif
if (!my_thread_global_init_done)
return true; /* cannot proceed with uninitialized library */
#ifdef _WIN32
install_sigabrt_handler();
#endif
#ifndef NDEBUG
if (mysys_thread_var()) return false;
if (!(tmp = (struct st_my_thread_var *)calloc(1, sizeof(*tmp)))) return true;
mysql_mutex_lock(&THR_LOCK_threads);
tmp->id = ++thread_id;
if (thread_id == 1) THR_mysys_main = tmp;
++THR_thread_count;
mysql_mutex_unlock(&THR_LOCK_threads);
set_mysys_thread_var(tmp);
#endif
return false;
}
my_thread_init 函数是 MySQL 内部用于初始化线程局部存储 (Thread-Local Storage, TLS)的核心函数,通常在每一个工作线程(如 handle_connection 中的线程)启动时调用。
-
检查全局初始化标志:
my_thread_global_init_done在调用my_init()或mysql_server_init()时被设置为true。如果全局初始化未完成,则无法创建线程局部结构,直接失败。if (!my_thread_global_init_done) return true; /* cannot proceed with uninitialized library */ -
Windows 信号处理(仅 Windows):在 Windows 平台上安装 SIGABRT 信号处理程序,用于异常终止时的处理。对非 Windows 平台无影响。
-
核心逻辑(非 Debug 模式):整个核心初始化代码被
#ifndef NDEBUG包裹,这意味着在 Release 模式下,my_thread_init几乎什么都不做(只检查全局初始化标志和安装 Windows 信号处理)。
init_new_thd
cpp
static THD *init_new_thd(Channel_info *channel_info) {
THD *thd = channel_info->create_thd();
if (thd == nullptr) {
channel_info->send_error_and_close_channel(ER_OUT_OF_RESOURCES, 0, false);
delete channel_info;
return nullptr;
}
thd->set_new_thread_id();
if (channel_info->get_prior_thr_create_utime() != 0) {
/*
A pthread was created to handle this connection:
increment slow_launch_threads counter if it took more than
slow_launch_time seconds to create the pthread.
*/
const ulonglong launch_time =
thd->start_utime - channel_info->get_prior_thr_create_utime();
if (launch_time >= slow_launch_time * 1000000ULL)
Per_thread_connection_handler::slow_launch_threads++;
}
delete channel_info;
/*
handle_one_connection() is normally the only way a thread would
start and would always be on the very high end of the stack ,
therefore, the thread stack always starts at the address of the
first local variable of handle_one_connection, which is thd. We
need to know the start of the stack so that we could check for
stack overruns.
*/
thd_set_thread_stack(thd, (char *)&thd);
thd->store_globals();
return thd;
}
init_new_thd 函数是 MySQL 中 初始化一个新线程(THD 对象) 的核心函数。它通常由连接处理器(如 Per_thread_connection_handler)在创建新连接线程时调用。该函数负责创建 THD 对象、分配线程 ID、记录启动耗时、设置线程栈边界,并将 THD 与当前线程关联。
- 参数 :
Channel_info* channel_info:包含新连接信息的对象,如网络套接字、SSL 状态、认证数据等。
创建 THD 对象
cpp
THD *thd = channel_info->create_thd();
if (thd == nullptr) {
channel_info->send_error_and_close_channel(ER_OUT_OF_RESOURCES, 0, false);
delete channel_info;
return nullptr;
}
-
调用
channel_info->create_thd()创建一个新的 THD 实例。Channel_info的不同子类(如Channel_info_tcpip_socket、Channel_info_local_socket)会创建不同种类的 THD。 -
如果创建失败(内存不足等),则:
-
通过
send_error_and_close_channel向客户端发送错误消息(错误码ER_OUT_OF_RESOURCES)。 -
删除
channel_info对象(避免内存泄漏)。 -
返回
nullptr。
-
create_thd
cpp
THD *Channel_info::create_thd() {
DBUG_EXECUTE_IF("simulate_resource_failure", return nullptr;);
Vio *vio_tmp = create_and_init_vio();
if (vio_tmp == nullptr) return nullptr;
THD *thd = new (std::nothrow) THD;
if (thd == nullptr) {
vio_delete(vio_tmp);
return nullptr;
}
thd->get_protocol_classic()->init_net(vio_tmp);
return thd;
}
因为我们探讨的是tcp/ip的网络链接,所以底层的Channel_info结构其实是 Channel_info_tcpip_socket
cpp
/**
This class abstracts the info. about TCP/IP socket mode of communication with
the server.
*/
class Channel_info_tcpip_socket : public Channel_info {
// connect socket object
MYSQL_SOCKET m_connect_sock;
/*
Flag specifying whether a connection is admin connection or
ordinary connection.
*/
bool m_is_admin_conn;
#ifdef HAVE_SETNS
/*
Network namespace associated with the socket.
*/
std::string m_network_namespace;
#endif
protected:
Vio *create_and_init_vio() const override {
Vio *vio = mysql_socket_vio_new(m_connect_sock, VIO_TYPE_TCPIP, 0);
#ifdef USE_PPOLL_IN_VIO
if (vio != nullptr) {
vio->thread_id.reset();
vio->signal_mask = mysqld_signal_mask;
}
#endif
#ifdef HAVE_SETNS
strncpy(vio->network_namespace, m_network_namespace.c_str(),
sizeof(vio->network_namespace) - 1);
vio->network_namespace[sizeof(vio->network_namespace) - 1] = '\0';
#endif
return vio;
}
}
-
调用MySQL 内部函数mysql_socket_vio_new,创建一个基于 socket 的 Vio 对象。该函数内部会分配一个Vio结构体,初始化其读写函数指针(如vio_read、vio_write)为 TCP socket 的对应实现,并将传入的 socket 存入 Vio。-
m_connect_sock:MYSQL_SOCKET类型,代表已接受的客户端连接套接字。 -
VIO_TYPE_TCPIP:指定 Vio 类型为 TCP/IP 套接字(非 SSL 非本地套接字)。 -
第三个参数
0:通常是vio_flags,0 表示默认行为。
-
-
针对
USE_PPOLL_IN_VIO宏的附加设置-
USE_PPOLL_IN_VIO是 MySQL 编译宏(通常在 Linux 上启用,使用ppoll系统调用以安全处理信号)。 -
vio->thread_id.reset():thread_id可能是存储线程 ID 的对象(如std::atomic或包装类),reset()可能将其设置为未初始化状态或清空。目的是让 Vio 后续能正确记录当前使用的线程 ID,用于调试或资源跟踪。 -
vio->signal_mask = mysqld_signal_mask:signal_mask是 Vio 中保存的信号掩码,mysqld_signal_mask是全局变量(通常设置为主线程希望阻塞的信号集)。将信号掩码存入 Vio 后,ppoll系统调用可以使用该掩码,确保 I/O 操作期间只处理允许的信号,避免中断。
-
-
针对
HAVE_SETNS宏的网络命名空间设置-
HAVE_SETNS表示操作系统支持 Linux 网络命名空间(setns系统调用)。这允许 MySQL 绑定到特定的网络命名空间,实现网络隔离。 -
m_network_namespace是Channel_info_tcpip_socket的成员变量,存储该连接所属的网络命名空间名称(字符串)。 -
将网络命名空间名称复制到
vio->network_namespace缓冲区中,确保 Vio 知道后续 I/O 操作应在哪个网络命名空间中进行。 -
strncpy并手动添加'\0'是常见的防止缓冲区溢出的安全操作。
-
-
返回 Vio 对象
cpp
/* Create a new VIO for socket or TCP/IP connection. */
Vio *mysql_socket_vio_new(MYSQL_SOCKET mysql_socket, enum_vio_type type,
uint flags) {
Vio *vio;
my_socket sd = mysql_socket_getfd(mysql_socket);
DBUG_TRACE;
DBUG_PRINT("enter", ("sd: " MY_SOCKET_FMT, sd));
if ((vio = internal_vio_create(flags))) {
if (vio_init(vio, type, sd, flags)) {
internal_vio_delete(vio);
return nullptr;
}
vio->mysql_socket = mysql_socket;
}
return vio;
}
-
提取原始 socket 描述符:
mysql_socket_getfd是一个内联函数或宏,从MYSQL_SOCKET结构体中提取出底层的系统 socket 描述符(返回值其实是mysql自定义的一个结构 my_socket ,在上面我们有解释过它的意义,简而言之是为了消除平台差异),存入sd。后续vio_init需要原始描述符来设置非阻塞等属性。 -
分配 VIO 结构体:
-
internal_vio_create负责分配Vio结构体内存,并根据flags进行一些初步设置(例如初始化锁、引用计数等)。 -
如果分配失败(返回
NULL),则整个函数直接返回NULL。
-
-
初始化 VIO 的 I/O 函数表:
-
vio_init是核心初始化函数,它根据type设置 VIO 的读写函数指针:-
对于
VIO_TYPE_TCPIP,会设置vio->read为vio_read_socket,vio->write为vio_write_socket等。 -
同时会将传入的
sd存入vio->sd,并根据flags设置 socket 的阻塞/非阻塞模式。
-
-
如果
vio_init返回非零(失败),则调用internal_vio_delete释放已分配的 VIO 内存,并返回NULL。
-
-
保存原始 MYSQL_SOCKET:将传入的
MYSQL_SOCKET结构体保存到 VIO 中。虽然 VIO 内部已经保存了原始描述符sd,但保留mysql_socket可能用于某些需要原始 socket 对象的场景(如关闭连接、获取地址族信息等)。 -
返回 VIO 指针
cpp
Vio *internal_vio_create(uint flags) {
void *rawmem = my_malloc(key_memory_vio, sizeof(Vio), MYF(MY_WME));
if (rawmem == nullptr) return nullptr;
return new (rawmem) Vio(flags);
}
internal_vio_create 负责 分配并构造一个 Vio 对象 。它使用 MySQL 的内存分配器 my_malloc 分配内存,然后通过 placement new 在已分配的内存上调用 Vio 的构造函数。
-
参数
flags:控制 Vio 行为的标志位(例如是否使用非阻塞 I/O、是否启用 SSL 等)。 -
内存分配:
-
my_malloc:MySQL 内部的内存分配函数,类似于malloc,但集成了性能监控和错误处理。 -
key_memory_vio:Performance Schema 的监控 key,用于统计 Vio 对象的内存使用(分配大小、次数等)。 -
sizeof(Vio):需要分配的字节数,即Vio结构体的大小。 -
MYF(MY_WME):标志位,MY_WME(Write Message on Error)表示如果内存分配失败,会输出错误消息到日志。 -
返回值 :分配成功的原始内存指针,失败则返回
nullptr。
-
-
Placement new 构造 Vio 对象:
-
placement new 语法:在已经分配好的内存
rawmem上构造一个Vio对象,调用Vio的构造函数,并传入flags参数。 -
与普通
new不同,placement new 不会 再次分配内存,只负责调用构造函数。 -
返回值是
rawmem转换为Vio*后的指针(也就是Vio对象的起始地址)。
-
cpp
Vio::Vio(uint flags) {
mysql_socket = MYSQL_INVALID_SOCKET;
local = sockaddr_storage();
remote = sockaddr_storage();
#ifdef USE_PPOLL_IN_VIO
sigemptyset(&signal_mask);
#elif defined(HAVE_KQUEUE)
kq_fd = -1;
#endif
if (flags & VIO_BUFFERED_READ)
read_buffer = (char *)my_malloc(key_memory_vio_read_buffer,
VIO_READ_BUFFER_SIZE, MYF(MY_WME));
}
-
设置 socket 为无效:
-
mysql_socket是MYSQL_SOCKET类型的成员,用于保存操作系统 socket 描述符(或 Windows SOCKET)的抽象。 -
将其初始化为
MYSQL_INVALID_SOCKET(通常是一个无效值,如-1或INVALID_SOCKET),表示该 Vio 尚未与任何有效的 socket 关联。后续vio_init会将其设置为实际连接的 socket。
-
-
清零地址结构
local = sockaddr_storage(); remote = sockaddr_storage();-
local和remote是struct sockaddr_storage类型的成员,分别用于存储本端和对端的网络地址(支持 IPv4 和 IPv6)。 -
通过调用
sockaddr_storage()的默认构造函数(或使用值初始化),将这两个结构体全部清零(所有字节设为 0),表示尚未存储地址信息。
-
-
平台相关的初始化(这样设计使得 Vio 在编译时适配不同操作系统的高效 I/O 通知机制)
-
USE_PPOLL_IN_VIO宏(通常在 Linux 上定义):-
signal_mask是 Vio 中保存的信号掩码(类型sigset_t)。 -
sigemptyset(&signal_mask)将信号掩码清空,表示初始时不阻塞任何信号。后续vio_init可能会将其设置为mysqld_signal_mask(如之前分析)。 -
这是为了支持
ppoll系统调用,使得 I/O 操作能以原子方式设置信号掩码。
-
-
HAVE_KQUEUE宏(通常在 BSD/macOS 上定义):-
kq_fd是用于 kqueue 的文件描述符。 -
初始化为
-1,表示尚未创建或关联 kqueue 实例。
-
-
-
可选的缓冲读缓冲区分配:
-
VIO_BUFFERED_READ是一个标志位,如果flags中包含它,则说明该 Vio 需要支持缓冲读(例如用于 TLS/SSL 连接或某些特定协议)。 -
调用
my_malloc分配大小为VIO_READ_BUFFER_SIZE(通常是某个预定义值,如 16384 字节)的缓冲区,用于存储从 socket 读取但尚未被上层消费的数据。 -
key_memory_vio_read_buffer是 Performance Schema 的监控 key,用于统计该缓冲区的内存使用。 -
MYF(MY_WME)表示分配失败时输出错误信息。 -
如果
flags不包含该标志,则read_buffer保持未初始化(通常后续不会使用)。注意:构造函数未显式将read_buffer初始化为nullptr,这是一个潜在隐患;通常 Vio 的其他成员(如read_pos、read_end)会在vio_init中根据read_buffer是否为空来设置。但严格来说,构造函数应将其初始化为nullptr,以避免未定义行为。不过 MySQL 的实现中可能依赖于调用者确保在未设置VIO_BUFFERED_READ时不会访问read_buffer。
-
分配新线程 ID
cpp
thd->set_new_thread_id();
-
为 THD 分配一个新的、全局唯一的线程 ID。该函数内部会调用
Global_THD_manager::get_new_thread_id()并设置到thd->thread_id。 -
此时 THD 还未加入全局列表(后续由调用者负责调用
add_thd)。
cpp
void THD::set_new_thread_id() {
m_thread_id = Global_THD_manager::get_instance()->get_new_thread_id();
variables.pseudo_thread_id = m_thread_id;
thr_lock_info_init(&lock_info, m_thread_id, &COND_thr_lock);
}
这里就引用到了前面讲的 Global_THD_manager ,他是THD 的管理器,我们来看具体实现:
cpp
my_thread_id Global_THD_manager::get_new_thread_id() {
my_thread_id new_id;
MUTEX_LOCK(lock, &LOCK_thread_ids);
do {
new_id = thread_id_counter++;
} while (!thread_ids.insert_unique(new_id).second);
return new_id;
}
-
加锁 :通过
MUTEX_LOCK获取LOCK_thread_ids互斥锁,保护thread_id_counter和thread_ids容器的并发访问。 -
循环尝试:
-
读取当前
thread_id_counter的值作为候选 ID。 -
立即将计数器
++(即使该 ID 可能已被占用)。 -
调用
thread_ids.insert_unique(new_id),尝试将new_id插入到已分配 ID 的集合中。 -
insert_unique返回pair<iterator, bool>,second为true表示插入成功(即该 ID 当前未被使用)。
-
-
成功条件:一旦插入成功(ID 唯一),循环结束,返回该 ID。
-
失败重试 :如果
insert_unique返回false(ID 已存在),则继续循环,使用递增后的新thread_id_counter值再次尝试。
cpp
/**
Similar to std::set<>::insert()
Extends the array by inserting a new element, but only if it cannot be found
in the array already.
Assumes that the array is sorted with std::less<Element_type>
Insertion using this function will maintain order.
@retval A pair, with its member pair::first set an iterator pointing to
either the newly inserted element, or to the equivalent element
already in the array. The pair::second element is set to true if
the new element was inserted, or false if an equivalent element
already existed.
*/
std::pair<iterator, bool> insert_unique(const value_type &val) {
std::pair<iterator, iterator> p = std::equal_range(begin(), end(), val);
// p.first == p.second means we did not find it.
if (p.first == p.second) return std::make_pair(insert(p.first, val), true);
return std::make_pair(p.first, false);
}
insert_unique 它的实现非常精巧:在有序数组上模拟 std::set 的插入语义,利用二分查找实现快速判重,再通过移动元素完成插入。
-
std::equal_range在有序区间
[begin(), end())中,返回一个区间[first, second),其中所有元素均等价于val(按std::less<Element_type>比较)。-
如果
val不存在,则first == second,且该位置就是val应该插入的位置(保持有序)。 -
如果
val存在,则first指向第一个等于val的元素,second指向最后一个等于val的元素之后。
-
-
时间复杂度
-
查找阶段 :
std::equal_range对随机访问迭代器执行二分查找,复杂度 O(log n)。 -
插入阶段 :
insert(p.first, val)在数组中间插入元素,需要将[p.first, end())的所有元素向后移动一位,复杂度 O(n)。 -
因此整体最坏情况为 O(n),但查找本身很快。
-
-
前提条件
-
调用
insert_unique之前,数组必须已经按<排序。 -
该类通常会在所有插入操作(包括
insert_unique和普通insert)后保证有序性,或要求使用者维护。从代码看,insert_unique自身会维持有序性(插入在正确位置),因此反复调用后数组始终保持有序。
-
-
返回值
-
first:指向插入的新元素(如果成功)或指向已存在的等效元素(如果失败)。 -
second:true表示成功插入,false表示值已存在。
-
统计"慢启动线程"次数
cpp
if (channel_info->get_prior_thr_create_utime() != 0) {
const ulonglong launch_time =
thd->start_utime - channel_info->get_prior_thr_create_utime();
if (launch_time >= slow_launch_time * 1000000ULL)
Per_thread_connection_handler::slow_launch_threads++;
}
-
prior_thr_create_utime是创建 pthread 之前记录的时间戳(微秒)。如果该值非 0,说明操作系统线程创建耗时已被测量。 -
thd->start_utime是当前函数调用时的时间戳(在create_thd中设置)。 -
launch_time= 当前时间 - 线程创建前时间,即 从发起线程创建到 THD 初始化完成的总耗时。 -
如果
launch_time >= slow_launch_time * 1,000,000(slow_launch_time是系统变量,单位秒),则全局计数器slow_launch_threads递增,用于统计慢启动线程的数量。
我记录下设置 prior_thr_create_utime 的地方,前面其实有讲这个方法,也即是在需要创建新的线程前,会记录这个创建前一刻的时间:
cpp
bool Per_thread_connection_handler::add_connection(Channel_info *channel_info) {
int error = 0;
my_thread_handle id;
DBUG_TRACE;
// Simulate thread creation for test case before we check thread cache
DBUG_EXECUTE_IF("fail_thread_create", error = 1; goto handle_error;);
if (!check_idle_thread_and_enqueue_connection(channel_info)) return false;
/*
There are no idle threads available to take up the new
connection. Create a new thread to handle the connection
*/
channel_info->set_prior_thr_create_utime();
error =
mysql_thread_create(key_thread_one_connection, &id, &connection_attrib,
handle_connection, (void *)channel_info);
#ifndef NDEBUG
handle_error:
#endif // !NDEBUG
if (error) {
connection_errors_internal++;
if (!create_thd_err_log_throttle.log())
LogErr(ERROR_LEVEL, ER_CONN_PER_THREAD_NO_THREAD, error);
channel_info->send_error_and_close_channel(ER_CANT_CREATE_THREAD, error,
true);
Connection_handler_manager::dec_connection_count();
return true;
}
Global_THD_manager::get_instance()->inc_thread_created();
DBUG_PRINT("info", ("Thread created"));
return false;
}
清理 Channel_info
cpp
delete channel_info;
Channel_info对象已完成使命(THD 已创建),将其删除以释放资源(如套接字描述符已转移给 THD)。
记录线程栈起始地址
cpp
thd_set_thread_stack(thd, (char *)&thd);
thd_set_thread_stack是一个宏或内联函数,将当前线程的栈起始地址保存到 THD 中。参数&thd是当前函数局部变量的地址,它位于栈的高地址(通常栈向下增长,起始地址是栈的最高地址)。MySQL 利用这个地址在后续检测栈溢出(stack overflow)。
将 THD 与当前线程绑定
cpp
thd->store_globals();
-
将 THD 对象存入线程局部存储(TLS)中,使得后续在该线程中可以通过
my_thread_get_THD()或直接访问全局current_thd获得当前 THD。 -
同时也会设置一些全局状态,如事务上下文、字符集等。
cpp
/*
Remember the location of thread info, the structure needed for
(*THR_MALLOC)->Alloc() and the structure for the net buffer
*/
void THD::store_globals() {
/*
Assert that thread_stack is initialized: it's necessary to be able
to track stack overrun.
*/
assert(thread_stack);
current_thd = this;
THR_MALLOC = &mem_root;
/*
is_killable is concurrently readable by a killer thread.
It is protected by LOCK_thd_data, it is not needed to lock while the
value is changing from false not true. If the kill thread reads
true we need to ensure that the thread doesn't proceed to assign
another thread to the same TLS reference.
*/
is_killable = true;
#ifndef NDEBUG
/*
Let mysqld define the thread id (not mysys)
This allows us to move THD to different threads if needed.
*/
set_my_thread_var_id(m_thread_id);
#endif
real_id = my_thread_self();
}
/*
Remove the thread specific info (THD and mem_root pointer) stored during
store_global call for this thread.
*/
void THD::restore_globals() {
// Remove reference to specific OS thread.
real_id = null_thread_initializer;
/*
Assert that thread_stack is initialized: it's necessary to be able
to track stack overrun.
*/
assert(thread_stack);
/* Undocking the thread specific data. */
current_thd = nullptr;
THR_MALLOC = nullptr;
}
THD::store_globals() 的作用是将当前 THD 对象与正在执行的系统线程绑定,使得 MySQL 服务器能够识别当前线程的上下文(如连接状态、事务信息、内存分配器等)。这是线程初始化的关键步骤,通常在创建新连接或为后台线程分配 THD 后调用。
-
断言线程栈已初始化:
thread_stack记录了线程栈的起始地址,用于后续栈溢出检测。断言确保它已被正确设置(例如通过my_thread_init())。 -
设置线程局部指针
current_thd-
current_thd是一个线程局部变量(通常通过thread_local或pthread_setspecific实现),指向当前线程正在服务的 THD 对象。 -
MySQL 内部大量函数(如
my_error()、open_tables())通过current_thd获取当前连接的上下文。
-
-
设置全局内存分配器指针
THR_MALLOCTHR_MALLOC = &mem_root;-
THR_MALLOC是另一个线程局部指针,指向当前 THD 的MEM_ROOT(内存池)。 -
这样,像
alloc_root()这样的内存分配函数就能使用该 THD 专用的内存池,避免锁竞争,并便于在连接结束时统一释放。
-
-
允许被
KILLis_killable = true;-
is_killable标志表示当前线程是否可以接收KILL命令。在 THD 初始化早期,该标志为false,防止在未完成设置时被中断。 -
调用
store_globals()后,线程已基本就绪,故设为true,允许其他连接执行KILL终止此查询或连接。
-
-
记录操作系统线程 ID(Debug 模式)
#ifndef NDEBUG set_my_thread_var_id(m_thread_id); #endif-
set_my_thread_var_id是 MySQL 内部 mysys 库提供的函数,用于设置线程局部变量my_thread_var::id为逻辑线程 ID(m_thread_id)。 -
仅在 Debug 构建下生效,便于调试和日志记录。
-
-
保存原生线程 ID
real_id = my_thread_self();-
my_thread_self()返回操作系统层面的线程标识(如 pthread_t 或 Windows 的 DWORD)。 -
real_id用于某些需要区分原生线程的场景(如死锁检测、性能监控)。
-
thd_prepare_connection
cpp
bool thd_prepare_connection(THD *thd) {
thd->enable_mem_cnt();
bool rc;
lex_start(thd);
rc = login_connection(thd);
if (rc) return rc;
prepare_new_connection_state(thd);
return false;
}
-
作用:一个连接的"入口"准备函数,负责在正式处理命令前完成会话初始化、认证和初始环境设置。
-
流程:
-
开启内存计数(用于后续查询的内存统计)。
-
调用
lex_start重置THD->lex,使语法解析器处于初始状态。 -
调用
login_connection进行认证,若失败则直接返回错误。 -
认证成功后调用
prepare_new_connection_state(该函数会初始化字符集、事务隔离级别、自动提交状态、设置当前数据库、初始化预编译语句缓存等)。
-
-
返回值 :
false表示成功(已认证并准备好接收命令),true表示失败(通常为认证失败或资源不足)。
lex_start
cpp
bool lex_start(THD *thd) {
DBUG_TRACE;
LEX *lex = thd->lex;
lex->thd = thd;
lex->reset();
// Initialize the cost model to be used for this query
thd->init_cost_model();
const bool status = lex->new_top_level_query();
assert(lex->current_query_block() == nullptr);
lex->m_current_query_block = lex->query_block;
assert(lex->m_IS_table_stats.is_valid() == false);
assert(lex->m_IS_tablespace_stats.is_valid() == false);
return status;
}
-
作用 :为即将到来的新 SQL 命令重置
LEX对象(LEX存储了解析树、查询选项、表列表等)。 -
关键点:
-
lex->reset()将 LEX 恢复到默认状态(不释放内存,仅重置标志和指针)。 -
thd->init_cost_model()初始化查询成本模型(用于优化器)。 -
new_top_level_query()会分配一个新的Query_block对象(表示顶层查询块),并将其加入 LEX 的查询块链表。
-
-
注意事项 :
lex_start在每次命令开始前都会调用(比如do_command中),但在thd_prepare_connection中调用是为了在认证前就有一个干净的 LEX(实际上认证阶段并不需要 LEX,可能是为了代码统一或后续扩展)。返回值status通常未在调用处检查(thd_prepare_connection忽略了它),但若内存分配失败可能返回 true,此时lex可能处于不一致状态,不过调用者一般假设成功。
login_connection
cpp
static bool login_connection(THD *thd) {
int error;
DBUG_TRACE;
DBUG_PRINT("info",
("login_connection called by thread %u", thd->thread_id()));
/* Use "connect_timeout" value during connection phase */
thd->get_protocol_classic()->set_read_timeout(connect_timeout, true);
thd->get_protocol_classic()->set_write_timeout(connect_timeout);
error = check_connection(thd);
thd->send_statement_status();
if (error) { // Wrong permissions
#ifdef _WIN32
if (vio_type(thd->get_protocol_classic()->get_vio()) == VIO_TYPE_NAMEDPIPE)
my_sleep(1000); /* must wait after eof() */
#endif
return true;
}
/* Connect completed, set read/write timeouts back to default */
thd->get_protocol_classic()->set_read_timeout(
thd->variables.net_read_timeout);
thd->get_protocol_classic()->set_write_timeout(
thd->variables.net_write_timeout);
return false;
}
-
作用:与客户端完成握手认证(检查用户名、密码、SSL/TLS 状态、账号锁、权限等),并发送响应包。
-
核心 :
check_connection(thd)执行实际认证逻辑(涉及acl_authenticate、密码验证、插件调用等),返回 0 表示成功,非 0 表示失败。 -
超时处理:
-
认证阶段使用较短的
connect_timeout(默认 10 秒),防止恶意连接长时间占用。 -
认证成功后,恢复为会话变量
net_read_timeout和net_write_timeout(默认 30 秒),用于后续命令交互。
-
-
Windows 特殊处理:若认证失败且是命名管道连接,额外 sleep 1 秒,减缓暴力破解尝试。
-
返回值 :
false表示认证成功,true表示失败(thd_prepare_connection会据此增加aborted_connects计数)。
do_command
cpp
/**
Read one command from connection and execute it (query or simple command).
This function is called in loop from thread function.
For profiling to work, it must never be called recursively.
@retval
0 success
@retval
1 request of thread shutdown (see dispatch_command() description)
*/
bool do_command(THD *thd) {
bool return_value;
int rc;
NET *net = nullptr;
enum enum_server_command command = COM_SLEEP;
COM_DATA com_data;
DBUG_TRACE;
assert(thd->is_classic_protocol());
/*
indicator of uninitialized lex => normal flow of errors handling
(see my_message_sql)
*/
thd->lex->set_current_query_block(nullptr);
/*
XXX: this code is here only to clear possible errors of init_connect.
Consider moving to prepare_new_connection_state() instead.
That requires making sure the DA is cleared before non-parsing statements
such as COM_QUIT.
*/
thd->clear_error(); // Clear error message
thd->get_stmt_da()->reset_diagnostics_area();
/*
This thread will do a blocking read from the client which
will be interrupted when the next command is received from
the client, the connection is closed or "net_wait_timeout"
number of seconds has passed.
*/
net = thd->get_protocol_classic()->get_net();
my_net_set_read_timeout(net, thd->variables.net_wait_timeout);
net_new_transaction(net);
/*
WL#15369 : to make connections threads sleep to test if
ER_THREAD_STILL_ALIVE, and ER_NUM_THREADS_STILL_ALIVE are
being logged in the intended way, i.e. when connection threads
are still alive, even after forcefully disconnecting them in
close_connections().
*/
DBUG_EXECUTE_IF("simulate_connection_thread_hang", sleep(15););
/*
Synchronization point for testing of KILL_CONNECTION.
This sync point can wait here, to simulate slow code execution
between the last test of thd->killed and blocking in read().
The goal of this test is to verify that a connection does not
hang, if it is killed at this point of execution.
(Bug#37780 - main.kill fails randomly)
Note that the sync point wait itself will be terminated by a
kill. In this case it consumes a condition broadcast, but does
not change anything else. The consumed broadcast should not
matter here, because the read/recv() below doesn't use it.
*/
DEBUG_SYNC(thd, "before_do_command_net_read");
/* For per-query performance counters with log_slow_statement */
struct System_status_var query_start_status;
thd->clear_copy_status_var();
if (opt_log_slow_extra) {
thd->copy_status_var(&query_start_status);
}
rc = thd->m_mem_cnt.reset();
if (rc)
thd->m_mem_cnt.set_thd_error_status();
else {
/*
Because of networking layer callbacks in place,
this call will maintain the following instrumentation:
- IDLE events
- SOCKET events
- STATEMENT events
- STAGE events
when reading a new network packet.
In particular, a new instrumented statement is started.
See init_net_server_extension()
*/
thd->m_server_idle = true;
rc = thd->get_protocol()->get_command(&com_data, &command);
thd->m_server_idle = false;
}
if (rc) {
#ifndef NDEBUG
char desc[VIO_DESCRIPTION_SIZE];
vio_description(net->vio, desc);
DBUG_PRINT("info", ("Got error %d reading command from socket %s",
net->error, desc));
#endif // NDEBUG
MYSQL_NOTIFY_STATEMENT_QUERY_ATTRIBUTES(thd->m_statement_psi, false);
/* Instrument this broken statement as "statement/com/error" */
thd->m_statement_psi = MYSQL_REFINE_STATEMENT(
thd->m_statement_psi, com_statement_info[COM_END].m_key);
/* Check if we can continue without closing the connection */
/* The error must be set. */
assert(thd->is_error());
thd->send_statement_status();
/* Mark the statement completed. */
MYSQL_END_STATEMENT(thd->m_statement_psi, thd->get_stmt_da());
thd->m_statement_psi = nullptr;
thd->m_digest = nullptr;
if (rc < 0) {
return_value = true; // We have to close it.
goto out;
}
net->error = NET_ERROR_UNSET;
return_value = false;
goto out;
}
#ifndef NDEBUG
char desc[VIO_DESCRIPTION_SIZE];
vio_description(net->vio, desc);
DBUG_PRINT("info", ("Command on %s = %d (%s)", desc, command,
Command_names::str_notranslate(command).c_str()));
expected_from_debug_flag = TDM::ANY;
DBUG_EXECUTE_IF("tdon", { expected_from_debug_flag = TDM::ON; });
DBUG_EXECUTE_IF("tdzero", { expected_from_debug_flag = TDM::ZERO; });
DBUG_EXECUTE_IF("tdna", { expected_from_debug_flag = TDM::NOT_AVAILABLE; });
#endif // NDEBUG
DBUG_PRINT("info", ("packet: '%*.s'; command: %d",
(int)thd->get_protocol_classic()->get_packet_length(),
thd->get_protocol_classic()->get_raw_packet(), command));
if (thd->get_protocol_classic()->bad_packet)
assert(0); // Should be caught earlier
// Reclaim some memory
thd->get_protocol_classic()->get_output_packet()->shrink(
thd->variables.net_buffer_length);
/* Restore read timeout value */
my_net_set_read_timeout(net, thd->variables.net_read_timeout);
DEBUG_SYNC(thd, "before_command_dispatch");
return_value = dispatch_command(thd, &com_data, command);
thd->get_protocol_classic()->get_output_packet()->shrink(
thd->variables.net_buffer_length);
out:
/* The statement instrumentation must be closed in all cases. */
assert(thd->m_digest == nullptr);
assert(thd->m_statement_psi == nullptr);
return return_value;
}
do_command 是 MySQL 服务器中每个客户端连接线程的核心处理函数 。它在线程主循环中被反复调用,负责从客户端网络连接中读取一个命令(比如查询、COM_QUIT、COM_PING 等),然后执行它。可以说,它是从"接收用户请求"到"执行请求"的入口点。
do_command 函数的功能:
-
阻塞等待从客户端接收一个完整的命令包。
-
解析命令类型 (如
COM_QUERY、COM_EXECUTE等)。 -
调用
dispatch_command将命令分发给相应的处理逻辑(如执行 SQL、处理 Prepared Statement、处理关闭连接等)。 -
处理网络错误,决定是否应该关闭当前连接。
-
维护性能监控 (慢查询、状态变量、
performance_schema埋点)。 -
保证健壮性(清理错误状态、内存回收、栈溢出保护等)。
返回值:
-
false:成功处理一个命令,连接可以继续使用。 -
true:需要关闭连接(例如客户端断开、协议错误、COM_QUIT等)。
初始化与状态清理
cpp
thd->lex->set_current_query_block(nullptr);
thd->clear_error();
thd->get_stmt_da()->reset_diagnostics_area();
-
清空
THD中可能残留的错误信息与诊断区域,防止上个命令的错误影响当前命令。 -
将
LEX中的当前查询块置空,避免误用。
准备网络读取
cpp
net = thd->get_protocol_classic()->get_net();
my_net_set_read_timeout(net, thd->variables.net_wait_timeout);
net_new_transaction(net);
-
获取经典协议的
NET对象。 -
设置网络等待超时 (
net_wait_timeout),这是等待客户端发送下一个命令的时间。 -
net_new_transaction重置网络压缩/序列号等状态,相当于一个逻辑"事务"边界。
记录性能计数器起点
cpp
if (opt_log_slow_extra) {
thd->copy_status_var(&query_start_status);
}
- 若启用了慢查询日志扩展信息(
log_slow_extra),则保存当前状态变量的快照,用于后续计算命令执行耗时。
实际读取命令
cpp
thd->m_server_idle = true;
rc = thd->get_protocol()->get_command(&com_data, &command);
thd->m_server_idle = false;
-
核心网络读取 :调用
Protocol::get_command会阻塞式地从 socket 读取一个完整的数据包,解析出命令类型(如COM_QUERY)和命令参数(存入COM_DATA联合体)。 -
在读取期间,
m_server_idle标志被置为true,用于性能监控(区分空闲和活动状态)。 -
返回码
rc:-
0:成功读取一个命令。 -
>0:网络错误(但可能仍可继续)。 -
<0:严重错误,需要关闭连接。
-
注:请求信息都是在这一个函数内完成的,如果多个数据包,这里会等待知道所有数据都收到为止。同事按照MYSQL协议,报问的首个字节就是命令标识符,即参数里的command枚举。
处理读取失败的情况
cpp
if (rc) {
// 记录错误,发送错误响应,标记语句结束
if (rc < 0) return_value = true; // 关闭连接
else return_value = false; // 连接保留
goto out;
}
- 若读取失败,会清理
statement相关的 instrumentation(performance_schema),发送错误状态给客户端,并决定是否关闭连接。
分发命令
cpp
return_value = dispatch_command(thd, &com_data, command);
-
成功读取到命令后,调用
dispatch_command执行真正的业务逻辑(会单独起一个坎姆将SQL的解析与执行)。 -
dispatch_command内部会根据command类型处理:-
COM_QUERY:解析并执行 SQL。 -
COM_QUIT:关闭连接。 -
COM_PING:返回 OK 包。 -
等等。
-
收尾工作
cpp
thd->get_protocol_classic()->get_output_packet()->shrink(
thd->variables.net_buffer_length);
-
每次命令处理后,将输出缓冲区收缩到
net_buffer_length大小,避免内存膨胀。 -
最后检查
m_digest和m_statement_psi必须为空(由dispatch_command保证清理)。
other
mysql_mutex_init
cpp
#define PSI_MUTEX_CALL(M) pfs_##M##_v1
static inline int inline_mysql_mutex_init(PSI_mutex_key key [[maybe_unused]],
mysql_mutex_t *that,
const native_mutexattr_t *attr,
const char *src_file [[maybe_unused]],
uint src_line [[maybe_unused]]) {
#ifdef HAVE_PSI_MUTEX_INTERFACE
that->m_psi = PSI_MUTEX_CALL(init_mutex)(key, &that->m_mutex);
#else
that->m_psi = nullptr;
#endif
return my_mutex_init(&that->m_mutex, attr
#ifdef SAFE_MUTEX
,
src_file, src_line
#endif
);
}
-
PSI_mutex_key key:Performance Schema 为该互斥锁分配的唯一标识 key,用于在 P_S 中区分不同用途的锁。在Global_THD_manager构造函数中,传入的是key_LOCK_thd_list、key_LOCK_thd_remove等。 -
mysql_mutex_t *that:MySQL 封装的互斥锁结构体,通常包含一个原生互斥体m_mutex和一个 P_S 描述符m_psi。 -
attr:原生互斥锁的属性(如递归、共享等),通常传入MY_MUTEX_INIT_FAST或NULL。 -
src_file/src_line:调用点的源文件名和行号,用于SAFE_MUTEX调试模式下的跟踪。
函数体解析:
cpp
#ifdef HAVE_PSI_MUTEX_INTERFACE
that->m_psi = PSI_MUTEX_CALL(init_mutex)(key, &that->m_mutex);
#else
that->m_psi = nullptr;
#endif
-
如果编译时启用了 Performance Schema 的互斥锁监控(
HAVE_PSI_MUTEX_INTERFACE),则调用 P_S 提供的init_mutex函数,传入 key 和原生互斥体的地址。该函数会返回一个PSI_mutex*句柄,存储在that->m_psi中。后续对该互斥锁的所有操作(lock、unlock)都会通过这个句柄通知 P_S,从而记录等待时间、争用次数等。 -
如果未启用 P_S,则直接将
m_psi设为nullptr,表示不需要监控。
cpp
return my_mutex_init(&that->m_mutex, attr
#ifdef SAFE_MUTEX
, src_file, src_line
#endif
);
-
my_mutex_init是 MySQL 底层(通常是my_pthread_mutex_init的封装)真正初始化 原生互斥体(如 pthread_mutex_t)的函数。 -
如果定义了
SAFE_MUTEX(调试模式),会额外传入源文件和行号,用于在死锁或错误时报告锁的创建位置,便于调试。
my_mutex_init
cpp
static inline int my_mutex_init(my_mutex_t *mp, const native_mutexattr_t *attr
#ifdef SAFE_MUTEX
,
const char *file, uint line
#endif
) {
#ifdef SAFE_MUTEX
assert(mp != nullptr);
mp->m_u.m_safe_ptr = (safe_mutex_t *)malloc(sizeof(safe_mutex_t));
return safe_mutex_init(mp->m_u.m_safe_ptr, attr, file, line);
#else
return native_mutex_init(&mp->m_u.m_native, attr);
#endif
}
-
my_mutex_t *mp:MySQL 封装的互斥锁类型。它是一个union,在安全模式下包含一个指向safe_mutex_t的指针,在普通模式下包含一个原生互斥体(如pthread_mutex_t)。 -
attr:原生互斥锁属性(例如MY_MUTEX_INIT_FAST或NULL),用于底层pthread_mutex_init或类似函数。 -
file/line:仅在SAFE_MUTEX模式下存在,表示调用该初始化函数的源文件名和行号,用于调试和错误报告。
安全模式(SAFE_MUTEX 已定义)
cpp
#ifdef SAFE_MUTEX
assert(mp != nullptr);
mp->m_u.m_safe_ptr = (safe_mutex_t *)malloc(sizeof(safe_mutex_t));
return safe_mutex_init(mp->m_u.m_safe_ptr, attr, file, line);
#endif
-
首先断言
mp非空。 -
为
safe_mutex_t结构体动态分配内存,并将指针赋给mp->m_u.m_safe_ptr。 -
调用
safe_mutex_init来初始化这个安全互斥锁,同时传入源文件名和行号。 -
safe_mutex_init内部通常会:-
再调用
native_mutex_init初始化真正的原生互斥体。 -
记录锁的创建位置、线程 ID 等信息,以便在死锁检测、锁定顺序检查或错误报告时提供调试信息。
-
-
这种模式主要用于 调试版本,帮助开发者发现互斥锁使用问题(如重复锁定、死锁、未释放等)。
普通模式(SAFE_MUTEX 未定义)
cpp
#else
return native_mutex_init(&mp->m_u.m_native, attr);
#endif
-
直接调用
native_mutex_init初始化mp->m_u.m_native中的原生互斥体(例如pthread_mutex_t或 Windows 的CRITICAL_SECTION)。 -
没有任何额外的调试或安全包装,性能最高。这是 生产版本 的典型行为。
native_mutex_init
cpp
static inline int native_mutex_init(native_mutex_t *mutex,
const native_mutexattr_t *attr
[[maybe_unused]]) {
#ifdef _WIN32
InitializeCriticalSection(mutex);
return 0;
#else
return pthread_mutex_init(mutex, attr);
#endif
}
-
native_mutex_t *mutex:平台相关的原生互斥锁类型。-
Windows 下:通常是
CRITICAL_SECTION。 -
POSIX(Linux/macOS 等)下:通常是
pthread_mutex_t。
-
-
attr:互斥锁属性,在 POSIX 平台上传递给pthread_mutex_init;在 Windows 平台上不使用,标记为maybe_unused避免编译警告。
函数体解析:
Windows 平台
-
调用
InitializeCriticalSection(&mutex)初始化一个临界区对象(CRITICAL_SECTION)。 -
临界区是 Windows 上用于线程同步的轻量级机制,与互斥锁类似但仅限于单进程内。
-
该函数总是返回
0(成功),在 Windows 上初始化临界区不会失败(除非传入无效指针,但这里不会)。
POSIX 平台(Linux、Unix、macOS 等)
-
调用
pthread_mutex_init(mutex, attr)初始化一个 POSIX 互斥锁。 -
attr参数可以是NULL(使用默认属性)或指向pthread_mutexattr_t的指针,例如设置快速互斥锁(PTHREAD_MUTEX_FAST_NP)或递归锁等。 -
返回值:成功返回
0,失败返回错误码(如ENOMEM)。