You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

线程本地单例与CURL线程冲突引发段错误问题排查

问题描述

我有两个类:一个是全局单例(用作通用DBmanager,负责表检查/创建等),另一个是线程本地单例(用作数据管理器,处理预编译语句和数据缓冲区),二者均采用相同方式实现。
我还有一个函数arbitrary_curl_something(),通过easy handle发起调用。在Ubuntu服务器上使用GCC/G++ 12.1,通过Visual Studio远程开发编译程序。

程序在CURL执行时触发段错误,即便instance()在之后调用也会出现。但将instance()中的实例改为static而非thread_local时,程序运行正常。

段错误发生在res_mkquery.c中,调用栈如下:

libc.so.6!__GI___res_context_mkquery(struct resolv_context * ctx, int op, const char * dname, int class, int type, const unsigned char * data, unsigned char * buf, int buflen) Line 117
    at resolv\resolv\res_mkquery.c(117)
libc.so.6!__GI___res_context_query(struct resolv_context * ctx, const char * name, int class, int type, unsigned char * answer, int anslen, unsigned char ** answerp, unsigned char ** answerp2, int * nanswerp2, int * resplen2, int * answerp2_malloced) Line 139
    at resolv\resolv\res_query.c(139)
libc.so.6!__res_context_querydomain(int * answerp2_malloced, int * resplen2, int * nanswerp2, unsigned char ** answerp2, unsigned char ** answerp, int anslen, unsigned char * answer, int type, int class, const char * domain, const char * name, struct resolv_context * ctx) Line 625
    at resolv\resolv\res_query.c(625)
libc.so.6!__GI___res_context_search(struct resolv_context * ctx, const char * name, int class, int type, unsigned char * answer, int anslen, unsigned char ** answerp, unsigned char ** answerp2, int * nanswerp2, int * resplen2, int * answerp2_malloced) Line 381
    at resolv\resolv\res_query.c(381)
libc.so.6!__GI__nss_dns_gethostbyname4_r(const char * name, struct gaih_addrtuple ** pat, char * buffer, size_t buflen, int * errnop, int * herrnop, int32_t * ttlp) Line 373
    at resolv\nss_dns\dns-host.c(373)
libc.so.6!gaih_inet(const char * name, const struct gaih_service * service, const struct addrinfo * req, struct addrinfo ** pai, unsigned int * naddrs, struct scratch_buffer * tmpbuf) Line 747
    at \sysdeps\posix\getaddrinfo.c(747)
libc.so.6!__GI_getaddrinfo(const char * name, const char * service, const struct addrinfo * hints, struct addrinfo ** pai) Line 2240
    at \sysdeps\posix\getaddrinfo.c(2240)
libcurl.so.4![Unknown/Just-In-Time compiled code]
libc.so.6!start_thread(void * arg) Line 442
    at nptl\nptl\pthread_create.c(442)
libc.so.6!clone3() Line 81
    at \sysdeps\unix\sysv\linux\x86_64\clone3.S(81)

或

libc.so.6!___ns_name_pack(const unsigned char * src, unsigned char * dst, int dstsiz, const unsigned char ** dnptrs, const unsigned char ** lastdnptr) Line 106
    at resolv\resolv\ns_name_pack.c(106)
libc.so.6!___ns_name_compress(const char * src, unsigned char * dst, size_t dstsiz, const unsigned char ** dnptrs, const unsigned char ** lastdnptr) Line 41
    at resolv\resolv\ns_name_compress.c(41)
libc.so.6!__GI___res_context_mkquery(struct resolv_context * ctx, int op, const char * dname, int class, int type, const unsigned char * data, unsigned char * buf, int buflen) Line 153
    at resolv\resolv\res_mkquery.c(153)
libc.so.6!__GI___res_context_query(struct resolv_context * ctx, const char * name, int class, int type, unsigned char * answer, int anslen, unsigned char ** answerp, unsigned char ** answerp2, int * nanswerp2, int * resplen2, int * answerp2_malloced) Line 139
    at resolv\resolv\res_query.c(139)
libc.so.6!__res_context_querydomain(int * answerp2_malloced, int * resplen2, int * nanswerp2, unsigned char ** answerp2, unsigned char ** answerp, int anslen, unsigned char * answer, int type, int class, const char * domain, const char * name, struct resolv_context * ctx) Line 625
    at resolv\resolv\res_query.c(625)
libc.so.6!__GI___res_context_search(struct resolv_context * ctx, const char * name, int class, int type, unsigned char * answer, int anslen, unsigned char ** answerp, unsigned char ** answerp2, int * nanswerp2, int * resplen2, int * answerp2_malloced) Line 381
    at resolv\resolv\res_query.c(381)
libc.so.6!__GI__nss_dns_gethostbyname4_r(const char * name, struct gaih_addrtuple ** pat, char * buffer, size_t buflen, int * errnop, int * herrnop, int32_t * ttlp) Line 373
    at resolv\nss_dns\dns-host.c(373)
libc.so.6!gaih_inet(const char * name, const struct gaih_service * service, const struct addrinfo * req, struct addrinfo ** pai, unsigned int * naddrs, struct scratch_buffer * tmpbuf) Line 747
    at \sysdeps\posix\getaddrinfo.c(747)
libc.so.6!__GI_getaddrinfo(const char * name, const char * service, const struct addrinfo * hints, struct addrinfo ** pai) Line 2240
    at \sysdeps\posix\getaddrinfo.c(2240)
libcurl.so.4![Unknown/Just-In-Time compiled code]
libc.so.6!start_thread(void * arg) Line 442
    at nptl\nptl\pthread_create.c(442)
libc.so.6!clone3() Line 81
    at \sysdeps\unix\sysv\linux\x86_64\clone3.S(81)

复现代码如下:

#include <cstddef>
#include <string>
#include <iostream>
#include <array>
#include <vector>
#include <memory>

//#define CURL_STATICLIB // doesnt matter
#include <curl/curl.h>
#include <curl/easy.h>

using std::size_t;
using std::cout;
using std::endl;

constexpr size_t cnt_sml = 6;
constexpr size_t cnt_big = 1 << 16;

class ThrdLcl
{
    using qr_ptr = std::unique_ptr<int32_t>; // some sql query class
    using dt_ptr = std::unique_ptr<int64_t>; // some data storage struct

private:
    std::array<std::array<            qr_ptr,  cnt_big>, cnt_sml> queries;
    std::array<std::array<std::vector<dt_ptr>, cnt_big>, cnt_sml> buffer;

    ThrdLcl()
    {
        cout << "ThrdLcl constructed!" << endl;
    }
    ~ThrdLcl()
    {
        cout << "ThrdLcl destructed!" << endl;
    }
    ThrdLcl(const ThrdLcl&) = delete;
    ThrdLcl(const ThrdLcl&&) = delete;
    ThrdLcl& operator=(const ThrdLcl&) = delete;
    ThrdLcl& operator=(const ThrdLcl&&) = delete;

public:
    static ThrdLcl* i()
    {
        thread_local ThrdLcl instance;
        return &instance;
    }
};

void do_curl_bullshit()
{
    cout << "Checking CURL..." << endl;
    CURL* ch = curl_easy_init();
    curl_easy_setopt(ch, CURLOPT_URL, "https://api.ipify.org"); // any url
    curl_easy_perform(ch);
    curl_easy_cleanup(ch);
}

int main(int argc, char* argv[])
{
    ThrdLcl::i(); // doesnt matter if here
    curl_global_init(CURL_GLOBAL_NOTHING);
    do_curl_bullshit();
    ThrdLcl::i(); // ...or here
    curl_global_cleanup();
    return 0;
}

编译需链接libcurl,采用G++ 12.1,C标准11、C++标准20。当实例为static时,一切正常(仅在数据规模扩大千倍后出现链接错误);当实例为thread_local时,仅一半缓冲区大小可正常运行;自行创建std::thread而非调用CURL时运行正常;实例在堆上创建并存储于unique_ptr中搭配CURL时也正常。

由此引发以下疑问:

  1. 为何非并行接口的CURL会创建线程?
  2. 为何会出现段错误?
  3. 为何仅修改类内变量数量(而非总大小)就会改变调用栈?
  4. 为何CURL线程中未调用instance()仍会引发问题?

问题解答

1. 为何非并行接口的CURL会创建线程?

libcurl的easy接口虽是同步阻塞,但内部可能为特定操作创建后台线程,比如异步DNS解析、连接池维护或SSL会话管理。另外,系统的DNS解析库(如glibc的resolv)本身也可能在内部创建线程处理解析请求,这从调用栈里的start_thread和DNS相关函数也能得到印证。

2. 为何会出现段错误?

核心原因是ThrdLcl类实例体积过大。thread_local变量默认存储在线程的栈空间,而Linux线程的默认栈大小通常仅8MB左右:

  • queries部分:6个包含65536个unique_ptr<int32_t>的数组,每个指针占8字节,总大小约3MB。
  • buffer部分:6个包含65536个空vector<dt_ptr>的数组,每个空vector至少占24字节(含三个内部指针),总大小约9MB。

两者总和远超线程栈的默认容量,直接触发栈溢出,覆盖了栈上的关键数据(比如线程上下文、函数返回地址、libc内部的resolv上下文),最终导致段错误。

换成static变量时,数据存储在进程的全局/静态存储区(属于堆的扩展区域),空间不受栈大小限制;用unique_ptr在堆上创建实例也是同理,堆空间远大于栈,因此不会出现溢出问题。

3. 为何仅修改类内变量数量(而非总大小)就会改变调用栈?

栈溢出属于未定义行为,溢出的内存会覆盖栈上的不同区域,具体覆盖位置取决于变量的内存布局和栈的增长方向。修改类内变量的数量或类型会改变ThrdLcl实例的内存结构,导致溢出时破坏的栈内数据不同,进而触发不同位置的段错误,反映在调用栈上就是不同的函数调用点。

4. 为何CURL线程中未调用instance()仍会引发问题?

当你在主线程调用ThrdLcl::i()时,主线程的栈上已创建了这个超大的thread_local实例,导致主线程栈空间被严重耗尽。虽然新线程的栈是独立的,但进程的全局线程管理结构(如glibc的线程控制块TCB)是所有线程共享的,主线程栈溢出可能破坏这些结构;或者libcurl创建新线程时,线程初始化过程需要使用主线程的栈空间(比如传递临时参数),此时主线程栈已无剩余空间,直接导致新线程初始化阶段就触发栈溢出,表现为DNS解析过程中的段错误。

另一种可能是,thread_local变量的元数据(比如记录每个线程是否已初始化该变量)存储在进程全局数据区,主线程栈溢出破坏了这些元数据,新线程启动时libc内部的线程初始化逻辑访问被破坏的元数据,从而触发段错误。

内容的提问来源于stack exchange,提问作者herhor67

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 03:08:18