美文网首页
从一次栈溢出问题讨论thread_local变量与线程栈

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

作者: 猿佑 | 来源:发表于2021-06-08 09:10 被阅读0次

我的开发环境,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 <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>

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变量与线程栈

    我的开发环境,linux系统、x86_64架构 一.栈溢出问题记录 1.背景 大家都知道栈的大小是有上限的,在li...

  • JVM

    1、一般什么情况会发生栈溢出、堆溢出 栈溢出(StackOverflowError) 1、栈是线程私有的,他的生命...

  • C# 值类型与引用类型

    基本概念 区别 线程栈与托管堆 Stack 栈:线程栈,由操作系统管理,存放值类型、引用类型变量。栈是基于线程的,...

  • Day4 JVM内存模型与参数

    栈:存放局部变量。栈是线程栈,只有一个线程执行,会在栈内存中分配空间,存放线程私有的区域。 stackoverfl...

  • jvm垃圾回收

    1、判断垃圾 垃圾回收主要讨论的是堆中对象的回收问题GC Root:虚拟机栈中的本地变量表(线程中的栈帧会指向本地...

  • _01_《高性能iOS应用开发》——内存管理

    应用中新创建的每个线程都有专用的栈空间,线程的最大栈空间很小,如果层级太深,可能造成栈溢出。 每个进程的所有线程共...

  • JVM Java虚拟机相关基础知识问答

    1. 什么情况下会产生栈溢出错误? 首先要明白什么是栈:栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时...

  • 深入理解java虚拟机

    一 java内存区域与内存溢出 程序计数器:当前线程执行行号,线程私有 虚拟机栈:存储局部变量表(基本类型和对象引...

  • 2019-04-28

    线程 每个线程都有自己独立专用的栈区 栈区之间的数据是独立的,栈区之间的数据是线程独立的。 栈区之内的局部变量和其...

  • jvm溢出实现

    java堆溢出 设置参数 抛出异常 虚拟机和本地方法栈溢出 栈溢出 -Xss设置栈容量大小单线程只抛出stacko...

网友评论

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

      本文链接:https://www.haomeiwen.com/subject/xrjxeltx.html