C++开发常用调试技巧


1、死锁的调试

一个正在生产环境下运行的进程死锁了,然后并没有在调试器里面打开它,但发现没有响应,日志输出也停止了。那么我们会想到“我刚刚加上了新的锁策略,不一定稳定,这可能是死锁了”。

产生死锁的四个必要条件

1) 互斥条件:一个资源每次只能被一个进程(线程)使用。

2) 请求与保持条件:一个进程(线程)因请求资源而阻塞时,对已获得的资源保持不放。

3) 不剥夺条件 : 此进程(线程)已获得的资源,在末使用完之前,不能强行剥夺。

4) 循环等待条件 : 多个进程(线程)之间形成一种头尾相接的循环等待资源关系。

可以使用 pstack gdb 工具对死锁程序进行分析:

1pstack Linux(比如 Red Hat Linux 系统、Ubuntu Linux 系统等)下一个很有用的工具,它的功能是打印输出此进程的堆栈信息。可以输出所有线程的调用关系栈。

2)使用gdb处理死锁问题的方式有以下两种:

1.1 gdb附加进程调试

第一种方法是使用gdb附加进程调试

使用gdb <program> pid附加到进行中进行调试。

调试过程其实比较简单。程序卡死的情况下,多数为线程死锁。

1)使用info threads来查看各个线程状态。

2)使用thread id进行线程之间的切换。

3)使用bt/backtrace进行线程堆栈信息查看,可以看到线程的运行情况。

1.2 kill -11产生core dump

第二种方法是kill -11 产生core dump

Core的意思是内存, Dump的意思是扔出来, 堆出来.开发和使用Unix程序时, 有时程序莫名其妙的down, 却没有任何的提示(有时候会提示core dumped). 这时候可以查看一下有没有形如core.进程号的文件生成运行过程中发生异常, 程序异常退出时, 由操作系统把程序当前的内存状况存储在一个core文件中, core dump.这个文件便是操作系统把程序down掉时的内存内容扔出来生成的, 它可以做为调试程序的参考.Core Dump又叫核心转储。

(1)使用ulimit -c unlimited命令允许操作系统在程序运行挂掉时抛出core文件;

(2)当程序无日志输出或疑似死锁时,使用kill -11 pid命令,让进程产生一个段错误(Segmentation Fault),从而生成core文件;core 文件里面包含了死锁时进程的内存镜像;

(3) gdb 打开这个 core 文件,然后使用命令

thread apply all bt

打出所有线程的栈,如果你发现有那么几个栈停在 pthread_wait 或者类似调用上,大致就可以得出结论;

举一个简单的例子(为了代码尽量简单,使用了C++11thread library)。

#include <iostream>

#include <thread>

#include <mutex>

#include <chrono>

using namespace std;

 

mutex m1,m2;

 

 

void func_2()

{

    m2.lock();

    cout<< "about to dead_lock"<<endl;

    m1.lock();

    

}

 

void func_1()

{

    m1.lock();

    

    chrono::milliseconds dura( 1000 );// delay to trigger dead_lock

    this_thread::sleep_for( dura );

        

    m2.lock();

    

}

 

 

int main()

{

    thread t1(func_1);

    thread t2(func_2);

    t1.join();

    t2.join();

    return 0;

}

 

  编译代码

  $> g++ -Wall -std=c++11 dead_lock_demo.cpp -o dead_lock_demo -g -pthread

  运行程序,发现程序打印出about to dead_lock” 就不动了,现在我们使用gdb来调试。注意gdb的版本要高于7.0,之前使用过gdb6.3调试多线程是不行的。

  在这之前需要先产生core dump文件:

  $> ps -aux | grep dead_lock_demo

  找出 dead_lock_demo 线程号,然后:

  $> kill -11 pid

  此时会生成core dump 文件,在我的系统上名字就是 core

  然后调试:

  $> gdb dead_lock_demo core

$> thread apply all bt

说明:

Kill命令的常用信号名称:

9  - SIGKILL    无条件终止进程

11 - SIGSEGV   段错误

15 - SIGTERM   向进程发送终止信号

kill命令可以带信号号码选项,也可以不带。如果没有信号号码,kill命令就会发出终止信号(15),这个信号可以被进程捕获,使得进程在退出之前可以清理并释放资源。

 

2、预处理功能:assert和NDEBUG

C++中有时会用到类似于头文件保护的技术,以便有选择的执行调试代码。基本思想是,程序可以包含一些用于调试的代码,但是这些代码只在开发程序的时候使用。当应用程序编写完成准备发布时,要先屏蔽掉调试代码。这种方法用到了两种预处理功能:assertNDEBUG

2.1 assert预处理宏

 assert是一种预处理宏。所谓预处理宏其实就是一个预处理变量,它的行为有点类似于内联函数。assert宏使用一个表达式作为它的条件:

assertexpr;

首先对expr求值,如果表达式为假,assert输出信息并终止程序的执行。如果表达式为真,assert什么也不做。

assert宏常用于检查“不能发生”的条件。例如,一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阀值。此时,程序可以包含一条如下所示的语句:

assertword.size()>threshold

2.1 NDEBUG预处理变量

assert的行为依赖于一个名为NDEBUG的预处理变量的状态。如果定义了NDEBUG,则assert什么也不做。默认状态下没有定义NDEBUG,此时assert将执行运行时检查。

我们可以使用一个#define语句定义NDEBUG,从而关闭调试状态。

定义NDEBUG能避免检查各种条件所需的运行时开销,当然此时根本就不会执行运行时检查。因此,assert应该仅用于验证那些确实不可能发生的事。我们可以把assert当成调试程序的一种辅助手段,但是不能用它代替真正的运行时逻辑检查,也不能替代程序本身应该包含的错误检查。

除了使用assert外,也可以使用NDEBUG编写自己的条件调试代码。如果NDEBUG未定义,将执行#ifndef#endif之间的代码;如果定义了NDEBUG,这些代码将被忽略。

void pring(const int ia[], size_t size)

{

    #ifndef NDEBUG

    // __func__  编译器定义的一个局部静态变量,用于存放函数的名字

    cerr << __func__ << ":array size is " << size << endl;

    #endlf

}

 

C++编译器除了定义了之外,预处理器还定义了另外4个对于调试很有用的名字:

名字

作用

__func__

当前调试的函数名字

__FILE__

存放文件名的字符串字面值

__LINE__

存放当前行号的整型字面值

__TIME__

存放文件编译时间的字符串字面值

__DATE__

存放文件编译日期的字符串字面值


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM