分析core不是一件容易的事情。試想,一個系統運行了很長一段時間,在這段時間裏,系統會積累大量正常、甚至不正常的狀態。這個時候若是系統忽然出現了一個問題,那這個問題十有八九跟長時間積累下來的狀態有關係。分析core,就是分析出問題時,系統產生的「快照」,追溯歷史,找出問題發生源頭。這有點像是從案發現場,推導案發通過同樣。java
今天這個「案件」,咱們從soft lockup提及。python
soft lockup是內核實現的夯機自我診斷功能。這個功能的實現,和線程的優先級有關係。linux
這裏咱們假設有三個線程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緩存。函數
若是咱們對全部夯機問題的調用棧作一個統計的話,咱們確定會發現,tlb和ipi是一對如影隨行的老搭檔。其實這不是偶然的。系統中,相對於內存,tlb是處理器本地的cache。這樣的共享內存和本地cache的架構,必然會提出一致性的要求。若是每一個處理器的tlb「各自爲政」的話,那系統確定會亂套。知足tlb一致性的要求,本質上來講只須要一種操做,就是刷新本地tlb的同時,同步地刷新其餘處理器的tlb。系統正是靠tlb和ipi這對老搭檔的完美配合來完成這個操做的。oop
這個操做自己的代價是比較大的。一方面,爲了不產生競爭,線程在刷新本地tlb的時候,會停掉搶佔。這就致使一個結果:其餘的線程,固然包括watchdog線程,沒有辦法被調度執行(soft lockup)。另一方面,爲了要求其餘cpu同步地刷新tlb,當前線程會使用ipi和其餘cpu同步進展,直到其餘cpu也完成刷新爲止。其餘cpu若是遲遲不配合,那麼當前線程就會死等。this
爲何其餘cpu不配合去刷新tlb呢?理論上來講,ipi是中斷,中斷的優先級是很高的。若是有cpu不配合去刷新tlb,基本上有兩種可能:一種是這個cpu刷新了tlb,可是作到一半也卡住了;另一種是,它根本沒有辦法響應ipi中斷。spa
經過查看系統中全部佔用cpu的線程,能夠看到cpu基本上在作三件事情:idle,正在刷新tlb,和正在運行java程序。其中idle的cpu,確定能在須要的時候,響應ipi並刷新tlb。而正在刷新tlb的cpu,由於停掉了搶佔,且在等待其餘cpu完成tlb刷新,因此在重複輸出soft lockup記錄。這裏問題的關鍵,是運行java的cpu,這個咱們在下一節講。線程
java線程運行在0號cpu上,這個線程的調用棧,滿滿的都是故事。咱們能夠簡單地把線程調用棧分爲上下兩部分。下邊的是system call調用棧,是java從系統調用進入內核的執行記錄。上邊的是中斷棧,java在執行系統調用的時候,正好有一箇中斷進來,因此這個cpu臨時去處理了中斷。在linux內核中,中斷和系統調用使用的是不一樣的內核棧,因此咱們能夠看到第二列,上下兩部分地址是不連續的。
分析中斷處理這部分調用棧,從下往上,咱們首先會發現,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 ¤t->nsproxy->uts_ns->name;
這行代碼經過當前進程的task_struct指針current,訪問了uts namespace相關的內容。這一行代碼,之因此會編譯成截圖中的四條彙編指令,是由於gs寄存器的0xcbc0項,保存的就是current指針。這四條彙編指令作的事情分別是,取current指針,讀nsproxy項,讀uts_ns項,以及計算name的地址。第三條指令踩到非法地址,是由於nsproxy這個值爲空值。
咱們能夠在兩個地方驗證nsproxy爲空這個結論。第一個地方是讀取當前進程task_sturct的nsproxy項。另一個是看缺頁異常的時候,保存下來的rax寄存器的值。保存下來的rax寄存器值能夠在圖三中看到,下邊是從task_struct裏讀出來的nsproxy值。
那麼,爲何當前進程task_struct這個結構的nsproxy這一項爲空呢?咱們能夠回頭看一下,java線程調用棧的下半部份內容。這部分調用棧其實是在執行exit系統調用,也就是說進程正在退出。實際上參考代碼,咱們能夠肯定,這個進程已經處於殭屍(zombie)狀態了。於是nsproxy相關的資源,已經被釋放了。
最後咱們簡單看一下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錯誤。