MYSQL 网络连接

网络初始化

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服务器启动时的网络服务初始化,包括:

  1. 解析绑定地址配置

  2. 创建和管理各种连接监听器

  3. 初始化连接接收器

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 服务器网络初始化过程中最核心的函数之一,它负责实际创建并配置所有监听套接字,为后续接受客户端连接做好准备。下面我将详细解析这个函数的每一步。

该函数依次处理三种类型的监听器(按顺序):

  1. 管理接口(admin interface,如果配置了独立地址)

  2. 普通 TCP 接口(业务端口,默认 3306,可能绑定多个 IP)

  3. Unix 域套接字(仅当系统支持且路径非空)

函数执行完毕后,所有成功的监听套接字会被收集到 m_socket_vector 中,并最终通过 setup_connection_events() 注册到 I/O 多路复用机制(如 pollselect),使服务器能够响应连接请求。

我们主要关注普通 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;
  }
  • 准备 addrinfo hints: 设置 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 结构体的生命周期。让我们分解它的每个部分:

  1. using :C++11 引入的别名声明,等价于 typedef,但更直观,可以为复杂类型起一个简短的名字。

  2. AddrInfoPtr :新类型的名称,代表一个指向 addrinfo 的独占智能指针。

  3. 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 为跨平台兼容而定义的类型别名。 标识实际的网络套接字句柄,用于调用系统级的套接字函数(如 sendrecvaccept 等)。所有网络 I/O 操作最终都通过该成员所代表的套接字进行。
m_psi struct PSI_socket * 指向性能模式(Performance Schema)套接字监控结构体的指针。PSI_socket 是 MySQL 内部用于收集和报告套接字相关性能数据的结构,其中记录了该套接字的 I/O 操作次数、字节数、等待时间、连接状态等统计信息。 MYSQL_SOCKET 对象与性能监控数据关联起来,使得服务器能够实时跟踪每个套接字的资源消耗情况,并通过 Performance Schema 表(如 socket_instancessocket_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 宏控制),该函数会分别使用 pollselect 两种不同的后端实现。

情况 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);
  }
}
  1. 检查初始化标志
    if (waiting_channel_info_list != nullptr) 判断列表指针是否非空。由于 init() 在成功分配列表后才设置该指针,因此此条件用于确认静态成员是否已初始化,避免对未初始化的对象进行销毁操作。

  2. 释放等待列表
    delete waiting_channel_info_list 释放动态分配的 std::list<Channel_info *> 对象,并将指针置 nullptr,防止悬挂指针。

  3. 销毁同步原语

    • 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;
}
  1. DBUG 模拟失败
    DBUG_EXECUTE_IF("fail_thread_create", ...) 用于测试:在调试模式下,若设置了该标记,则直接跳到错误处理,模拟线程创建失败。

  2. 尝试使用线程缓存

    复制代码
    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 == falseadd_connection 直接返回 false(成功)。

    • 若无空闲线程 :函数返回 false,表示未能使用缓存,需要创建新线程。此时 !false == true,继续执行后续创建线程的代码。

  3. 创建新线程

    • 记录线程创建前的微秒时间戳(用于性能统计,在下面讲线程慢启动时候会用到)。

    • 调用 mysql_thread_create 创建线程,线程函数为 handle_connection,参数为 channel_info

    • connection_attrib 是线程属性(通常为默认)。

    • 若创建成功,增加全局线程创建计数(Global_THD_manager::inc_thread_created),并返回 false 表示成功。

  4. 错误处理

    • 若线程创建失败,内部错误计数器 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

    条件变量,用于唤醒等待的空闲线程。

