网站做的比较好的公司,分销商城加盟,济宁市兖州区城市建设局网站,中国百强企业1. 问题描述
在一次代码调试的过程中#xff0c;遇到过一个问题#xff0c;线程在调用pthread_cancel时#xff0c;提示未找到目标线程#xff0c;然后程序阻塞在了与目标线程相关的条件变量的释放上#xff0c;造成了死锁的现象。
2. 问题复现
#include pthread.h…1. 问题描述
在一次代码调试的过程中遇到过一个问题线程在调用pthread_cancel时提示未找到目标线程然后程序阻塞在了与目标线程相关的条件变量的释放上造成了死锁的现象。
2. 问题复现
#include pthread.h
#include iostream
#include unistd.h
#include cstring#define BUF_SIZE 8192unsigned char g_buf[BUF_SIZE] {0xff};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 signalsstd::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() {Worker work;// avoid main thread exit before work threadsleep(3);return 0;
}程序卡死执行结果如下 打印堆栈信息如下 可以看到主线程阻塞在析构函数中销毁条件变量m_cond的位置而工作线程则阻塞在pthread_cond_wait处等待条件变量的满足。因此看到的现象就如第一张图那样主线程卡死无法完成资源释放。 从pthread_cancel的返回值可以看出工作线程并没有按照预期那样在取消点pthread_cond_wait处被成功释放。返回值3意味着没有找到要取消的线程。 表面原因我们找到了但是根本原因还没有找出来也就是为什么会造成这种结果正常情况下pthread_cancel是不会失败的。猜测一种可能的原因线程的栈空间被其他数据覆盖线程相关信息找不到了。
3. 问题分析与定位 从上图就可以看出0x7ffff6e83700是工作线程该值应该是线程控制信息的地址打印地址内容发现结果为0这就有问题了正常情况下应该是非0值参考当前线程地址的打印信息就可以对比出。所以对该线程的任何操作pthread_cancel、pthread_join都不会正常执行。 知道了原因那就来找找是什么地方导致了这种结果呢猜测是不是赋值操作时数组越界了并且超出了足够大的范围。那么什么操作会有这种结果呢
循环赋值且循环次数足够大通过memcpy、memset、memmove进行赋值且size足够大。
根据以上猜测我们再来看看代码发现在调用memcpy时局部变量buf的大小只有4096而传入的size值确是8192远远超出了buf的大小。将size的大小改为5000后再次执行程序。 可以看到程序正常结束但是5000依然超出了实际内存大小这说明要复现上述问题需要的size值得足够大才行。
实际上通过检测工具可以更方便的check数组越界的情况此处使用gcc自带的工具sanitizers只需要在编译选项中增加-g -fsanitizeaddress选项即可增加调试信息可以更容易定位代码位置。 可以看到该工具定位到可能越界位置在45行代码也就是调用memcpy的位置。
4. 总结
pthread_cancel并不一定保证线程被释放它只是给目标线程发送了一个信号而只有当目标线程到达一个取消点系统调用时目标线程才会退出。如果需要等待线程退出应该调用pthread_join来保证这一点。上述代码中如果在cancel之后调用该函数程序会出现段错误因为在使用线程相关的信息时拿到的是一个空值。在对字符数组进行赋值时c语言提供的一些函数并不安全很多时候越界却不自知。所以我们需要有一些检测工具来帮助我们避免这些情况的发生常用的工具如sanitizers、valgrind等。