本文簡介:
內核死鎖問題一般是讀寫鎖(rw_semaphore)和互斥鎖(mutex)引起的,本文主要講如何通過ramdump+crash工具來分析這類死鎖問題。
0、背景知識點
ramdump是內存轉存機制,我們可以在某個時刻把系統的內存轉存到一個文件中,然后與符號信息(vmlinux)一起導入到trace32或crash等內存分析工具中做離線分析。是分析崩潰、死鎖、內存泄露等內核疑難問題的重要調試手段。
crash是用于解析ramdump的開源工具(http://people.redhat.com/anderson/),是命令行式的交互模式,提供諸多功能強大的調試命令,是分析定位內核復雜問題的利器。
死鎖是指兩個或兩個以上的執行流在執行過程中,由于競爭鎖資源而造成的一種阻塞的現象。如圖:
1、問題描述
在Android7.1系統中跑monkey時出現界面卡死現象:
1)沒有任何刷新,所有輸入事件無效,包括電源鍵
2)watchdog沒有重啟system_server
3)可以連adb,但ps等調試命令卡住
2、初步分析
由于無法直接用adb調試,用長按電源鍵的方式進入dump模式并導出ramdump文件,之后再用crash工具載入randump文件開始離線分析。
一般卡死時可能是因為核心線程處在UNINTERRUPTIBLE狀態,所以先在crash環境下用ps命令查看手機中UNINTERRUPTIBLE狀態的線程,參數-u可過濾掉內核線程:
bt命令可查看某個線程的調用棧,我們看一下上面UN狀態的最關鍵的watchdog線程:
從調用棧中可以看到proc_pid_cmdline_read()函數中被阻塞的,對應的代碼為:
這里是要獲取被某個線程mm的mmap_sem鎖,而這個鎖又被另外一個線程持有。
3、推導讀寫鎖
要想知道哪個線程持有了這把鎖,我們得先用匯編推導出這個鎖的具體值。可用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鎖被其他線程給拿住了,那到底是誰持了這把鎖呢?
4、持讀寫鎖的線程
帶著問題我們繼續分析,首先通過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狀態的線程阻塞的原因。
5、其他被阻塞的線程(互斥鎖的推導)
先看一下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鎖給阻塞的。
總結起來的話:1)一共有4個線程在等待同一個mutex鎖,持鎖的是3337線程2)包括3337的19個線程等待著同一個讀寫鎖,持鎖的是2124線程。
也就是說大部分的線程都是直接或者間接地被2124線程給阻塞了。
6、死鎖
最后一個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
+關注
關注
87文章
11320瀏覽量
209842
原文標題:樸英敏: 用crash工具分析Linux內核死鎖的一次實戰
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論