2023-03-28 18:32:40 96阅读
有同事反馈在测试环境有个Linux的云桌面出现了卡死的现象,表现为云桌面内无法进行任何操作,但从其它网络互通的环境还可以ssh登录到卡死的云桌面上。
经过简单分析,我初步断定这个问题是进程死锁造成的卡死,于是让该同事手动触发内核crash以留下coredump做进一步分析。以下是针对该问题的一些分析记录。
通过加载出问题时的 coredump,可以发现 kworker/3:2 这个线程处于D状态,卡死在 qxl 相关的 work 里在等待 mutex lock:
顺着上面的函数调用栈来看,ttm_eu_reserve_buffers()在下图的位置处调用了__ww_mutex_lock():
结合代码可以看出对应的是下面的内核代码:
__ttm_bo_reserve()是个内联函数,其会逐级调用到__ww_mutex_lock(),并且调用过程中涉及到的函数也都是内联函数,因而从代码上可以看出ttm_eu_reserve_buffers()调用__ttm_bo_reserve()时传递的第一个参数 bo ,该参数里的一个成员 bo->resv 最终就对应到上面 kworker/3:2 卡死时的函数栈里的 mutex lock 这个锁的地址:
因而,如果能找出ttm_eu_reserve_buffers()调用__ttm_bo_reserve()时传递的第一个参数 bo 对应的地址,就能直接获得mutex lock 这个锁的地址并查看其内容了。
分析 ttm_eu_reserve_buffers()的反汇编代码,可以知道ttm_eu_reserve_buffers()调用__ww_mutex_lock()时传递的第一个参数(也即 mutex lock 的地址)是这么来的:
因此,R14的值就是ttm_eu_reserve_buffers()调用__ttm_bo_reserve()时传递的第一个参数 bo 对应的地址了。由于 __ww_mutex_lock() 里会把R14压栈保存:
故而我们可以从栈上保存的内容找到 R14 的值是0xffff9ae457386858:
因此,bo 参数(对应的结构体类型是struct ttm_buffer_object)的内容就可以从 coredump 中查看了。我们关心的是bo->resv:
从上图就可以看到 mutex lock 里面的内容了。当前持有这把 mutex lock 的线程的 task_struct 结构体指针保存在 owner 成员上,可以看到锁的持有者就是 kworker/3:2 内核线程它自己:
另一方面,从上图 mutex lock 的 wait_list 成员,我们也可以看到当前在等待这把锁的线程只有一个,也是 kworker/3:2 内核线程它自己!
从上述分析可见这是一个因为重复上锁导致的死锁的case,怀疑这是 qxl 驱动的一个bug。qxl驱动将预留buffer等操作以 workqueue 的形式扔给 kworker 线程来完成,这些操作会涉及不少获取和释放锁的操作,这里时序处理不当就可能造成死锁。目前推测这个问题的可能原因:
重新编译替换云桌面用的内核,开启 lockdep 相关的机制,然后重新尝试复现问题,看看 lockdep 的打印能不能抓到第一次获取 mutex lock 的函数调用栈,以此来进一步分析此调用路径为何没有释放锁。
lockdep 是 Linux 内核用于做spinlock、mutex lock等锁的依赖性检查的一个机制,通常用于在开发阶段 debug 死锁相关的issue或者检测可能的潜在死锁路径。
2023-03-28 18:32:40 96阅读
有同事反馈在测试环境有个Linux的云桌面出现了卡死的现象,表现为云桌面内无法进行任何操作,但从其它网络互通的环境还可以ssh登录到卡死的云桌面上。
经过简单分析,我初步断定这个问题是进程死锁造成的卡死,于是让该同事手动触发内核crash以留下coredump做进一步分析。以下是针对该问题的一些分析记录。
通过加载出问题时的 coredump,可以发现 kworker/3:2 这个线程处于D状态,卡死在 qxl 相关的 work 里在等待 mutex lock:
顺着上面的函数调用栈来看,ttm_eu_reserve_buffers()在下图的位置处调用了__ww_mutex_lock():
结合代码可以看出对应的是下面的内核代码:
__ttm_bo_reserve()是个内联函数,其会逐级调用到__ww_mutex_lock(),并且调用过程中涉及到的函数也都是内联函数,因而从代码上可以看出ttm_eu_reserve_buffers()调用__ttm_bo_reserve()时传递的第一个参数 bo ,该参数里的一个成员 bo->resv 最终就对应到上面 kworker/3:2 卡死时的函数栈里的 mutex lock 这个锁的地址:
因而,如果能找出ttm_eu_reserve_buffers()调用__ttm_bo_reserve()时传递的第一个参数 bo 对应的地址,就能直接获得mutex lock 这个锁的地址并查看其内容了。
分析 ttm_eu_reserve_buffers()的反汇编代码,可以知道ttm_eu_reserve_buffers()调用__ww_mutex_lock()时传递的第一个参数(也即 mutex lock 的地址)是这么来的:
因此,R14的值就是ttm_eu_reserve_buffers()调用__ttm_bo_reserve()时传递的第一个参数 bo 对应的地址了。由于 __ww_mutex_lock() 里会把R14压栈保存:
故而我们可以从栈上保存的内容找到 R14 的值是0xffff9ae457386858:
因此,bo 参数(对应的结构体类型是struct ttm_buffer_object)的内容就可以从 coredump 中查看了。我们关心的是bo->resv:
从上图就可以看到 mutex lock 里面的内容了。当前持有这把 mutex lock 的线程的 task_struct 结构体指针保存在 owner 成员上,可以看到锁的持有者就是 kworker/3:2 内核线程它自己:
另一方面,从上图 mutex lock 的 wait_list 成员,我们也可以看到当前在等待这把锁的线程只有一个,也是 kworker/3:2 内核线程它自己!
从上述分析可见这是一个因为重复上锁导致的死锁的case,怀疑这是 qxl 驱动的一个bug。qxl驱动将预留buffer等操作以 workqueue 的形式扔给 kworker 线程来完成,这些操作会涉及不少获取和释放锁的操作,这里时序处理不当就可能造成死锁。目前推测这个问题的可能原因:
重新编译替换云桌面用的内核,开启 lockdep 相关的机制,然后重新尝试复现问题,看看 lockdep 的打印能不能抓到第一次获取 mutex lock 的函数调用栈,以此来进一步分析此调用路径为何没有释放锁。
lockdep 是 Linux 内核用于做spinlock、mutex lock等锁的依赖性检查的一个机制,通常用于在开发阶段 debug 死锁相关的issue或者检测可能的潜在死锁路径。