Linux下排除死锁详细教程(基于C++11、GDB)

本文通过一个C++11的多线程死锁实例,详细介绍了如何在Linux环境下使用GDB排查死锁问题。首先模拟了两个线程互相等待对方持有资源导致的死锁场景,然后利用`ps`和`top`命令观察进程状态,最后通过GDB的`attach`和`thread apply all bt`命令定位到死锁发生的线程及原因。

Linux下排除死锁详细教程(基于C++11、GDB)

1. 前言

在实际编写项目的过程中,经常涉及到多线程。多线程的程序编程很大概率会涉及到线程安全的问题,因此往往使用互斥锁来保证线程安全。

然而,互斥锁的使用却经常会导致另外一个问题:死锁。

所谓死锁,通俗的讲就是有两个共享资源,一个在你手上,一个在我手上。我等着你用完把另一个给我,你等我用完给你,这样相互等待就形成了死锁。

所以,在这里,基于Linux的环境,使用C++11提供的多线程编程来模拟死锁,并尝试从“不知情”的角度,使用shell + gbd 进行死锁的排查。

2. 模拟死锁

  1 #include <iostream>
  2 #include <thread>
  3 #include <mutex>
  4 
  5 using namespace std;
  6 
  7 mutex mtxA;
  8 mutex mtxB;
  9 
 10 void taskA() {
 11     lock_guard<mutex> lockA(mtxA);
 12     cout << "Thread A get Lock A!" << endl;
 13     this_thread::sleep_for(chrono::seconds(1));
 14     lock_guard<mutex> lockB(mtxB);
 15     cout << "Thread A get Lock A and B!" << endl;
 16     cout << "Thread A relese all source!" << endl;
 17 
 18 }
 19 
 20 void taskB() {
 21     lock_guard<mutex> lockB(mtxB);
 22     cout << "Thread B get Lock B!" << endl;
 23     this_thread::sleep_for(chrono::seconds(1));
 24     lock_guard<mutex> lockA(mtxA);
 25     cout << "Thread B get Lock B and A!" << endl;
 26     cout << "Thread B relese all source!" << endl;
 27 }
 28 
 29 int main() {
 30 
 31     thread t1(taskA);
 32     thread t2(taskB);
 33 
 34     t1.join();
 35     t2.join();
 36 
 37     return 0;
 38 }

这里我们使用了C++11中的多线程编程来模拟了一个死锁的场景。

首先,对于线程t1,会执行工作函数taskA。在taskA里面,线程t1首先会拿到 mtxA 这把互斥锁。然后睡眠1秒,让 线程t2执行工作函数taskB,由线程t2拿到 mtxB 这把互斥锁。这时,线程t1醒过来想拿锁B,而B却在t2手里。t2在拿到 mtxB 时,马上想获取 mtxA,但是在睡眠的t1手里。

因此,线程t1和t2循环等待对方的资源,又释放自己手里的资源,这样就形成了死锁。

我们将这个文件命名为:deadLock.cc ,并使用g++进行编译,生成可执行文件 (tip:注意一定要加上 -pthread,因为C++11里面的多线程库在Linux下也是基于pthread这个库实现的)::

g++ deadLock.cc -pthread -o deadLock -g

如此生成了可执行文件,因此可以运行:

./deadLock

运行结果:

在这里插入图片描述

可以看见,线程t1和t2各自获取了一把锁,同时它们都在等待对面释放手里的锁,因此造成了死锁。

3. 排查死锁

首先,我们怀疑一个程序发生了死锁,首先可以查看该进程CPU利用率、内存利用率的情况。因为如果发生了死锁(这里假设是互斥锁),进程里面发生死锁的线程会处于阻塞的状态,此时基本不占有CPU,因此CPU的利用率、内存占有率将会比较低。 我们可以使用 ps aux 命令来拿到一个进程的状态:

ps aux | grep deadLock

在这里插入图片描述

这里可以看见有两个和deadLock相关的进程:6586、6674。后面一个是执行了grep这个命令之后产生的进程,所以第一个进程是我们想排查的进程。

然后,使用 top 命令来查看进程的CPU利用率、内存占有率:

top -Hp 6586

在这里插入图片描述
可以看见,这个进程里面一共存在三个线程。仔细思考,应该是对应线程t1、t2、和 main线程。它们的CPU利用率、内存都是0,很有可能发生了死锁。

此时,进程已经运行起来了。在实际的项目中,我们一般也不可能把一个进程停掉用GDB调试。因此,只能用GDB 的 attach 命令来跟踪这个进程:

首先需要超级权限,输入密码:

su

然后:

gdb attach 6586

在这里插入图片描述

再查看三个线程的堆栈的调用情况:

