檢視原始碼 追蹤

實作

呼叫、返回和例外追蹤

追蹤的實作方式是在被追蹤的函式中設定中斷點,並在觸發時傳送適當的追蹤訊息。

  • 呼叫追蹤訊息會立即傳送。
  • 返回追蹤會將一個框架推入堆疊,該框架會返回到一個在遇到時會傳送追蹤訊息的指令。
  • 例外追蹤也會將一個框架推入堆疊,但只有在拋出例外時在堆疊掃描中遇到它才會傳送追蹤訊息。

這表示必須小心不要在永遠不會返回的函式上使用返回或例外追蹤,因為每次呼叫都會推入一個永遠不會被移除的框架。

另一個限制是,由於中斷點位於被呼叫者而不是呼叫者中,因此我們只能取得函式進入時所擁有的資訊。這表示我們實際上無法知道是誰呼叫我們:由於我們只能檢查堆疊,因此我們只能說我們要返回到哪裡,這與實際的呼叫者並不完全相同。

舉例來說,當啟用 caller 選項時,所有來自 bar/1 的追蹤訊息都會說它們是從 foo/0 呼叫的,即使它在過程中經過了一堆其他函式。

foo() ->
    lots(),
    ok.

lots() ->
    'of'().

'of'() ->
    indirections().

indirections() ->
    bar(10).

bar(0) ->
    done;
bar(N) ->
    bar(N - 1).

匯出追蹤

在直譯器中,中斷點會設定在匯出項目的程式碼跳轉區內,並且它們的位址向量會更新為指向它們。這樣,只有遠端呼叫會觸發中斷點,而對同一函式的本地呼叫則會保持不變,但其餘行為與本地中斷點相同。

在 JIT 中情況會變得稍微複雜一些。詳細資訊請參閱 BeamAsm.md

設定中斷點

簡介

在 OTP R16 之前,當透過 erlang:trace_pattern 變更追蹤設定時,VM 中的所有其他執行都會暫停,同時以單執行緒模式執行追蹤操作。與程式碼載入類似,這可能會對可用性造成嚴重的問題,該問題會隨著核心數量的增加而增加。

在 OTP R16 中,中斷點是在程式碼中設定的,而不會阻塞 VM。Erlang 程序可以在整個操作過程中繼續並行執行而不受干擾。與程式碼載入使用相同的基本技術。準備中斷點的暫存區域,然後透過單一原子操作使其生效。

中斷點輪的重新設計

為了更容易在沒有單執行緒模式的情況下管理中斷點,對中斷點機制進行了重新設計。舊的「中斷點輪」資料結構是每個被檢測函式的中斷點的循環雙向鏈結串列。它是在 SMP 模擬器之前發明的。為了在 SMP 模擬器中支援它,它基本上被擴展為每個排程器一個中斷點輪。隨著更多中斷點類型的增加,實作變得雜亂無章且難以理解和維護。

在新的設計中,舊的輪被捨棄,而是由一個結構 (GenericBp) 取代,以保存每個被檢測函式的所有類型中斷點的資料。一個位旗標欄位用於指示啟用了哪些不同類型的中斷動作。

似是而非

即使 trace_pattern 使用與非阻塞程式碼載入相同的技術,即資料結構的複寫世代和原子切換,但實作彼此完全獨立。最初的一個想法是使用現有的程式碼載入機制來執行虛擬載入操作,該操作會複製受影響的模組。然後可以在使該副本可透過與程式碼載入相同的原子切換訪問之前,使用中斷點對其進行檢測。這種方法看起來很直接,但存在一些缺點,其中之一是當檢測許多模組時會產生大量的記憶體佔用。另一個問題是執行將如何訪問新的檢測程式碼。通常,載入的程式碼只能透過外部函式呼叫訪問。追蹤設定必須立即啟動,而無需外部函式呼叫。

選擇的解決方案是讓追蹤在用於中斷點的資料結構上應用複寫技術。保留兩個世代的中斷點,並以索引 0 和 1 識別。全域原子變數 erts_active_bp_index 將決定執行中的程式碼將使用哪個世代的中斷點。

沒有原子操作的原子性

不使用程式碼載入世代 (或任何其他程式碼複製) 表示 trace_pattern 必須在某個時間點寫入活動的 Beam 程式碼,以便執行中的程序可以訪問暫存的中斷點結構。這可以透過每個檢測函式單一原子寫入操作來完成。但是,Beam 指令字組是透過普通記憶體載入讀取的,而不是透過原子 API。我們唯一需要保證的是,寫入的指令字組被視為原子的。要么完全寫入,要么根本不寫入。這對於我們使用的所有硬體架構上的字組對齊寫入操作都是如此。

新增中斷點