核心逻辑

  1. 加锁mysql_mutex_lock(&LOCK_thread_cache) 保护所有共享数据。

  2. 检查空闲线程可用性
    if (blocked_pthread_count > wake_pthread)

    • blocked_pthread_count 当前被阻塞的线程数,wake_pthread已经被唤醒但尚未取走连接的线程数量

    • 当空闲线程数量(blocked_pthread_count大于 已经被唤醒但尚未取走连接的线程数量wake_pthread)时,说明还有真正的空闲线程在等待,可以将新连接交给它们,而无需创建新线程。

  3. 若条件成立

    • channel_info 追加到等待列表(waiting_channel_info_list)的尾部。

    • wake_pthread++:记录又多了一个待处理的连接。

    • mysql_cond_signal(&COND_thread_cache):唤醒一个等待的空闲线程(该线程被唤醒后会从队列中取出连接并处理)。

    • res 设为 false(表示成功使用缓存)。

  4. 若条件不成立

    这意味着所有空闲线程都已分配了任务(或没有空闲线程),此时不应再入队,而是让调用者创建新线程。res 保持为 true

  5. 解锁 ,返回 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()(生产者)成对出现,共同实现高效的线程池管理。

  1. 加锁 :通过条件 LOCK_thread_cache ,保护所有静态成员(blocked_pthread_countwake_pthreadwaiting_channel_info_list 等)。

  2. 条件判断

    • blocked_pthread_count < max_blocked_pthreads:当前空闲线程数未达到系统允许的上限,可以继续增加空闲线程。

    • !shrink_cache:未设置"收缩缓存"标志(通常由管理命令 FLUSH THREADS 或动态调整线程缓存大小触发)。

    • 如果条件不满足,则跳过整个阻塞逻辑,直接返回 nullptr,线程将退出。

  3. 调试状态清理mysys_var 是与物理线程绑定的调试上下文。由于该线程即将处理一个新的连接(可能来自不同的会话),需要将调试堆栈恢复到干净状态,避免前一个会话的调试信息干扰。DBUG_POP() 用于弹出所有调试标记,断言确保没有残留。

  4. 增加空闲计数blocked_pthread_count++ 表示当前线程即将进入空闲等待状态。

  5. 等待条件(有阻塞,blocking)

    • !connection_events_loop_aborted():服务器未关闭。

    • !wake_pthread:没有待处理的连接(wake_pthread 为0,即没有新连接在排队等待分配)。如果有待处理的连接,那么需要立即去处理。

    • !shrink_cache:未要求收缩缓存。

    • 如果以上条件全为真,线程在 COND_thread_cache 上等待,释放锁,直到被 mysql_cond_signal 唤醒。

  6. 唤醒后 :首先减少空闲计数 blocked_pthread_count--,表示线程不再处于阻塞状态。

  7. 处理收缩缓存 :如果线程被唤醒是因为 shrink_cache 变为 true(例如收到 FLUSH THREADS 命令),且当前空闲线程数未超过最大值,则发送 COND_flush_thread_cache 信号,通知其他可能也在等待的线程继续收缩流程。这通常用于协调多个空闲线程同时退出。

  8. 取走待处理连接

    • wake_pthread > 0 表示至少有一个连接在等待被分配(由生产者放入列表)。

    • wake_pthread--:减少待处理计数。

    • waiting_channel_info_list 前端弹出一个 Channel_info 对象,赋值给 new_conn

    • 如果列表为空(正常情况下不应发生,因为 wake_pthread 应该与列表中的元素数一致),则触发断言。

  9. 解锁并返回 :如果线程成功取到了新连接,则返回该连接对象;否则返回 nullptr(例如因服务器关闭或缓存收缩而退出)。

cpp 复制代码
static 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_filesrc_line,以便在发生错误(如死锁)时报告位置信息。

  • 结束监控

    • 如果 locker 不为空,则调用 PSI_CALL(end_cond_wait) 结束等待事件的记录,并传入结果(result,即 my_cond_wait 的返回值)。Performance Schema 会统计等待时间、成功/失败等信息。
  • 提前返回 :直接返回 result,不再执行后面的非监控代码。

cpp 复制代码
static 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
}
  1. 非调试模式(未定义 SAFE_MUTEX
  • 直接调用 native_cond_wait,它是对操作系统原生条件变量等待函数的直接封装(例如 pthread_cond_wait)。

  • 参数 &mp->m_u.m_nativemy_mutex_t 联合体中取出原生的互斥锁对象(通常是一个 pthread_mutex_t)。

  • 这种模式追求最大性能,无额外检查或记录。

  1. 调试模式(定义了 SAFE_MUTEX
  • 调用 safe_cond_wait,这是一个增加了运行时检查的版本。

  • mp->m_u.m_safe_ptr 指向一个包含调试信息的互斥锁包装结构(例如记录哪个线程持有锁、加锁位置等)。

  • safe_cond_wait 会执行额外的校验,例如:

    • 确保当前线程确实持有该互斥锁(因为条件等待要求调用时已加锁)。

    • 记录等待开始和结束的时间戳,帮助检测死锁或长时间等待。

    • 在发生错误时,输出 fileline 指示调用位置,便于开发者定位问题。

  • 这种模式会带来一定的性能开销,但大大增强了调试能力,常用于开发测试环境。

方法 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_countshrink_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 会:

  1. 更新系统变量对应的静态成员(如 Per_thread_connection_handler::max_blocked_pthreads)为新值。

  2. 调用 modify_thread_cache_size(N) 来收缩当前超出新上限的空闲线程(如果当前空闲线程数大于新上限)。

  3. 后续所有空闲线程的进入和退出都将以新的 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 对象指针 。它通过 pollselect 返回的就绪事件信息,优先处理管理接口(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_vectorstd::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(资源临时不可用,通常非阻塞模式下),则跳出循环。

  • 如果错误码是 EINTREAGAIN,循环继续重试,直到达到 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_idsignal_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 层面)。

  • 然后显式对原始套接字执行 shutdownSHUT_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 上,线程操作(如 WaitForSingleObjectTerminateThread)需要 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 参数CreateThreaddwStackSize 参数。

  • 特殊值:若为 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_MALLOCmy_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_thddo_for_all_thd_copydo_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_idsPrealloced_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_mutexesall_thd_manager_conds 是预定义的数组,包含了本类中所有锁和条件变量的 PSI_mutex_info / PSI_cond_info 描述信息。

  • mysql_mutex_registermysql_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_listkey_LOCK_thd_removekey_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 时,如果尝试使用该值,会发现它已经存在,就会跳过并继续尝试下一个。

这个构造函数的核心任务是:

  1. 创建 8 个独立分区的 THD 列表,降低锁竞争。

  2. 初始化所有用于并发控制的互斥锁和条件变量。

  3. 注册 Performance Schema 监控支持。

  4. 设置初始计数器(运行线程数 0,累计创建线程数 0)。

  5. 预留第一个可分配的普通线程 ID(reserved_thread_id + 1)。

  6. 将保留 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_socketChannel_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_readvio_write)为 TCP socket 的对应实现,并将传入的 socket 存入 Vio。

    • m_connect_sockMYSQL_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_masksignal_mask 是 Vio 中保存的信号掩码,mysqld_signal_mask 是全局变量(通常设置为主线程希望阻塞的信号集)。将信号掩码存入 Vio 后,ppoll 系统调用可以使用该掩码,确保 I/O 操作期间只处理允许的信号,避免中断。

  • 针对 HAVE_SETNS 宏的网络命名空间设置

    • HAVE_SETNS 表示操作系统支持 Linux 网络命名空间(setns 系统调用)。这允许 MySQL 绑定到特定的网络命名空间,实现网络隔离。

    • m_network_namespaceChannel_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->readvio_read_socketvio->writevio_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_socketMYSQL_SOCKET 类型的成员,用于保存操作系统 socket 描述符(或 Windows SOCKET)的抽象。

    • 将其初始化为 MYSQL_INVALID_SOCKET(通常是一个无效值,如 -1INVALID_SOCKET),表示该 Vio 尚未与任何有效的 socket 关联。后续 vio_init 会将其设置为实际连接的 socket。

  • 清零地址结构

    复制代码
    local = sockaddr_storage();
    remote = sockaddr_storage();
    • localremotestruct 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_posread_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;
}
  1. 加锁 :通过 MUTEX_LOCK 获取 LOCK_thread_ids 互斥锁,保护 thread_id_counterthread_ids 容器的并发访问。

  2. 循环尝试

    • 读取当前 thread_id_counter 的值作为候选 ID。

    • 立即将计数器 ++(即使该 ID 可能已被占用)。

    • 调用 thread_ids.insert_unique(new_id),尝试将 new_id 插入到已分配 ID 的集合中。

    • insert_unique 返回 pair<iterator, bool>secondtrue 表示插入成功(即该 ID 当前未被使用)。

  3. 成功条件:一旦插入成功(ID 唯一),循环结束,返回该 ID。

  4. 失败重试 :如果 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 的插入语义,利用二分查找实现快速判重,再通过移动元素完成插入

  1. std::equal_range

    在有序区间 [begin(), end()) 中,返回一个区间 [first, second),其中所有元素均等价于 val(按 std::less<Element_type> 比较)。

    • 如果 val 不存在,则 first == second,且该位置就是 val 应该插入的位置(保持有序)。

    • 如果 val 存在,则 first 指向第一个等于 val 的元素,second 指向最后一个等于 val 的元素之后。

  2. 时间复杂度

    • 查找阶段std::equal_range 对随机访问迭代器执行二分查找,复杂度 O(log n)

    • 插入阶段insert(p.first, val) 在数组中间插入元素,需要将 [p.first, end()) 的所有元素向后移动一位,复杂度 O(n)

    • 因此整体最坏情况为 O(n),但查找本身很快。

  3. 前提条件

    • 调用 insert_unique 之前,数组必须已经按 < 排序

    • 该类通常会在所有插入操作(包括 insert_unique 和普通 insert)后保证有序性,或要求使用者维护。从代码看,insert_unique 自身会维持有序性(插入在正确位置),因此反复调用后数组始终保持有序。

  4. 返回值

    • first:指向插入的新元素(如果成功)或指向已存在的等效元素(如果失败)。

    • secondtrue 表示成功插入,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,000slow_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 后调用。

  1. 断言线程栈已初始化:thread_stack 记录了线程栈的起始地址,用于后续栈溢出检测。断言确保它已被正确设置(例如通过 my_thread_init())。

  2. 设置线程局部指针 current_thd

    1. current_thd 是一个线程局部变量(通常通过 thread_localpthread_setspecific 实现),指向当前线程正在服务的 THD 对象。

    2. MySQL 内部大量函数(如 my_error()open_tables())通过 current_thd 获取当前连接的上下文。

  3. 设置全局内存分配器指针 THR_MALLOC

    复制代码
    THR_MALLOC = &mem_root;
    1. THR_MALLOC 是另一个线程局部指针,指向当前 THD 的 MEM_ROOT(内存池)。

    2. 这样,像 alloc_root() 这样的内存分配函数就能使用该 THD 专用的内存池,避免锁竞争,并便于在连接结束时统一释放。

  4. 允许被 KILL

    复制代码
    is_killable = true;
    1. is_killable 标志表示当前线程是否可以接收 KILL 命令。在 THD 初始化早期,该标志为 false,防止在未完成设置时被中断。

    2. 调用 store_globals() 后,线程已基本就绪,故设为 true,允许其他连接执行 KILL 终止此查询或连接。

  5. 记录操作系统线程 ID(Debug 模式)

    复制代码
    #ifndef NDEBUG
      set_my_thread_var_id(m_thread_id);
    #endif
    1. set_my_thread_var_id 是 MySQL 内部 mysys 库提供的函数,用于设置线程局部变量 my_thread_var::id 为逻辑线程 ID(m_thread_id)。

    2. 仅在 Debug 构建下生效,便于调试和日志记录。

  6. 保存原生线程 ID

    复制代码
    real_id = my_thread_self();
    1. my_thread_self() 返回操作系统层面的线程标识(如 pthread_t 或 Windows 的 DWORD)。

    2. 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;
}
  • 作用:一个连接的"入口"准备函数,负责在正式处理命令前完成会话初始化、认证和初始环境设置。

  • 流程

    1. 开启内存计数(用于后续查询的内存统计)。

    2. 调用 lex_start 重置 THD->lex,使语法解析器处于初始状态。

    3. 调用 login_connection 进行认证,若失败则直接返回错误。

    4. 认证成功后调用 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_timeoutnet_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_QUITCOM_PING 等),然后执行它。可以说,它是从"接收用户请求"到"执行请求"的入口点。

