从一次栈溢出问题讨论thread_local变量与线程栈

我的开发环境,linux系统、x86_64架构

一.栈溢出问题记录

1.背景

大家都知道栈的大小是有上限的,在linux下可以通过命令ulimit -s查看栈的size上限,也可以使用ulimit -a。我的机器默认是8M:

stack_size.png

并且,我们也可以通过ulimit -s命令来设置这个上限。大多数情况下,这个8M的空间已经够用了。但是偶尔也会遇到栈空间不足的情况。栈空间不足我们遇到更多的是下面两种情况:

1.1 函数调用栈帧太深

这种一般见于递归调用栈帧太深或者发生了死循环调用,直到把栈撑爆。我所在的项目,之前遇到过一个函数的const版本实现有问题,导致函数的非const版本和const版本死循环调用,最后栈撑爆,线上服务core掉。

1.2 函数内的非静态局部变量占用空间太大

最简单的一种是直接在函数内放一个数组char buff[100*1024*1024]; ,这种也会轻松把栈撑满,导致程序无法继续运行。

2.问题过程

上面这两种情况,对于一个有经验的程序员,只需要gdb一挂,分分钟就看出问题,甚至仔细看一下代码,就能分析出问题所在。然而我们这次遇到了一个很罕见的问题:现象是,服务器在启动时会core在一个线程的入口函数处(即pthread_create函数的第三个参数)。汇编展开以后发现最后的指令就是一个mov指令,并且操作数都是看起来很正常的。
这个时候组里几个大佬一起分析了一下近期的提交,发现在把中间某个版本的修改回退以后,服务就可以正常起来了。看了这个版本提交以后,发现中间在UserDB上加了一个数组ar,使UserDB大小增长了不少,由于UserDB是放在共享内存的,所以我们我们怀疑是不是资源占用太多,或者中间共享内存上数据的初始化有问题。这个时候另外一个大佬发现,把数组ar的长度减小到1,服务可以正常启动,这个时候我们更坚信的是我们对共享内存上的数据处理有问题。然而却走错了方向。在经历了一番折腾以后,发现并没有任何收获。最后不得不回到起点,从core文件开始分析。这次我们仔细的disassemble一下(为了方便描述,图片处理过):


dis2.png

我们发现在最初的一波寄存器压栈以后,在第9行进行了栈顶指针sp的一个移动操作,即分配了栈上的内存,为下面的局部变量使用,但是在紧接着第11行core掉了。此处只是简单的向刚分配的栈上内存的一个赋值操作。我们紧接着看了两个操作数的内容:

r.png

看着貌似内容也是合法值。那么现在能想到的只有一个解释了,sp已经指向了一个stack外部的地址了——栈溢出了。下面就是验证问题了。我们ulimit -s 20480把栈的上限从8M改成了20M,然后重新起服务,果不其然,服务正常起来了。问题已经确认,为什么栈会溢出。首先,我们看过core文件的函数调用栈,调用层次并不深,可以排除由于函数栈太深的原因。现在就是要确认栈上存的到底是什么数据,会导致栈被塞满。我找了栈帧上所有的函数的局部变量,占用总空间也不会超过1M。又陷入了僵局。
这个时候团队的力量又发挥了作用,组里另外一个大佬指出,自己为了提升性能,减少内存分配次数,做了一个内存cache的模板类:负责管理经常被使用、对象个数不会太多、线程能独占的局部变量。然后这个cache类会用thread_local的形式来存放被管理的对象。static thread_local修饰的局部变量大家经常使用,但是很少有人注意过这种变量的存放位置。
大佬把这个管理类修改了一下,利用一个宏屏蔽了其中的static thread_local的存储方式。重新编译,栈重新改回8M上限,服务可以正常起来了。可以得出一个初步的结论,这种static thread_local的变量挤占了栈的空间。问题虽然解决,但是why?我们下面具体分析。

二.thread_local变量分析

现在用一段简单的代码来分析一下thread_local变量

#include 
#include 
#include 
#include 

void *func(void *arg)
{
    static thread_local int64_t x = 99;
    thread_local int64_t y = 999;
    int64_t l = 9999;
    printf("addl = %p, addr x = %p, addr y = %p, diff = %ld \n", &l, &x, &y, (int64_t)&y - (int64_t)&x);
    while (true) {
    }
}

int main()
{
    int64_t m_l = 12345;
    printf("addr m_l = %p\n", &m_l);
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    pthread_t tid5;
    pthread_create(&tid1, NULL, func, NULL);
    pthread_create(&tid2, NULL, func, NULL);
    pthread_create(&tid3, NULL, func, NULL);
    pthread_create(&tid4, NULL, func, NULL);
    pthread_create(&tid5, NULL, func, NULL);
    sleep(1000);
    return 0;
}

输出是这个样子的:

addr m_l = 0x7fff02a057e8
addl = 0x7fd255762f08, addr x = 0x7fd2557636f8, addr y = 0x7fd2557636f0, diff = -8 
addl = 0x7fd254f61f08, addr x = 0x7fd254f626f8, addr y = 0x7fd254f626f0, diff = -8 
addl = 0x7fd254760f08, addr x = 0x7fd2547616f8, addr y = 0x7fd2547616f0, diff = -8 
addl = 0x7fd253f5ff08, addr x = 0x7fd253f606f8, addr y = 0x7fd253f606f0, diff = -8 
addl = 0x7fd25375ef08, addr x = 0x7fd25375f6f8, addr y = 0x7fd25375f6f0, diff = -8

然后我们再看一下内存布局(篇幅问题,只复制了其中的一部分):

7fd252f5f000-7fd252f60000 ---p 00000000 00:00 0 
7fd252f60000-7fd253760000 rw-p 00000000 00:00 0                          [stack:10354]
7fd253760000-7fd253761000 ---p 00000000 00:00 0 
7fd253761000-7fd253f61000 rw-p 00000000 00:00 0                          [stack:10353]
7fd253f61000-7fd253f62000 ---p 00000000 00:00 0 
7fd253f62000-7fd254762000 rw-p 00000000 00:00 0                          [stack:10352]
7fd254762000-7fd254763000 ---p 00000000 00:00 0 
7fd254763000-7fd254f63000 rw-p 00000000 00:00 0                          [stack:10351]
7fd254f63000-7fd254f64000 ---p 00000000 00:00 0 
7fd254f64000-7fd255764000 rw-p 00000000 00:00 0                          [stack:10350]

我们现在看thread_local变量x、y的地址,是在stack的地址空间内的。验证了我们之前的结论:在linux下,这种thread_local的变量会挤占栈的空间。(并且在C++11标准下,thread_local的作用等同于static thread_local
另外,上面这位大佬统计了项目中符号表中TLS变量的综合,已经超过5M,已经超过8M的一半了,亟需整理。

TODO:可以利用pthread_attr_getstack/pthread_attr_setstack等一系列线程栈的操作来更详细的分析上述问题

你可能感兴趣的:(从一次栈溢出问题讨论thread_local变量与线程栈)