這是一個簡化的序列,描述了新增新中斷點時 trace_pattern 的流程。

  1. 取得獨佔程式碼修改權限(暫停程序,直到我們取得該權限)。

  2. 分配中斷點結構 GenericBp,包括兩個世代。將活動區域設定為停用,並使用歸零的旗標欄位。將原始指令字組儲存在中斷點中。

  3. 從第一個指令 ErtsFuncInfo 標頭的偏移量 -sizeof(UWord) 處寫入指向中斷點的指標。

  4. 將中斷點的暫存區域設定為已啟用,並具有指定的中斷點資料。

  5. 等待執行緒進度。

  6. op_i_generic_breakpoint 作為函式的第一個指令寫入。此指令將執行在偏移量 -sizeof(UWord) 處找到的中斷點。

  7. 等待執行緒進度。

  8. 透過切換 erts_active_bp_index 來提交中斷點。

  9. 等待執行緒進度。

  10. 「合併」準備透過將中斷點的新暫存區域 (舊的活動區域) 更新為與新的活動區域相同,為下一次呼叫 trace_pattern 做準備。

  11. 釋放程式碼修改權限並從 trace_pattern 返回。

步驟 1 中取得的程式碼修改權限「鎖定」也由程式碼載入取得。這可確保一次只有一個程序可以暫存新的追蹤設定,並防止並行程式碼載入,並確保我們在整個序列中看到 Beam 程式碼的一致視圖。

在步驟 6 和 8 之間,執行中的程序可能會執行寫入的 op_i_generic_breakpoint 指令。它們將取得在步驟 3 中寫入的中斷點結構,讀取 erts_active_bp_index 並執行中斷點的對應部分。但是在步驟 8 中的切換變得可見之前,它們將執行中斷點結構的停用部分,並且除了執行儲存的原始指令外,不會執行任何其他操作。

步驟 10 中的合併將使新的暫存區域與新的活動區域相同。這將使下一次呼叫 trace_pattern 變得更簡單,該呼叫可能不會影響所有現有的中斷點。所有未受影響的中斷點的暫存區域都已準備好成為活動的,而無需 trace_pattern 的任何訪問。

更新和移除中斷點

上述序列僅描述了新增新的中斷點。我們基本上執行相同的序列來更新現有中斷點的設定,但可以跳過步驟 2、3 和 6,因為它們已完成。

要移除中斷點,需要更多步驟。其想法是先將中斷點暫存為停用,進行切換,等待執行緒進度,然後透過還原原始的 Beam 指令來移除停用的中斷點。

這是一個更完整的序列,其中包含新增、更新和移除中斷點。

  1. 取得獨佔程式碼修改權限(暫停程序,直到我們取得該權限)。

  2. 分配新的中斷點結構,其中包含停用的活動區域和原始的 Beam 指令。在偏移量 -sizeof(UWord) 處的 ErtsFuncInfo 標頭中寫入指向中斷點的指標。

  3. 更新所有受影響中斷點的暫存區域。停用要移除的中斷點。

  4. 等待執行緒進度。

  5. 為所有具有新中斷點的函式寫入 op_i_generic_breakpoint 作為第一個指令。

  6. 等待執行緒進度。

  7. 透過切換 erts_active_bp_index 來提交所有暫存的中斷點。

  8. 等待執行緒進度。

  9. 解除安裝。還原停用中斷點的原始 Beam 指令。

  10. 等待執行緒進度。

  11. 合併。透過更新所有已啟用中斷點的新暫存區域 (舊的活動區域),為下一次呼叫 trace_pattern 做準備。

  12. 解除分配已停用的中斷點結構。

  13. 釋放程式碼修改權限並從 trace_pattern 返回。

所有等待執行緒進度的過程

在上述序列中有四輪等待執行緒進度的過程。在程式碼載入序列中,我們犧牲了三個世代的記憶體開銷,以避免第二輪執行緒進度。但是,trace_pattern 的延遲應該不會是一個大問題,因為它通常不會以快速的序列呼叫。

步驟 4 中的等待是為了確保所有執行緒在透過步驟 5 中寫入的 op_i_generic_breakpoint 指令可訪問中斷點結構時,都能看到中斷點結構的更新視圖。

步驟 6 中的等待是為了使新追蹤設定的啟動「盡可能原子化」。不同的核心可能會在不同的時間看到 erts_active_bp_index 的新值,因為它是在沒有任何記憶體屏障的情況下讀取的。但這是在沒有更昂貴的執行緒同步的情況下,我們能做的最好的。

步驟 8 中的等待是為了確保我們在知道沒有執行緒仍在訪問停用中斷點的舊啟用區域之前,不會還原停用中斷點的原始 Beam 指令。

步驟 10 中的等待是為了確保沒有仍然在訪問停用中斷點結構的執行緒,以便在步驟 12 中解除分配。

全域追蹤

使用 global 選項進行呼叫追蹤只會影響外部函式呼叫。先前這部分是透過在匯出入口插入特殊的追蹤指令來處理,而未使用中斷點。隨著新的非阻塞追蹤機制,我們希望避免針對全域追蹤進行特殊處理,並利用中斷點機制內的暫存和原子切換功能。解決方案是為全域呼叫追蹤建立相同類型的中斷點結構。與本機追蹤的不同之處在於,我們是在匯出入口而不是程式碼中插入 op_i_generic_breakpoint 指令(其指標位於偏移量 -4)。

未來工作

當為正在追蹤的模組載入新程式碼,或當設定預設追蹤模式時載入程式碼時,我們仍然會進入單執行緒模式。這並非無法修復,但需要追蹤 BIF 和載入器 BIF 之間更緊密的合作。