分析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 tasks 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錯誤。

本文作者:聲東

原文鏈接

更多技術乾貨敬請關注云棲社區知乎機構號:阿里云云棲社區 - 知乎

本文為雲棲社區原創內容,未經允許不得轉載。

推薦閱讀:

相關文章