用crash工具分析Linux内核死锁的一次实战原创
背景知识点
ramdump是内存转存机制,我们可以在某个时刻把系统的内存转存到一个文件中,然后与符号信息(vmlinux)一起导入到trace32或crash等内存分析工具中做离线分析。是分析崩溃、死锁、内存泄露等内核疑难问题的重要调试手段。
crash是用于解析ramdump的开源工具,是命令行式的交互模式,提供诸多功能强大的调试命令,是分析定位内核复杂问题的利器。
死锁是指两个或两个以上的执行流在执行过程中,由于竞争锁资源而造成的一种阻塞的现象。如图:
问题描述
在Android7.1系统中跑monkey时出现界面卡死现象:
-
没有任何刷新,所有输入事件无效,包括电源键
-
watchdog没有重启system_server
-
可以连adb,但ps等调试命令卡住
初步分析
由于无法直接用adb调试,用长按电源键的方式进入dump模式并导出ramdump文件,之后再用crash工具载入randump文件开始离线分析。
一般卡死时可能是因为核心线程处在UNINTERRUPTIBLE状态,所以先在crash环境下用ps命令查看手机中UNINTERRUPTIBLE状态的线程,参数-u可过滤掉内核线程:
bt命令可查看某个线程的调用栈,我们看一下上面UN状态的最关键的watchdog线程:
从调用栈中可以看到proc_pid_cmdline_read()
函数中被阻塞的,对应的代码为:
这里是要获取被某个线程mm的mmap_sem锁,而这个锁又被另外一个线程持有。
推导读写锁
要想知道哪个线程持有了这把锁,我们得先用汇编推导出这个锁的具体值。可用dis命令看一下proc_pid_cmdline_read()
的汇编代码:
0xffffff99a680aaa0
处就是调用down_read()的地方,它的第一个参数x0就是sem锁,如:
x0和x28寄存器存放的就是sem的值,那x21自然就是mm_struct的地址了,因为mm_struct
的mmap_sem
成员的offset就是104(0x68),用whatis命令可以查看结构体的声明,如:
因此我们只需要知道x21或者x28就知道mm和mmap_sem锁的值。
函数调用时被调用函数会在自己的栈帧中保存即将被修改到的寄存器,所以我们可以在down_read()及它之后的函数调用中找到这两个寄存器:
也就是说下面几个函数中,只要找到用到x21或x28,必然会在它的栈帧中保存这些寄存器。
先从最底部的down_read()开始找:
显然它没有用到x21或x28,继续看rwsem_down_read_failed()的汇编代码:
在这个函数中找到x21,它保存在rwsem_down_read_failed栈帧的偏移32字节的位置。
rwsem_down_read_failed()的sp是0xffffffd6d9e4bcb0
sp + 32 =0xffffffd6d9e4bcd0,用rd命令查看地址0xffffffd6d9e4bcd0中存放的x21的值为:
用struct命令查看这个mm_struct:
这里的owner是mm_struct所属线程的task_struct:
sem锁的地址为0xffffffd76e349a00+0x68= 0xffffffd76e349a68
,因此:
分析到这里我们知道watchdog线程是在读取1651线程的proc节点时被阻塞了,原因是这个进程的mm,它的mmap_sem锁被其他线程给拿住了,那到底是谁持了这把锁呢?
持读写锁的线程
带着问题我们继续分析,首先通过list命令遍历wait_list来看一下共有多少个线程在等待这个读写锁:
从上面的输出可以看到一共有2个写者和有17个读者在等待,这19个线程都处于UNINTERRUPTIBLE状态。
再回顾一下当前系统中所有UNINTERRUPTIBLE状态的线程:
其中除标注红颜色的5个线程外的19个线程,都是上面提到的等待读写锁的线程。当持锁线程是写者,我们可以通过rw_semaphore结构的owner找到持锁线程。可惜这里owner是0,这表示持锁者是读者线程,因此我们无法通过owner找到持锁线程。这种情况下可以通过search命令加-t参数从系统中所有的线程的栈空间里查找当前锁:
一般锁的值都会保存在寄存器中,而寄存器又会在子函数调用过程中保存在栈中。所以只要在栈空间中找到当前锁的值(0xffffffd76e349a68),那这个线程很可能就是持锁或者等锁线程
这里搜出的20个线程中19个就是前面提到的等锁线程,剩下的1个很可能就是持锁线程了:
查看这个线程的调用栈:
由于2124线程中存放锁的地址是0xffffffd6d396b8b0
,这个是在handle_mm_fault()
的栈帧范围内,因此可以推断持锁的函数应该是在handle_mm_fault()
之前。
我们先看一下do_page_fault
函数:
代码中确实是存在持mmap_sem的地方,并且是读者,因此可以确定是2124持有的读写锁阻塞了watchdog在内的19个线程。
接下来我们需要看一下2124线程为什么会持锁后迟迟不释放就可以了,但在这之前我们先看一下system_server
的几个UNINTERRUPTIBLE状态的线程阻塞的原因。
其他被阻塞的线程(互斥锁的推导)
先看一下ActivityManager线程:
通过调用栈能看到是在binder_alloc_new_buf
时候被挂起的,我们得先找出这个锁的地址。
首先从mutex_lock()函数入手:
从它的声明中可以看到它的参数只有1个,就是mutex结构体指针。
再看看mutex_lock
函数的实现:
mutex_lock
的第一个参数x0就是我们要找的struct mutex,在0xffffff99a74e1648
处被保存在x19寄存器中,接着在0xffffff99a74e1664
处调用了__mutex_lock_slowpath()
,因此我们可以在__mutex_lock_slowpath()
中查找x19:
由于__mutex_lock_slowpath()
的sp是0xffffffd75ca379a0
:
因此x19的值保存在0xffffffd75ca379a0+ 16 = 0xffffffd75ca379b0
我们要找的mutex就是0xffffffd6dfa02200
:
其中owner就是持有该所的线程的task_struct
指针。它的pid为:
查看这个线程的调用栈:
这个3337线程就是前面提到的被读写锁锁住的19个线程之一。
用同样的方法可找到audioserver的1643线程、system_server的1909、2650线程也都是被这个3337线程持有的mutex锁给阻塞的。
总结起来的话:
- 一共有4个线程在等待同一个mutex锁,持锁的是3337线程
- 包括3337的19个线程等待着同一个读写锁,持锁的是2124线程。
也就是说大部分的线程都是直接或者间接地被2124线程给阻塞了。
死锁
最后一个UNINTERRUPTIBLE状态的线程就是2767(sdcard)线程:
可以看出2124线程是等待fuse的处理结果,而我们知道fuse的请求是sdcard来处理的。
这很容易联想到2124的挂起可能跟2767(sdcard)线程有关,但2124线程是在做read请求,而2767线程是在处理open请求时被挂起的。
就是说sdcard线程并不是在处理2124线程的请求,不过即使这种情况下sdcard线程依然能阻塞2124线程。因为对于一个APP进程来说,只会有一个特定的sdcard线程服务于它,如果同一个进程的多线程sdcard访问请求,sdcard线程会串行的进行处理。
如果前一个请求得不到处理,那后来的请求都会被阻塞。跟之前mutex锁的推导方法一样,得2767线程等待的mutex锁是0xffffffd6948f4090
,
它的owner的task和pid为:
先通过bt命令查找2124的栈范围为0xffffffd6d396b4b0~0xffffffd6d396be70
:
从栈里面可以找到mutex:
mutex值在ffffffd6d396bc40
这个地址上找到了,它是在__generic_file_write_iter
的栈帧里。
那可以肯定是在__generic_file_write_iter
之前就持锁了,并且很可能是ext4_file_write_iter
中,查看其源码:
这下清楚了,原来2124在等待2767处理fuse请求,而2767又被2124线程持有的mutex锁给锁住了,也就是说两个线程互锁了。
本文来自:Linux阅码场公众号,作者:朴英敏,现就职于国内一家手机研发公司,任职资深系统工程师,主要负责安卓系统方面的调试工作。