先看一段代码,猜猜 Linux 下编译后执行会发生什么(编译参数 -pthread -O2 -std=c++11,在线执行):
#include <iostream>
#include <thread>
thread_local char tls[8 << 20];
void func() {
char arr[1024 * 6];
arr[sizeof(arr) - 1] = 'A';
printf("%p %p\n", &tls, &arr);
}
int main() { std::thread(func).join(); }
如果你系统的栈大小是默认的 8MB,那么上述代码会导致栈溢出。原因在于 pthread 实现中 TLS 和线程栈使用的是同一块内存空间,TLS 使用的多时留给线程栈的就少了。
pthread 初始化时会根据系统设定的栈大小、TLS 占用的内存空间大小计算默认的栈空间:
// 1. 获取 TLS 内存大小
size_t static_tls_align;
_dl_get_tls_static_info (&__static_tls_size, &static_tls_align);
// 2. 获取系统栈大小
if (__getrlimit (RLIMIT_STACK, &limit) != 0 || limit.rlim_cur == RLIM_INFINITY) {
limit.rlim_cur = ARCH_STACK_DEFAULT_SIZE;
}
// 3. 设定 pthread 默认栈大小
const size_t minstack = pagesz + __static_tls_size + MINIMAL_REST_STACK;
if (limit.rlim_cur < minstack) {
limit.rlim_cur = minstack;
}
limit.rlim_cur = ALIGN_UP (limit.rlim_cur, pagesz);
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
__default_pthread_attr.stacksize = limit.rlim_cur;
__default_pthread_attr.guardsize = GLRO (dl_pagesize);
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
pthread_create 时如果没有指定使用的栈大小,则会使用上述计算的默认栈大小:
if (attr->stacksize != 0) {
size = attr->stacksize;
} else {
lll_lock (__default_pthread_attr_lock, LLL_PRIVATE);
size = __default_pthread_attr.stacksize;
lll_unlock (__default_pthread_attr_lock, LLL_PRIVATE);
}
如果担心 TLS 触发栈溢出,可以在程序启动时通过下方代码获取 TLS 需要使用的内存空间总大小(在线执行),可以根据该值进行阈值的判断:
#include <iostream>
extern "C" void _dl_get_tls_static_info(size_t *sizep, size_t *alignp);
int main() {
size_t size;
size_t align;
_dl_get_tls_static_info(&size, &align);
printf("size %lu\n", size);
}
没想到在 2026 年还能再次遇到这个问题。项目中混合了 Rust 和 C++,Rust 侧新增了后台线程,并且单元测试启动线程一切正常,和 C++ 联合编译后就直接炸掉。使用 LLDB attach 上去排查,能看到 fault address 是 0x7f6440079fe8,继续查看线程启动时栈底的地址:
frame select 23
reg read rsp
栈底的地址是 0x7f644007f000,整个栈的可用空间太小了。查看进程内存映射:
cat /proc/<PID>/maps | grep 7f644007
#> 7f6440079000-7f644007a000 ---p 00000000 00:00 0
#> 7f644007a000-7f64404a6000 rw-p 00000000 00:00 0
显然线程执行之初能用的栈空间就已经没多少了。值得注意的是这段内存的大小只有 4.17MiB,并非 8MiB。简单查询后得知 Rust 默认的栈大小只有可怜的 2MiB,Rust 自身没有多少 TLS 变量,但 C++ 侧有超过 4MiB 的 TLS,所以单独跑没事,联合编译炸掉。修复方法也很简单,Rust 创建线程时将栈大小与 C++ 侧对齐即可。