樸英敏: 用crash工具分析Linux核心死鎖的一次實戰
本文簡介:
核心死鎖問題一般是讀寫鎖(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狀態的執行緒,引數
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
因此我們只需要知道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鎖給鎖住了,也就是說兩個執行緒互鎖了。
本文只限於介紹如何定位死鎖問題,至於如何解決涉及到模組的具體實現,由於篇幅的關係這裡就不再贅述了。