利用 KGDB 解决内核死锁问题——实战调试案例
一、前言
在嵌入式Linux开发与内核驱动调试过程中,“死锁”是一种极难通过普通日志定位和还原的疑难杂症。尤其在多核SMP环境下,内核代码中对锁(mutex/spinlock)管理失误,很容易造成系统卡死、进程无法调度、整体系统“假死”或重启。
本文将以实际发生过的“音频驱动初始化死锁”为例,详细展示如何通过KGDB(内核级GDB调试器),一步步定位问题根源、还原bug现场、最终高效解决死锁,帮助开发者建立起内核实战调试的完整思路。
二、问题背景与现象描述
1. 项目背景
- 平台:NXP i.MX8MP + 定制音频Codec芯片
- 场景:新定制驱动合入内核,集成多任务并发音频流播放
- 现象:系统启动后偶发性卡死,无log输出,shell失去响应,串口console阻塞,进程无法切换,只能硬复位
2. 常规排查手段失败
printk加日志无法输出关键信息- 死锁发生后,甚至
Magic SysRq都无法唤起 - 传统netconsole、日志回显无帮助
此时,仅有源码级实时调试工具——KGDB能解决问题。
三、KGDB调试环境配置
1. 内核配置(menuconfig)
进入 Kernel hacking 菜单,配置如下:
[*] Kernel debugging
[*] KGDB: kernel debugger
<*> KGDB: use kgdb over the serial console

2. 启动参数设置
假设开发板UART0为调试口,波特率115200:
console=ttyS0,115200 kgdboc=ttyS0,115200 kgdbwait
kgdboc指定KGDB调试串口kgdbwait系统启动即进入调试等待
3. 硬件连接
- 开发板 UART0 ↔ 主机 USB转串口(如ttyUSB0)
- 主机安装交叉GDB(如
aarch64-linux-gnu-gdb)
四、KGDB调试实战全流程
1. 启动KGDB,连接主机
开发板加电启动,串口log会出现:
KGDB: Waiting for connection from remote GDB...
在主机上运行:
aarch64-linux-gnu-gdb vmlinux
(gdb) target remote /dev/ttyUSB0
GDB连接后内核挂起,进入调试态。
2. 还原死锁现场——下断点、单步跟踪
2.1 定位可疑入口
经验推测,音频驱动的 probe()、open()、ioctl()、trigger() 等接口存在多线程资源竞争。
在驱动入口设置断点:
(gdb) b audio_driver_probe
(gdb) b audio_playback_trigger
(gdb) b audio_ioctl
继续运行,让系统触发死锁:
(gdb) c
2.2 捕捉死锁时刻
几次尝试后,系统在 audio_playback_trigger() 内停下,分析当前线程调用栈:
(gdb) bt
输出示例:
#0 audio_playback_trigger (substream=0xffff0000234ff500, cmd=1) at drivers/audio/audio_driver.c:430
#1 snd_pcm_do_start (substream=0xffff0000234ff500) at sound/core/pcm_lib.c:2053
#2 __snd_pcm_lib_xfer (substream=0xffff0000234ff500) at sound/core/pcm_lib.c:2208
...
#N schedule () at kernel/sched/core.c:4566
继续查看当前持有锁对象:
(gdb) p my_audio_lock
发现 my_audio_lock 状态为已被持有(locked=1),但没有释放。
2.3 全局线程/任务排查
GDB支持多线程切换,查看所有线程:
(gdb) info threads
依次切换线程:
(gdb) thread N
(gdb) bt
最终发现另一个内核线程(如kworker/2:1)同样在等待同一个锁对象,并且调用栈停在驱动的 audio_stream_cleanup()。
2.4 死锁链条复盘
分析锁使用代码:
// drivers/audio/audio_driver.c
static DEFINE_MUTEX(my_audio_lock);
int audio_playback_trigger(...) {
mutex_lock(&my_audio_lock);
...
do_playback();
...
mutex_unlock(&my_audio_lock);
}
void audio_stream_cleanup(...) {
mutex_lock(&my_audio_lock);
...
do_cleanup();
...
mutex_unlock(&my_audio_lock);
}
通过GDB查看变量、堆栈,推断死锁链:
- 线程A已持有
my_audio_lock,未及时释放 - 线程B又请求同一个锁,导致双方互等
- 死锁触发点与某一并发调度相关
3. 变量/数据结构动态查看与分析
通过GDB命令,动态检查相关结构体/链表:
(gdb) p *substream
(gdb) x/16x 0xffff0000234ff500
(gdb) p my_audio_state
结合代码与变量状态,还原死锁根因:
- 发现某次异常退出路径中
mutex_unlock()没有被调用 - 具体原因是错误处理跳转中遗漏了解锁动作(常见于
goto错误路径)
4. 现场修正与验证
修改代码:
确保无论何种错误路径退出,均要解锁:
int audio_playback_trigger(...) {
mutex_lock(&my_audio_lock);
...
if (error) {
mutex_unlock(&my_audio_lock);
return -EINVAL;
}
...
mutex_unlock(&my_audio_lock);
}
或用 goto out 统一出口:
int audio_playback_trigger(...) {
int ret = 0;
mutex_lock(&my_audio_lock);
...
if (error)
goto out;
...
out:
mutex_unlock(&my_audio_lock);
return ret;
}
5. 重复验证
- 重新编译驱动/内核,烧录到开发板
- 再次用KGDB下断点,单步跟踪相关流程
- 验证死锁已消失,多线程/多进程并发工作流正常
五、GDB调试指令与技巧总结
| 命令 | 用途 |
|---|---|
| b func/addr | 设置断点 |
| c | 继续执行 |
| s/n | 单步调试 |
| bt | 打印当前线程调用栈 |
| info threads | 查看所有线程 |
| thread N | 切换到N号线程 |
| p / x / print | 查看变量/内存/结构体内容 |
| set var | 动态修改变量 |
| call func | 调用内核函数(需小心) |
| info locals | 查看局部变量 |
六、KGDB使用心得与最佳实践
- 善用断点和单步还原全流程,不要仅靠变量检查。
- 多线程场景下反复切换线程,逐一排查线程堆栈,找出死锁/等待链。
- 变量和结构体动态打印,比 printk 更加高效和真实,还原真实运行态。
- 记得用
continue让系统恢复正常运行,不要一直卡在断点。 - 养成代码中正确使用锁的习惯,可考虑内核提供的
lockdep辅助检查潜在死锁。
七、结语与经验升华
通过本次案例,可以看到KGDB在Linux内核调试中的巨大价值:
- 能现场还原任何函数、变量、锁对象、线程切换过程
- 对于传统日志难以复现的多线程死锁、资源竞争等问题,具备无可替代的定位能力
- 真正实现“以源码为中心”的故障定位,极大提升内核开发效率和系统稳定性
建议每位内核、驱动工程师都要掌握KGDB,并配备必要硬件(串口线),这是进阶高级Linux开发的必经之路。
视频教程请关注 B 站:“嵌入式 Jerry”
403

被折叠的 条评论
为什么被折叠?



