linux下多线程死锁调试

多线程编程是一直多比较难的一个部分, 今天我就来介绍一下在Linux下调试c++程序死锁的一个简单方式,环境是Ubuntu16.04, gcc版本是4.9.3,gdb版本是7.11.1

获得死锁程序崩溃后的core文件

  1. 这个部分比较重要, 因为很多时候死锁并不是每次都会出现的, 因此我们需要在遇到死锁的时候尽可能的保存现场, 方便我们分析,这里我们就需要获得死锁程序崩溃后的core文件,否则这次难得的死锁出现机会就错失了.
  2. 先介绍下core文件, core文件就是core dump生成的文件,core是内存的意思,dump是丢出来的意思,在我们开发unix程序的时候,相信大家肯定都遇到过Segmentation fault (core dumped)这种情况, 这个时候一般就会生成core文件,当一个进程运行过程中异常退出,操作系统会把当前进程的内存状态储存在一个core文件内,也就是core dumped产生的文件,这个就是我们在调试的时候需要用到的东西. 不过有些同学可能奇怪, 死锁的程序不是不会退出吗, 那么怎么产生core dump文件呢, 这个问题我们后来来介绍
  3. 首先说下产生core文件的必须步骤

ulimit -c

如果上面的输出是0,那么说明系统目前是无法生成core文件的,因为限制了core文件的大小是0,所以自然是无法生成core文件的, 所以我们需要改变这个限制, 命令如下:

ulimit -c unlimited

这个命令设置core文件大小不受限制,因此core文件就可以正常产生.

  1. 杀死死锁的进程

kill -11 pid

值得注意的是我们使用的是-11选项, 前面我们提到过死锁的程序实际上是不会退出的, 因此我们就需要手动来杀死程序, 但是不能让程序直接退出, 必须要"留下点东西"下来, 也就是我们想要的core文件.
实际上我们使用

kill -l

可以看到11对应的是SIGSEGV的信号,也就是我们一般在看到Segmentation fault (core dumped)之前进程收到的信号,也就说实际上进程收到的信号决定了进程产生core dumped文件.我们使用之前说的 -11 选项, 进程就会退出并且产生core dumped文件,这个时候我们就能获得我们"梦寐以求"(笑)的core文件了.

使用gdb来调试core文件

  1. 这里我手动给出一个死锁的例子,代码如下

    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    mutex m1, m2;
    
    void thread1() {
      lock_guard< mutex > guard( m1 );
      cout << "thread1 lock mutex1" << endl;
      std::cout << "then try to lock mutex2" << std::endl;
      lock_guard< mutex > guard2( m2 );
    }
    
    void thread2() {
      lock_guard< mutex > guard( m2 );
      std::cout << "thread2 locked mutex2" << std::endl;
      std::cout << "then sleep for 1 seconds and try to lock mutex1" << std::endl;
      sleep( 1 );
      lock_guard< mutex > guard2( m1 );
    }
    
    int main( int argc, char const* argv[] ) {
      thread t1( thread1 );
      thread t2( thread2 );
    
      t1.join();
      t2.join();
      return 0;
    }
    
  2. 然后编译运行, 这个比较简单,给出命令

g++ -std=c++11 test_dead_lock.cpp -o test_dead_lock -g -pthread

我们会获得可执行文件, 然后我们运行可执行文件,就会发现程序果然死锁了,这个时候我们不管做什么, 程序都已经失去效应, 这个时候我们需要做的就是按照步骤1的杀掉死锁进程了, 先找到进程先

ps -ef | grep test_dead_lock | grep -v grep | awk '{print $2}'

获得死锁进程的pid, 这个时候我们向这个进程发送SIGSECV信号, 也就是

kill -11 pid

进程退出, 我们可以看到熟悉的Segmentation fault (core dumped), 然后core文件也会随之产生,我这里产生的core文件名称就是core, 然后我们开始操作(笑), gdb调试开始

gdb test_dead_lock core
thread apply all bt

下面我们来看看结果:
image.png

结果很明显, 有三个线程, 一个是主线程也就是thread1, 两个是我们开的子线程,分别为thread2和thread3, 可以看到主线程是卡在等待子线程中, 也就是pthread_join.其他两个线程基本算是相似了, 都是在mutex::lock那个地方卡住了,看到这里我们一般基本就能确定两个线程是死锁了,因为我们这里比较简单的场景, 如果真的是在生产环境下的话, 一般可能需要多跑几次才能确定哪些线程是真的死锁了,哪些线程只是正好在等待锁.

  1. 上面比较有趣的地方有两个:

    1. 第一个是gdb调试指令,我们这次并没有用bt和info stack, 原因很简单, 那两个命令都只能查看目前正在运行的某个线程的栈信息, 这个对于我们来说是远远不够的, 而thread apply all命令, gdb会让对线程都执行这个命令, 就比如bt, 就是查看所有线程的具体栈信息.
    2. 第二个是我们使用thread apply all bt命令之后, 两个子线程的调用过程, 可以看到创建线程的系统是clone, 也就是说在Linux系统下, 操作系统用的是和产生新进程的系统调用来创建新的线程的, 按照操作系统的书籍来说的, Linux系统下创建新的线程用的就是clone, 但传递给clone的参数是和创建新的进程不同的, 比如创建新的线程的时候肯定是要共享地址空间的, 另外还要共享打开文件描述符等等,这里就不再细说, 具体可以看看操作系统相关的书籍.

写到这里就差不多了, 多线程编程一直是在c++非常难的部分,这些需要多coding和多reading才能有所提高, 希望和大家一起提高!

你可能感兴趣的:(linux下多线程死锁调试)