thread apply all bt

在这里插入图片描述
(按 enter键)
在这里插入图片描述
(按 enter键)
在这里插入图片描述
可以看见,上面的三张图显示了三个线程的堆栈的调用情况。我们仔细观察每个线程:

在这里插入图片描述
这里红色框中有main,说明线程1是主线程。

在这里插入图片描述
这里红色框中有taskA,说明线程2是线程t1。

在这里插入图片描述
这里红色框中有taskB,说明线程3是线程t2。

因此,到此,我们知道了线程1是main线程、线程2是t1、线程3是t2。所以,下一步我们单独查看每个线程的堆栈调用情况。 使用:

info threads

得到:
在这里插入图片描述
各个线程的索引。

使用 thread + 线程索来切换到某个线程:

thread 1

在这里插入图片描述
使用 bt 来查看堆栈当前线程的堆栈调用:

在这里插入图片描述
可以大概浏览一下,当前线程没有和锁相关的调用,所以大概率死锁不发生在这个线程中。

接下来切换到线程2,再使用bt 查看堆栈:

在这里插入图片描述
在这里插入图片描述

在上面这张图中,从上往下看,找到进程名+行数的组合最后出现的地方,出现在程序的14行。我们用vim看一下程序的14行是什么:

在这里插入图片描述
刚好是线程t1醒来后想去拿锁B的行数。所以基本可以判断,这个线程拿不到锁,一直阻塞等待。

我们再查看线程3的情况 :

在这里插入图片描述
在这里插入图片描述
可以看见,线程3在执行完进程的23行基本阻塞住了,我们再去看看23行是什么:

在这里插入图片描述
可以看见,线程t2想拿锁A,但锁A在t1手里,所以它们俩循环等待对方先释放锁,造成了死锁。

至此,排除基本结束。接下来解除死锁的办法有很多,比如使用资源有序分配法,对于线程t1、t2按相同的顺序分配锁(这里线程t1先A再B,线程t2先B再A,因此死锁了),或者先把锁分配给一个线程,用完再释放全部的锁,这样也能解决死锁的问题。

