线程本地单例与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时也正常。
由此引发以下疑问:
- 为何非并行接口的CURL会创建线程?
- 为何会出现段错误?
- 为何仅修改类内变量数量(而非总大小)就会改变调用栈?
- 为何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