do_command 函数的功能:

  • 阻塞等待从客户端接收一个完整的命令包。

  • 解析命令类型 (如 COM_QUERYCOM_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_digestm_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_listkey_LOCK_thd_remove 等。

  • mysql_mutex_t *that:MySQL 封装的互斥锁结构体,通常包含一个原生互斥体 m_mutex 和一个 P_S 描述符 m_psi

  • attr:原生互斥锁的属性(如递归、共享等),通常传入 MY_MUTEX_INIT_FASTNULL

  • 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_FASTNULL),用于底层 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)。

相关推荐
蜜獾云1 小时前
mongoDB之分片集群
数据库·mongodb
码不停蹄的玄黓1 小时前
MySQL索引设计核心注意事项
android·数据库·mysql
Gauss松鼠会2 小时前
GaussDB(DWS)性能问题处理套路
服务器·数据库·postgresql·性能优化·gaussdb
AllData公司负责人2 小时前
亲测丝滑,体验跃迁|AllData通过集成开源项目RustFS,多模态数据存储新范式
java·大数据·数据库·算法·数据分析·rustfs
SelectDB技术团队2 小时前
97% 召回率、900 QPS:Apache Doris 4.1 生产级向量检索的工程实践
数据库·人工智能·数据分析·apache doris·selectdb
Trouvaille ~2 小时前
【Redis篇】Hash 哈希:字段级操作与对象存储的最佳实践
数据库·redis·后端·算法·缓存·哈希算法·键值对
happyprince2 小时前
10-Hugging Face Transformers 量化系统深度分析
java·前端·数据库
夜郎king2 小时前
PostgreSQL 16 搭配 PgVector:Windows 11 完整安装教程
数据库·windows·postgresql
迷枫7122 小时前
Oracle 到达梦 DTS 迁移实验记录
数据库·oracle