1. 程式人生 > >分析core,是從案發現場,推導案發經過

分析core,是從案發現場,推導案發經過

分析core不是一件容易的事情。試想,一個系統運行了很長一段時間,在這段時間裡,系統會積累大量正常、甚至不正常的狀態。這個時候如果系統突然出現了一個問題,那這個問題十有八九跟長時間積累下來的狀態有關係。分析core,就是分析出問題時,系統產生的“快照”,追溯歷史,找出問題發生源頭。這有點像是從案發現場,推導案發經過一樣。

soft lockup!

今天這個“案件”,我們從soft lockup說起。

soft lockup是核心實現的夯機自我診斷功能。這個功能的實現,和執行緒的優先順序有關係。

這裡我們假設有三個執行緒A、B、和C。他們的優先順序關係是A<B<C。這意味著C優先於B執行,B優先於A執行。這個優先順序關係,如果倒過來敘述,就會產生一個規則:如果C不能執行,那麼B也沒有辦法執行,如果B不能執行,那基本上A也沒法執行。

soft lockup實際上就是對這個規則的實現:soft lockup使用一個核心定時器(C執行緒),週期性地檢查,watchdog(B執行緒)有沒有正常執行。如果沒有,那就意味著普通執行緒(A執行緒)也沒有辦法正常執行。這時核心定時器(C執行緒)會輸出類似上圖中的soft lockup記錄,來告訴使用者,卡在cpu上的,有問題的執行緒的資訊。

具體到這個“案件”,卡在cpu上的執行緒是python,這個執行緒正在重新整理tlb快取。

老搭檔ipi和tlb

如果我們對所有夯機問題的呼叫棧做一個統計的話,我們肯定會發現,tlb和ipi是一對形影不離的老搭檔。其實這不是偶然的。系統中,相對於記憶體,tlb是處理器本地的cache。這樣的共享記憶體和本地cache的架構,必然會提出一致性的要求。如果每個處理器的tlb“各自為政”的話,那系統肯定會亂套。滿足tlb一致性的要求,本質上來說只需要一種操作,就是重新整理本地tlb的同時,同步地重新整理其他處理器的tlb。系統正是靠tlb和ipi這對老搭檔的完美配合來完成這個操作的。

這個操作本身的代價是比較大的。一方面,為了避免產生競爭,執行緒在重新整理本地tlb的時候,會停掉搶佔。這就導致一個結果:其他的執行緒,當然包括watchdog執行緒,沒有辦法被排程執行(soft lockup)。另外一方面,為了要求其他cpu同步地重新整理tlb,當前執行緒會使用ipi和其他cpu同步進展,直到其他cpu也完成重新整理為止。其他cpu如果遲遲不配合,那麼當前執行緒就會死等。

不配合的cpu

為什麼其他cpu不配合去重新整理tlb呢?理論上來說,ipi是中斷,中斷的優先順序是很高的。如果有cpu不配合去重新整理tlb,基本上有兩種可能:一種是這個cpu重新整理了tlb,但是做到一半也卡住了;另外一種是,它根本沒有辦法響應ipi中斷。

通過檢視系統中所有佔用cpu的執行緒,可以看到cpu基本上在做三件事情:idle,正在重新整理tlb,和正在執行java程式。其中idle的cpu,肯定能在需要的時候,響應ipi並重新整理tlb。而正在重新整理tlb的cpu,因為停掉了搶佔,且在等待其他cpu完成tlb重新整理,所以在重複輸出soft lockup記錄。這裡問題的關鍵,是執行java的cpu,這個我們在下一節講。

java不是問題,踩到的坑才是問題

java執行緒執行在0號cpu上,這個執行緒的呼叫棧,滿滿的都是故事。我們可以簡單地把執行緒呼叫棧分為上下兩部分。下邊的是system call呼叫棧,是java從系統呼叫進入核心的執行記錄。上邊的是中斷棧,java在執行系統呼叫的時候,正好有一箇中斷進來,所以這個cpu臨時去處理了中斷。在linux核心中,中斷和系統呼叫使用的是不同的核心棧,所以我們可以看到第二列,上下兩部分地址是不連續的。

netoops持有等待

分析中斷處理這部分呼叫棧,從下往上,我們首先會發現,netoops函式觸發了缺頁異常。缺頁異常其實就是給系統一個機會,把指令踩到的虛擬地址,和真正想要訪問的物理機之間的對映關係給建立起來。但是有些虛擬地址,這種對映根本就是不存在的,這些地址就是非法地址(坑)。如果指令踩到這樣的地址,會有兩種後果,segment fault(程序)和oops(核心)。

