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 或更高 回溯调用链,找到锁的获取点
核心思路:死锁排查不是靠猜,而是靠「线程 → 调用栈 → 锁地址 → 锁状态」这条链一步步往下追。info threads 给你地图,thread apply all bt 给你路线,frame 让你看清每个路口。

4.6 一张图看懂三者关系

下面这张 SVG 图把三个命令的协作关系画清楚了:

info threads / thread apply all bt / frame 协作关系图 GDB 线程调试三剑客协作流程 info threads 列出所有线程及当前帧 thread apply all bt 打印所有线程完整调用栈 thread N 切换到目标线程 frame N 切换栈帧,查看参数/变量 p *lock 确认锁状态与持有者 排查要点 • 先看 info threads • 再跑 bt 看全貌 • 最后 frame 查细节 常见死锁模式 • 多个线程等锁 • 锁持有者被阻塞 • 锁 count=0 有等待者

4.7 避坑指南

最后分享几个我踩过的坑:

  • 别只看当前线程:很多人一进 GDB 就 bt,但当前线程可能是个空闲线程。先 info threads 看看全局。
  • frame 编号别搞反:frame 0 是最顶层(当前执行点),frame N 越大越靠近入口。我刚开始时经常搞混,切到 frame 10 发现是 start_kernel,白费功夫。
  • 锁地址要记下来:如果多个线程都在等同一个锁地址,那基本实锤死锁了。我会把锁地址抄下来,然后用 p *lock 看 owner 字段。
  • 内核线程名也有用:有些内核线程有名字(比如 kworker/u8:2),info threads 里能看到。名字能帮你快速判断这个线程是干什么的。
一个小技巧:如果 coredump 里线程太多,可以用 thread apply all bt | grep -E "(mutex_lock|spin_lock|down)" 快速过滤出卡在锁上的线程。我经常这么干,省时间。

嗯,这三个命令你练熟了,死锁排查就迈出了最关键的一步。下次遇到驱动卡死,别急着重启,先连上 GDB 跑一遍这套流程——你会发现,死锁其实没那么神秘。