4. 查看内核线程与调用栈:info threads、thread apply all bt、frame 命令解析
死锁排查,说白了就是找「谁在等谁」。但光知道进程号还不够,你得钻进内核线程的肚子里,看看它到底卡在哪一行代码上。今天我就带你把 GDB 里这三个最核心的线程调试命令吃透。
4.1 为什么必须看线程?
驱动死锁有个特点——它通常不发生在主线程,而是藏在某个内核 worker 线程、中断下半部或者软中断上下文里。我遇到过一回,系统看起来还活着,但某个 USB 设备就是枚举不过去。折腾了两天,最后用 info threads 一列,发现一个内核线程卡在 mutex_lock() 上,另一个线程正拿着那个锁在等 DMA 完成——典型的 ABBA 死锁。
所以,第一步永远是:看看当前到底有多少线程,它们都在哪。
4.2 info threads —— 列出所有线程
在 GDB 里连上内核或 coredump 后,直接敲:
(gdb) info threads
Id Target Id Frame
1 Thread 1 (CPU#0) schedule+0x32
2 Thread 2 (CPU#1) __mutex_lock_slowpath+0x1a0
3 Thread 3 (CPU#2) do_IRQ+0x6c
* 4 Thread 4 (CPU#3) console_unlock+0x48
注意那个 * 号,它表示当前 GDB 正在查看的线程。每个线程对应一个 CPU 核心(在 SMP 系统里),或者对应一个内核线程。输出里最关键的是 Frame 列——它告诉你这个线程当前执行到哪个函数了。
info threads,扫一眼有没有多个线程都卡在锁相关的函数上(比如 mutex_lock、spin_lock、down)。如果超过两个线程都在等锁,那八成是死锁了。
4.3 thread apply all bt —— 一键打印所有调用栈
info threads 只能看到最顶层函数,但死锁的根因往往藏在调用链深处。这时候 thread apply all bt 就是你的核武器:
(gdb) thread apply all bt
Thread 4 (Thread 4):
#0 console_unlock (flags=...) at kernel/printk/printk.c:234
#1 vprintk_emit (facility=0, level=..., dict=..., dictlen=..., text=...)
#2 printk (fmt=...) at kernel/printk/printk.c:1890
#3 my_driver_write (file=..., buf=..., count=...) at drivers/my_drv.c:120
#4 vfs_write (file=..., buf=..., count=..., pos=...) at fs/read_write.c:580
Thread 3 (Thread 3):
#0 __mutex_lock_slowpath (lock=0x...)
#1 mutex_lock (lock=0x...) at kernel/locking/mutex.c:240
#2 my_driver_ioctl (file=..., cmd=..., arg=...) at drivers/my_drv.c:85
#3 vfs_ioctl (file=..., cmd=..., arg=...) at fs/ioctl.c:46
Thread 2 (Thread 2):
#0 schedule () at kernel/sched/core.c:3500
#1 schedule_timeout (timeout=...) at kernel/time/timer.c:1850
#2 wait_for_completion (x=...) at kernel/sched/completion.c:104
#3 my_driver_read (file=..., buf=..., count=...) at drivers/my_drv.c:60
#4 vfs_read (file=..., buf=..., count=..., pos=...) at fs/read_write.c:480
Thread 1 (Thread 1):
#0 cpu_idle_loop () at kernel/sched/idle.c:250
看到没?Thread 3 在等锁,Thread 2 在等完成量,而 Thread 4 正拿着锁在 printk——这就是典型的「锁持有者被阻塞」模式。我曾经靠这一招,五分钟定位了一个困扰团队两周的网卡驱动死锁。
thread apply all bt 的输出会很长。我建议先 info threads 过滤出可疑线程,再用 thread apply 2 3 bt 只打印指定线程的调用栈。
4.4 frame —— 钻进栈帧看细节
调用栈有了,但还不够。比如你看到 mutex_lock,你想知道它等的是哪个锁?锁的地址是多少?这时候就要用 frame 命令切换栈帧。
假设我们想查 Thread 3 的 mutex_lock 细节:
(gdb) thread 3
[Switching to thread 3 (Thread 3)]
#0 __mutex_lock_slowpath (lock=0x...) at kernel/locking/mutex.c:240
240 if (!__mutex_trylock_fast(lock))
(gdb) frame 0
#0 __mutex_lock_slowpath (lock=0xffff800012345678) at kernel/locking/mutex.c:240
240 if (!__mutex_trylock_fast(lock))
(gdb) p lock
$1 = (struct mutex *) 0xffff800012345678
(gdb) p *lock
$2 = {count = {counter = 0}, wait_lock = {...}, wait_list = {...}}
这里 frame 0 就是当前栈帧(最顶层)。如果你想看调用者传进来的参数,可以切到 frame 1:
(gdb) frame 1
#1 mutex_lock (lock=0xffff800012345678) at kernel/locking/mutex.c:240
240 if (!__mutex_trylock_fast(lock))
(gdb) info args
lock = 0xffff800012345678
我个人习惯是:先 thread apply all bt 拿到全景,然后对每个卡在锁上的线程,切过去用 frame 0 看锁地址,再用 p *lock 看锁的状态。如果 count 是 0 但 wait_list 不为空,那基本就是死锁了。
4.5 实战排查流程
把上面三个命令串起来,就是一套标准死锁排查流程:
| 步骤 | 命令 | 目的 |
|---|---|---|
| 1 | info threads |
列出所有线程,找出卡在锁上的线程 |
| 2 | thread apply all bt |
打印所有线程的完整调用栈 |
| 3 | thread N |
切换到可疑线程 |
| 4 | frame 0 |
查看当前栈帧的局部变量和参数 |
| 5 | p *lock |
检查锁结构体,确认是否被持有 |
| 6 | frame 1 或更高 |
回溯调用链,找到锁的获取点 |
4.6 一张图看懂三者关系
下面这张 SVG 图把三个命令的协作关系画清楚了:
4.7 避坑指南
最后分享几个我踩过的坑:
- 别只看当前线程:很多人一进 GDB 就
bt,但当前线程可能是个空闲线程。先info threads看看全局。 - frame 编号别搞反:
frame 0是最顶层(当前执行点),frame N越大越靠近入口。我刚开始时经常搞混,切到frame 10发现是start_kernel,白费功夫。 - 锁地址要记下来:如果多个线程都在等同一个锁地址,那基本实锤死锁了。我会把锁地址抄下来,然后用
p *lock看 owner 字段。 - 内核线程名也有用:有些内核线程有名字(比如
kworker/u8:2),info threads里能看到。名字能帮你快速判断这个线程是干什么的。
thread apply all bt | grep -E "(mutex_lock|spin_lock|down)" 快速过滤出卡在锁上的线程。我经常这么干,省时间。
嗯,这三个命令你练熟了,死锁排查就迈出了最关键的一步。下次遇到驱动卡死,别急着重启,先连上 GDB 跑一遍这套流程——你会发现,死锁其实没那么神秘。