<think>好的,我现在需要帮用户介绍如何使用GDB工具以及调试步骤。用户之前询问了关于`make vmlinux`的内容,现在转向GDB调试,可能是想在内核开发或相关项目中进行调试。用户可能是开发者或学生,对系统级编程有一定了解,但需要具体的调试指导。 首先,我应该确定用户的使用场景。用户可能是在编译了vmlinux后,想要用GDB来调试内核或者用户态程序。需要明确GDB的基本使用步骤,以及可能涉及的不同情况,比如调试用户程序还是内核。根据用户之前的提问,可能更关注内核调试,但也不能排除用户态程序的可能性,所以最好两者都涵盖,或者询问清楚,但根据问题本身,可能更偏向于一般程序调试。 接下来,我需要考虑用户的潜在需求。用户可能希望了解从启动GDB到设置断点、检查变量、控制程序流程等基本步骤。此外,用户可能需要知道如何结合vmlinux进行内核调试,比如使用QEMU和GDB调试内核启动过程。但问题中没有明确说明,所以可能需要分两部分:一般程序调试和内核调试。 需要确保步骤清晰,结构合理。先介绍GDB的基本概念,然后分步骤讲解如何编译、启动、设置断点、运行、查看变量、控制执行等。如果有高级技巧,比如查看内存、回溯调用栈、条件断点等,也应该包含在内,但不要太过复杂,以免用户混淆。 同时,用户可能遇到的常见问题,比如如何安装GDB,如何处理段错误,如何调试多线程程序等,可以简要提及或者给出提示。但主要按照用户的问题,集中在调试步骤上。 需要检查是否遗漏了重要步骤,比如编译时需要添加调试信息(-g选项),否则GDB无法有效调试。此外,可能需要提到如何加载符号表,特别是当涉及到内核调试时,需要加载vmlinux文件中的符号。 在回答结构上,可以按照以下顺序: 1. 编译程序时包含调试信息 2. 启动GDB 3. 设置断点 4. 运行程序 5. 单步执行和检查变量 6. 处理崩溃和查看堆栈 7. 其他常用命令(如查看内存、修改变量等) 8. 高级用法或内核调试的简要介绍 另外,考虑到用户之前的问题关于vmlinux,可能需要在最后补充如何使用GDB调试内核,例如通过QEMU启动内核并连接GDB,加载vmlinux符号表,设置内核断点等。这部分可以作为扩展内容,但需要明确区分用户态和内核态调试的不同步骤。 需要确保语言简洁明了,步骤具体,并给出示例命令,比如如何编译带调试信息的程序,如何设置断点等。此外,注意使用中文,避免专业术语过于晦涩,必要时进行解释。 最后,检查是否有错误,比如GDB命令的正确性,步骤的逻辑顺序是否正确,是否覆盖了用户可能需要的核心功能。确保回答结构清晰,用户能够按照步骤逐步进行调试。</think>以下是使用GDB(GNU Debugger)调试程序的详细步骤和核心操作指南: --- ### **1. 准备工作:编译时添加调试信息** 在编译程序时,必须添加`-g`选项以生成调试符号(否则GDB无法查看源码和变量): ```bash gcc -g -o my_program my_program.c # C语言 g++ -g -o my_program my_program.cpp # C++ ``` --- ### **2. 启动GDB** ```bash gdb ./my_program # 加载可执行文件 ``` 或直接附加到正在运行的进程: ```bash gdb -p <进程PID> # 调试已运行的程序 ``` --- ### **3. 基本调试命令** #### **(1) 设置断点** ```bash (gdb) break main # 在main函数入口设断点 (gdb) break file.c:20 # 在file.c的第20行设断点 (gdb) break func_name # 在指定函数处设断点 (gdb) info breakpoints # 查看所有断点 (gdb) delete <断点编号> # 删除断点 ``` #### **(2) 运行程序** ```bash (gdb) run # 启动程序(遇到断点暂停) (gdb) run arg1 arg2 # 带参数启动程序 ``` #### **(3) 控制程序执行** ```bash (gdb) next (n) # 单步执行(不进入函数) (gdb) step (s) # 单步执行(进入函数内部) (gdb) continue (c) # 继续运行到下一个断点或结束 (gdb) finish # 执行完当前函数并暂停 ``` #### **(4) 查看变量和内存** ```bash (gdb) print x # 打印变量x的值 (gdb) print *(ptr) # 查看指针指向的内容 (gdb) x/8xw &array # 以16进制查看数组的8个元素(每个4字节) (gdb) info locals # 查看当前函数局部变量 (gdb) info registers # 查看寄存器值 ``` #### **(5) 处理崩溃和信号** ```bash (gdb) backtrace (bt) # 查看函数调用栈(定位崩溃位置) (gdb) frame <编号> # 切换到指定栈帧 (gdb) signal SIGCONT # 向程序发送信号(如继续执行) ``` --- ### **4. 高级调试技巧** #### **(1) 条件断点** ```bash (gdb) break file.c:30 if x == 5 # 当x等于5时触发断点 ``` #### **(2) 观察点(监控变量修改)** ```bash (gdb) watch x # 当变量x被修改时暂停 (gdb) watch *(0x1234) # 监控指定内存地址 ``` #### **(3) 修改运行时变量** ```bash (gdb) set var x = 10 # 强制修改变量x的值 ``` #### **(4) 调试多线程** ```bash (gdb) info threads # 查看所有线程 (gdb) thread <编号> # 切换到指定线程 ``` --- ### **5. 调试内核(结合vmlinux)** 若需调试Linux内核(需提前编译`vmlinux`): 1. **启动QEMU虚拟机并附加GDB**: ```bash qemu-system-x86_64 -kernel bzImage -s -S # -S: 启动时暂停,-s: 开启GDB端口1234 ``` 2. **在另一个终端连接GDB**: ```bash gdb vmlinux # 加载内核符号表 (gdb) target remote :1234 # 连接到QEMU (gdb) break start_kernel # 在内核入口函数设断点 (gdb) continue # 继续执行内核启动 ``` --- ### **6. 常用调试场景示例** #### **示例1:段错误(Segmentation Fault)** 1. 编译程序时添加`-g`选项。 2. 启动GDB并运行程序: ```bash (gdb) run ``` 3. 程序崩溃时,使用`backtrace`查看调用栈: ```bash (gdb) bt ``` #### **示例2:死锁问题** 1. 附加到正在运行的进程: ```bash gdb -p <PID> ``` 2. 查看所有线程状态: ```bash (gdb) info threads ``` 3. 切换线程并检查状态: ```bash (gdb) thread 2 (gdb) print mutex_var ``` --- ### **7. 注意事项** - **符号一致性**:调试时使用的程序/内核版本需与编译时的完全一致。 - **GDB脚本**:可通过编写`.gdbinit`文件预加载常用命令。 - **图形化工具**:若偏好可视化界面,可尝试`gdb -tui`或VS Code集成调试。 --- ### **总结** GDB是调试程序和分析崩溃的核心工具。掌握基本断点、单步执行和堆栈分析后,可快速定位代码逻辑错误、内存问题或并发缺陷。对于内核级调试,需结合`vmlinux`符号文件和虚拟机环境(如QEMU)。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值