记录一次数组越界导致的线程死锁问题

1. 问题描述

在一次代码调试的过程中,遇到过一个问题,线程在调用pthread_cancel时,提示未找到目标线程,然后程序阻塞在了与目标线程相关的条件变量的释放上,造成了死锁的现象。

2. 问题复现

#include <pthread.h>
#include <iostream>
#include <unistd.h>
#include <cstring>

#define BUF_SIZE 8192

unsigned char g_buf[BUF_SIZE];

class Worker {
public:
  Worker() {
    m_run = false;
    pthread_mutex_init(&m_mtx, NULL);
    pthread_cond_init(&m_cond, NULL);
    pthread_create(&m_tid, 0, writeThread, (void*)this);
    std::cout << "create thread " << std::hex << std::showbase << m_tid << std::endl;
  }

  ~Worker() {
    pthread_t id = pthread_self();
    std::cout << "cur thread " << id << std::endl;
    m_run = false;
    int ret = pthread_cancel(m_tid);
    std::cout << "ret:" << ret << std::endl;
    // pthread_join(m_tid, NULL);
    // Destroy the signals
    std::cout << "finished 1" << std::endl;
    pthread_cond_destroy(&m_cond);
    std::cout << "finished 2" << std::endl;
    pthread_mutex_destroy(&m_mtx);
    std::cout << "finished 3" << std::endl;
  }

  static void* writeThread(void* obj) {
    ((Worker*)obj)->writeWork();
    return 0;
  }

  void writeWork() {
    char buf[4096];
    while(true) {
      sched_yield();
      if(!m_run) {
        memcpy(buf, g_buf, BUF_SIZE);
        pthread_mutex_lock(&m_mtx);
        while (!m_run)
        {
          pthread_cond_wait(&m_cond, &m_mtx);
        }
        pthread_mutex_unlock(&m_mtx);
       // write buf
    }
  }
private:
  bool m_run{false};
  pthread_cond_t m_cond;
  pthread_mutex_t m_mtx;
  pthread_t m_tid;
};

int main() {
  memset(g_buf, 0xff, BUF_SIZE);
  Worker work;
  // avoid main thread exit before work thread
  sleep(3);
  return 0;
}

程序卡死,执行结果如下:
在这里插入图片描述
打印堆栈信息如下:
在这里插入图片描述
可以看到主线程阻塞在析构函数中销毁条件变量m_cond的位置,而工作线程则阻塞在pthread_cond_wait处等待条件变量的满足。因此看到的现象就如第一张图那样,主线程卡死,无法完成资源释放。
从pthread_cancel的返回值可以看出,工作线程并没有按照预期那样在取消点(pthread_cond_wait)处被成功释放。返回值3意味着没有找到要取消的线程。
表面原因我们找到了,但是根本原因还没有找出来,也就是为什么会造成这种结果,正常情况下pthread_cancel是不会失败的。猜测一种可能的原因:线程的栈空间被其他数据覆盖,线程相关信息找不到了

3. 问题分析与定位

![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/ba47ef76ca1d4e77ae02bfab5cccbd3f.png在这里插入图片描述
从上图就可以看出,0x7ffff6e83700是工作线程,该值应该是线程控制信息TCB的地址,打印地址内容发现结果为0xFFFFFFFF,正常情况下TCB的第一个8字节存放的是线程id,这里明显不对,其值被g_buf里的值给覆盖了(初始化为全1),所以对该线程的任何操作(pthread_cancel、pthread_join)都不会正常执行。
知道了原因,那就来找找是什么地方导致了这种结果呢?猜测是不是赋值操作时,数组越界了,并且超出了足够大的范围。那么什么操作会有这种结果呢:

  1. 循环赋值,且循环次数足够大;
  2. 通过memcpy、memset、memmove进行赋值,且size足够大。

根据以上猜测,我们再来看看代码,发现在调用memcpy时,局部变量buf的大小只有4096,而传入的size值确是8192,远远超出了buf的大小。所以超出部分的操作将会覆盖当前栈空间的其他变量的内容,如线程控制信息。将size的大小改为5000后,再次执行程序。
在这里插入图片描述
可以看到程序正常结束,但是5000依然超出了实际内存大小,这说明要复现上述问题,需要的size值得足够大才行。

实际上通过检测工具可以更方便的check数组越界的情况,此处使用gcc自带的工具sanitizers,只需要在编译选项中增加-g -fsanitize=address选项即可,增加调试信息可以更容易定位代码位置。
在这里插入图片描述
可以看到该工具定位到可能越界位置在45行代码,也就是调用memcpy的位置。

4. 总结

  • pthread_cancel并不一定保证线程被释放,它只是给目标线程发送了一个信号,而只有当目标线程到达一个取消点(系统调用)时,目标线程才会退出。
  • 如果需要等待线程退出,应该调用pthread_join来保证这一点。上述代码中,如果在cancel之后调用该函数,程序会出现段错误,因为在使用线程相关的信息时,拿到的是一个空值。
  • 在对字符数组进行赋值时,c语言提供的一些函数并不安全,很多时候越界却不自知。所以我们需要有一些检测工具来帮助我们避免这些情况的发生,常用的工具如sanitizers、valgrind等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值