很顯然netoops踩到了非法地址,使得系統進入了oops邏輯。系統進入oops邏輯,做的第一件事情就是禁用中斷。這個非常好理解。oops邏輯要做的事情是儲存現場,它當然不希望,中斷在這個時候破壞問題現場。

接下來,為了儲存現場的需要,netoops再一次被呼叫,然後這個函式在幾條指令之後,等在了spinlock上。要拿到這個spinlock,netoops必須要等它當前的owner執行緒釋放它。這個spinlock的owner是誰呢?其實就是當前執行緒。換句話說,netoops拿了spinlock,回過頭來又去要這個spinlock,導致當前執行緒死鎖了自己。

驗證上邊的結論,我們當然可以去讀程式碼。但是有另外一個技巧。我們可以看到netoops函式在踩到非法地址的時候,指令rip地址是ffffffff8137ca64,而在嘗試拿spinlock的時候,rip是ffffffff8137c99f。很顯然拿spinlock在踩到非法地址之前。雖然程式碼裡的跳轉指令,讓這種判斷不是那麼的準確,但是大部分情況下,這個技巧是很有用的。

缺頁異常,錯誤的時間,錯誤的地點

這個執行緒進入死鎖的根本原因是,缺頁異常在錯誤的時間發生在了錯誤的地點。對netoops函式的彙編和原始碼進行分析,我們會發現,缺頁發生在ffffffff8137ca64這條指令,而這條指令是inline函式utsname的指令。下圖中框出來的四條指令,就是編譯後的utsname函式。

而utsname函式的原始碼其實就一行。

return &current->nsproxy->uts_ns->name;

這行程式碼通過當前程序的task_struct指標current,訪問了uts namespace相關的內容。這一行程式碼,之所以會編譯成截圖中的四條彙編指令,是因為gs暫存器的0xcbc0項,儲存的就是current指標。這四條彙編指令做的事情分別是,取current指標,讀nsproxy項,讀uts_ns項,以及計算name的地址。第三條指令踩到非法地址,是因為nsproxy這個值為空值。

空值nsproxy

我們可以在兩個地方驗證nsproxy為空這個結論。第一個地方是讀取當前程序task_sturct的nsproxy項。另外一個是看缺頁異常的時候,儲存下來的rax暫存器的值。儲存下來的rax暫存器值可以在圖三中看到,下邊是從task_struct裡讀出來的nsproxy值。

正在退出的執行緒

那麼,為什麼當前程序task_struct這個結構的nsproxy這一項為空呢?我們可以回頭看一下,java執行緒呼叫棧的下半部分內容。這部分呼叫棧實際上是在執行exit系統呼叫,也就是說程序正在退出。實際上參考程式碼,我們可以確定,這個程序已經處於殭屍(zombie)狀態了。因而nsproxy相關的資源,已經被釋放了。

namespace訪問規則

最後我們簡單看一下nsproxy的訪問規則。規則一共有三條,netoops踩到空指標的原因,某種意義上來說,是因為它間接地違背了第三條規則。netoops通過utsname訪問程序的namespace,因為它在中斷上下文,所以並不算是訪問當前的程序,也就是說它應該查空。另外我加亮的部分,進一步佐證了上一小節的結論。

/*
* the namespaces access rules are:
*
* 1. only current task is allowed to change tsk->nsproxy pointer or
* any pointer on the nsproxy itself
*
* 2. when accessing (i.e. reading) current task's namespaces - no
* precautions should be taken - just dereference the pointers
*
* 3. the access to other task namespaces is performed like this
* rcu_read_lock();
* nsproxy = task_nsproxy(tsk);
* if (nsproxy != NULL) {
* / *
* * work with the namespaces here
* * e.g. get the reference on one of them
* * /
* } / *
* * NULL task_nsproxy() means that this task is
* * almost dead (zombie)
* * /
* rcu_read_unlock();
*
*/

回顧

最後我們復原一下案發經過。開始的時候,是java程序退出。java退出需要完成很多步驟。當它馬上就要完成自己使命的時候,一箇中斷打斷了它。這個中斷做了一系列的動作,之後呼叫了netoops函式。netoops函式拿了一個鎖,然後回頭去訪問java的一個被釋放掉的資源,這觸發了一個缺頁。因為訪問的是非法地址,所以這個缺頁導致了oops。oops過程禁用了中斷,然後呼叫netoops函式,netoops需要再次拿鎖,但是這個鎖已經被自己拿了,這是典型的死鎖。再後來其他cpu嘗試同步重新整理tlb,因為java程序關閉了中斷而且死鎖了,它根本收不到其他cpu發來的ipi訊息,所以其他cpu只能不斷的報告soft lockup錯誤。

原文連結